文档章节

[Material Design] 打造简单朴实的CheckBox

Qiujuer
 Qiujuer
发布于 2015/01/05 11:45
字数 2195
阅读 181
收藏 4

========================================================
作者:qiujuer
博客:my.oschina.net/u/1377710
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://my.oschina.net/u/1377710/blog/363790
========================================================

就系统的 CheckBox 而言稍显累赘;原因无他,很多时候我们使用 CheckBox 只是为了能记录是否选中而已,很多时候用不到文字等复杂的布局。今天打造了一款 Material Design 风格的 CheckBox 控件,该控件简单,朴实,效率不错。

结构

在开始前,我们先看看系统的 CheckBox 的结构:

public class CheckBox extends CompoundButton

java.lang.Object
            ↳android.view.View
                ↳android.widget.TextView
                     ↳android.widget.Button
                         ↳android.widget.CompoundButton
                             ↳android.widget.CheckBox

可以看出其继承关系是相当的....

今天打造一款直接继承 View 的 CheckBox ;当然直接继承,则会少去很多中间控件的属性,但是就我使用来看是值得的。

效果

分析

  1. 首先我们点击后需要绘制的地方无非就是两个地方:圆圈、圆弧
  2. 圆圈在动画开始的时候是颜色逐渐进行渐变
  3. 圆弧在动画开始的时候是在原有的圆弧上再绘制一个圆弧,圆弧的长度随着时间变化
  4. 由于是继承View所以enable和checked属性需要自己实现
  5. 同样Checked属性变化回掉依然需要自己实现
  6. 另外需要注意的是未实现Text属性,要的是简单,如需要可以自己绘制

代码

全局变量

private static final Interpolator ANIMATION_INTERPOLATOR = new DecelerateInterpolator();
    private static final ArgbEvaluator ARGB_EVALUATOR = new ArgbEvaluator();
    private static final int THUMB_ANIMATION_DURATION = 250;
    private static final int RING_WIDTH = 5;
    private static final int[] DEFAULT_COLORS = new int[]{
            Color.parseColor("#ffc26165"), Color.parseColor("#ffdb6e77"),
            Color.parseColor("#ffef7e8b"), Color.parseColor("#fff7c2c8"),
            Color.parseColor("#ffc2cbcb"), Color.parseColor("#ffe2e7e7")};
    public static final int AUTO_CIRCLE_RADIUS = -1;

我们定义了动画为逐渐变慢,颜色渐变,动画时间为 250 毫秒,圆弧宽度 5 像素,静态颜色(颜色其是是我的控件的属性,在这里就静态化了),圆心宽度默认值。

动画变量

// Animator
    private AnimatorSet mAnimatorSet;
    private float mSweepAngle;
    private int mCircleColor;

    private int mUnCheckedPaintColor = DEFAULT_COLORS[4];
    private int mCheckedPaintColor = DEFAULT_COLORS[2];

    private RectF mOval;
    private Paint mCirclePaint;
    private Paint mRingPaint;
动画类、圆弧角度,圆心颜色,两个是否选择颜色,用户画圆弧的RectF,两支画笔

动画形状

private float mCenterX, mCenterY;
    private boolean mCustomCircleRadius;
    private int mCircleRadius = AUTO_CIRCLE_RADIUS;
    private int mRingWidth = RING_WIDTH;
所画的中心点XY,是否自定义圆心半径(如果有自定义切合法则使用自定义,否则使用运算后的半径),圆心半径(取决于运算与自定义的结合),圆弧宽度

基础属性

private boolean mChecked;
    private boolean mIsAttachWindow;
    private boolean mBroadcasting;
    private OnCheckedChangeListener mOnCheckedChangeListener;
是否选择,是否AttachWindow用于控制是否开始动画,mBroadcasting用于控制避免重复通知回调,回调类

初始化

public GeniusCheckBox(Context context) {
        super(context);
        init(null, 0);
    }

    public GeniusCheckBox(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(attrs, 0);
    }

    public GeniusCheckBox(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(attrs, defStyle);
    }

    private void init(AttributeSet attrs, int defStyle) {
        // Load attributes
        boolean enable = isEnabled();
        boolean check = isChecked();

        if (attrs != null) {
            // Load attributes
            final TypedArray a = getContext().obtainStyledAttributes(
                    attrs, R.styleable.GeniusCheckBox, defStyle, 0);

            // getting custom attributes
            mRingWidth = a.getDimensionPixelSize(R.styleable.GeniusCheckBox_g_ringWidth, mRingWidth);
            mCircleRadius = a.getDimensionPixelSize(R.styleable.GeniusCheckBox_g_circleRadius, mCircleRadius);
            mCustomCircleRadius = mCircleRadius != AUTO_CIRCLE_RADIUS;

            check = a.getBoolean(R.styleable.GeniusCheckBox_g_checked, false);
            enable = a.getBoolean(R.styleable.GeniusCheckBox_g_enabled, true);

            a.recycle();
        }
        // To check call performClick()
        setOnClickListener(null);

        // Refresh display with current params
        refreshDrawableState();

        // Init
        initPaint();
        initSize();
        initColor();

        // Init
        setEnabled(enable);
        setChecked(check);
    }

    private void initPaint() {
        if (mCirclePaint == null) {
            mCirclePaint = new Paint(ANTI_ALIAS_FLAG);
            mCirclePaint.setStyle(Paint.Style.FILL);
            mCirclePaint.setAntiAlias(true);
            mCirclePaint.setDither(true);
        }

        if (mRingPaint == null) {
            mRingPaint = new Paint();
            mRingPaint.setStrokeWidth(mRingWidth);
            mRingPaint.setStyle(Paint.Style.STROKE);
            mRingPaint.setStrokeJoin(Paint.Join.ROUND);
            mRingPaint.setStrokeCap(Paint.Cap.ROUND);
            mRingPaint.setAntiAlias(true);
            mRingPaint.setDither(true);
        }
    }

    private void initSize() {
        int paddingLeft = getPaddingLeft();
        int paddingTop = getPaddingTop();
        int paddingRight = getPaddingRight();
        int paddingBottom = getPaddingBottom();

        int contentWidth = getWidth() - paddingLeft - paddingRight;
        int contentHeight = getHeight() - paddingTop - paddingBottom;

        if (contentWidth > 0 && contentHeight > 0) {
            int center = Math.min(contentHeight, contentWidth) / 2;
            int areRadius = center - (mRingWidth + 1) / 2;
            mCenterX = center + paddingLeft;
            mCenterY = center + paddingTop;

            if (mOval == null)
                mOval = new RectF(mCenterX - areRadius, mCenterY - areRadius, mCenterX + areRadius, mCenterY + areRadius);
            else {
                mOval.set(mCenterX - areRadius, mCenterY - areRadius, mCenterX + areRadius, mCenterY + areRadius);
            }

            if (!mCustomCircleRadius)
                mCircleRadius = center - mRingWidth * 2;
            else if (mCircleRadius > center)
                mCircleRadius = center;

            // Refresh view
            if (!isInEditMode()) {
                invalidate();
            }
        }
    }

    private void initColor() {
        if (isEnabled()) {
            mUnCheckedPaintColor = DEFAULT_COLORS[4];
            mCheckedPaintColor = DEFAULT_COLORS[2];
        } else {
            mUnCheckedPaintColor = DEFAULT_COLORS[5];
            mCheckedPaintColor = DEFAULT_COLORS[3];
        }
        setCircleColor(isChecked() ? mCheckedPaintColor : mUnCheckedPaintColor);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // Init this Layout size
        initSize();
    }
初始化包括画笔、颜色、大小

另外初始化中除了实例化的时候会触发以外在 onMeasure 方法中有调用,目的是为了适应控件使用中变化时自适应。

在初始化大小中就进行了是否自定义判断,是否使用自定义值还是使用运算后的值,另外运算出 XY 坐标等操作;这些操作之所以不放在 onDraw() 中就是为了让动画尽量的流畅。

OnAttachWindow

@Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        mIsAttachWindow = true;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        mIsAttachWindow = false;
    }
这两个存在的目的就是为了在初始化的时候就开启动画的可能,因为动画是随着选中值变化而变化,所以需要排除未加载显示控件的情况下就开始动画的可能。

自定义设置

public void setRingWidth(int width) {
        if (mRingWidth != width) {
            mRingWidth = width;
            mRingPaint.setStrokeWidth(mRingWidth);
            initSize();
        }
    }

    public void setCircleRadius(int radius) {
        if (mCircleRadius != radius) {
            if (radius < 0)
                mCustomCircleRadius = false;
            else {
                mCustomCircleRadius = true;
                mCircleRadius = radius;
            }
            initSize();
        }
    }
提供两个方法用于变量的设置,另外可以实现颜色的自定义。

回调接口

public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
        mOnCheckedChangeListener = listener;
    }

    /**
     * Interface definition for a callback to be invoked when the checked state
     * of a compound button changed.
     */
    public static interface OnCheckedChangeListener {
        /**
         * Called when the checked state of a compound button has changed.
         *
         * @param checkBox  The compound button view whose state has changed.
         * @param isChecked The new checked state of buttonView.
         */
        void onCheckedChanged(GeniusCheckBox checkBox, boolean isChecked);
    }
这里进行回掉接口的设计以及提供设置回掉的接口。

实现Checkable接口

/**
 * Created by Qiujuer
 * on 2014/12/29.
 */
public class GeniusCheckBox extends View implements Checkable{

    @Override
    public boolean performClick() {
        toggle();
        return super.performClick();
    }

    @Override
    public void setEnabled(boolean enabled) {
        if (enabled != isEnabled()) {
            super.setEnabled(enabled);
            initColor();
        }
    }

    @Override
    public boolean isChecked() {
        return mChecked;
    }

    @Override
    public void toggle() {
        setChecked(!mChecked);
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    @Override
    public void setChecked(boolean checked) {
        if (mChecked != checked) {
            mChecked = checked;
            refreshDrawableState();

            // To Animator
            if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && isAttachedToWindow() && isLaidOut())
                    || (mIsAttachWindow && mOval != null)) {
                animateThumbToCheckedState(checked);
            } else {
                // Immediately move the thumb to the new position.
                cancelPositionAnimator();
                setCircleColor(checked ? mCheckedPaintColor : mUnCheckedPaintColor);
                setSweepAngle(checked ? 360 : 0);
            }

            // Avoid infinite recursions if setChecked() is called from a listener
            if (mBroadcasting) {
                return;
            }
            mBroadcasting = true;
            if (mOnCheckedChangeListener != null) {
                mOnCheckedChangeListener.onCheckedChanged(this, checked);
            }
            mBroadcasting = false;
        }
    }

}
继承Checkable接口并实现它,另外在类中重写performClick()方法用于点击事件调用。

在实现的setChecked 方法中实现开启,取消动画操作。

动画部分

private void setSweepAngle(float value) {
        mSweepAngle = value;
        invalidate();
    }

    private void setCircleColor(int color) {
        mCircleColor = color;
        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (isInEditMode()) {
            initSize();
        }

        mCirclePaint.setColor(mCircleColor);
        canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint);

        if (mOval != null) {
            mRingPaint.setColor(mUnCheckedPaintColor);
            canvas.drawArc(mOval, 225, 360, false, mRingPaint);
            mRingPaint.setColor(mCheckedPaintColor);
            canvas.drawArc(mOval, 225, mSweepAngle, false, mRingPaint);
        }
    }

    /**
     * =============================================================================================
     * The Animate
     * =============================================================================================
     */

    private void animateThumbToCheckedState(boolean newCheckedState) {
        ObjectAnimator sweepAngleAnimator = ObjectAnimator.ofFloat(this, SWEEP_ANGLE, newCheckedState ? 360 : 0);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
            sweepAngleAnimator.setAutoCancel(true);

        ObjectAnimator circleColorAnimator = newCheckedState ? ObjectAnimator.ofObject(this, CIRCLE_COLOR, ARGB_EVALUATOR, mUnCheckedPaintColor, mCheckedPaintColor) :
                ObjectAnimator.ofObject(this, CIRCLE_COLOR, ARGB_EVALUATOR, mCheckedPaintColor, mUnCheckedPaintColor);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2)
            circleColorAnimator.setAutoCancel(true);

        mAnimatorSet = new AnimatorSet();
        mAnimatorSet.playTogether(
                sweepAngleAnimator,
                circleColorAnimator
        );
        // set Time
        mAnimatorSet.setDuration(THUMB_ANIMATION_DURATION);
        mAnimatorSet.setInterpolator(ANIMATION_INTERPOLATOR);
        mAnimatorSet.start();
    }

    private void cancelPositionAnimator() {
        if (mAnimatorSet != null) {
            mAnimatorSet.cancel();
        }
    }

    /**
     * =============================================================================================
     * The custom properties
     * =============================================================================================
     */

    private static final Property<GeniusCheckBox, Float> SWEEP_ANGLE = new Property<GeniusCheckBox, Float>(Float.class, "sweepAngle") {
        @Override
        public Float get(GeniusCheckBox object) {
            return object.mSweepAngle;
        }

        @Override
        public void set(GeniusCheckBox object, Float value) {
            object.setSweepAngle(value);
        }
    };
    private static final Property<GeniusCheckBox, Integer> CIRCLE_COLOR = new Property<GeniusCheckBox, Integer>(Integer.class, "circleColor") {
        @Override
        public Integer get(GeniusCheckBox object) {
            return object.mCircleColor;
        }

        @Override
        public void set(GeniusCheckBox object, Integer value) {
            object.setCircleColor(value);
        }
    };
两个方法分别设置颜色与弧度,当弧度变化时触发 onDraw() 操作。

动画采用属性动画,并把属性动画打包为一个 Set 进行控制,弧度 0~360 之间变化;颜色就是选择与不选择颜色之间的变化。

自定义属性

<!-- GeniusCheckBox -->
    <declare-styleable name="GeniusCheckBox">
        <attr name="g_ringWidth" format="dimension" />
        <attr name="g_circleRadius" format="dimension" />

        <attr name="g_checked" format="boolean" />
        <attr name="g_enabled" format="boolean" />
    </declare-styleable>

成果

代码

xmlns:genius="http://schemas.android.com/apk/res-auto"  
        <!-- CheckBox -->
        <net.qiujuer.genius.widget.GeniusTextView
            android:id="@+id/title_checkbox"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="5dip"
            android:layout_marginTop="10dip"
            android:gravity="center_vertical"
            android:maxLines="1"
            android:text="CheckBox"
            android:textSize="20sp"
            genius:g_textColor="main" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="5dip"
            android:paddingLeft="10dip"
            android:paddingRight="10dip"
            android:weightSum="2">

            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical">

                <net.qiujuer.genius.widget.GeniusTextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="5dip"
                    android:gravity="center_vertical"
                    android:text="Enabled"
                    android:textSize="16dip"
                    genius:g_textColor="main" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="10dip"
                    android:orientation="vertical">

                    <net.qiujuer.genius.widget.GeniusCheckBox
                        android:id="@+id/checkbox_enable_blue"
                        android:layout_width="match_parent"
                        android:layout_height="24dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dip"
                        genius:g_theme="@array/ScubaBlue" />

                    <net.qiujuer.genius.widget.GeniusCheckBox
                        android:id="@+id/checkbox_enable_strawberryIce"
                        android:layout_width="match_parent"
                        android:layout_height="24dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dip"
                        genius:g_checked="true"
                        genius:g_ringWidth="2dp"
                        genius:g_theme="@array/StrawberryIce" />

                </LinearLayout>

            </LinearLayout>

            <LinearLayout
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:orientation="vertical">

                <net.qiujuer.genius.widget.GeniusTextView
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_margin="5dip"
                    android:gravity="center_vertical"
                    android:text="Disabled"
                    android:textSize="16dip"
                    genius:g_textColor="main" />

                <LinearLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:layout_marginLeft="10dip"
                    android:orientation="vertical">

                    <net.qiujuer.genius.widget.GeniusCheckBox
                        android:id="@+id/checkbox_disEnable_blue"
                        android:layout_width="match_parent"
                        android:layout_height="24dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dip"
                        genius:g_enabled="false"
                        genius:g_theme="@array/ScubaBlue" />

                    <net.qiujuer.genius.widget.GeniusCheckBox
                        android:id="@+id/checkbox_disEnable_strawberryIce"
                        android:layout_width="match_parent"
                        android:layout_height="24dp"
                        android:layout_gravity="center"
                        android:layout_margin="5dip"
                        genius:g_checked="true"
                        genius:g_enabled="false"
                        genius:g_ringWidth="2dp"
                        genius:g_theme="@array/StrawberryIce" />

                </LinearLayout>

            </LinearLayout>
        </LinearLayout>

效果




话说,写一篇这个好累的;光是写就花了我3个小时,汗!包括动画图片制作等。

总的源码太长就不贴出来了,上面已经拆分的弄出来了,如果要请点击这里


——学之开源,用于开源;初学者的心态,与君共勉!


========================================================
作者:qiujuer
博客:my.oschina.net/u/1377710
网站:www.qiujuer.net
开源库:Genius-Android
转载请注明出处:http://my.oschina.net/u/1377710/blog/363790
========================================================

© 著作权归作者所有

Qiujuer

Qiujuer

粉丝 143
博文 27
码字总数 65038
作品 2
深圳
程序员
私信 提问
加载中

评论(3)

zhu_ch
zhu_ch
写的不错
Qiujuer
Qiujuer 博主

引用来自“小肥侠”的评论

为毛写这么好,没人赞。。。。
Thanks 因为这个没有推荐 所以基本没有人看见 在 CSDN 也有同样的一篇。
小肥侠
小肥侠
为毛写这么好,没人赞。。。。
material2 7.0.0-beta.2,Angular 的 Material Design 风格框架

Angular 的 Material Design 风格框架 material2 发布了 7.0.0-beta.2 版本,此版本包含许多更改,以使组件更符合 2018 Material Design 的更新。 如果要覆盖默认样式,可能会需要对它们进行...

局长
2018/09/24
582
0
安卓 Material 皮肤--flex Android Material Skins

从Anroid 4.x开始的Android Design到现在的Material Design(原质设计),Android的设计语言再上升新高度。 这个项目提供了flex的Material皮肤,由as3实现,支持css 虽然Apache Flex已提供了...

clschen
2015/03/19
2.6K
0
Flutter 与 Material Design 双剑合璧,助您构建精美应用

作者: Michael Thomsen, Google Dart & Flutter 产品经理 在 Google I/O 2018 开发者大会上,Material 团队宣布对 Material Design 进行了重要更新,其核心目标是通过系统化地打造品牌专属设...

谷歌开发者
01/06
0
0
Material UI 3.0.3 发布,Material Design 开发框架

Material UI 是一组实现 Google Material Design 规范的 react 组件,是一个前端 JS 框架,主要用在 web 领域。 Material UI 3.0.3 已发布,更新内容包括: [typescript] Fix ModalClasses ...

王练
2018/09/09
1K
0
[Angular Material完全攻略] Day 01 - 开始 & 简介

转载 从Angular第2版正式release后,根据全球最大工程师讨论区StackOverflow的统计,从2016开始的Angular讨论度就不断窜升,甚至超越了React,直到了2017年,甚至摆脱了前一代Angularjs的阴影...

readilen
2018/05/21
0
0

没有更多内容

加载失败,请刷新页面

加载更多

Spring Boot 2 实战:使用 Spring Boot Admin 监控你的应用

1. 前言 生产上对 Web 应用 的监控是十分必要的。我们可以近乎实时来对应用的健康、性能等其他指标进行监控来及时应对一些突发情况。避免一些故障的发生。对于 Spring Boot 应用来说我们可以...

码农小胖哥
41分钟前
4
0
ZetCode 教程翻译计划正式启动 | ApacheCN

原文:ZetCode 协议:CC BY-NC-SA 4.0 欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远。 ApacheCN 学习资源 贡献指南 本项目需要校对,欢迎大家提交 Pull Request。 ...

ApacheCN_飞龙
51分钟前
4
0
CSS定位

CSS定位 relative相对定位 absolute绝对定位 fixed和sticky及zIndex relative相对定位 position特性:css position属性用于指定一个元素在文档中的定位方式。top、right、bottom、left属性则...

studywin
今天
6
0
从零基础到拿到网易Java实习offer,我做对了哪些事

作为一个非科班小白,我在读研期间基本是自学Java,从一开始几乎零基础,只有一点点数据结构和Java方面的基础,到最终获得网易游戏的Java实习offer,我大概用了半年左右的时间。本文将会讲到...

Java技术江湖
昨天
5
0
程序性能checklist

程序性能checklist

Moks角木
昨天
7
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部