在 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
的边界范围内。
补间动画和属性动画改变的是同一个 Matrix 么?
负责任的说:肯定不是。
属性动画所影响的 Matrix
,是在 View
的 mRenderNode
中的 stagingProperties
里面的,这个 Matrix
在每个 View
之间都是独立的,所以可以各自保存不同的变换状态。
而补间动画所操作的 Matrix
,其实是借用了它父容器的一个叫 mChildTransformation
的属性 ( 里面有 Matrix
),通过 getChildTransformation
获得。
也就是说,一个 ViewGroup
中,无论它有几个子 View
都好,在这些子 View
播放补间动画的时候,都是共用同一个 Transformation
对象的(也就是共用一个 Matrix
),这个对象放在 ViewGroup
里面。
有同学可能会问:共用?不可能吧,那为什么可以同时播放好几个动画,而互相不受影响呢?
是的,在补间动画更新每一帧的时候,父容器的 mChildTransformation
里面的 Matrix
,都会被 reset()
。
每次重置 Matrix
而不受影响的原因:
是因为这些补间动画,都是基于当前播放进度,来计算出绝对的动画值并应用的,保存旧动画值是没有意义的。
就拿位移动画 TranslateAnimation
来说,比如它要向右移动 500,当前的播放进度是 50%,那就是已经向右移动了 250,在 View
更新帧的时候,就会把这个向右移动了250的 Matrix
应用到 Canvas
上,当下次更新帧时,比如进度是 60%,那计算出来的偏移量就是 300,这时候,已经不需要上一次的旧值 250 了,就算 Matrix
在应用前被重置了,也不影响最后的效果。
感叹,今天又发现了一些非常通用却被我们忽略掉的东西,不得不说,鸿洋的 wanandroid 带给了我们很多东西,更加惊叹的是「陈小缘」同学的 View
相关功底确实很强,这也难怪,他能写出如何有逼格的自定义 View 了。
对 View
相关的非常渴望了解的可以到小缘的博客去一探究竟。 https://me.csdn.net/u011387817