1、文本绘制基线测量
文本绘制的方法是Canvas类的drawText,对于x点坐标其实和正常流程类似,但Y坐标的确定需要考虑Baseline问题
@param text The text to be drawn @param x X方向的坐标,开始绘制的左上角横轴坐标点@param y Y坐标,该坐标是Y轴方向上的”基线”坐标@param paint 画笔工具*/ public void drawText(@NonNull String text, float x, float y, @NonNull Paint paint)
基线到中线的距离=(Descent+Ascent)/2-Descent ,Android中,实际获取到的Ascent是负数。
公式推导过程如下:
中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。 有了基线到中线的距离,我们只要知道任何一行文字中线的位置,就可以马上得到基线的位置,从而得到Canvas的drawText方法中参数y的值。/** * 计算绘制文字时的基线到中轴线的距离,Android获取中线到基线距离的代码,Paint需要设置文字大小textsize。 * * @param p * @param centerY * @return 基线和centerY的距离 */ public static float getBaseline(Paint p) { FontMetrics fontMetrics = p.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent; }
说道这里我们只是计算出了基线高度,Y坐标一般区文本高度的中点位置。比如竖直方向,公式为。
Y = centerY + getBaseline(paint);
此外,对于宽度的测量,一般使用如下方法
mPaint.getTextBounds(text, 0, text.length(), mBounds); float textwidth = mBounds.width();
2、Path闭合区域填充问题
在常见的绘制View的过程中,我们通过Path对象构建复杂的闭合图像,最后一般来通过Paint设置Style.FILL填充区域,但是对于闭合的Path填充,在Android某些版本中不支持填充Path的区域。实际上Path同样提供了填充方法,可以做到很好的兼容。
Android的Path.FillType除了支持上面两种模式外,还支持了上面两种模式的反模式,一共定义了EVEN_ODD, INVERSE_EVEN_ODD, WINDING, INVERSE_WINDING 四种模式。
实际上,WINDING类似Paint中的Style.FILL
3、Path 图像合成
一般情况下我们图像是将Bitmap合成,合成时使用Xfermodes,当然Path也可以转为Bitmap图像数据。
但是Path同样提供了一系列合成方法
DIFFERENCE:从path1中减去path2
INTERSECT:取path1和path2重合的部分
REVERCE_DIFFERENCE:从path2中减去path1
UNION:联合path1和path2
XOR:取path1和path2不重合的部分
4、StrokeWidth与区域大小问题
对于带边框的View,StrokeWidth在很多情况下被认为不挤占区域大小,实际上,与此相反,我们计算坐标时一定要计算线宽问题。比如绘制线宽StrokeWidth的起点矩形,如果不这样计算,绘制将会出现边框宽度不一致的情况。
startX = StrokeWidth;startY = StrokeWidth;endX = getWidth() - StrokeWidth;endY = getHeight- StrokeWidth;
5、触摸MOVE事件问题
很多时候绘制View我们需要处理TouchEvent事件,然而,Android中View默认无法监听,需要设置一个莫名其妙的参数。
setClickable(true);
5、事件状态转移问题
很多时候,我们判断到某一区域时达到某种条件需要主动结束事件事务,或者改变事件状态如下然后在传递出去,方法如下
MotionEvent actionUP = MotionEvent.obtain(event); //增量式拷贝,比如修修改开始时间、修改修改时间序列 actionUP.setAction(MotionEvent.ACTION_UP); dispatchTouchEvent(actionUP); //传递事件,注意不要造成死循环问题
基于以上问题的解决,实现了一个SwitchButton,虽然没用到Path,但还是考虑了很多问题。
public class SwitchButtonView extends View { // 实例化画笔 private TextPaint mPaint = null; private Path mPath;// 路径对象 private int lineWidth = 1; private final int STATUS_LEFT = 0x00; private final int STATUS_RIGHT = 0x01; private volatile int mStatus = STATUS_LEFT; private int textSize = 18; private volatile float startX = 0; //触摸开始位置 private volatile boolean isTouchState = false; private volatile float currentX = 0; private final String[] STATUS = {"开","关"}; private OnStatusChangedListener mOnStatusChangedListener; public void setLeftText(String text){ STATUS[0] = text; } public void setRightText(String text){ STATUS[1] = text; } public SwitchButtonView(Context context) { this(context,null); } public SwitchButtonView(Context context, @Nullable AttributeSet attrs) { this(context, attrs,0); } public SwitchButtonView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initPaint(); setClickable(true); //设置此项true,否则无法滑动 } private void initPaint() { // 实例化画笔并打开抗锯齿 mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG ); mPaint.setAntiAlias(true); mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型 mPaint.setStrokeWidth(dpTopx(lineWidth)); mPaint.setTextSize(dpTopx(textSize)); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int widthMode = MeasureSpec.getMode(widthMeasureSpec); int width = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); if(widthMode!=MeasureSpec.EXACTLY){ width = (int) dpTopx(105*2); } if(heightMode!=MeasureSpec.EXACTLY){ height = (int) dpTopx(35*2); } setMeasuredDimension(width,height); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); int width = getWidth(); int height = getHeight(); if(width<=0 || height<=0) return; int centerX = width/2; int centerY = height/2; int lineWidthPixies = (int) dpTopx(lineWidth); int R = getHeight()/2; mPaint.setStyle(Paint.Style.STROKE); int startX = lineWidthPixies; int startY = lineWidthPixies; int endX = width - 2*lineWidthPixies; //宽度应该减去左右两边的线宽 int endY = height - 2*lineWidthPixies; //宽度应该减去上下两边的线宽 canvas.drawRoundRect(new RectF(startX,startY,endX,endY),R,R,mPaint); //中间分割线 canvas.drawLine(centerX,height*2/5,centerX,height*3/5,mPaint); drawText(canvas, width, centerY); drawSlider(canvas,width,height,lineWidthPixies); } private void drawText(Canvas canvas, int width, int centerY) { Rect mBounds = new Rect(); mPaint.getTextBounds(STATUS[0], 0, STATUS[0].length(), mBounds); float textwidth = mBounds.width(); float textBaseline = centerY + getTextPaintBaseline(mPaint); mPaint.setStyle(Paint.Style.FILL); canvas.drawText(STATUS[0],width/4-textwidth/2,textBaseline,mPaint); canvas.drawText(STATUS[1],width*3/4-textwidth/2, textBaseline,mPaint);//文本位置以基线为准 mPaint.setStyle(Paint.Style.STROKE); } /** * 基线到中线的距离=(Descent+Ascent)/2-Descent 注意,实际获取到的Ascent是负数。公式推导过程如下: 中线到BOTTOM的距离是(Descent+Ascent)/2,这个距离又等于Descent+中线到基线的距离,即(Descent+Ascent)/2=基线到中线的距离+Descent。 */ public static float getTextPaintBaseline(Paint p) { Paint.FontMetrics fontMetrics = p.getFontMetrics(); return (fontMetrics.descent - fontMetrics.ascent) / 2 -fontMetrics.descent; } private void drawSlider(Canvas canvas, int outwidth, int outheight, int lineWidthPixies) { int color = mPaint.getColor(); mPaint.setColor(Color.GREEN); mPaint.setStyle(Paint.Style.FILL); float width = outwidth - 2*lineWidthPixies; float height = outheight - 2 * lineWidthPixies; int slideBarX = 2* lineWidthPixies; int slideBarY = 2*lineWidthPixies; int R = (int) (height/2); if(isTouchState){ canvas.drawRoundRect(new RectF(currentX, slideBarY, currentX+width/2-3*lineWidthPixies, height - lineWidthPixies), R, R, mPaint); }else { if (mStatus == STATUS_RIGHT) { slideBarX = (int) (slideBarY+width/2+lineWidthPixies); canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width - lineWidthPixies, height - lineWidthPixies), R, R, mPaint); } else { canvas.drawRoundRect(new RectF(slideBarX, slideBarY, width / 2 - lineWidthPixies, height - lineWidthPixies), R, R, mPaint); } } mPaint.setColor(color); } @Override public boolean onTouchEvent(MotionEvent event) { float lineWidthPixies = dpTopx(lineWidth); float width = (getWidth()- 2*lineWidthPixies); float sliderWidth = width/2; int actionMasked = event.getActionMasked(); switch (actionMasked){ case MotionEvent.ACTION_DOWN: { isTouchState = true; startX = event.getX(); if (startX > (width / 2) && startX<(width-lineWidthPixies) && mStatus == STATUS_LEFT) { MotionEvent actionUP = MotionEvent.obtain(event); actionUP.setAction(MotionEvent.ACTION_UP); dispatchTouchEvent(actionUP); } else if (startX > lineWidthPixies && (startX < width / 2 && mStatus == STATUS_RIGHT)) { MotionEvent actionUP = MotionEvent.obtain(event); actionUP.setAction(MotionEvent.ACTION_UP); dispatchTouchEvent(actionUP); }else if(startX(width-lineWidthPixies)){ MotionEvent actionOUTSIDE = MotionEvent.obtain(event); actionOUTSIDE.setAction(MotionEvent.ACTION_OUTSIDE); dispatchTouchEvent(actionOUTSIDE); } } break; case MotionEvent.ACTION_MOVE: currentX = event.getX()- sliderWidth/2; //滑块移动位置应该相对于中心位置为基准 if(currentX<(2* lineWidthPixies)){ currentX = 2* lineWidthPixies; //最左边 }else if(currentX>((lineWidthPixies+sliderWidth)+2*lineWidthPixies)){ //最右边 currentX = (sliderWidth)+2*lineWidthPixies; } postInvalidate(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_OUTSIDE: isTouchState = false; float xPos = event.getX(); if((xPos>width/2&& mStatus==STATUS_LEFT)){ mStatus = STATUS_RIGHT; onStatusChanged(mStatus); }else if(xPos>lineWidthPixies && (xPos