1. 程式人生 > >每日一問:到底為什麼屬性動畫後 View 在新位置還能響應事件

每日一問:到底為什麼屬性動畫後 View 在新位置還能響應事件

在 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 做了屬性上的改變,但其實並沒有更改 Viewleftrighttopbottom 這些屬性,而這些屬性恰恰決定了 ViewGroup 的觸控區域判斷。

tv1.animate().setDuration(1000).translationX(500f)

那麼,假定我們的 View 經過了上面的平移操作後,為什麼點選新的位置能夠響應到這個點選事件呢?

看了「陳小緣」的回答,我順便深入了一波原始碼,想想必須在這分享給大家。

我們知道,在 ViewGroup 沒有重寫 onInterceptTouchEvent() 方法進行事件攔截的時候,我們一定會通過其 dispatchTouchEvent() 方法進行事件分發,而決定我們哪一個子 View 響應我們的觸控事件的條件又是 我們手指的位置必須在這個子 View 的邊界範圍內,也就是 leftrighttopbottom 這四個屬性形成的矩形區域。

那麼,如果我們的 View 已經進行了屬性動畫後,現在手指響應的觸控位置區域肯定不是 View 自己的leftrighttopbottom 這四個屬性形成的區域了,但這個 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;
}   

原來,ViewGroupgetTransformedMotionEvent() 方法中會通過子 ViewhasIdentityMatrix() 方法來判斷子 View 是否應用過位移、縮放、旋轉之類的屬性動畫。如果應用過的話,那還會呼叫子 ViewgetInverseMatrix() 做「反平移」操作,然後再去判斷處理後的觸控點是否在子 View 的邊界範圍內。

感嘆,今天又發現了一些非常通用卻被我們忽略掉的東西,不得不說,鴻洋的 wanandroid 帶給了我們很多東西,更加驚歎的是「陳小緣」同學的 View 相關功底確實很強,這也難怪,他能寫出如何有逼格的自定義 View 了。

View 相關的非常渴望瞭解的可以到小緣的部落格去一探究竟。 https://me.csdn.net/u011387