在 Android 開發中,我們難免會使用動畫來處理各種各樣的動畫效果,以滿足 UI 的高逼格設計。對於比較復雜的動畫效果,我們通常會采用著名的開源庫:lottie-android,或許你會對 lottie 的原理充滿好奇,但這並不在我們這篇文章的討論范圍,感興趣的自行 Google 吧~
屬性動畫和補間動畫的基本編寫方式
我一度在論壇上看到人使用了 TranslateAnimation
對控件做了移動操作,然后發現在 View 的新位置點擊並沒有響應自己的點擊事件,反倒是之前的位置能夠響應。實際上,補間動畫僅僅是對 View 在視覺效果上做了移動、縮放、旋轉和淡入淡出的效果,其實並沒有真正改變 View 的屬性。但我們大多數情況下肯定希望 View 在經過動效后響應觸摸事件的位置和視覺效果相同,所以在 Android 3.0 之后引入了屬性動畫,徹底解決了這個難題。
可能還有一些小伙伴不明白怎樣的代碼是屬性動畫,怎樣的代碼是補間動畫。下面針對 View 向右平移 500 px 做一下簡單的演示。
對於屬性動畫,你可以用下面的兩種方式。
ObjectAnimator.ofFloat(tv1, "translationX", 0f, 500f)
.setDuration(1000)
.start()
// 或者像這樣
tv1.animate().setDuration(1000).translationX(500f)
但用補間動畫,並且你想達到同樣的效果的話。
val anim = TranslateAnimation(0f, 500f, 0f, 0f)
anim.duration = 1000
anim.fillAfter = true // 設置保留動畫后的狀態
tv1.startAnimation(anim)
屬性動畫的使用注意點
對於屬性動畫來說,尤其需要注意的是操作的屬性需要有 set 和 get 方法,不然你的 ObjectAnimator
操作就不會生效。比如水平平移,我們知道,View 的 translationX
屬性設置方法接受的是 float
值,所以你把上面的操作編寫為 ofInt
就不會生效,比如:
ObjectAnimator.ofInt(tv1, "translationX", 0, 500)
.setDuration(1000)
.start()
對於我們需要用到但又沒有寫好的屬性,比如我們自定義一個進度條 View,我們需要實時展示進度,這時候我們就可以自己定義一個屬性,並讓它支持 set 和 get,那么在外面就可以對這個自定義的 View 做屬性動畫操作了。
屬性動畫和補間動畫工作原理
屬性動畫
屬性動畫的工作原理很簡單,其實就是在一定的時間間隔內,通過不斷地對值進行改變,並不斷將該值賦給對象的屬性,從而實現該對象在屬性上的動畫效果。
這個屬性可以是任意對象的屬性。
從上述工作原理可以看出屬性動畫有兩個非常重要的類:ValueAnimator
類 & ObjectAnimator
類,二者的區別在於:
ValueAnimator
類是先改變值,然后 手動賦值 給對象的屬性從而實現動畫;是 間接 對對象屬性進行操作;而 ValueAnimator
類本質上是一種 改變值 的操作機制。
ObjectAnimator
類是先改變值,然后 自動賦值 給對象的屬性從而實現動畫;是 直接 對對象屬性進行操作;可以理解為:ObjectAnimator
更加智能、自動化程度更高。
補間動畫
而對於補間動畫,我們不妨跟進源碼,看看到底做了什么操作。
/**
* Start the specified animation now.
*
* @param animation the animation to start now
*/
public void startAnimation(Animation animation) {
animation.setStartTime(Animation.START_ON_FIRST_FRAME);
setAnimation(animation);
invalidateParentCaches();
invalidate(true);
}
看到了非常明顯 invalidate()
方法,很明顯,補間動畫在執行的時候,直接導致了 View
執行 onDraw()
方法。總的來說,補間動畫的核心本質就是在一定的持續時間內,不斷改變 Matrix
變換,並且不斷刷新的過程。
為什么屬性動畫移動一個 View 后,目標位置還可以響應觸摸事件呢?
這個問題來自 wanandroid,在此前,我一直認為既然 View
的屬性得到了改變,那么經過屬性動畫后的控件應該所有屬性都等同於直接設置在動畫后的位置的控件。
看完「陳小緣」的回答后,我突然才想到,雖然 View
做了屬性上的改變,但其實並沒有更改 View
的 left
、right
、top
、bottom
這些屬性,而這些屬性恰恰決定了 ViewGroup
的觸摸區域判斷。
tv1.animate().setDuration(1000).translationX(500f)
那么,假定我們的 View
經過了上面的平移操作后,為什么點擊新的位置能夠響應到這個點擊事件呢?
看了「陳小緣」的回答,我順便深入了一波源碼,想想必須在這分享給大家。
我們知道,在 ViewGroup
沒有重寫 onInterceptTouchEvent()
方法進行事件攔截的時候,我們一定會通過其 dispatchTouchEvent()
方法進行事件分發,而決定我們哪一個子 View
響應我們的觸摸事件的條件又是 我們手指的位置必須在這個子 View
的邊界范圍內,也就是 left
、right
、top
、bottom
這四個屬性形成的矩形區域。
那么,如果我們的 View
已經進行了屬性動畫后,現在手指響應的觸摸位置區域肯定不是 View
自己的left
、right
、top
、bottom
這四個屬性形成的區域了,但這個 View
卻神奇的響應了我們的點擊事件。
/**
* Returns a MotionEvent that's been transformed into the child's local coordinates.
*
* It's the responsibility of the caller to recycle it once they're finished with it.
* @param event The event to transform.
* @param child The view whose coordinate space is to be used.
* @return A copy of the the given MotionEvent, transformed into the given View's coordinate
* space.
*/
private MotionEvent getTransformedMotionEvent(MotionEvent event, View child) {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
final MotionEvent transformedEvent = MotionEvent.obtain(event);
transformedEvent.offsetLocation(offsetX, offsetY);
if (!child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
return transformedEvent;
}
/**
* Returns true if the transform matrix is the identity matrix.
* Recomputes the matrix if necessary.
*
* @return True if the transform matrix is the identity matrix, false otherwise.
*/
final boolean hasIdentityMatrix() {
return mRenderNode.hasIdentityMatrix();
}
/**
* Utility method to retrieve the inverse of the current mMatrix property.
* We cache the matrix to avoid recalculating it when transform properties
* have not changed.
*
* @return The inverse of the current matrix of this view.
* @hide
*/
public final Matrix getInverseMatrix() {
ensureTransformationInfo();
if (mTransformationInfo.mInverseMatrix == null) {
mTransformationInfo.mInverseMatrix = new Matrix();
}
final Matrix matrix = mTransformationInfo.mInverseMatrix;
mRenderNode.getInverseMatrix(matrix);
return matrix;
}
原來,ViewGroup
在 getTransformedMotionEvent()
方法中會通過子 View
的 hasIdentityMatrix()
方法來判斷子 View
是否應用過位移、縮放、旋轉之類的屬性動畫。如果應用過的話,那還會調用子 View
的 getInverseMatrix()
做「反平移」操作,然后再去判斷處理后的觸摸點是否在子 View
的邊界范圍內。
感嘆,今天又發現了一些非常通用卻被我們忽略掉的東西,不得不說,鴻洋的 wanandroid 帶給了我們很多東西,更加驚嘆的是「陳小緣」同學的 View
相關功底確實很強,這也難怪,他能寫出如何有逼格的自定義 View 了。
對 View
相關的非常渴望了解的可以到小緣的博客去一探究竟。 https://me.csdn.net/u011387817