打造極致Material Design動畫風格Button,materialdesign
========================================================
作者:qiujuer
部落格:blog.csdn.net/qiujuer
網站:www.qiujuer.net
開源庫:Genius-Android
轉載請註明出處:http://blog.csdn.net/qiujuer/article/details/42471119
——學之開源,用於開源;初學者的心態,與君共勉!
========================================================
序
在我的文章中曾經有兩篇關於Material Design風格的按鈕實現。在第一章中只是簡單的實現了動畫的波紋效果,而在第二篇中對此進行了一定的擴充與最佳化,最後實現可以自動移動到中心位置的動畫;雖然兩者都可用,但是在我的使用中卻發現了一定的問題,如有些位置點擊會出現波紋速度的運算上的問題。
在這一章中將帶你打造一個極致的Material Design動畫風格Button;至少在我看來與官方的相當接近了。
效果個人
官方
可以看出其基本上差不多了。
分析
首先我們來解析一下官方的:
在這裡我截取了最後一個按鈕相應的連續幾張圖片的情況,從圖片我們可以看出以下情況:
- 官方也是採用圓形水波,非圓角矩形水波(這個與我最開始所想不太一樣)
- 其擴散速度逐漸遞減,圓心的時候基本一閃就過
- 圓形波紋顏色一直沒有變化
- 控制項按鈕整體背景色逐漸加深
- 點擊位置在右下角,但是從擴散情況來看其水波圓心逐漸向按鈕控制項中心靠攏
- 這些也就是我們需要實現的部分。
實現原理
- 圓形擴散,第一章中有講述 [Material Design] 教你做一個Material風格、動畫的按鈕(MaterialButton)
- 中心靠攏,第二章中有講述 [Material Design] MaterialButton 效果進階 動畫自動移動進行對齊效果,不過有一定的偏差,不過其偏差在於時間的控制,並不影響大局
- 擴散速度遞減,這個可以通過設定動畫的 Interpolator 來解決,這個在 滲透理解Animation時間插值Interpolator類 中有詳細的介紹
- 背景色加深,這個無非就是在圓形波紋下面再繪製一層,該層的顏色逐漸加深就OK;當然還需要注意的是其圓角情況。
我們第二張中的按鈕之所以有很大的差距我總結出以下幾點:
代碼
不知道你們在做的過程中是否想過,我們的動畫是在使用者點擊 onTouch() 的基礎上不斷的重新整理觸發 onDraw() 然後繪製來的,與一個按鈕的結合點也就是這麼兩個地方,最多為了方便我們結合的地方還有一個 onMeasure() .所以我們能得出這樣一個類:
Class
public class TouchEffectAnimator { public TouchEffectAnimator(View mView) { } public void onMeasure() { } public void onTouchEvent(final MotionEvent event) { } public void onDraw(final Canvas canvas) { } private void startAnimation() { } private void cancelAnimation() { } private void fadeOutEffect() { }}一個類,這個類作用於一個控制項,所以我們需要傳入一個 View.
然後我們提供一個 onMeasure() 方法用於初始化高度寬度等資料;onTouchEvent() 當然是用來在控制項中觸發點擊事件所用的;onDraw() 這個無需說也是控制項中調用,用來繪製所用;一個動畫當然需要啟動方法和取消方法,當然在波紋動畫後我們還需要的是 "淡出" 的動畫。
而後我們想想,其是我們需要的動畫類型無非就是那麼幾種,我們何不合在一起呢?
枚舉
public enum TouchEffect { Move, Ease, Ripple, None}在這個枚舉中分別代表:一邊擴散一邊移動到中心,無波紋只有淡入淡出,純擴散不移動的類型,沒有動畫的類型。
下面我們來看看主類中的變數情況。
靜態變數
private static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator(2.8f); private static final Interpolator ACCELERATE_INTERPOLATOR = new AccelerateInterpolator(); private static final int EASE_ANIM_DURATION = 200; private static final int RIPPLE_ANIM_DURATION = 300; private static final int MAX_RIPPLE_ALPHA = (int) (255 * 0.8);
分別是:動畫減速、加速效果;淡入淡出預設時間200毫秒,擴散時間預設300毫秒,最大的透明度為255的80%用於淡入淡出。主色為255 100%。
在這裡,減速效果中之所有一個2.8,其主要作用是使擴散效果在初期盡量的快 (起到隱藏小圓圈),而後期盡量的慢(增強觸摸感覺)
必須變數
private View mView; private int mClipRadius; private int mAnimDuration = RIPPLE_ANIM_DURATION; private TouchEffect mTouchEffect = TouchEffect.Move; private Animation mAnimation = null;
一個View,一個圓角弧度,一個動畫時間,一個動畫類型,最後一個動畫類(在這裡沒有使用屬性動畫,而是準備採用最基本的動畫,採用回調來直接設定參數)
圓形半徑變數
private float mMaxRadius; private float mRadius;
一個最大半徑,一個當前半徑;之所以有最大半徑,在我看來有多種情況:如果是移動模式那麼其最大半徑掃過地地區能達到最長邊的75%就行了;如果是純擴散,如果使用者點擊的是最右下角,那麼其掃過地區最好能達到其對角的長度;更具勾股定理可以得出其為最長邊的1.25倍。
座標變數
private float mDownX, mDownY; private float mCenterX, mCenterY; private float mPaintX, mPaintY;
點擊座標,中心座標,當前圓心座標
畫筆變數
private Paint mPaint = new Paint(); private RectF mRectRectR = new RectF(); private Path mRectPath = new Path(); private int mRectAlpha = 0;
一隻畫筆,一個地區,一個地區所產生的Path路徑,一個地區透明度
淡出控制變數
private boolean isTouchReleased = false; private boolean isAnimatingFadeIn = false;
這兩個變數主要用於控制淡齣動畫觸發的時機,我們可以這麼想:
在使用者一直按著控制項的時候就算擴散動畫完成了也不進行淡齣動畫,該動畫在使用者釋放時觸發;如果使用者點擊後立刻抬起那麼在抬起時肯定不能觸發淡齣動畫,要等到擴散動畫完成後才觸發;所以一個變數是是否釋放按鈕,另外一個是是否動畫結束。
動畫監聽
private Animation.AnimationListener mAnimationListener = new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { isAnimatingFadeIn = true; } @Override public void onAnimationEnd(Animation animation) { isAnimatingFadeIn = false; // Is un touch auto fadeOutEffect() if (isTouchReleased) fadeOutEffect(); } @Override public void onAnimationRepeat(Animation animation) { } };上面剛剛說了,控制其釋放觸發淡齣動畫,那麼這裡這個監聽器就是用來監聽其開始動畫狀態的,結束後調整值,如果此時使用者釋放了按鈕則觸發淡出效果。OK,繼續!
初始化
public TouchEffectAnimator(View mView) { this.mView = mView; onMeasure(); } public void onMeasure() { mCenterX = mView.getWidth() / 2; mCenterY = mView.getHeight() / 2; mRectRectR.set(0, 0, mView.getWidth(), mView.getHeight()); mRectPath.reset(); mRectPath.addRoundRect(mRectRectR, mClipRadius, mClipRadius, Path.Direction.CW); }在控制項觸發 onMeasure() 方法的時候回調該類的 onMeasure() 方法,在該方法中我們得出其中心座標,初始化一個長方形地區,然後根據地區與圓角半徑初始化一個Path路徑。
參數設定
public void setAnimDuration(int animDuration) { this.mAnimDuration = animDuration; } public TouchEffect getTouchEffect() { return mTouchEffect; } public void setTouchEffect(TouchEffect touchEffect) { mTouchEffect = touchEffect; if (mTouchEffect == TouchEffect.Ease) mAnimDuration = EASE_ANIM_DURATION; } public void setEffectColor(int effectColor) { mPaint.setColor(effectColor); } public void setClipRadius(int mClipRadius) { this.mClipRadius = mClipRadius; }既然上面有那麼多的變數,那麼這裡提供了一些方法用於初始化使用,分別是:
動畫時間,擷取動畫類型,設定動畫類型,設定顏色,設定控制項的圓角弧度。
動畫部分
private void startAnimation() { Animation animation = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { if (mTouchEffect == TouchEffect.Move) { mRadius = mMaxRadius * interpolatedTime; mPaintX = mDownX + (mCenterX - mDownX) * interpolatedTime; mPaintY = mDownY + (mCenterY - mDownY) * interpolatedTime; } else if (mTouchEffect == TouchEffect.Ripple) { mRadius = mMaxRadius * interpolatedTime; } mRectAlpha = (int) (interpolatedTime * MAX_RIPPLE_ALPHA); mView.invalidate(); } }; animation.setInterpolator(DECELERATE_INTERPOLATOR); animation.setDuration(mAnimDuration); animation.setAnimationListener(mAnimationListener); mView.startAnimation(animation); } private void cancelAnimation() { if (mAnimation != null) { mAnimation.cancel(); mAnimation.setAnimationListener(null); } } private void fadeOutEffect() { Animation animation = new Animation() { @Override protected void applyTransformation(float interpolatedTime, Transformation t) { mRectAlpha = (int) (MAX_RIPPLE_ALPHA - (MAX_RIPPLE_ALPHA * interpolatedTime)); mView.invalidate(); } }; animation.setInterpolator(ACCELERATE_INTERPOLATOR); animation.setDuration(EASE_ANIM_DURATION); mView.startAnimation(animation); }
- 三個方法中,取消最簡單了,調用時判斷,然後取消,並把監聽器設定為 null.
- 淡齣動畫中:我們在其方法回調中設定我們的透明度為遞減的形式,從最大遞減到最小;每次都重新整理一次介面;後面是設定其時間,動畫為先慢然後一下變快消失掉,然後啟動動畫。
- 在開始動畫方法中:我們同樣在回調中除了我們的變數資料;在這裡我們需要判斷,如果是普通擴散,那麼我們就擴散到對應的半徑就OK,如果是Move 類型我們則需要變化其座標。其公式為 C = A+(B-A)*T;而後設定透明度逐漸增加到最大,該透明度是用於全部地區非圓形地區。
觸發方法
public void onTouchEvent(final MotionEvent event) { if (event.getActionMasked() == MotionEvent.ACTION_CANCEL) { isTouchReleased = true; if (!isAnimatingFadeIn) { fadeOutEffect(); } } if (event.getActionMasked() == MotionEvent.ACTION_UP) { isTouchReleased = true; if (!isAnimatingFadeIn) { fadeOutEffect(); } } else if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { // Gets the bigger value (width or height) to fit the circle mMaxRadius = mCenterX > mCenterY ? mCenterX : mCenterY; // This circle radius is 75% or fill all if (mTouchEffect == TouchEffect.Move) mMaxRadius *= 0.75; else mMaxRadius *= 2.5; // Set default operation to fadeOutEffect() isTouchReleased = false; isAnimatingFadeIn = true; // Set this start point mPaintX = mDownX = event.getX(); mPaintY = mDownY = event.getY(); // This color alpha mRectAlpha = 0; // Cancel and Start new animation cancelAnimation(); startAnimation(); } }在觸發方法中,我們分別需要判斷是:取消/抬起/按下 操作。
onDraw()
public void onDraw(final Canvas canvas) { // Draw Area mPaint.setAlpha(mRectAlpha); canvas.drawPath(mRectPath, mPaint); // Draw Ripple if (isAnimatingFadeIn && (mTouchEffect == TouchEffect.Move || mTouchEffect == TouchEffect.Ripple)) { // Canvas Clip canvas.clipPath(mRectPath); mPaint.setAlpha(MAX_RIPPLE_ALPHA); canvas.drawCircle(mPaintX, mPaintY, mRadius, mPaint); } }這個方法是最後一個方法,也是較核心的一個地方,我們的成果就靠這個方法了。
首先當然是畫出背景部分,在畫之前當然就是設定背景色;該背景色是一個隨動畫時間變化的量,具體詳見上面動畫部分。
然後判斷是否是啟動動畫,因為淡出時也會觸發該方法但是卻不繪製圓形地區部分,所以需要判斷;之後判斷是否是屬於需要繪製圓形的動畫類型;再然後就是繪製具體的圓形地區了,分別就是座標和半徑;但是這裡需要注意的是,在繪製前我們調用了 canvas.clipPath(mRectPath); 。
canvas.clipPath(mRectPath):這個的作用就是剪下,意思是剪下畫布部分,然後在剪下後的畫布上繪製;這樣就解決了圓角時溢出的問題,因為剪下後的畫布就那麼大你就算畫到外部也是無法顯示的。
使用
public class GeniusButton extends Button implements Attributes.AttributeChangeListener { private TouchEffectAnimator touchEffectAnimator = null; public void setTouchEffect(TouchEffect touchEffect) { if (touchEffect == TouchEffect.None) touchEffectAnimator = null; else { if (touchEffectAnimator == null) { touchEffectAnimator = new TouchEffectAnimator(this); touchEffectAnimator.setTouchEffect(touchEffect); touchEffectAnimator.setEffectColor("this color"); touchEffectAnimator.setClipRadius(20); } } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (touchEffectAnimator != null) touchEffectAnimator.onMeasure(); } @Override protected void onDraw(Canvas canvas) { if (touchEffectAnimator != null) touchEffectAnimator.onDraw(canvas); super.onDraw(canvas); } @Override public boolean onTouchEvent(MotionEvent event) { if (touchEffectAnimator != null) touchEffectAnimator.onTouchEvent(event); return super.onTouchEvent(event); }}在你自訂的控制項中按著上面的方式進行執行個體化調用就OK。
其實現在來說該動畫類,並不局限於Button,你可以隨意的設定到你的控制項上面,如TextView 也可以不是自訂的控制項,Android 原生的也可以;只需要設定其中的3個方法回調也就OK;大家可以試試;然後把效果分別切換一下;個人感覺很棒的~
附件
算是分析完了,下面附上源碼和我分析時畫的一些圖,輔助解釋。
圖
代碼
點擊查看
========================================================
作者:qiujuer
部落格:blog.csdn.net/qiujuer
網站:www.qiujuer.net
開源庫:Genius-Android
轉載請註明出處:http://blog.csdn.net/qiujuer/article/details/42471119
——學之開源,用於開源;初學者的心態,與君共勉!
========================================================