概述
switch控件是我们最常用的控件之一,网上虽有较多源码,但是通常我们的项目中用到的都是他的变种,初学者有时候不知如何修改,希望通过本文的分析,我们可以掌握这个控件的关键点。
详解:
我们来做一个下图这样的switch控件。
ezgif.com-9dd15217dd.gif
可以分解为下面三个过程:
- 绘制图像
- 添加事件
- 测量布局
绘制图像:
此控件的绘制的过程可分解为:
1.绘制边框线条
2.绘制填充色
3.绘制小圆球
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFrameRoundRect(canvas);
drawRect(canvas);
drawCircle(canvas);
}
在绘制之前我们需要准备好画笔,我们定义三种画笔。
/**
* 初始化画笔
*/
private void initPaint() {
//填充画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(0);
mPaint.setStyle(Paint.Style.FILL);
//边线画笔
fPaint = new Paint();
fPaint.setAntiAlias(true);
fPaint.setColor(Color.parseColor("#BDBDBD"));
fPaint.setAlpha(255);
fPaint.setStrokeWidth(1);
fPaint.setStyle(Paint.Style.STROKE);
//圆圈画笔
cPaint = new Paint();
cPaint.setAntiAlias(true);
cPaint.setStyle(Paint.Style.FILL_AND_STROKE);
cPaint.setStrokeWidth(1);
cPaint.setColor(Color.WHITE);
}
1.绘制边框线条
边线是两边圆弧的矩形,所以我们选择drawRoundRect方法。
/**
* 绘制边框线条
* @param canvas
*/
private void drawFrameRoundRect(Canvas canvas) {
if (rectF == null) {
rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
}
canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, fPaint);
}
2.绘制填充色
- 绘制填充色和绘制线条的实现几乎一样,不一样的也就是画笔了。
- 填充色的画笔setStyle设置的是Paint.Style.FILL,边线画笔设置的是Paint.Style.STROKE
- setARGB设置画笔的色值,通过alpha值的变化使得渐变过度更平缓。
/**
* 绘制填充的色值
* @param canvas
*/
private void drawRect(Canvas canvas) {
mPaint.setARGB(getColorAlpha(), Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor));
if (rectF == null) {
rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
}
canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, mPaint);
}
3.绘制小圆球
- setShadowLayer用来实现小圆球的阴影效果,使其更有立体感。
- isPressed用来区分按下和正常效果,按下之后小圆球变大。
- drawCircle,drawRoundRect的这些坐标读者可以自己微调,没有固定的规则。
/**
* 绘制点击的原点
* @param canvas
*/
private void drawCircle(Canvas canvas) {
if (isPressed) {
cPaint.setShadowLayer(12, 0, 12, Color.argb(61, 0x00, 0x00, 0x00));
canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2 + 6, cPaint);
} else {
cPaint.setShadowLayer(6, 0, 6, Color.argb(61, 0x00, 0x00, 0x00));
canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2, cPaint);
}
}
至此,静态的switch完成了。为了让其动起来,我们来响应下onTouchEvent事件的实现。
添加事件:
一般我们响应onTouch事件,只需处理这几个事件:ACTION_DOWN,ACTION_MOVE, ACTION_CANCEL, ACTION_UP。如果事件不清楚的可以看:View, ViewGroup, Layout
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;//控件处于disable状态的时候不处理
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getX();
lastProcess = getProcess();
isPressed = true;
break;
case MotionEvent.ACTION_MOVE:
setProcess(lastProcess + (event.getX() - lastX)/getWidth());
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isPressed = false;
if (getProcess() > 0.5) {
setProcess(1f);
} else {
setProcess(0f);
}
break;
default:
break;
}
return true;
- MotionEvent.ACTION_DOWN:获取按下时的滑动位置
- MotionEvent.ACTION_MOVE:滑动过程中设置坐标,并且刷新界面
- MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP:事件结束处理
在滑动过程中,最好将坐标打印出来看看就比较清楚了。
测量布局:
添加完响应事件,我们switch控件已经基本可以用了。用户如果在xml中不指定控件的长宽,我们的控件就会铺满全屏,直接变形了,所以我们还需要重写下onMeasure,当然你也可把这个测量布局放在最开始。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//AT_MOST或者UNSPECIFIED,即用户没有指定宽度时,显示默认宽度
width = dip2px(getContext(), 128);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//AT_MOST或者UNSPECIFIED,即用户没有指定高度时,显示默认高度
height = dip2px(getContext(), 48);
}
setMeasuredDimension(width, height);
}
完整代码:
测试代码不贴了,新建一个工程,将这个类拷贝到工程,和普通button控件用法一样就可以使用了。
package com.wayne.android.widget;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Checkable;
public class Switch extends View implements Checkable {
private static final int FILL_COLOR_1 = Color.parseColor("#EEEEEE");
private static final int FILL_COLOR_2 = Color.parseColor("#03A9F4");
private static final int ANIMATION_DURATION = 200;
private static final int OFFSET = 12;
private ObjectAnimator processAnimator;
private boolean isChecked;
private float process = 0;
private float lastX = 0;
private float lastProcess = 0;
private int fillColor;
private boolean isPressed;
private Paint cPaint;
private Paint fPaint;
private Paint mPaint;
private RectF rectF = null;
public Switch(Context context) {
super(context);
init();
}
public Switch(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int width;
int height;
if (widthMode == MeasureSpec.EXACTLY) {
width = widthSize;
} else {
//AT_MOST或者UNSPECIFIED,即用户没有指定宽度时,显示默认宽度
width = dip2px(getContext(), 128);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//AT_MOST或者UNSPECIFIED,即用户没有指定高度时,显示默认高度
height = dip2px(getContext(), 48);
}
setMeasuredDimension(width, height);
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFrameRoundRect(canvas);
drawRect(canvas);
drawCircle(canvas);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = event.getX();
lastProcess = getProcess();
isPressed = true;
break;
case MotionEvent.ACTION_MOVE:
setProcess(lastProcess + (event.getX() - lastX)/getWidth());
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
isPressed = false;
if (getProcess() > 0.5) {
setProcess(1f);
} else {
setProcess(0f);
}
break;
default:
break;
}
return true;
}
@Override
public void setChecked(boolean b) {
if (isChecked != b) {
isChecked = b;
animateSwitch(isChecked);
}
}
@Override
public boolean isChecked() {
return isChecked;
}
@Override
public void toggle() {
setChecked(!isChecked);
}
/**
* 初始化
*/
private void init() {
isPressed = false;
process = 0;
isChecked = false;
fillColor = FILL_COLOR_1;
setLayerType(LAYER_TYPE_SOFTWARE, null);
initAnimation();
initPaint();
}
/**
* 初始化动画对象
*/
private void initAnimation() {
processAnimator = ObjectAnimator.ofFloat(this, "process", 0, 1);
processAnimator.setDuration(ANIMATION_DURATION);
processAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
}
/**
* 初始化画笔
*/
private void initPaint() {
//填充画笔
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(0);
mPaint.setStyle(Paint.Style.FILL);
//frame画笔
fPaint = new Paint();
fPaint.setAntiAlias(true);
fPaint.setColor(Color.parseColor("#BDBDBD"));
fPaint.setAlpha(255);
fPaint.setStrokeWidth(1);
fPaint.setStyle(Paint.Style.STROKE);
//圆圈画笔
cPaint = new Paint();
cPaint.setAntiAlias(true);
cPaint.setStyle(Paint.Style.FILL_AND_STROKE);
cPaint.setStrokeWidth(1);
cPaint.setColor(Color.WHITE);
}
public float getProcess() {
return process;
}
/**
* 设置滑动的进度
* @param process
*/
public void setProcess(float process) {
if (process >= 1f) {
this.process = 1;
isChecked = true;
} else if (process <= 0f) {
this.process = 0;
isChecked = false;
} else {
this.process = process;
}
if (this.process > 0.5) {
fillColor = FILL_COLOR_2;
} else {
fillColor = FILL_COLOR_1;
}
postInvalidate();
}
/**
* 开关动画
* @param checked
*/
private void animateSwitch(boolean checked) {
if (processAnimator.isRunning()) {
processAnimator.cancel();
}
if (checked) {
processAnimator.setFloatValues(process, 1f);
} else {
processAnimator.setFloatValues(process, 0f);
}
processAnimator.start();
}
/**
* 获取滑动时的alpha值
* @return
*/
private int getColorAlpha() {
int alpha;
if (getProcess() >= 0 && getProcess() < 0.5) {
alpha = (int) (255 * (1 - getProcess()));
} else {
alpha = (int) (255 * getProcess());
}
int colorAlpha = Color.alpha(fillColor);
colorAlpha = colorAlpha * alpha / 255;
return colorAlpha;
}
/**
* 绘制填充的色值
* @param canvas
*/
private void drawRect(Canvas canvas) {
mPaint.setARGB(getColorAlpha(), Color.red(fillColor), Color.green(fillColor), Color.blue(fillColor));
if (rectF == null) {
rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
}
canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, mPaint);
}
/**
* 绘制边框线条
* @param canvas
*/
private void drawFrameRoundRect(Canvas canvas) {
if (rectF == null) {
rectF = new RectF(OFFSET, OFFSET, getWidth()-OFFSET, getHeight()-OFFSET);
}
canvas.drawRoundRect(rectF, (getHeight() - OFFSET)/2, (getHeight() - OFFSET)/2, fPaint);
}
/**
* 绘制点击的原点
* @param canvas
*/
private void drawCircle(Canvas canvas) {
if (isPressed) {
cPaint.setShadowLayer(12, 0, 12, Color.argb(61, 0x00, 0x00, 0x00));
canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2 + 6, cPaint);
} else {
cPaint.setShadowLayer(6, 0, 6, Color.argb(61, 0x00, 0x00, 0x00));
canvas.drawCircle(getHeight()/2 + (getWidth() - getHeight())*process, getHeight()/2, (getHeight() - getHeight()/3)/2, cPaint);
}
}
/**
* dip转换的px
* @param context
* @param dpValue
* @return
*/
private int dip2px(Context context, float dpValue) {
final float scale = context.getResources().getDisplayMetrics().density;
return (int) ((dpValue * scale) + 0.5f);
}
}
结语:
为了尽量简单,代码量少些,本文没有按照前面文章中写的那样,如:定义drawable将绘制移入其中,并且定义属性文件。有空的话大家可以改造下,好了,就到这里,周末愉快。
上篇:自定义控件(progresses)(360手机助手下载进度示例)
网友评论