每日一问:到底为什么属性动画后 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做了属性上的改变,但其实并没有更改
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
- CCMenu超过CCScrollView边框还能点击处理,以及拖动CCMenuItem不响应点击事件
- 利用Associative机制为UIAlertView添加一个Block属性,将AlertView与button的响应事件关联
- Android用属性动画拖动view到任意位置
- 隐藏tabbar之后在原位置新建View不响应点击事件的方法
- listview中的textview使用autolink 属性时造成的itemclick事件无法响应
- 解释为什么imgView要打开点击事件【事件响应链】
- 疑问:xcode中为什么按钮被图片覆盖了,还能响应事件?
- 一款基jquery超炫的动画导航菜单可响应单击事件
- 自定义view加属性动画
- 属性动画、事件分发和自定义控件
- iOS开发之隐藏tabbar--解决隐藏后依然不能响应对应位置事件的问题
- UIImageView响应点击事件
- 自定义view实心圆加属性动画
- iphone 让一个UIImageView响应点击事件
- Android View体系(三)属性动画
- UI15-UIImageView响应点击事件
- 从一个view中获取到它的ViewController (事件响应机制,层层传递事件)
- iOS开发-UIImageView响应点击事件
- 从零开始学Android自定义View之动画系列——属性动画(3)
- 关于一个view焦点转移时响应按键事件的问题