Github地址:TickView,一個精緻的打鉤小動畫 https://github.com/ChengangFeng/TickViewjava
最近在看輕芒雜誌的時候,看到一個動畫很帶感很精緻;git
剛好這段時間也在看【HenCoder】的自定義view教程(裏面寫得很是很是詳細,也有相應的習題等等),因此就趁熱打鐵,熟悉一下學習的知識。github
國際慣例,先上輕芒雜誌標記已讀的動畫canvas
看了後是否是感受很精緻,很帶感?數組
那下面來看一下我本身模仿的效果ide
靜態圖函數
是否是模仿得有幾分類似,哈哈~,下面來看一下我實現的思路吧post
這個動畫實現起來並不複雜,掌握幾個基本的自定義view的方法便可。學習
實現的思路分爲選中狀態
和未選中狀態
動畫
未選中的狀態很簡單,須要繪製的有兩個圖形
繪製選中的動畫稍微複雜一點,主要包括
繪製圓環進度條 這個簡單,直接使用drawArc()
便可實現
繪製向圓心收縮的動畫 這個一開始的時候想用drawArc()
加上設置畫筆的寬度strokeWidth
來實現,不過改變的寬度是往外擴張的,因此這個想法果斷放棄。 以後,個人想法是這樣的,看下圖 我就打算先繪製一個黃色的背景,而後在這個圖層上面繪製一個白色的圓,半徑不斷的縮小,直至爲0,這就反過來獲得了一個向中心收縮的動畫,這能夠叫逆轉思惟吧,最近看的一本書裏面說到有時候反過來思考也許會有不同的效果。
顯示勾出來 關於這個√,我在網上搜了一波,也沒有明確的指明怎麼畫法纔是標準的,因此這裏能夠隨意發揮,本身以爲好看就行。這裏直接可使用drawLine()
能夠一步搞定。
最後是圓環放大再回彈的效果 放大回彈可使用drawArc()
,配合改變畫筆的寬度來實現便可
通過上面分析,不管是選中狀態仍是未選中狀態,進度圓環和鉤的位置是不變的,因此咱們先來肯定圓環的位置和鉤的位置
@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); //設置打鉤的幾個點座標(具體座標點的位置不用怎麼理會,本身定一個就好,沒有統一的標準) //畫一個√,須要肯定3個座標點的位置 //因此這裏我先用一個float數組來記錄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; }
既然分選中狀態和未選中狀態,那個繪製過程當中,就必須判斷當前到底是繪製未選中的呢仍是選中了的呢。
所以在這裏,我定義了一個變量isChecked
//是否被點亮 private boolean isChecked = false; //暴露外部接口,改變繪製狀態 public void setChecked(boolean checked) { if (this.isChecked != checked) { isChecked = checked; reset(); } }
繪製過程當中那些畫筆就不詳細說了,一開始初始化畫筆最後繪製的時候調用便可
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!isChecked) { //繪製圓環,mRectF就是以前肯定的外切矩形 //由於是靜態的,因此設置掃過的角度爲360度 canvas.drawArc(mRectF, 90, 360, false, mPaintRing); //根據以前定好的鉤的座標位置,進行繪製 canvas.drawLines(mPoints, mPaintTick); return; } }
選中狀態是個動畫,所以咱們這裏須要調用postInvalidate()
不斷進行重繪,直到動畫執行完畢;另外,我這裏用計數器的方式來控制繪製的進度。
繪製進度圓環這裏,咱們定義一個計數器ringCounter
,峯值爲360(也就是360度),每執行一次onDraw()
方法,咱們對ringCounter
進行自加,進而模擬進度。
最後記得調用postInvalidate()
進行重繪
//計數器 private int ringCounter = 0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (!isChecked) { ... return; } //畫圓弧進度,每次繪製都自加12個單位,也就是圓弧又掃過了12度 //這裏的12個單位先寫死,後面咱們能夠作一個配置來實現自定義 ringCounter += 12; if (ringCounter >= 360) { ringCounter = 360; } canvas.drawArc(mRectF, 90, ringCounter, false, mPaintRing); ... //強制重繪 postInvalidate(); }
這一步後效果圖以下
圓心收縮的動畫在圓環進度達到100%的時候才進行,同理,也採用計數器circleCounter
的方法來控制繪製的時間和速度
//計數器 private int circleCounter = 0; @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... //在圓環進度達到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(); }
這一步後效果圖以下
當白色的圓半徑收縮到0後,就該繪製打鉤了。
繪製打鉤,這裏問題不大,由於在onMeasure()
中已經將鉤的三個座標點已經計算出來了,直接使用drawLine()
便可畫出來。
@Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); ... canvas.drawCircle(centerX, centerY, radius - circleCounter, mPaintCircle); //當白色的圓半徑收縮到0後, //也就是計數器circleCounter大於背景圓的半徑的時候,就該將鉤√顯示出來了 //這裏加40是爲了加一個延遲時間,不那麼倉促的將鉤顯示出來 if (circleCounter >= radius + 40) { //顯示打鉤(外加一個透明的漸變) alphaCount += 20; if (alphaCount >= 255) alphaCount = 255; mPaintTick.setAlpha(alphaCount); //最後就將以前在onMeasure中計算好的座標傳進去,繪製鉤出來 canvas.drawLines(mPoints, mPaintTick); } postInvalidate(); }
這一步後效果圖以下
放大再回彈的效果,開始的時機應該也是收縮動畫結束後開始,也就是說跟打鉤的動畫同時進行
由於這裏要放大而且回彈,因此這裏的計數器我設置成一個不爲0的數值,先設置成45(隨意,這不是標準),而後沒重繪一次,自減4個單位。
最後畫筆的寬度是關鍵的地方,畫筆的寬度根據scaleCounter
的正負來決定是加仍是減
//計數器 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); } //動畫執行完畢,就補在須要重繪了 if (scaleCounter != -45) { postInvalidate(); } }
完成最後一步的最終效果圖
爲了靈活的能夠控制繪製的狀態,咱們能夠暴露一個接口給外部設置是否選中
/** * 是否選中 */ public void setChecked(boolean checked) { if (this.isChecked != checked) { isChecked = checked; reset(); } } /** * 重置,並重繪 */ private void reset() { //畫筆重置 ... //計數器重置 ringCounter = 0; circleCounter = 0; scaleCounter = 45; alphaCount = 0; ... invalidate(); }
控件到這裏已經基本作好了,但還不是特別的完善。
想一想checkbox
,它不須要暴露外部接口也能經過點擊控件來實現選中仍是取消選中,因此接下來要實現的就是爲控件添加點擊事件
先定義一個接口OnCheckedChangeListener
,實現監聽此控件的監聽事件
private OnCheckedChangeListener mOnCheckedChangeListener; public interface OnCheckedChangeListener { void onCheckedChanged(TickView tickView, boolean isCheck); } public void setOnCheckedChangeListener(OnCheckedChangeListener listener) { this.mOnCheckedChangeListener = listener; }
接下來,初始化控件的點擊事件
/** * 在構造函數中初始化 */ 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); } } }); }
看看效果圖
<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" /> <!--動畫執行的速度--> <attr name="rate"> <enum name="slow" value="0"/> <enum name="normal" value="1"/> <enum name="fast" value="2"/> </attr> </declare-styleable>
這裏簡單說一下動畫執行速度的配置,這裏我設置了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; //圓環進度增長的單位 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的配置,獲取對應的枚舉,從而獲得配好的動畫速度的一些參數
/** * 構造函數 */ 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