第 7 章 Android 动画深入分析
Android 动画分类
- View 动画
平移、旋转、缩放、透明度。对场景里的对象不断做图像变换,渐进式动画。 - 帧动画
顺序播放一系列图像从而产生动画效果,即图片切换动画。图片过大容易 OOM - 属性动画
API 11 新特性,低版本可以使用兼容库来使用它
7.1 View 动画
作用对象是 View,支持四种动画效果:平移动画、缩放动画、旋转动画、透明度动画。
帧动画也属于 View 动画,但是它的表现形式和 View 动画四种变换效果不太一样。
7.1.1 View 动画的种类
四种变换效果对应着 Animation 的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation、AlphaAnimation。
这四种动画可以用 XML 定义,也可以用代码动态创建。(View 动画用 XML 定义比较好,可读性好)
创建动画 XML 文件在文件夹 res/anim/filename.xml
,语法如下所示:
View 动画既可以是单个动画,也可以由一系列动画组成
<set>
表示动画集合,对应 AnimationSet 类。可以包含若干动画,也可以在嵌套其他动画集合。- android:interpolator
表示动画集合所采用的插值器,插值器影响动画速度。默认为 @android:anim/accelerate_decelerate_interpolator
,即加速减速插值器。 - android:shareInterpolator
表示集合中的动画是否要和集合共享同一个插值器。
如果集合不指定插值器,子动画就需要单独指定插值器或者使用默认插值器
- android:interpolator
<translate>
表示平移动画,对应 TranslateAnimation 类,可以使一个 View 在水平和竖直的方向完成平移的动画效果。- android:fromXDelta——表示 x 的起始值,比如 0;
- android:toXDelta——表示 x 的结束值,比如 100;
- android:fromYDelta——表示 y 的起始值;
- android:toYDelta——表示 y 的结束值。
<scale>
表示缩放动画,对应 ScaleAnimation,可以使 View 具有放大或缩小的动画效果。- android:fromXScale——水平方向缩放的起始值,比如 0.5
- android:toXScale——水平方向缩放的结束值,比如 1.2
- android:fromYScale——竖直方向缩放的起始值
- android:toYScale——竖直方向缩放的结束值
- android:pivotX——缩放的轴点 x 坐标,它会影响缩放的效果
- android:pivotY——缩放的轴点 y 坐标,它会影响缩放的效果
默认情况下轴点坐标是 View 的
左上角(0,0),这时候进行水平缩放,就会在 轴点的右侧进行缩放;如果设置轴点在 View 的右边界,那缩放就会在轴点的左侧进行;如果设置轴点在 View 的中心点,那缩放就会在轴点的左右进行
图解 Android 动画中 android:pivotX 和 android:pivotY 属性的含义
xml 方式 和 代码方式创建动画,还有很多不一样的地方,多看看源码
<rotate>
表示旋转动画,对应于 RotateAnimation 类,使 View 具有旋转的动画效果- ——旋转开始的角度,比如 0;
- ——旋转结束的角度,比如 180;
- android:pivotX——旋转的轴点 x 坐标;
- android:pivotY——旋转的轴点 y 坐标。
轴点,会影响到旋转效果,同样是旋转 90 度,延中心点旋转和延左上角坐标旋转明显不一样
<alpha>
表示透明度动画,对应 AlphaAnimation 类,它可以改变 View 的透明度。- android:fromAlpha——表示透明度的起始值,比如 0.1;
- android:toAlpha——表示透明度的结束值,比如 1。
View 动画还有一些常用属性
- android:duration——动画的持续时间;
- android:fillAfter——动画结束以后 View 是否停留在结束位置,true 表示 View 停留在结束位置,false 则不停留。
使用示例:
也可以用代码动态创建动画:
Animation 的 setAnimationListener 方法可以给 View 动画添加过程监听:
7.1.2 自定义 View 动画
实际开发中很少用到自定义 View 动画
继承 Animation 类,重写 initialize 和 applyTransformation
方法。
initialize 方法做一些初始化的工作
applyTransformation 方法进行相应的矩阵变换
Rotate3dAnimation 示例,类似 3d 效果,围绕 y 轴旋转并沿着 z 轴平移:
7.1.3 帧动画
是顺序播放一组预先定义好的图片,类似电影播放。
系统提供 AnimationDrawable 来使用帧动画。
使用:
1. XML 定义一个 AnimationDrawable
1. 设置为 View 背景并通过 AnimationDrawable 来播放
帧动画比较简单,但是容易引起 OOM。
尽量避免使用过多较大尺寸的图片
7.2 View 动画的特殊使用场景
- ViewGroup 中控制子元素的出场效果
- Activity 中实现不同 Activity 之间的切换效果
7.2.1 LayoutAnimation
作用于 ViewGroup,为 ViewGroup 指定一个动画,这样当它的子元素出场时就会有这种动画效果。
LayoutAnimation 也是一个 View 动画,为了给 ViewGroup 的子元素加上出场效果,遵循如下几个步骤:
定义 LayoutAnimation,如下所示:
android:delay
子元素开始动画的时间延迟。
例:子元素入场动画时间设定 300ms,那么 0.5 表示都要延迟 150ms * n 才能播放入场动画。n 表示第 n 个要显示的子 View,不一定是按顺序的第几个,可能是随机的,这取决于 android:animationOrder
第一个子元素延迟 150ms,第二个子元素延迟 300ms 开始播放入场动画,依次类推。android:animationOrder
表示子元素动画的顺序。- normal——顺序显示
- reverse——逆向显示
- random——随机播放入场动画
android:animation——为子元素制定具体的入场动画
为子元素制定具体的入场动画:
为 ViewGroup 指定
android:layoutAnimation
属性。对于 ListView 或 RecyclerView(GridLayoutManager 需要特殊处理)来说,这样他们的 item 就有出场动画了,这种方式适用于所有 ViewGroup:
还可以通过 LayoutAnimationController 在代码里边实现:
拓展:为 RecyclerView 的 item 设置入场动画:RecyclerView——LayoutAnimation
7.2.2 Activity 的切换效果
Activity 有默认的切换效果,我们也可以自定义这个效果。用到 overridePendingTransition(int enterAnim, int exitAnim)
这个方法,它必须在 startActivity(Intent)
或者 finish()
后被调用才能生效,它的参数含义:
- enterAnim——Activity 被打开时,所需的动画资源 id
- exitAnim——Activity 被暂停时,所需的动画资源 id
当启动一个 Activity 时,给将要启动的 Activity
添加切换效果:
当 Activity 退出时,指定自己的
切换效果:
Fragment 也可以添加切换动画。通过 FragmentTransaction 中的 setCustomAnimations() 方法来添加切换动画。最好是用 View 动画
7.3 属性动画
API 11 加入的新特性,和 View 动画不同,它对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至可以没有对象。效果也得到了加强,不再像 View 动画只支持四种简单的变换。
属性动画有 ValueAnimator、ObjectAnimator、AnimatorSet 等概念。
7.3.1 使用属性动画
动画默认时间间隔 300ms,默认帧率 10ms/帧。
可以达到的效果:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。
属性动画几乎是无所不能的,只要对象有这个属性,他都能实现动画效果。
兼容性(现在基本都 API 17 起步了,基本没啥兼容性问题,用不到 nineoldandroids)
使用示例:
1. 改变一个对象(myObject)的 translationY 属性,让其沿着 Y 轴向上平移一段距离:它的高度。该动画在默认时间内完成。
改变一个对象的背景色属性,典型的情形是改变 View 的背景色。下边动画会让背景色在 3 秒内实现从 0xFFFF8080 到 0xFF8080FF 的渐变,动画会无限循环且会有反转的效果:
动画集合,5 秒内对 View 的旋转、平移、缩放和透明度都进行了改变
属性动画也可以用 XML 来实现。 目录 res/animator/
如下所示:
- 在 XML 中可以定义 ValueAnimator、ObjectAnimator、AnimatorSet。
标签对应 AnimatorSet
- android:ordering
两个可选值: “together” 和 “sequentially”。“together” 表示动画集合中的子动画同时播放;“sequentially” 表示动画集合中的子动画按照前后顺序依次播放。它的默认值是 “together”
- android:ordering
标签对应 ValueAnimator
- 它的属性就不用介绍了,它比 只少了 android:propertyName 属性
标签对应 ObjectAnimator
- android:propertyName——表示属性动画的作用对象的属性名称
- android:duration——表示动画的时长
- android:valueFrom——表示属性的起始值
- android:valueTo——表示属性的结束值
- android:startOffset——表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画
- android:repeatCount
表示动画的重复次数,默认 0,-1 表示无限循环 - android:repeatMode
表示动画的重复模式。有两个选项:“repeat” 和 “reverse”,分别表示连续重复和逆向重复。连续重复指动画每次重新开始播放;逆向重复是指第一次播放完后,第二次倒着播放,第三次重头播放,第四次再倒着播放,如此反复 - android:valueType——表示
android:propertyName
所指定的属性的类型,有 “intType” 和 “floatType” 两个可选项,分别表示属性的类型为整型和浮点型。如果android:propertyName
制定的属性表示颜色,那么不需要指定android:valueType
,系统会自己处理
示例,使用 XML 定义属性动画并作用在 View 上:
实际开发中建议用代码实现属性动画,用代码实现比较简单。而且很多时候属性的起始值是无法提前确定的,比如屏幕宽高
7.3.2 理解插值器和估值器
TimeInterpolator
- 中文翻译为时间插值器
- 作用是
根据时间流逝的百分比来计算出当前
属性值改变的百分比
- 系统预置:
- LinearInterpolator——线性插值器,匀速动画
- AccelerateDeceleratrInterpolator——加速减速插值器,动画两头慢中间快
- DecelerateInterpolator——减速插值器,动画越来越慢
- 等等
TypeEvaluator
- 中文翻译为估值算法,也叫估值器
- 作用是
根据当前
属性值改变的百分比
来
计算改变后的属性值
- 系统预置:
- IntEvaluator——针对整型属性
- FloatEvaluator——针对浮点型属性
- ArgbEvaluator——针对 Color 属性
属性动画中的估值器和插值器共同作用才计算出非匀速动画所需要的值,所以它俩很重要。
看 ValueAnimator 的源码:可以知道它先用 Interpolator 计算出属性改变的百分比,然后再用 Evaluator 计算出具体的属性改变值
示例,匀速动画采用线性插值器和整型估值算法,在 40ms 内,View 的 x 属性实现从 0 到 40 的变换,如下图:
动画默认刷新率 10ms/帧,所以该动画将分 5 帧进行。分析第 3 帧(x = 20,t = 20ms),当时间是 20ms,时间流逝百分比是 0.5。由于是线性插值器,所以 x 改变的百分比是 0.5 。由于是整型估值算法,所以最终改变值是 0+0.5*(40-0)=20
下面看一下线性插值器和整型估值算法的源码:
属性动画要求对象的属性有 set 方法和 get 方法(可选)。
插值器和估值器也可以自定义,然后就可以实现千奇百怪的效果了。
自定义插值器需要实现 Interpolator 或 TimeInterpolator;自定义估值算法需要实现 TypeEvaluator。
7.3.3 属性动画的监听器
AnimatorListener
它可以监听动画的开始、结束、取消、重复播放。
为了开发方便,系统还提供了AnimatorListenerAdapter
类,他是 AnimatorListener 的适配器类,可以有选择的实现上边的部分方法,不用全部都实现AnimatorUpdateListener
public static interface AnimatorUpdateListener { void onAnimationUpdate(ValueAnimator animation); }
它监听整个动画过程,每播放一帧,它就会调用一次。利用这个特性可以做一些特殊的事情
7.3.4 对任意属性做动画
对 object 的属性 abc 做动画,想让动画生效,要同时满足以下条件:
1. object 必须提供 setAbc 方法。如果动画的时候没有传递初始值,那么还需要 getAbc 方法,因为系统要去取初始值,如果没有 get 方法,直接 crash
1. object 的 setAbc 对属性 abc 所做的改变必须能够通过某种方式反映出来。比如 ui 的改变(如果这条不满足,动画无效果但不会 crash)
如果不满足上述两个条件,可以这样做:
- 给你的对象加上 get 和 set 方法,如果你有权限的话
- 用一个类来包装原始对象,间接为其提供 get 和 set 方法
采用 ValueAnimator,监听动画过程,自己实现属性的改变
给你的对象加上 get 和 set 方法,如果你有权限的话
一般情况是没有权限的,当你对系统的控件去做属性动画的时候,如果它没有这个属性的 get、set 方法,你也没权限去加 get、set 方法用一个类来包装原始对象,间接为其提供 get 和 set 方法
采用 ValueAnimator,监听动画过程,自己实现属性的改变
ValueAnimator 本身不做用于任何对象,直接使用它没有任何动画效果。
它可以对一个值做动画,监听动画过程,在动画过程中修改我们对象的属性值,这样相当于对我们的对象做了动画。
动画的每一帧都会调用 onAnimationUpdate 在 onAnimationUpdate 方法里可以获取当前的值和值所占的比例
7.3.5 属性动画的工作原理
属性动画要求动画作用的对象提供该属性的 set 方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用 set 方法。每次传递给 set 方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供 get 方法,因为系统要去获取属性的初始值。
分析一下 ObjectAmnimator 的源码
- 入口
ObjectAnimator.ofInt(mBotton, "width", 500).setDuration(5000).start()
书里的源码分析跟我现在看的源码已经有很大的差别了,但是整体思路是一样的(我看的 api-27)
@Override
public void start() {
AnimationHandler.getInstance().autoCancelBasedOn(this);
if (DBG) {
Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
for (int i = 0; i < mValues.length; ++i) {
PropertyValuesHolder pvh = mValues[i];
Log.d(LOG_TAG, " Values[" + i + "]: " +
pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
pvh.mKeyframes.getValue(1));
}
}
super.start();
}
上边代码做的工作就是:判断如果这个动画之前存在并且需要取消之前的,就取消掉;然后调用父类的 start() 方法。(父类是 ValueAnimator)
再看 ValueAnimator 的 start() 方法,它里边调用了自身的 start(boolean playBackwards) 方法:
/**
* Start the animation playing. This version of start() takes a boolean flag that indicates
* whether the animation should play in reverse. The flag is usually false, but may be set
* to true if called from the reverse() method.
*
* <p>The animation started by calling this method will be run on the thread that called
* this method. This thread should have a Looper on it (a runtime exception will be thrown if
* this is not the case). Also, if the animation will animate
* properties of objects in the view hierarchy, then the calling thread should be the UI
* thread for that view hierarchy.</p>
*
* @param playBackwards Whether the ValueAnimator should start playing in reverse.
*/
private void start(boolean playBackwards) {
if (Looper.myLooper() == null) {
throw new AndroidRuntimeException("Animators may only be run on Looper threads");
}
mReversing = playBackwards;
mSelfPulse = !mSuppressSelfPulseRequested;
// Special case: reversing from seek-to-0 should act as if not seeked at all.
if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
if (mRepeatCount == INFINITE) {
// Calculate the fraction of the current iteration.
float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
mSeekFraction = 1 - fraction;
} else {
mSeekFraction = 1 + mRepeatCount - mSeekFraction;
}
}
mStarted = true;
mPaused = false;
mRunning = false;
mAnimationEndRequested = false;
// Resets mLastFrameTime when start() is called, so that if the animation was running,
// calling start() would put the animation in the
// started-but-not-yet-reached-the-first-frame phase.
mLastFrameTime = -1;
mFirstFrameTime = -1;
mStartTime = -1;
//调用 addAnimationCallback 方法创建并实现 AnimationHandler 里的 AnimationFrameCallback 接口,同时会把 callback 注册到 AnimationHandler 里,之后就可以接收到动画每一帧的回调了
addAnimationCallback(0);
if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
// If there's no start delay, init the animation and notify start listeners right away
// to be consistent with the previous behavior. Otherwise, postpone this until the first
// frame after the start delay.
startAnimation();
if (mSeekFraction == -1) {
// No seek, start at play time 0. Note that the reason we are not using fraction 0
// is because for animations with 0 duration, we want to be consistent with pre-N
// behavior: skip to the final value immediately.
setCurrentPlayTime(0);
} else {
setCurrentFraction(mSeekFraction);
}
}
}
private void addAnimationCallback(long delay) {
if (!mSelfPulse) {
return;
}
getAnimationHandler().addAnimationFrameCallback(this, delay);
}
从 AnimationHandler 的 addAnimationFrameCallback 一直分析调用链,很快就到了 JNI 层,从底层回来后,最终会调用到 ValueAnimator#doAnimationFrame(long frameTime) -> animateBasedOnTime(long currentTime):
/**
* This internal function processes a single animation frame for a given animation. The
* currentTime parameter is the timing pulse sent by the handler, used to calculate the
* elapsed duration, and therefore
* the elapsed fraction, of the animation. The return value indicates whether the animation
* should be ended (which happens when the elapsed time of the animation exceeds the
* animation's duration, including the repeatCount).
*
* @param currentTime The current time, as tracked by the static timing handler
* @return true if the animation's duration, including any repetitions due to
* <code>repeatCount</code> has been exceeded and the animation should be ended.
*/
boolean animateBasedOnTime(long currentTime) {
boolean done = false;
if (mRunning) {
final long scaledDuration = getScaledDuration();
final float fraction = scaledDuration > 0 ?
(float)(currentTime - mStartTime) / scaledDuration : 1f;
final float lastFraction = mOverallFraction;
final boolean newIteration = (int) fraction > (int) lastFraction;
final boolean lastIterationFinished = (fraction >= mRepeatCount + 1) &&
(mRepeatCount != INFINITE);
if (scaledDuration == 0) {
// 0 duration animator, ignore the repeat count and skip to the end
done = true;
} else if (newIteration && !lastIterationFinished) {
// Time to repeat
if (mListeners != null) {
int numListeners = mListeners.size();
for (int i = 0; i < numListeners; ++i) {
mListeners.get(i).onAnimationRepeat(this);
}
}
} else if (lastIterationFinished) {
done = true;
}
mOverallFraction = clampFraction(fraction);
float currentIterationFraction = getCurrentIterationFraction(
mOverallFraction, mReversing);
animateValue(currentIterationFraction);
}
return done;
}
上边代码调用到了 void animateValue(float fraction)
,现在再回到 ObjectAnimator 源码中去,它重载了 ValueAnimator 的 animateValue 方法:
//ObjectAnimator#animateValue(float fraction)
/**
* This method is called with the elapsed fraction of the animation during every
* animation frame. This function turns the elapsed fraction into an interpolated fraction
* and then into an animated value (from the evaluator. The function is called mostly during
* animation updates, but it is also called when the <code>end()</code>
* function is called, to set the final value on the property.
*
* <p>Overrides of this method must call the superclass to perform the calculation
* of the animated value.</p>
*
* @param fraction The elapsed fraction of the animation.
*/
@CallSuper
@Override
void animateValue(float fraction) {
final Object target = getTarget();
if (mTarget != null && target == null) {
// We lost the target reference, cancel and clean up. Note: we allow null target if the
/// target has never been set.
cancel();
return;
}
super.animateValue(fraction);
int numValues = mValues.length;
for (int i = 0; i < numValues; ++i) {
mValues[i].setAnimatedValue(target);
}
}
上边代码,它先调用父类 animateValue 方法,然后再 调用 PropertyValuesHolder#setAnimatedValue 去设置属性的值:
//PropertyValuesHolder#setAnimatedValue
/**
* Internal function to set the value on the target object, using the setter set up
* earlier on this PropertyValuesHolder object. This function is called by ObjectAnimator
* to handle turning the value calculated by ValueAnimator into a value set on the object
* according to the name of the property.
* @param target The target object on which the value is set
*/
void setAnimatedValue(Object target) {
if (mProperty != null) {
mProperty.set(target, getAnimatedValue());
}
if (mSetter != null) {
try {
mTmpValueArray[0] = getAnimatedValue();
//反射调用 set 方法去设置值
mSetter.invoke(target, mTmpValueArray);
} catch (InvocationTargetException e) {
Log.e("PropertyValuesHolder", e.toString());
} catch (IllegalAccessException e) {
Log.e("PropertyValuesHolder", e.toString());
}
}
}
知道了动画更新属性值的流程,现在再找找动画调用 get 方法拿初始值的流程:
上边分析 ObjectAnimator 的 start 方法时的调用链 ObjectAnimator#start() -> ValueAnimator#start() -> ValueAnimator#start(false)
在 ValueAnimator#start(false) 方法里也调用了 startAnimation
方法:
ValueAnimator#startAnimation()
/** * Called internally to start an animation by adding it to the active animations list. Must be * called on the UI thread. */ private void startAnimation() { if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(), System.identityHashCode(this)); } mAnimationEndRequested = false; initAnimation(); mRunning = true; if (mSeekFraction >= 0) { mOverallFraction = mSeekFraction; } else { mOverallFraction = 0f; } if (mListeners != null) { notifyStartListeners(); } }
里边调用了
initAnimation()
方法,而 ObjectAnimator 重写了该方法ObjectAnimator#initAnimation()
/** * This function is called immediately before processing the first animation * frame of an animation. If there is a nonzero <code>startDelay</code>, the * function is called after that delay ends. * It takes care of the final initialization steps for the * animation. This includes setting mEvaluator, if the user has not yet * set it up, and the setter/getter methods, if the user did not supply * them. * * <p>Overriders of this method should call the superclass method to cause * internal mechanisms to be set up correctly.</p> */ @CallSuper @Override void initAnimation() { if (!mInitialized) { // mValueType may change due to setter/getter setup; do this before calling super.init(), // which uses mValueType to set up the default type evaluator. final Object target = getTarget(); if (target != null) { final int numValues = mValues.length; for (int i = 0; i < numValues; ++i) { //调用 PropertyValuesHolder#setupSetterAndGetter(target) 来设置初始值 mValues[i].setupSetterAndGetter(target); } } super.initAnimation(); } }
调用 PropertyValuesHolder#setupSetterAndGetter(target) 来设置初始值
PropertyValuesHolder#setupSetterAndGetter(target)
/** * Internal function (called from ObjectAnimator) to set up the setter and getter * prior to running the animation. If the setter has not been manually set for this * object, it will be derived automatically given the property name, target object, and * types of values supplied. If no getter has been set, it will be supplied iff any of the * supplied values was null. If there is a null value, then the getter (supplied or derived) * will be called to set those null values to the current value of the property * on the target object. * @param target The object on which the setter (and possibly getter) exist. */ void setupSetterAndGetter(Object target) { ... for (int i = 0; i < keyframeCount; i++) { Keyframe kf = keyframes.get(i); if (!kf.hasValue() || kf.valueWasSetOnStart()) { ... try { Object value = convertBack(mGetter.invoke(target)); kf.setValue(value); kf.setValueWasSetOnStart(true); } catch (InvocationTargetException e) { Log.e("PropertyValuesHolder", e.toString()); } catch (IllegalAccessException e) { Log.e("PropertyValuesHolder", e.toString()); } } } ... }
里边也是用反射
mGetter.invoke(target)
去调用的 get 方法拿到初始值
7.4 使用动画的注意事项
OOM 问题
主要出现在帧动画中,图片过多过大时极易出现。尽量避免使用帧动画内存泄漏
属性动画做的无限循环动画,需要在 Activity 退出时即时停止,否则将导致 Activity 无法释放造成内存泄漏。通过验证 View 动画不存在此问题兼容性问题
在 android 3.0 以下的系统有兼容性问题。View 动画的问题
View 动画是对 View 的影像做动画,并没有真正改变 View 的状态,因此有时会出现动画完成后 View 无法隐藏的现象,即 setVisibility(View.GONE) 失效,此时只要调用 view.clearAnimation() 清除动画就能解决此问题不要使用 px
进行动画的过程中尽量使用 dp,使用 px 会导致不同设备上有不同的效果动画元素的交互
将 view 移动后,在 android 3.0 以前的系统上,不管是 View 动画还是属性动画,新位置均无法触发点击事件,同时老位置仍然可以触发点击事件。尽管 View 已经在视觉上不存在了,将 View 移回原位置点击事件继续生效。从 android 3.0 开始,属性动画的单击事件触发位置为移动后的位置,但是 View 动画仍然在原位置。硬件加速
使用动画过程中建议开启硬件加速,提高动画的流畅性