一、前言
上一篇我们处理论述过相关绘制难点,本篇其实同样的需要克服上一篇的困难,唯一不同的是本篇内容中添加了文本绘制,温习一下文本绘制技巧。
二、绘制文本基本知识
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 四种模式。
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); //传递事件,注意不要造成死循环问题
7、变色算法
float maxSlideWidth = outline.width() - mSlideBarWidth - lineWidthPixies;
if (maxSlideWidth > 0) {
float x = animSlideBarX - lineWidthPixies - lineWidthPixies / 2F;
float ratio = x / maxSlideWidth;
int Alpha = (int) (Color.alpha(COLOR_TABLE[0]) * (1 - ratio) + Color.alpha(COLOR_TABLE[1]) * ratio);
int Red = (int) (Color.red(COLOR_TABLE[0]) * (1 - ratio) + Color.red(COLOR_TABLE[1]) * ratio);
int Green = (int) (Color.green(COLOR_TABLE[0]) * (1 - ratio) + Color.green(COLOR_TABLE[1]) * ratio);
int Blue = (int) (Color.blue(COLOR_TABLE[0]) * (1 - ratio) + Color.blue(COLOR_TABLE[1]) * ratio);
currentFillColor = Color.argb(Alpha, Red, Green, Blue);
Alpha = (int) (Color.alpha(COLOR_TABLE[1]) * (1 - ratio) + Color.alpha(COLOR_TABLE[0]) * ratio);
Red = (int) (Color.red(COLOR_TABLE[1]) * (1 - ratio) + Color.red(COLOR_TABLE[0]) * ratio);
Green = (int) (Color.green(COLOR_TABLE[1]) * (1 - ratio) + Color.green(COLOR_TABLE[0]) * ratio);
Blue = (int) (Color.blue(COLOR_TABLE[1]) * (1 - ratio) + Color.blue(COLOR_TABLE[0]) * ratio);
outLineColor = Color.argb(Alpha, Red, Green, Blue);
Log.d("Colors", "ratio = " + ratio + ",animSlideBarX=" + animSlideBarX + ",maxSlideWidth=" + maxSlideWidth);
} else {
currentFillColor = COLOR_TABLE[0];
outLineColor = COLOR_TABLE[1];
}
基于以上问题的解决,实现了一个 SwitchButton,虽然没用到 Path,但还是考虑了很多问题。
public class SwitchButtonView extends View {
// 实例化画笔
private TextPaint mPaint = null;
private int lineWidth = 5;
private final int STATUS_LEFT = 0x00;
private final int STATUS_RIGHT = 0x01;
private volatile int mStatus = STATUS_LEFT;
private RectF outline = new RectF();
private Rect mBounds = new Rect();
private RectF rect = new RectF();
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 float mSlideBarWidth = 0;
private boolean shouldAnimation = false;
private ValueAnimator mSlideAnimator = null;
private final int[] COLOR_TABLE = {0xFF394038, 0xFFf20a7e};
private int currentFillColor = COLOR_TABLE[0];
private int outLineColor;
public void setLeftText(String text) {
STATUS[0] = text;
}
public void setRightText(String text) {
STATUS[1] = text;
}
public void setLeftColor(int Color) {
COLOR_TABLE[0] = Color;
}
public void setRightColor(int color) {
COLOR_TABLE[1] = color;
}
private float animSlideBarX = 0;
public SwitchButtonView(Context context) {
this(context, null);
}
public SwitchButtonView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SwitchButtonView(Context context, 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();
float lineWidthPixies = dpTopx(lineWidth);
if (width <= lineWidthPixies || height <= lineWidthPixies) return;
float centerX = width / 2F;
float centerY = height / 2F;
float R = Math.min(width / 2F, height / 2F);
float maxSlideWidth = outline.width() - mSlideBarWidth - lineWidthPixies;
if (maxSlideWidth > 0) {
float x = animSlideBarX - lineWidthPixies - lineWidthPixies / 2F;
float ratio = x / maxSlideWidth;
int Alpha = (int) (Color.alpha(COLOR_TABLE[0]) * (1 - ratio) + Color.alpha(COLOR_TABLE[1]) * ratio);
int Red = (int) (Color.red(COLOR_TABLE[0]) * (1 - ratio) + Color.red(COLOR_TABLE[1]) * ratio);
int Green = (int) (Color.green(COLOR_TABLE[0]) * (1 - ratio) + Color.green(COLOR_TABLE[1]) * ratio);
int Blue = (int) (Color.blue(COLOR_TABLE[0]) * (1 - ratio) + Color.blue(COLOR_TABLE[1]) * ratio);
currentFillColor = Color.argb(Alpha, Red, Green, Blue);
Alpha = (int) (Color.alpha(COLOR_TABLE[1]) * (1 - ratio) + Color.alpha(COLOR_TABLE[0]) * ratio);
Red = (int) (Color.red(COLOR_TABLE[1]) * (1 - ratio) + Color.red(COLOR_TABLE[0]) * ratio);
Green = (int) (Color.green(COLOR_TABLE[1]) * (1 - ratio) + Color.green(COLOR_TABLE[0]) * ratio);
Blue = (int) (Color.blue(COLOR_TABLE[1]) * (1 - ratio) + Color.blue(COLOR_TABLE[0]) * ratio);
outLineColor = Color.argb(Alpha, Red, Green, Blue);
Log.d("Colors", "ratio = " + ratio + ",animSlideBarX=" + animSlideBarX + ",maxSlideWidth=" + maxSlideWidth);
} else {
currentFillColor = COLOR_TABLE[0];
outLineColor = COLOR_TABLE[1];
}
drawRectOutline(canvas, width, height, lineWidthPixies, R);
//中间分割线
mPaint.setStrokeCap(Paint.Cap.ROUND);
mPaint.setColor(Color.LTGRAY);
canvas.drawLine(centerX - mPaint.getStrokeWidth() / 2F, (float) (height / 2F - height / 2 * 0.4F - lineWidthPixies / 2F), centerX - mPaint.getStrokeWidth() / 2F, (float) (height / 2F + height / 2F * 0.4F) - lineWidthPixies / 2F, mPaint);
mPaint.setStrokeCap(Paint.Cap.BUTT);
drawSlider(canvas, width, height, lineWidthPixies);
drawText(canvas, width, centerY);
}
private void drawRectOutline(Canvas canvas, int width, int height, float lineWidthPixies, float r) {
float startX = lineWidthPixies;
float startY = lineWidthPixies;
float endX = width - 2 * lineWidthPixies; //宽度应该减去左右两边的线宽
float endY = height - 2 * lineWidthPixies; //宽度应该减去上下两边的线宽
mPaint.setStyle(Paint.Style.FILL);
int color = mPaint.getColor();
mPaint.setColor(currentFillColor);
outline.set(startX, startY, endX, endY);
canvas.drawRoundRect(outline, r, r, mPaint);
mPaint.setStyle(Paint.Style.STROKE);
float strokeWidth = mPaint.getStrokeWidth();
mPaint.setStrokeWidth(lineWidthPixies/2);
mPaint.setColor(Color.LTGRAY);
canvas.drawRoundRect(outline, r, r, mPaint);
mPaint.setColor(color);
mPaint.setStrokeWidth(strokeWidth);
}
private void drawText(Canvas canvas, int width, float centerY) {
mPaint.getTextBounds(STATUS[0], 0, STATUS[0].length(), mBounds);
float textWidth = mBounds.width();
float textBaseline = centerY + getTextPaintBaseline(mPaint);
Paint.Style style = mPaint.getStyle();
mPaint.setStyle(Paint.Style.FILL);
mPaint.setTextSize(52);
int color = mPaint.getColor();
mPaint.setColor(Color.WHITE);
if (mStatus == STATUS_LEFT) {
mPaint.setFakeBoldText(true);
} else {
mPaint.setFakeBoldText(false);
}
canvas.drawText(STATUS[0], width / 4F - textWidth / 2F, textBaseline, mPaint);
if (mStatus == STATUS_RIGHT) {
mPaint.setFakeBoldText(true);
} else {
mPaint.setFakeBoldText(false);
}
canvas.drawText(STATUS[1], width * 3 / 4F - textWidth / 2F, textBaseline, mPaint);//文本位置以基线为准
mPaint.setStyle(style);
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, float outWidth, float outheight, float lineWidthPixies) {
int color = mPaint.getColor();
mPaint.setColor(outLineColor);
mPaint.setStyle(Paint.Style.FILL);
float width = outWidth - 2 * lineWidthPixies;
float height = outheight - 2 * lineWidthPixies;
float slideBarX = lineWidthPixies + lineWidthPixies / 2F;
float slideBarY = lineWidthPixies + lineWidthPixies / 2F;
mSlideBarWidth = (width / 2F - slideBarX - mSlideInMiddleSpace);
float R = Math.min(width / 2F, height / 2F);
if (isTouchState) {
animSlideBarX = currentX;
rect.set(animSlideBarX, slideBarY, currentX + mSlideBarWidth, height - lineWidthPixies / 2F);
canvas.drawRoundRect(rect, R, R, mPaint);
} else {
if (mStatus == STATUS_RIGHT) {
slideBarX = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
if (!shouldAnimation) {
animSlideBarX = slideBarX;
rect.set(animSlideBarX, slideBarY, slideBarX + mSlideBarWidth, height - lineWidthPixies / 2F);
canvas.drawRoundRect(rect, R, R, mPaint);
} else {
rect.set(animSlideBarX, slideBarY, animSlideBarX + mSlideBarWidth, height - lineWidthPixies / 2F);
canvas.drawRoundRect(rect, R, R, mPaint);
}
} else {
if (!shouldAnimation) {
animSlideBarX = slideBarX;
rect.set(slideBarX, slideBarY, slideBarX + mSlideBarWidth, height - lineWidthPixies / 2F);
canvas.drawRoundRect(rect, R, R, mPaint);
} else {
rect.set(animSlideBarX, slideBarY, animSlideBarX + mSlideBarWidth, height - lineWidthPixies / 2F);
canvas.drawRoundRect(rect, R, R, mPaint);
}
}
}
mPaint.setColor(color);
}
TimeInterpolator interpolator = new AccelerateDecelerateInterpolator();
public void startSlideBarAnimation(float from, float to) {
if (mSlideAnimator != null) {
shouldAnimation = false;
mSlideAnimator.cancel();
}
mSlideAnimator = ValueAnimator.ofFloat(from, to).setDuration(300);
mSlideAnimator.setInterpolator(interpolator);
mSlideAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animSlideBarX = (float) 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 / 2F) && 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 < lineWidthPixies || startX > (width - lineWidthPixies)) {
MotionEvent actionOUTSIDE = MotionEvent.obtain(event);
actionOUTSIDE.setAction(MotionEvent.ACTION_OUTSIDE);
dispatchTouchEvent(actionOUTSIDE);
}
}
return true;
case MotionEvent.ACTION_MOVE:
currentX = event.getX() - sliderWidth / 2F;
//滑块移动位置应该相对于中心位置为基准
float maxRight = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
if (currentX < (lineWidthPixies)) {
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 (mStatus == STATUS_LEFT) {
if (xPos > width / 2F) {
mStatus = STATUS_RIGHT;
onStatusChanged(mStatus);
if (!isMoveTouch) {
float to = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
float from = (lineWidthPixies + lineWidthPixies / 2F);
startSlideBarAnimation(from, to);
shouldAnimation = true;
} else {
animSlideBarX = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
}
} else {
animSlideBarX = (lineWidthPixies + lineWidthPixies / 2F);
}
} else if (mStatus == STATUS_RIGHT) {
if (xPos > lineWidthPixies && xPos < width / 2) {
mStatus = STATUS_LEFT;
onStatusChanged(mStatus);
if (!isMoveTouch) {
float from = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
float to = (lineWidthPixies + lineWidthPixies / 2F);
startSlideBarAnimation(from, to);
shouldAnimation = true;
} else {
animSlideBarX = (lineWidthPixies + lineWidthPixies / 2F);
}
} else {
animSlideBarX = (width / 2F + mSlideInMiddleSpace + lineWidthPixies);
}
}
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(float 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);
}
}
三、总结
本篇相比上一篇,完善程度更高,不仅处理了滑动问题,还通过自定义事件的方式成功实现了点击侧滑。