android 自定义 switch,Android 自定义SwitchButtonView实践
一、效果展示 二、绘制文本基本知识1、文本绘制基线测量文本绘制的方法是Canvas类的drawText,对于x点坐标其实和正常流程类似,但Y坐标的确定需要考虑Baseline问题@param text The text to be drawn@param x X方向的坐标,开始绘制的左上角横轴坐标点@param y Y坐标,该坐标是Y轴方向上的”基线”坐标@param paint 画笔工具*/pu
一、效果展示
二、绘制文本基本知识
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 = 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);
}
}
更多推荐
所有评论(0)