========================================================
作者: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 ;當然直接繼承,則會少去很多中間控件的屬性,但是就我使用來看是值得的。
效果
分析
- 首先我們點擊後需要繪製的地方無非就是兩個地方:圓圈、圓弧
- 圓圈在動畫開始的時候是顏色逐漸進行漸變
- 圓弧在動畫開始的時候是在原有的圓弧上再繪製一個圓弧,圓弧的長度隨着時間變化
- 由於是繼承View所以enable和checked屬性需要自己實現
- 同樣Checked屬性變化回掉依然需要自己實現
- 另外需要注意的是未實現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
========================================================