[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
========================================================

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章