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

========================================================

作者:qiujuer

博客:blog.csdn.net/qiujuer

网站:www.qiujuer.net

开源库:Genius-Android

转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42399129

========================================================

就系统的 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

博客:blog.csdn.net/qiujuer

网站:www.qiujuer.net

开源库:Genius-Android

转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42399129

========================================================

时间: 2024-08-08 09:37:18

[Material Design] 打造简单朴实的CheckBox的相关文章

Android(Lollipop/5.0) Material Design(四) 创建列表和卡片

Material Design系列 Android(Lollipop/5.0)Material Design(一) 简单介绍 Android(Lollipop/5.0)Material Design(二) 入门指南 Android(Lollipop/5.0)Material Design(三) 使用Material主题 Android(Lollipop/5.0)Material Design(四) 创建列表和卡片 Android(Lollipop/5.0)Material Design(五) 定

打造极致Material Design动画风格Button

======================================================== 作者:qiujuer 博客:blog.csdn.net/qiujuer 网站:www.qiujuer.net 开源库:Genius-Android 转载请注明出处:http://blog.csdn.net/qiujuer/article/details/42471119 --学之开源,用于开源:初学者的心态,与君共勉! ================================

手把手教你打造一个Material Design风格的App(二)

--接上文. 3.1添加ToolBar(ActionBar) 添加ToolBar非常简单,你需要做的仅仅是为toolbar创建一个单独的layout布局,如果你想在哪里展示toolbar,只要在对应布局里将toolbar的布局文件include进来即可. (8)在res-->layout文件夹下创建一个名为toolbar.xml的文件,然后在里面添加一个android.support.v7.widget.Toolbar元素,这样就创建了一个具有特定高度和主题的toolbar. toolbar.x

手把手教你打造一个Material Design风格的App(三)

--接上文. 3.2添加抽屉导航 添加导航抽屉跟Android 5.0之前是一样的,只是以前我们使用ListView来作为菜单容器,现在我们则使用Material Design风格的RecyclerView. (14)在你工程的java文件夹中,创建3个名为activity.adapter.model的包,将MainActivity.java移到activtiy包中,这样做使得你的代码可以很好地组织和管理. (15)打开位于app模块下的build.gradle文件并添加如下依赖.添加完依赖之后

打造Material Design风格的TabBar

自从Material Design问世以来, 各种Material Design风格的控件层出不穷, 尤其是google家的几个APP更是将Material Design应用到了极致. 最近在使用google photos的时候发现这款软件的Tabbar做的非常不错, 内容突出, Material Design风味很浓, 再者, 我还没有做过一个Material Design风格的Tabbar, 所以萌生了仿照一个google photos这种tabbar的念想, 今天我们就来一步步的去实现一下

手把手教你打造一个Material Design风格的App(一)

你应该听说过Android的Material Design,它是在Android 5.0(Lollipop)版本引入的.在Material Design中还引入了很多新东西,比如Material Theme,新的小部件,自定义的阴影,矢量图片及自定义动画等.如果你之前没有用过Material Design,那么本文将是一个很好的入门教程. 在这篇教程中,我们将会学习Material Design开发的基本步骤,即编写自定义的主题以及使用RecyclerView来实现抽屉导航. 通过下面的两个链接

Material Design

资源 原文 http://www.uisdc.com/comprehensive-material-design-note 新鲜热辣!一组实用的 (http://www.uisdc.com/material-design-freebie)[Material Design] 风格素材 ttp://www.uisdc.com/tag/material-design "查看 Material Design 中的全部文章")> 该跟上潮流了!一大波优质的MATERIAL DESIGN资源

【转】MATERIAL DESIGN设计规范学习心得

编者按:自学笔记就该这么做!今天分享@東門王三 同学关于Material Design的自学成果,他的学习笔记严谨有序,触类旁通,从Material Design到其他系统的设计规范都有所研究,还认真地做了思维导图,同学们可以边学习边借鉴他的自学方法,一举两得呦. 自学的一大重点就是读书,推荐同学们看一下华为设计总监的经验:<华为设计总监尤原庆:怎样读设计书> 想读好书的同学,可直接到:设计师图书导航 挑选. @東門王三 :随着Android系统从Android 4.4逐步升级到Android

Material Design 开发利器:Android Design Support Library 介绍

转自:https://blog.leancloud.cn/3306/ Android 5.0 Lollipop 是迄今为止最重大的一次发布,很大程度上是因为 material design —— 这是一门新的设计语言,它刷新了整个 Android 的用户体验.但是对于开发者来说,要设计出完全符合 material design 哲学的应用,是一个很大的挑战.Android Design Support Library 对此提供了很好的支持,里面汇集了很多重要的 material design 控