Android實(shí)現(xiàn)炫酷的CheckBox效果
首先貼出實(shí)現(xiàn)的效果圖:

gif的效果可能有點(diǎn)過(guò)快,在真機(jī)上運(yùn)行的效果會(huì)更好一些。我們主要的思路就是利用屬性動(dòng)畫來(lái)動(dòng)態(tài)地畫出選中狀態(tài)以及對(duì)勾的繪制過(guò)程??吹缴厦娴男Ч麍D,相信大家都迫不及待地要躍躍欲試了,那就讓我們開始吧。
自定義View的第一步:自定義屬性。
<?xml version="1.0" encoding="utf-8"?> <resources> <declare-styleable name="SmoothCheckBox"> <!-- 動(dòng)畫持續(xù)時(shí)間 --> <attr name="duration" format="integer"></attr> <!-- 邊框?qū)挾?--> <attr name="strikeWidth" format="dimension|reference"></attr> <!-- 邊框顏色 --> <attr name="borderColor" format="color|reference"></attr> <!-- 選中狀態(tài)的顏色 --> <attr name="trimColor" format="color|reference"></attr> <!-- 對(duì)勾顏色 --> <attr name="tickColor" format="color|reference"></attr> <!-- 對(duì)勾寬度 --> <attr name="tickWidth" format="dimension|reference"></attr> </declare-styleable> </resources>
我們把CheckBox取名為SmoothCheckBox,定義了幾個(gè)等等要用到的屬性。這一步很簡(jiǎn)單,相信大家都熟練了。
接下來(lái)看一看onMeasure(int widthMeasureSpec, int heightMeasureSpec):
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = 40;
}
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = 40;
}
setMeasuredDimension(mWidth, mHeight);
int size = Math.min(mWidth, mHeight);
center = size / 2;
mRadius = (int) ((size - mStrokeWidth) / 2 / 1.2f);
startPoint.set(center * 14 / 30, center * 28 / 30);
breakPoint.set(center * 26 / 30, center * 40 / 30);
endPoint.set(center * 44 / 30, center * 20 / 30);
downLength = (float) Math.sqrt(Math.pow(startPoint.x - breakPoint.x, 2f) + Math.pow(startPoint.y - breakPoint.y, 2f));
upLength = (float) Math.sqrt(Math.pow(endPoint.x - breakPoint.x, 2f) + Math.pow(endPoint.y - breakPoint.y, 2f));
totalLength = downLength + upLength;
}
一開始是測(cè)量了SmoothCheckBox的寬、高度,默認(rèn)的寬高度隨便定義了一個(gè),當(dāng)然你們可以自己去修改和完善它。然后就是設(shè)置半徑之類的,最后的startPoint、breakPoint、endPoint分別對(duì)應(yīng)著選中時(shí)對(duì)勾的三個(gè)點(diǎn)(至于為何是這幾個(gè)數(shù)字,那完全是經(jīng)驗(yàn)值);downLength就是startPoint和breakPoint的距離,而相對(duì)應(yīng)的upLength就是breakPoint和endPoint的距離。即以下圖示:

在看onDraw(Canvas canvas)之前我們先來(lái)看兩組動(dòng)畫,分別是選中狀態(tài)時(shí)的動(dòng)畫以及未選中狀態(tài)的動(dòng)畫:
// 由未選中到選中的動(dòng)畫
private void checkedAnimation() {
animatedValue = 0f;
tickValue = 0f;
// 選中時(shí)底色的動(dòng)畫
mValueAnimator = ValueAnimator.ofFloat(0f, 1.2f, 1f).setDuration(2 * duration / 5);
mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
// 對(duì)勾的動(dòng)畫
mTickValueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(3 * duration / 5);
mTickValueAnimator.setInterpolator(new LinearInterpolator());
mTickValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 得到動(dòng)畫執(zhí)行進(jìn)度
tickValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 得到動(dòng)畫執(zhí)行進(jìn)度
animatedValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//當(dāng)?shù)咨膭?dòng)畫完成后再開始對(duì)勾的動(dòng)畫
mTickValueAnimator.start();
Log.i(TAG," mTickValueAnimator.start();");
}
});
mValueAnimator.start();
}
// 由選中到未選中的動(dòng)畫
private void uncheckedAnimation() {
animatedValue = 0f;
mValueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(2 * duration / 5);
mValueAnimator.setInterpolator(new AccelerateInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
animatedValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.start();
}
這兩組動(dòng)畫在點(diǎn)擊SmoothCheckBox的時(shí)候會(huì)調(diào)用。相似的,都是在動(dòng)畫執(zhí)行中得到動(dòng)畫執(zhí)行的進(jìn)度,再來(lái)調(diào)用postInvalidate();讓SmoothCheckBox重繪??赐赀@個(gè)之后就是終極大招onDraw(Canvas canvas)了:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
drawBorder(canvas);
drawTrim(canvas);
if (isChecked) {
drawTick(canvas);
}
canvas.restore();
}
// 畫對(duì)勾
private void drawTick(Canvas canvas) {
// 得到畫對(duì)勾的進(jìn)度
float temp = tickValue * totalLength;
Log.i(TAG, "temp:" + temp + "downlength :" + downLength);
//判斷是否是剛開始畫對(duì)勾的時(shí)候,即等于startPoint
if (Float.compare(tickValue, 0f) == 0) {
Log.i(TAG, "startPoint : " + startPoint.x + ", " + startPoint.y);
path.reset();
path.moveTo(startPoint.x, startPoint.y);
}
// 如果畫對(duì)勾的進(jìn)度已經(jīng)超過(guò)breakPoint的時(shí)候,即(breakPoint,endPoint]
if (temp > downLength) {
path.moveTo(startPoint.x, startPoint.y);
path.lineTo(breakPoint.x, breakPoint.y);
Log.i(TAG, "endPoint : " + endPoint.x + ", " + endPoint.y);
path.lineTo((endPoint.x - breakPoint.x) * (temp - downLength) / upLength + breakPoint.x, (endPoint.y - breakPoint.y) * (temp - downLength) / upLength + breakPoint.y);
} else {
//畫對(duì)勾的進(jìn)度介于startPoinit和breakPoint之間,即(startPoint,breakPoint]
Log.i(TAG, "down x : " + (breakPoint.x - startPoint.x) * temp / downLength + ",down y: " + (breakPoint.y - startPoint.y) * temp / downLength);
path.lineTo((breakPoint.x - startPoint.x) * temp / downLength + startPoint.x, (breakPoint.y - startPoint.y) * temp / downLength + startPoint.y);
}
canvas.drawPath(path, tickPaint);
}
// 畫邊框
private void drawBorder(Canvas canvas) {
float temp;
// 通過(guò)animatedValue讓邊框產(chǎn)生一個(gè)“OverShooting”的動(dòng)畫
if (animatedValue > 1f) {
temp = animatedValue * mRadius;
} else {
temp = mRadius;
}
canvas.drawCircle(center, center, temp, borderPaint);
}
// 畫checkbox內(nèi)部
private void drawTrim(Canvas canvas) {
canvas.drawCircle(center, center, (mRadius - mStrokeWidth) * animatedValue, trimPaint);
}
onDraw(Canvas canvas)代碼中的邏輯基本都加了注釋,主要就是原理搞懂了就比較簡(jiǎn)單了。在繪制對(duì)勾時(shí)要區(qū)分當(dāng)前處于繪制對(duì)勾的哪種狀態(tài),然后對(duì)應(yīng)做處理畫出線條,剩下的就簡(jiǎn)單了。關(guān)于SmoothCheckBox的講解到這里就差不多了。
下面就貼出SmoothCheckBox的完整代碼:
public class SmoothCheckBox extends View implements View.OnClickListener {
// 動(dòng)畫持續(xù)時(shí)間
private long duration;
// 邊框?qū)挾?
private float mStrokeWidth;
// 對(duì)勾寬度
private float mTickWidth;
// 內(nèi)飾畫筆
private Paint trimPaint;
// 邊框畫筆
private Paint borderPaint;
// 對(duì)勾畫筆
private Paint tickPaint;
// 默認(rèn)邊框?qū)挾?
private float defaultStrikeWidth;
// 默認(rèn)對(duì)勾寬度
private float defaultTickWidth;
// 寬度
private int mWidth;
// 高度
private int mHeight;
// 邊框顏色
private int borderColor;
// 內(nèi)飾顏色
private int trimColor;
// 對(duì)勾顏色
private int tickColor;
// 半徑
private int mRadius;
// 中心點(diǎn)
private int center;
// 是否是選中
private boolean isChecked;
//對(duì)勾向下的長(zhǎng)度
private float downLength;
//對(duì)勾向上的長(zhǎng)度
private float upLength;
// 對(duì)勾的總長(zhǎng)度
private float totalLength;
// 監(jiān)聽器
private OnCheckedChangeListener listener;
private ValueAnimator mValueAnimator;
private ValueAnimator mTickValueAnimator;
private float animatedValue;
private float tickValue;
// 對(duì)勾開始點(diǎn)
private Point startPoint = new Point();
// 對(duì)勾轉(zhuǎn)折點(diǎn)
private Point breakPoint = new Point();
// 對(duì)勾結(jié)束點(diǎn)
private Point endPoint = new Point();
private static final String TAG = "SmoothCheckBox";
private static final String KEY_INSTANCE_STATE = "InstanceState";
private Path path = new Path();
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
this.listener = listener;
}
public SmoothCheckBox(Context context) {
this(context, null);
}
public SmoothCheckBox(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public SmoothCheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox);
duration = a.getInt(R.styleable.SmoothCheckBox_duration, 600);
defaultStrikeWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, getResources().getDisplayMetrics());
mStrokeWidth = a.getDimension(R.styleable.SmoothCheckBox_strikeWidth, defaultStrikeWidth);
defaultTickWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, getResources().getDisplayMetrics());
mTickWidth = a.getDimension(R.styleable.SmoothCheckBox_tickWidth, defaultTickWidth);
borderColor = a.getColor(R.styleable.SmoothCheckBox_borderColor, getResources().getColor(android.R.color.darker_gray));
trimColor = a.getColor(R.styleable.SmoothCheckBox_trimColor, getResources().getColor(android.R.color.holo_green_light));
tickColor = a.getColor(R.styleable.SmoothCheckBox_tickColor, getResources().getColor(android.R.color.white));
a.recycle();
trimPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
trimPaint.setStyle(Paint.Style.FILL);
trimPaint.setColor(trimColor);
borderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
borderPaint.setStrokeWidth(mStrokeWidth);
borderPaint.setColor(borderColor);
borderPaint.setStyle(Paint.Style.STROKE);
tickPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
tickPaint.setColor(tickColor);
tickPaint.setStyle(Paint.Style.STROKE);
tickPaint.setStrokeCap(Paint.Cap.ROUND);
tickPaint.setStrokeWidth(mTickWidth);
setOnClickListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode == MeasureSpec.EXACTLY) {
mWidth = widthSize;
} else {
mWidth = 40;
}
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode == MeasureSpec.EXACTLY) {
mHeight = heightSize;
} else {
mHeight = 40;
}
setMeasuredDimension(mWidth, mHeight);
int size = Math.min(mWidth, mHeight);
center = size / 2;
mRadius = (int) ((size - mStrokeWidth) / 2 / 1.2f);
startPoint.set(center * 14 / 30, center * 28 / 30);
breakPoint.set(center * 26 / 30, center * 40 / 30);
endPoint.set(center * 44 / 30, center * 20 / 30);
downLength = (float) Math.sqrt(Math.pow(startPoint.x - breakPoint.x, 2f) + Math.pow(startPoint.y - breakPoint.y, 2f));
upLength = (float) Math.sqrt(Math.pow(endPoint.x - breakPoint.x, 2f) + Math.pow(endPoint.y - breakPoint.y, 2f));
totalLength = downLength + upLength;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
drawBorder(canvas);
drawTrim(canvas);
if (isChecked) {
drawTick(canvas);
}
canvas.restore();
}
@Override
protected Parcelable onSaveInstanceState() {
Bundle bundle = new Bundle();
bundle.putParcelable(KEY_INSTANCE_STATE, super.onSaveInstanceState());
bundle.putBoolean(KEY_INSTANCE_STATE, isChecked);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
boolean isChecked = bundle.getBoolean(KEY_INSTANCE_STATE);
setChecked(isChecked);
super.onRestoreInstanceState(bundle.getParcelable(KEY_INSTANCE_STATE));
return;
}
super.onRestoreInstanceState(state);
}
// 切換狀態(tài)
private void toggle() {
isChecked = !isChecked;
if (listener != null) {
listener.onCheckedChanged(this, isChecked);
}
if (isChecked) {
checkedAnimation();
} else {
uncheckedAnimation();
}
}
// 由未選中到選中的動(dòng)畫
private void checkedAnimation() {
animatedValue = 0f;
tickValue = 0f;
mValueAnimator = ValueAnimator.ofFloat(0f, 1.2f, 1f).setDuration(2 * duration / 5);
mValueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mTickValueAnimator = ValueAnimator.ofFloat(0f, 1f).setDuration(3 * duration / 5);
mTickValueAnimator.setInterpolator(new LinearInterpolator());
mTickValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
tickValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
animatedValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mTickValueAnimator.start();
Log.i(TAG," mTickValueAnimator.start();");
}
});
mValueAnimator.start();
}
// 由選中到未選中的動(dòng)畫
private void uncheckedAnimation() {
animatedValue = 0f;
mValueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(2 * duration / 5);
mValueAnimator.setInterpolator(new AccelerateInterpolator());
mValueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
animatedValue = (float) valueAnimator.getAnimatedValue();
postInvalidate();
}
});
mValueAnimator.start();
}
// 畫對(duì)勾
private void drawTick(Canvas canvas) {
float temp = tickValue * totalLength;
Log.i(TAG, "temp:" + temp + "downlength :" + downLength);
if (Float.compare(tickValue, 0f) == 0) {
Log.i(TAG, "startPoint : " + startPoint.x + ", " + startPoint.y);
path.reset();
path.moveTo(startPoint.x, startPoint.y);
}
if (temp > downLength) {
path.moveTo(startPoint.x, startPoint.y);
path.lineTo(breakPoint.x, breakPoint.y);
Log.i(TAG, "endPoint : " + endPoint.x + ", " + endPoint.y);
path.lineTo((endPoint.x - breakPoint.x) * (temp - downLength) / upLength + breakPoint.x, (endPoint.y - breakPoint.y) * (temp - downLength) / upLength + breakPoint.y);
} else {
Log.i(TAG, "down x : " + (breakPoint.x - startPoint.x) * temp / downLength + ",down y: " + (breakPoint.y - startPoint.y) * temp / downLength);
path.lineTo((breakPoint.x - startPoint.x) * temp / downLength + startPoint.x, (breakPoint.y - startPoint.y) * temp / downLength + startPoint.y);
}
canvas.drawPath(path, tickPaint);
}
// 畫邊框
private void drawBorder(Canvas canvas) {
float temp;
if (animatedValue > 1f) {
temp = animatedValue * mRadius;
} else {
temp = mRadius;
}
canvas.drawCircle(center, center, temp, borderPaint);
}
// 畫checkbox內(nèi)部
private void drawTrim(Canvas canvas) {
canvas.drawCircle(center, center, (mRadius - mStrokeWidth) * animatedValue, trimPaint);
}
@Override
public void onClick(View view) {
toggle();
}
/**
* 判斷checkbox是否選中狀態(tài)
*
* @return
*/
public boolean isChecked() {
return isChecked;
}
/**
* 設(shè)置checkbox的狀態(tài)
*
* @param isChecked 是否選中
*/
public void setChecked(boolean isChecked) {
this.setChecked(isChecked, false);
}
/**
* 設(shè)置checkbox的狀態(tài)
*
* @param isChecked 是否選中
* @param isAnimation 切換時(shí)是否有動(dòng)畫
*/
public void setChecked(boolean isChecked, boolean isAnimation) {
this.isChecked = isChecked;
if (isAnimation) {
if (isChecked) {
checkedAnimation();
} else {
uncheckedAnimation();
}
} else {
animatedValue = isChecked ? 1f : 0f;
tickValue = 1f;
invalidate();
}
if (listener != null) {
listener.onCheckedChanged(this, isChecked);
}
}
public interface OnCheckedChangeListener {
void onCheckedChanged(SmoothCheckBox smoothCheckBox, boolean isChecked);
}
}
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流。
- Android RecycleView使用(CheckBox全選、反選、單選)
- Android中CheckBox復(fù)選框控件使用方法詳解
- Android開發(fā)之CheckBox的簡(jiǎn)單使用與監(jiān)聽功能示例
- Android 中CheckBox多項(xiàng)選擇當(dāng)前的position信息提交的示例代碼
- Android中ListView + CheckBox實(shí)現(xiàn)單選、多選效果
- Android中ListView綁定CheckBox實(shí)現(xiàn)全選增加和刪除功能(DEMO)
- Android開發(fā)中CheckBox的簡(jiǎn)單用法示例
- 詳解Android Checkbox的使用方法
- Android中自定義Checkbox組件實(shí)例
- Android CheckBox中設(shè)置padding無(wú)效解決辦法
相關(guān)文章
Android使用ImageView實(shí)現(xiàn)支持手勢(shì)縮放效果
這篇文章主要介紹了Android使用ImageView實(shí)現(xiàn)支持手勢(shì)縮放效果,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2016-09-09
Android仿英語(yǔ)流利說(shuō)取詞放大控件的實(shí)現(xiàn)方法(附demo源碼下載)
這篇文章主要介紹了Android仿英語(yǔ)流利說(shuō)取詞放大控件的實(shí)現(xiàn)方法,較為詳細(xì)的分析了取詞放大控件的實(shí)現(xiàn)步驟與相關(guān)技巧,需要的朋友可以參考下2016-02-02
Android源碼學(xué)習(xí)之觀察者模式應(yīng)用及優(yōu)點(diǎn)介紹
定義對(duì)象間一種一對(duì)多的依賴關(guān)系,使得當(dāng)一個(gè)對(duì)象改變狀態(tài),則所有依賴于它的對(duì)象都會(huì)得到通知并被自動(dòng)更新等等,需要了解的朋友可以參考下2013-01-01
詳解Android提交數(shù)據(jù)到服務(wù)器的兩種方式四種方法
本篇文章主要介紹了Android提交數(shù)據(jù)到服務(wù)器的兩種方式四種方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2016-11-11
Android使用 PopupWindow 實(shí)現(xiàn)底部彈窗功能
這篇文章主要介紹了Android使用 PopupWindow 實(shí)現(xiàn)底部彈窗功能,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-12-12
Android使用GRPC進(jìn)行通信過(guò)程解析
這篇文章主要給大家介紹了在Android上使用grpc的方法教程,文中通過(guò)示例代碼給大家詳細(xì)介紹了在android上使用grpc的方法以及可能遇到的種種問(wèn)題的解決方法,對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧2023-02-02

