前言
项目中总会有涉及到控制开关的东西,比如是否设置默认的配置,是否开启夜光模式,是否浏览中加载图片....界面都是需要一个显示的开关图标的,最简单的方法是使用checkButton,用两张图片作为drawable的资源使用,这是很容易做到的。点击的时候就是两张图片的切换,但是这样做有很明显的缺点
- 动画很突兀,是一瞬间完成的,缺少观赏性
- 第二就麻烦了,当你找不到你所需要的图片的时候,此不是欢声笑语中打出GG?
所以我们有必要自己写一个view来代替这种东西,苹果手机自带的ToogleButton就是一个很好的选择,他们自带这种的确实比较漂亮。所以今天去撸一个仿ios的ToogleButton。
正文
先不说辣么多,老板,上效果图先
看上去还挺简单的,但里边可不是可不是两张图片换来换去,是自己通过自定义view画上去的,哈哈。里边还有一些动画细节你可能没看到。,我将动画时间设置得长一点你就看得清楚了。来,看下慢动作。
这样一来就看清楚里边的动画了吧,看清楚了就简单了
流程分析
自定义属性
用法
见名知义,相信大家都看得懂。
接下来来分析一下draw方法的具体步骤:【可以看着动画来分析】
- 首先画一个一个白色背景,然后画一个边界线
- 画环形宽度渐变的环形圆角矩形,它是怎么样渐变的呢?是根据属性动画完成的,这里有个很巧妙的方法实现这个,将画笔的style设置成Paint.Style.STROKE,然后将画笔宽度设置成环形宽度,就可以很容易实现这个效果,至于渐变,当从关闭状态到开启状态的时候,它就是从0到
圆形按钮半径按钮的一半渐变的,反之,当从开启状态到关闭状态的时候,它就是从圆形按钮半径按钮的一半到0渐变的,大家看看动画就知道了。比例值是根据圆形按钮的滑动的完成度计算的。 - 画圆形按钮 根据属性动画的比例算出mButtonX的值,然后根据这个值确定按钮的位置。下面的红色就是按钮需要滑动的轨迹,你可以根据属性动画的比例值算出当前时间占有这条线的宽度。
还有一个主意的地方,这个背景色是一个渐变色来的,是从白色到绿色的渐变过程。这个使用了
ArgbEvaluator,这个这个类渐变色专用的,挺方便的。
private final android.animation.ArgbEvaluator argbEvaluator
= new android.animation.ArgbEvaluator();
bgColor = (int) argbEvaluator.evaluate(
(Float) animation.getAnimatedValue(),
getResources().getColor(R.color.white),
checkedColor
);
- 画完按钮会有这个问题,看下面的图
可以看到圆形按钮左边有个空白的位置,这个不太好看,我们需要用背景色把这段空白遮住。
就是画一个圆和一个矩形把它遮住
canvas.drawCircle(left + borderWidth + viewRadius, borderWidth + viewRadius, viewRadius - borderWidth, paint);
canvas.drawRect(
left + viewRadius, top,
mButtonX, top + 2 * viewRadius,
paint);
完整的onDraw代码如下
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStrokeWidth(borderWidth);
paint.setColor(getResources().getColor(R.color.white));
paint.setStyle(Paint.Style.FILL);
//绘制白色背景
drawWhitBg(canvas, paint);
//绘制边线
paint.setStyle(Paint.Style.STROKE);
paint.setColor(borderColor);
drawWhitBg(canvas, paint);
//绘制按钮滑动过程中的渐变边框
float des = (mButtonX * viewRadius / (width - 2 * viewRadius)) * 0.5f;//滑动过程中渐变背景框的半径
paint.setColor(bgColor);
paint.setStrokeWidth(des * 2);
paint.setStyle(Paint.Style.STROKE);
if (des == 0) {
canvas.drawRoundRect(new RectF(left + des + borderWidth, top + des + borderWidth, right - des - borderWidth, bottom - des - borderWidth), viewRadius, viewRadius, paint);
}else{
canvas.drawRoundRect(new RectF(left + des, top + des, right - des, bottom - des), viewRadius, viewRadius, paint);
}
//填充按钮左边因为画渐变边框而留下来的白框
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(1);
canvas.drawCircle(left + borderWidth + viewRadius, borderWidth + viewRadius, viewRadius - borderWidth, paint);
canvas.drawRect(
left + viewRadius + borderWidth, top + borderWidth,
mButtonX + viewRadius - borderWidth, top + 2 * viewRadius - borderWidth,
paint);
// //绘制按钮
paint.setColor(buttonColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(borderWidth);
canvas.drawCircle(buttonRadius + mButtonX + borderWidth, centerY, buttonRadius, paint);
paint.setColor(shadowColor);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(buttonRadius + mButtonX+borderWidth, centerY, buttonRadius, paint);
}
分析完onDraw方法的流程,其他的东西就比较容易了,这里我没有在onTouchEvent处理Move事件了,感觉没必要。如果你要处理也是可以这基础上加,在move事件的时候加些逻辑。onTouchEvent代码如下
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
int eventAsked = event.getActionMasked();
switch (eventAsked) {
case MotionEvent.ACTION_DOWN:
if (isAnimation) {//正在动画中,不处理事件。等待完成在处理
return false;
}
if (checkState == UNCHECKED) {//检测当前状态
toogleOn();
} else if (checkState == CHECKED) {
toogleOff();
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* 打开
*/
private void toogleOn() {
isAnimation = true;
valueAnimator.start();
}
/**
* 关闭
*/
private void toogleOff() {
isAnimation = true;
valueAnimator.start();
}
onTouchEvent的逻辑比较简单,这里就不分析了。主要处理Down事件。如果当前是在动画中。也就是按钮在滑动,就不处理这个点击事件。
结语
完成的逻辑看下代码就知道了。实现还是比较简单的。记录一下,滴~打卡
完整代码如下
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import com.shopee.feeds.feedlibrary.R;
public class ToogleButton extends View {
private static final int DEFAULT_TOOGLE_WIDTH = 58;//默认的宽度
private static final int DEFAULT_TOOGLE_HEIGHT = 36;//默认的高度
private int borderWidth;//边线宽度
private int borderColor;//边线颜色
private int bgColor;//背景颜色
private int checkedColor;//开关为开的时候的背景色
private int shadowColor;//开关切换时需要绘制的一层背景色
private int buttonColor;//按钮的颜色
private int animationDuration;//动画时间
private Paint paint;
private int checkState = 1;//按钮的开关状态【默认为关闭状态】
private static final int CHECKED = 0;//打开状态
private static final int UNCHECKED = 1;//关闭状态
private boolean isAnimation = false;//是否在滑动中
/**
* 背景位置
*/
private float left;
private float top;
private float right;
private float bottom;
private float centerX;
private float centerY;
private float height;//背景高度
private float width;//背景宽度
private float viewRadius;//背景半径
private float buttonRadius;//按钮半径
private float mButtonX;//按钮的偏移量
private ValueAnimator valueAnimator;
private final android.animation.ArgbEvaluator argbEvaluator
= new android.animation.ArgbEvaluator();
public ToogleButton(Context context) {
this(context, null);
}
public ToogleButton(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public ToogleButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ToogleButton, defStyleAttr, 0);
borderWidth = (int) array.getDimension(R.styleable.ToogleButton_border_width,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics()));
borderColor = array.getColor(R.styleable.ToogleButton_border_color, getResources().getColor(R.color.grey_500));
bgColor = array.getColor(R.styleable.ToogleButton_bg_color, getResources().getColor(R.color.white));
checkedColor = array.getColor(R.styleable.ToogleButton_checked_color, getResources().getColor(R.color.toogle_green));
shadowColor = array.getColor(R.styleable.ToogleButton_shadow_color, getResources().getColor(R.color.grey_500));
buttonColor = array.getColor(R.styleable.ToogleButton_button_color, getResources().getColor(R.color.white));
animationDuration = array.getInt(R.styleable.ToogleButton_animation_duration, 500);
array.recycle();
init();
}
/**
* 初始化一些变量设置
*/
private void init() {
paint = new Paint();
paint.setAntiAlias(true);
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
valueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(animationDuration);
valueAnimator.setRepeatCount(0);
valueAnimator.addUpdateListener(animatorUpdateListener);
valueAnimator.addListener(animatorListener);
}
private ValueAnimator.AnimatorListener animatorListener = new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (checkState == UNCHECKED) {
checkState = CHECKED;
isAnimation = false;
if (null != onCheckListener) {
onCheckListener.onCheck(true);
}
} else if (checkState == CHECKED) {
checkState = UNCHECKED;
isAnimation = false;
if (null != onCheckListener) {
onCheckListener.onCheck(false);
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
};
private ValueAnimator.AnimatorUpdateListener animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float totalOffset = width - 2 * buttonRadius;
if (checkState == UNCHECKED) {//关闭状态时
mButtonX = totalOffset * (Float) animation.getAnimatedValue();
bgColor = (int) argbEvaluator.evaluate(
(Float) animation.getAnimatedValue(),
getResources().getColor(R.color.white),
checkedColor
);
} else if (checkState == CHECKED) {//打开状态时
mButtonX = totalOffset - totalOffset * (Float) animation.getAnimatedValue();
bgColor = (int) argbEvaluator.evaluate(
(Float) animation.getAnimatedValue(),
checkedColor, getResources().getColor(R.color.white)
);
}
postInvalidate();
}
};
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpec = MeasureSpec.getMode(widthMeasureSpec);
int heightSpec = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpec == MeasureSpec.AT_MOST) {
widthMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_TOOGLE_WIDTH, MeasureSpec.EXACTLY);
}
if (heightSpec == MeasureSpec.AT_MOST) {
heightMeasureSpec = MeasureSpec.makeMeasureSpec(DEFAULT_TOOGLE_HEIGHT, MeasureSpec.EXACTLY);
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
height = h - borderWidth - borderWidth;
width = w - borderWidth - borderWidth;
viewRadius = height * 0.5f;
buttonRadius = viewRadius - borderWidth;
//buttonRadius = viewRadius;
left = borderWidth;
top = borderWidth;
right = w - borderWidth;
bottom = h - borderWidth;
centerX = (left + right) * 0.5f;
centerY = (top + bottom) * 0.5f;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
paint.setStrokeWidth(borderWidth);
paint.setColor(getResources().getColor(R.color.white));
paint.setStyle(Paint.Style.FILL);
//绘制白色背景
drawWhitBg(canvas, paint);
//绘制边线
paint.setStyle(Paint.Style.STROKE);
paint.setColor(borderColor);
drawWhitBg(canvas, paint);
//绘制按钮滑动过程中的渐变边框
float des = (mButtonX * viewRadius / (width - 2 * viewRadius)) * 0.5f;//滑动过程中渐变背景框的半径
paint.setColor(bgColor);
paint.setStrokeWidth(des * 2);
paint.setStyle(Paint.Style.STROKE);
if (des == 0) {
canvas.drawRoundRect(new RectF(left + des + borderWidth, top + des + borderWidth, right - des - borderWidth, bottom - des - borderWidth), viewRadius, viewRadius, paint);
}else{
canvas.drawRoundRect(new RectF(left + des, top + des, right - des, bottom - des), viewRadius, viewRadius, paint);
}
//填充按钮左边因为画渐变边框而留下来的白框
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(1);
canvas.drawCircle(left + borderWidth + viewRadius, borderWidth + viewRadius, viewRadius - borderWidth, paint);
canvas.drawRect(
left + viewRadius + borderWidth, top + borderWidth,
mButtonX + viewRadius - borderWidth, top + 2 * viewRadius - borderWidth,
paint);
// //绘制按钮
paint.setColor(buttonColor);
paint.setStyle(Paint.Style.FILL);
paint.setStrokeWidth(borderWidth);
canvas.drawCircle(buttonRadius + mButtonX + borderWidth, centerY, buttonRadius, paint);
paint.setColor(shadowColor);
paint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(buttonRadius + mButtonX+borderWidth, centerY, buttonRadius, paint);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
int eventAsked = event.getActionMasked();
switch (eventAsked) {
case MotionEvent.ACTION_DOWN:
if (isAnimation) {
return false;
}
if (checkState == UNCHECKED) {
toogleOn();
} else if (checkState == CHECKED) {
toogleOff();
}
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
break;
}
return true;
}
/**
* 打开
*/
private void toogleOn() {
isAnimation = true;
valueAnimator.start();
}
/**
* 关闭
*/
private void toogleOff() {
isAnimation = true;
valueAnimator.start();
}
/**
* 绘制背景
*
* @param canvas
* @param paint
*/
private void drawWhitBg(Canvas canvas, Paint paint) {
canvas.drawRoundRect(new RectF(left, top, right, bottom), viewRadius, viewRadius, paint);
}
/**
* 定义一个选中接口回调
*/
OnCheckListener onCheckListener;
public interface OnCheckListener {
void onCheck(boolean isCheck);
}
public void setOnCheckListener(OnCheckListener onCheckListener) {
this.onCheckListener = onCheckListener;
}
}
喜欢就点个赞吧~