仿IOS效果的一个简单的ToogleButton

前言

项目中总会有涉及到控制开关的东西,比如是否设置默认的配置,是否开启夜光模式,是否浏览中加载图片....界面都是需要一个显示的开关图标的,最简单的方法是使用checkButton,用两张图片作为drawable的资源使用,这是很容易做到的。点击的时候就是两张图片的切换,但是这样做有很明显的缺点

  • 动画很突兀,是一瞬间完成的,缺少观赏性
  • 第二就麻烦了,当你找不到你所需要的图片的时候,此不是欢声笑语中打出GG?
GG

所以我们有必要自己写一个view来代替这种东西,苹果手机自带的ToogleButton就是一个很好的选择,他们自带这种的确实比较漂亮。所以今天去撸一个仿ios的ToogleButton。

正文

先不说辣么多,老板,上效果图先



看上去还挺简单的,但里边可不是可不是两张图片换来换去,是自己通过自定义view画上去的,哈哈。里边还有一些动画细节你可能没看到。,我将动画时间设置得长一点你就看得清楚了。来,看下慢动作。



这样一来就看清楚里边的动画了吧,看清楚了就简单了

流程分析

自定义属性

  <declare-styleable name="ToogleButton">
        <attr name="border_width" format="dimension"></attr>
        <attr name="border_color" format="color"></attr>
        <attr name="bg_color" format="color"></attr>
        <attr name="checked_color" format="color"></attr>
        <attr name="shadow_color" format="color"></attr>
        <attr name="button_color" format="color"></attr>
        <attr name="animation_duration" format="integer"></attr>
    </declare-styleable>

用法

<ToogleButton
      android:id="@+id/album_toogle"
       android:layout_width="44dp"
        android:layout_height="22dp"
      />

见名知义,相信大家都看得懂。
接下来来分析一下draw方法的具体步骤:【可以看着动画来分析】

  • 首先画一个一个白色背景,然后画一个边界线
  • 画环形宽度渐变的环形圆角矩形,它是怎么样渐变的呢?是根据属性动画完成的,这里有个很巧妙的方法实现这个,将画笔的style设置成Paint.Style.STROKE,然后将画笔宽度设置成环形宽度,就可以很容易实现这个效果,至于渐变,当从关闭状态到开启状态的时候,它就是从0到
    圆形按钮半径按钮的一半渐变的,反之,当从开启状态到关闭状态的时候,它就是从圆形按钮半径按钮的一半到0渐变的,大家看看动画就知道了。比例值是根据圆形按钮的滑动的完成度计算的。
  • 画圆形按钮 根据属性动画的比例算出mButtonX的值,然后根据这个值确定按钮的位置。下面的红色就是按钮需要滑动的轨迹,你可以根据属性动画的比例值算出当前时间占有这条线的宽度。
image.png

还有一个主意的地方,这个背景色是一个渐变色来的,是从白色到绿色的渐变过程。这个使用了
ArgbEvaluator,这个这个类渐变色专用的,挺方便的。

private final android.animation.ArgbEvaluator argbEvaluator
            = new android.animation.ArgbEvaluator();

bgColor = (int) argbEvaluator.evaluate(
                        (Float) animation.getAnimatedValue(),
                        getResources().getColor(R.color.white),
                        checkedColor
                );
  • 画完按钮会有这个问题,看下面的图
image.png

可以看到圆形按钮左边有个空白的位置,这个不太好看,我们需要用背景色把这段空白遮住。
就是画一个圆和一个矩形把它遮住

  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;
    }
}

喜欢就点个赞吧~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,641评论 6 525
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 96,754评论 3 408
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 173,075评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,369评论 1 303
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,386评论 6 402
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,800评论 1 317
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,122评论 3 431
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,130评论 0 281
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,689评论 1 327
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,693评论 3 348
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,804评论 1 356
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,399评论 5 351
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,128评论 3 341
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,528评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,705评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,409评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,878评论 2 368

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,138评论 4 61
  • <一> 今夜 我将离开 带上一点星光 带走一片灰暗 爱我的人辗转反侧 恨我的人欣然入眠 路途 我看见了 无声滴落的...
    我的笔名是青蒿阅读 452评论 0 4
  • 陽光曾普照大地(組詩) 兔子在野 一隻兔子被關在籠子里出售 安靜如雪 它的死被精心餵養 陽光下閒置的一個鐘頭 不像...
    桑子简书阅读 176评论 0 1
  • 原创:《做你自己》 诗/柳六风 蓝天送我三朵白云, 一朵是心情, 一朵是梦想。 还有一朵呢? 做你自己。 ​​​
    新观点读书阅读 132评论 0 0
  • 没关系,一切都会好起来的。 鉴于我认为它的实用性和功能性真的没多少,所以我不得不好好谈一谈这句话。 生活是这样的,...
    白日l梦长阅读 767评论 3 12