一、效果展示

533e7e22e464e8b0fb597a086cf2c504.gif

二、绘制文本基本知识

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)

0ef9116675b3fb475566f6dc2a33d580.png

基线到中线的距离=(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同样提供了填充方法,可以做到很好的兼容。

eebaff35372d4d3bab7cd359a7bdf8d2.png

Android的Path.FillType除了支持上面两种模式外,还支持了上面两种模式的反模式,一共定义了EVEN_ODD, INVERSE_EVEN_ODD, WINDING, INVERSE_WINDING 四种模式。

2114ccbd52c832f278af818a1db54615.png

实际上,WINDING类似Paint中的Style.FILL

3、Path 图像合成

一般情况下我们图像是将Bitmap合成,合成时使用Xfermodes,当然Path也可以转为Bitmap图像数据。

e221899f806ebf70aa5c30cf6aa11397.png

但是Path同样提供了一系列合成方法

DIFFERENCE:从path1中减去path2

INTERSECT:取path1和path2重合的部分

REVERCE_DIFFERENCE:从path2中减去path1

UNION:联合path1和path2

XOR:取path1和path2不重合的部分

7036bf7fed329e542286b4df582442ae.png

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 = 5;

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;

private int mSlideInMiddleSpace = 20;

private int mSlideBarWidth = 0;

private boolean shouldAnimation = false;

private ValueAnimator mSlideAnimator = null;

public void setLeftText(String text){

STATUS[0] = text;

}

public void setRightText(String text){

STATUS[1] = text;

}

private int animSlideBarX = 0;

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.setStrokeWidth(dpTopx(lineWidth));

mPaint.setTextSize(dpTopx(textSize));

mSlideInMiddleSpace = (int) dpTopx(8);

}

@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;

drawRectOutline(canvas, width, height, lineWidthPixies, R);

//中间分割线

canvas.drawLine(centerX-mPaint.getStrokeWidth()/2, (float) (height/2-height/2*0.4 - lineWidthPixies/2),centerX-mPaint.getStrokeWidth()/2, (float) (height/2+height/2*0.4)-lineWidthPixies/2,mPaint);

drawText(canvas, width, centerY);

drawSlider(canvas,width,height,lineWidthPixies);

}

private void drawRectOutline(Canvas canvas, int width, int height, int lineWidthPixies, int r) {

int startX = lineWidthPixies;

int startY = lineWidthPixies;

int endX = width - 2*lineWidthPixies; //宽度应该减去左右两边的线宽

int endY = height - 2*lineWidthPixies; //宽度应该减去上下两边的线宽

mPaint.setStyle(Paint.Style.FILL);

int color = mPaint.getColor();

mPaint.setColor(Color.GRAY);

canvas.drawRoundRect(new RectF(startX,startY,endX,endY), r, r,mPaint);

mPaint.setStyle(Paint.Style.STROKE);

mPaint.setColor(color);

canvas.drawRoundRect(new RectF(startX,startY,endX,endY), r, r,mPaint);

}

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);

mPaint.setTextSize(52);

int color = mPaint.getColor();

mPaint.setColor(Color.WHITE);

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);

mPaint.setColor(color);

}

/**

* 基线到中线的距离=(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.LTGRAY);

mPaint.setStyle(Paint.Style.FILL);

float width = outwidth - 2*lineWidthPixies;

float height = outheight - 2 * lineWidthPixies;

int slideBarX = lineWidthPixies+lineWidthPixies/2;

int slideBarY = lineWidthPixies+lineWidthPixies/2;

mSlideBarWidth = (int) (width / 2 - slideBarX - mSlideInMiddleSpace);

int R = (int) (height/2);

if(isTouchState){

canvas.drawRoundRect(new RectF(currentX, slideBarY, currentX+mSlideBarWidth, height - lineWidthPixies/2), R, R, mPaint);

}else {

if (mStatus == STATUS_RIGHT) {

slideBarX = (int) (width/2+mSlideInMiddleSpace+lineWidthPixies);

if(!shouldAnimation) {

canvas.drawRoundRect(new RectF(slideBarX, slideBarY, slideBarX + mSlideBarWidth, height - lineWidthPixies / 2), R, R, mPaint);

}else {

canvas.drawRoundRect(new RectF(animSlideBarX, slideBarY, animSlideBarX + mSlideBarWidth, height - lineWidthPixies / 2), R, R, mPaint);

}

} else {

if(!shouldAnimation){

canvas.drawRoundRect(new RectF(slideBarX, slideBarY, slideBarX+mSlideBarWidth, height-lineWidthPixies/2), R, R, mPaint);

}else {

canvas.drawRoundRect(new RectF(animSlideBarX, slideBarY, animSlideBarX + mSlideBarWidth, height - lineWidthPixies / 2), R, R, mPaint);

}

}

}

mPaint.setColor(color);

}

public void startSlideBarAnimation(int from,int to ) {

if(mSlideAnimator!=null){

shouldAnimation = false;

mSlideAnimator.cancel();

}

mSlideAnimator = ValueAnimator.ofInt(from,to).setDuration(300);

mSlideAnimator.setInterpolator(new AccelerateDecelerateInterpolator());

mSlideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

@Override

public void onAnimationUpdate(ValueAnimator animation) {

animSlideBarX = (int) animation.getAnimatedValue();

float fraction = animation.getAnimatedFraction();

if(fraction==1.0f){

shouldAnimation = false;

}

postInvalidate();

}

});

mSlideAnimator.start();

}

private boolean isMoveTouch = false;

@Override

public boolean onTouchEvent(MotionEvent event) {

float lineWidthPixies = dpTopx(lineWidth);

float width = (getWidth()- 2*lineWidthPixies);

float sliderWidth = mSlideBarWidth;

int actionMasked = event.getActionMasked();

switch (actionMasked){

case MotionEvent.ACTION_DOWN: {

isTouchState = true;

startX = event.getX();

if (startX > (width / 2) && startX

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;

//滑块移动位置应该相对于中心位置为基准

int maxRight = (int) (width/2+mSlideInMiddleSpace+lineWidthPixies);

if(currentX

currentX = lineWidthPixies+lineWidthPixies/2; //最左边

mStatus = STATUS_LEFT;

onStatusChanged(mStatus);

}else if(currentX>maxRight){ //最右边

mStatus = STATUS_RIGHT;

onStatusChanged(mStatus);

currentX = maxRight;

}

isMoveTouch = true;

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);

if(!isMoveTouch) {

int to = (int) (width / 2 + mSlideInMiddleSpace + lineWidthPixies);

int from = (int) (lineWidthPixies + lineWidthPixies / 2);

startSlideBarAnimation(from, to);

shouldAnimation = true;

}

}else if(xPos>lineWidthPixies && (xPos

mStatus = STATUS_LEFT;

onStatusChanged(mStatus);

if(!isMoveTouch) {

int from = (int) (width/2+mSlideInMiddleSpace+lineWidthPixies);

int to = (int) (lineWidthPixies+lineWidthPixies/2);

startSlideBarAnimation(from, to);

shouldAnimation = true;

}

}

if(!shouldAnimation) {

postInvalidate();

}

isMoveTouch = false;

break;

}

return super.onTouchEvent(event);

}

private void onStatusChanged(int status) {

if(this.mOnStatusChangedListener!=null){

this.mOnStatusChangedListener.onStatusChanged(status);

}

}

private float dpTopx(int dp){

return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,dp,getResources().getDisplayMetrics());

}

public void setOnStatusChangedListener(OnStatusChangedListener l){

this.mOnStatusChangedListener = l;

}

interface OnStatusChangedListener{

void onStatusChanged(int status);

}

}

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐