概述
switch控件是我們最常用的控件之一,網(wǎng)上雖有較多源碼,但是通常我們的項(xiàng)目中用到的都是他的變種,初學(xué)者有時(shí)候不知如何修改,希望通過本文的分析,我們可以掌握這個(gè)控件的關(guān)鍵點(diǎn)。
詳解:
我們來做一個(gè)下圖這樣的switch控件。

可以分解為下面三個(gè)過程:
- 繪制圖像
- 添加事件
- 測(cè)量布局
繪制圖像:
此控件的繪制的過程可分解為:
1.繪制邊框線條
2.繪制填充色
3.繪制小圓球
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawFrameRoundRect(canvas);
drawRect(canvas);
drawCircle(canvas);
}
在繪制之前我們需要準(zhǔn)備好畫筆,我們定義三種畫筆。
/**
* 初始化畫筆
*/
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.繪制填充色
- 繪制填充色和繪制線條的實(shí)現(xiàn)幾乎一樣,不一樣的也就是畫筆了。
- 填充色的畫筆setStyle設(shè)置的是Paint.Style.FILL,邊線畫筆設(shè)置的是Paint.Style.STROKE
- setARGB設(shè)置畫筆的色值,通過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用來實(shí)現(xiàn)小圓球的陰影效果,使其更有立體感。
- isPressed用來區(qū)分按下和正常效果,按下之后小圓球變大。
- drawCircle,drawRoundRect的這些坐標(biāo)讀者可以自己微調(diào),沒有固定的規(guī)則。
/**
* 繪制點(diǎn)擊的原點(diǎn)
* @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);
}
}
至此,靜態(tài)的switch完成了。為了讓其動(dòng)起來,我們來響應(yīng)下onTouchEvent事件的實(shí)現(xiàn)。
添加事件:
一般我們響應(yīng)onTouch事件,只需處理這幾個(gè)事件:ACTION_DOWN,ACTION_MOVE, ACTION_CANCEL, ACTION_UP。如果事件不清楚的可以看:View, ViewGroup, Layout
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;//控件處于disable狀態(tài)的時(shí)候不處理
}
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:獲取按下時(shí)的滑動(dòng)位置
- MotionEvent.ACTION_MOVE:滑動(dòng)過程中設(shè)置坐標(biāo),并且刷新界面
- MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP:事件結(jié)束處理
在滑動(dòng)過程中,最好將坐標(biāo)打印出來看看就比較清楚了。
測(cè)量布局:
添加完響應(yīng)事件,我們switch控件已經(jīng)基本可以用了。用戶如果在xml中不指定控件的長寬,我們的控件就會(huì)鋪滿全屏,直接變形了,所以我們還需要重寫下onMeasure,當(dāng)然你也可把這個(gè)測(cè)量布局放在最開始。
@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,即用戶沒有指定寬度時(shí),顯示默認(rèn)寬度
width = dip2px(getContext(), 128);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//AT_MOST或者UNSPECIFIED,即用戶沒有指定高度時(shí),顯示默認(rèn)高度
height = dip2px(getContext(), 48);
}
setMeasuredDimension(width, height);
}
完整代碼:
測(cè)試代碼不貼了,新建一個(gè)工程,將這個(gè)類拷貝到工程,和普通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,即用戶沒有指定寬度時(shí),顯示默認(rèn)寬度
width = dip2px(getContext(), 128);
}
if (heightMode == MeasureSpec.EXACTLY) {
height = heightSize;
} else {
//AT_MOST或者UNSPECIFIED,即用戶沒有指定高度時(shí),顯示默認(rèn)高度
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();
}
/**
* 初始化動(dòng)畫對(duì)象
*/
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;
}
/**
* 設(shè)置滑動(dòng)的進(jìn)度
* @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();
}
/**
* 開關(guān)動(dòng)畫
* @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();
}
/**
* 獲取滑動(dòng)時(shí)的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);
}
/**
* 繪制點(diǎn)擊的原點(diǎn)
* @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轉(zhuǎn)換的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);
}
}
結(jié)語:
為了盡量簡單,代碼量少些,本文沒有按照前面文章中寫的那樣,如:定義drawable將繪制移入其中,并且定義屬性文件。有空的話大家可以改造下,好了,就到這里,周末愉快。
上篇:自定義控件(progresses)(360手機(jī)助手下載進(jìn)度示例)