Github地址:TickView,一個精致的打鉤小動畫
https://github.com/ChengangFeng/TickView
1. 前言
最近在看輕芒雜志的時候,看到一個動畫很帶感很精致范抓;
恰好這段時間也在看【HenCoder】的自定義view教程(里面寫得非常非常詳細,也有相應的習題等等),所以就趁熱打鐵概龄,熟悉一下學習的知識丐谋。
國際慣例芍碧,先上輕芒雜志標記已讀的動畫
看了后是不是感覺很精致,很帶感号俐?
那下面來看一下我自己模仿的效果
靜態(tài)圖
是不是模仿得有幾分相似泌豆,哈哈~,下面來看一下我實現(xiàn)的思路吧
2. 分析
這個動畫實現(xiàn)起來并不復雜吏饿,掌握幾個基本的自定義view的方法即可踪危。
實現(xiàn)的思路分為選中狀態(tài)
和未選中狀態(tài)
2.1 未選中的狀態(tài)
未選中的狀態(tài)很簡單,需要繪制的有兩個圖形
- 圓環(huán)
- 勾
2.2 選中的狀態(tài)
繪制選中的動畫稍微復雜一點找岖,主要包括
繪制圓環(huán)進度條
這個簡單陨倡,直接使用drawArc()
即可實現(xiàn)-
繪制向圓心收縮的動畫
這個一開始的時候想用drawArc()
加上設置畫筆的寬度strokeWidth
來實現(xiàn)敛滋,不過改變的寬度是往外擴張的许布,所以這個想法果斷放棄。
之后绎晃,我的想法是這樣的蜜唾,看下圖
向圓心收縮的動畫分析
我就打算先繪制一個黃色的背景,然后在這個圖層上面繪制一個白色的圓庶艾,半徑不斷的縮小袁余,直至為0,這就反過來得到了一個向中心收縮的動畫咱揍,這可以叫逆轉思維吧颖榜,最近看的一本書里面說到有時候反過來思考也許會有不一樣的效果。
-
顯示勾出來
關于這個√煤裙,我在網(wǎng)上搜了一波掩完,也沒有明確的指明怎么畫法才是標準的,所以這里可以隨意發(fā)揮硼砰,自己覺得好看就行且蓬。這里直接可以使用drawLine()
可以一步搞定。 -
最后是圓環(huán)放大再回彈的效果
放大回彈可以使用drawArc()
题翰,配合改變畫筆的寬度來實現(xiàn)即可
3.具體實現(xiàn)
3.1 確定進度圓環(huán)和鉤的位置
經(jīng)過上面分析恶阴,無論是選中狀態(tài)還是未選中狀態(tài),進度圓環(huán)和鉤的位置是不變的豹障,所以我們先來確定圓環(huán)的位置和鉤的位置
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
...
//設置圓圈的外切矩形,radius是圓的半徑冯事,centerX,centerY是控件中心的坐標
mRectF.set(centerX - radius, centerY - radius, centerX + radius, centerY + radius);
//設置打鉤的幾個點坐標(具體坐標點的位置不用怎么理會血公,自己定一個就好昵仅,沒有統(tǒng)一的標準)
//畫一個√,需要確定3個坐標點的位置
//所以這里我先用一個float數(shù)組來記錄3個坐標點的位置坞笙,
//最后在onDraw()的時候使用canvas.drawLines(mPoints, mPaintTick)來畫出來
//其中這里mPoint[0]~mPoint[3]是確定第一條線"\"的兩個坐標點位置
//mPoint[4]~mPoint[7]是確定第二條線"/"的兩個坐標點位置
mPoints[0] = centerX - tickRadius + tickRadiusOffset;
mPoints[1] = (float) centerY;
mPoints[2] = centerX - tickRadius / 2 + tickRadiusOffset;
mPoints[3] = centerY + tickRadius / 2;
mPoints[4] = centerX - tickRadius / 2 + tickRadiusOffset;
mPoints[5] = centerY + tickRadius / 2;
mPoints[6] = centerX + tickRadius * 2 / 4 + tickRadiusOffset;
mPoints[7] = centerY - tickRadius * 2 / 4;
}
3.2 定義變量岩饼,標記狀態(tài)
既然分選中狀態(tài)和未選中狀態(tài)荚虚,那個繪制過程中,就必須判斷當前究竟是繪制未選中的呢還是選中了的呢籍茧。
因此在這里版述,我定義了一個變量isChecked
//是否被點亮
private boolean isChecked = false;
//暴露外部接口,改變繪制狀態(tài)
public void setChecked(boolean checked) {
if (this.isChecked != checked) {
isChecked = checked;
reset();
}
}
3.3 繪制未選中狀態(tài)
繪制過程中那些畫筆就不詳細說了寞冯,一開始初始化畫筆最后繪制的時候調用即可
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isChecked) {
//繪制圓環(huán)渴析,mRectF就是之前確定的外切矩形
//因為是靜態(tài)的,所以設置掃過的角度為360度
canvas.drawArc(mRectF, 90, 360, false, mPaintRing);
//根據(jù)之前定好的鉤的坐標位置吮龄,進行繪制
canvas.drawLines(mPoints, mPaintTick);
return;
}
}
3.4 繪制選中狀態(tài)
選中狀態(tài)是個動畫俭茧,因此我們這里需要調用postInvalidate()
不斷進行重繪,直到動畫執(zhí)行完畢漓帚;另外母债,我這里用計數(shù)器的方式來控制繪制的進度。
3.4.1 繪制圓環(huán)進度條
繪制進度圓環(huán)這里尝抖,我們定義一個計數(shù)器ringCounter
,峰值為360(也就是360度)毡们,每執(zhí)行一次onDraw()
方法,我們對ringCounter
進行自加昧辽,進而模擬進度衙熔。
最后記得調用postInvalidate()
進行重繪
//計數(shù)器
private int ringCounter = 0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!isChecked) {
...
return;
}
//畫圓弧進度,每次繪制都自加12個單位搅荞,也就是圓弧又掃過了12度
//這里的12個單位先寫死红氯,后面我們可以做一個配置來實現(xiàn)自定義
ringCounter += 12;
if (ringCounter >= 360) {
ringCounter = 360;
}
canvas.drawArc(mRectF, 90, ringCounter, false, mPaintRing);
...
//強制重繪
postInvalidate();
}
這一步后效果圖如下
3.4.2 繪制向圓心收縮的動畫
圓心收縮的動畫在圓環(huán)進度達到100%的時候才進行,同理咕痛,也采用計數(shù)器circleCounter
的方法來控制繪制的時間和速度
//計數(shù)器
private int circleCounter = 0;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
//在圓環(huán)進度達到100%的時候才開始繪制
if (ringCounter == 360) {
//先繪制背景的圓
mPaintCircle.setColor(checkBaseColor);
canvas.drawCircle(centerX, centerY, radius, mPaintCircle);
//然后在背景圓的圖層上痢甘,再繪制白色的圓(半徑不斷縮小)
//半徑不斷縮小,背景就不斷露出來暇检,達到向中心收縮的效果
mPaintCircle.setColor(checkTickColor);
//收縮的單位先試著設置為6产阱,后面可以進行自己自定義
circleCounter += 6;
canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle);
}
//必須重繪
postInvalidate();
}
這一步后效果圖如下
3.4.3 繪制鉤
當白色的圓半徑收縮到0后,就該繪制打鉤了。
繪制打鉤块仆,這里問題不大构蹬,因為在onMeasure()
中已經(jīng)將鉤的三個坐標點已經(jīng)計算出來了,直接使用drawLine()
即可畫出來悔据。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle);
//當白色的圓半徑收縮到0后庄敛,
//也就是計數(shù)器circleCounter大于背景圓的半徑的時候,就該將鉤√顯示出來了
//這里加40是為了加一個延遲時間科汗,不那么倉促的將鉤顯示出來
if (circleCounter >= radius + 40) {
//顯示打鉤(外加一個透明的漸變)
alphaCount += 20;
if (alphaCount >= 255) alphaCount = 255;
mPaintTick.setAlpha(alphaCount);
//最后就將之前在onMeasure中計算好的坐標傳進去藻烤,繪制鉤出來
canvas.drawLines(mPoints, mPaintTick);
}
postInvalidate();
}
這一步后效果圖如下
3.4.4 繪制放大再回彈的效果
放大再回彈的效果,開始的時機應該也是收縮動畫結束后開始,也就是說跟打鉤的動畫同時進行
因為這里要放大并且回彈怖亭,所以這里的計數(shù)器我設置成一個不為0的數(shù)值涎显,先設置成45(隨意,這不是標準)兴猩,然后沒重繪一次期吓,自減4個單位。
最后畫筆的寬度是關鍵的地方倾芝,畫筆的寬度根據(jù)scaleCounter
的正負來決定是加還是減
//計數(shù)器
private int scaleCounter = 45;
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
...
if (circleCounter >= radius + 40) {
//顯示打鉤
...
//顯示放大并回彈的效果
scaleCounter -= 4;
if (scaleCounter <= -45) {
scaleCounter = -45;
}
//放大回彈讨勤,主要看畫筆的寬度
float strokeWith = mPaintRing.getStrokeWidth() +
(scaleCounter > 0 ? dp2px(mContext, 1) : -dp2px(mContext, 1));
mPaintRing.setStrokeWidth(strokeWith);
canvas.drawArc(mRectF, 90, 360, false, mPaintRing);
}
//動畫執(zhí)行完畢,就補在需要重繪了
if (scaleCounter != -45) {
postInvalidate();
}
}
完成最后一步的最終效果圖
3.5 暴露外部接口
為了靈活的可以控制繪制的狀態(tài)晨另,我們可以暴露一個接口給外部設置是否選中
/**
* 是否選中
*/
public void setChecked(boolean checked) {
if (this.isChecked != checked) {
isChecked = checked;
reset();
}
}
/**
* 重置潭千,并重繪
*/
private void reset() {
//畫筆重置
...
//計數(shù)器重置
ringCounter = 0;
circleCounter = 0;
scaleCounter = 45;
alphaCount = 0;
...
invalidate();
}
3.6 添加點擊事件
控件到這里已經(jīng)基本做好了,但還不是特別的完善借尿。
想想checkbox
刨晴,它不需要暴露外部接口也能通過點擊控件來實現(xiàn)選中還是取消選中,所以接下來要實現(xiàn)的就是為控件添加點擊事件
先定義一個接口OnCheckedChangeListener
,實現(xiàn)監(jiān)聽此控件的監(jiān)聽事件
private OnCheckedChangeListener mOnCheckedChangeListener;
public interface OnCheckedChangeListener {
void onCheckedChanged(TickView tickView, boolean isCheck);
}
public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
this.mOnCheckedChangeListener = listener;
}
接下來垛玻,初始化控件的點擊事件
/**
* 在構造函數(shù)中初始化
*/
public TickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
...
setUpEvent();
}
/**
* 初始化點擊事件
*/
private void setUpEvent() {
this.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
isChecked = !isChecked;
reset();
if (mOnCheckedChangeListener != null) {
//此處回調
mOnCheckedChangeListener.onCheckedChanged((TickView) view, isChecked);
}
}
});
}
看看效果圖
3.7 自定義配置項
<declare-styleable name="TickView">
<!--沒有選中的基調顏色-->
<attr name="uncheck_base_color" format="color" />
<!--選中后的基調顏色-->
<attr name="check_base_color" format="color" />
<!--選中后鉤的顏色-->
<attr name="check_tick_color" format="color" />
<!--圓的半徑-->
<attr name="radius" format="dimension" />
<!--動畫執(zhí)行的速度-->
<attr name="rate">
<enum name="slow" value="0"/>
<enum name="normal" value="1"/>
<enum name="fast" value="2"/>
</attr>
</declare-styleable>
這里簡單說一下動畫執(zhí)行速度的配置割捅,這里我設置了3檔速度,我用枚舉定義了三個速度的配置項
enum TickRateEnum {
//低速
SLOW(6, 4, 2),
//正常速度
NORMAL(12, 6, 4),
//高速
FAST(20, 14, 8);
public static final int RATE_MODE_SLOW = 0;
public static final int RATE_MODE_NORMAL = 1;
public static final int RATE_MODE_FAST = 2;
//圓環(huán)進度增加的單位
private int ringCounterUnit;
//圓圈收縮的單位
private int circleCounterUnit;
//圓圈最后放大收縮的單位
private int scaleCounterUnit;
public static TickRateEnum getRateEnum(int rateMode) {
TickRateEnum tickRateEnum;
switch (rateMode) {
case RATE_MODE_SLOW:
tickRateEnum = TickRateEnum.SLOW;
break;
case RATE_MODE_NORMAL:
tickRateEnum = TickRateEnum.NORMAL;
break;
case RATE_MODE_FAST:
tickRateEnum = TickRateEnum.FAST;
break;
default:
tickRateEnum = TickRateEnum.NORMAL;
break;
}
return tickRateEnum;
}
...
}
獲取xml的配置帚桩,獲取對應的枚舉,從而得到配好的動畫速度的一些參數(shù)
/**
* 構造函數(shù)
*/
public TickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
...
initAttrs(attrs);
}
/**
* 獲取自定義配置
*/
private void initAttrs(AttributeSet attrs) {
TypedArray typedArray = mContext.obtainStyledAttributes(attrs, R.styleable.TickView);
...
//獲取配置的動畫速度
int rateMode = typedArray.getInt(R.styleable.TickView_rate, TickRateEnum.RATE_MODE_NORMAL);
mTickRateEnum = TickRateEnum.getRateEnum(rateMode);
typedArray.recycle();
}
最終成果圖
最終成果圖
That ' s all~
感謝大家閱讀嘹黔,最后再放一下項目的github地址
Github地址:TickView账嚎,一個精致的打鉤小動畫
https://github.com/ChengangFeng/TickView