View的Animation和Gone,大家已经非常熟悉了。Animation负责给View添加动画,Gone可以隐藏View。
那么,当一个View的Animation未执行结束的时候,设置Gone,是否会终止Animation呢?View是否会隐藏呢?
这是我在开发过程中遇到的一个现象,简单还原一下场景:
首先自定义LoadingView,实现非常简单。设置背景后,当可见时,开始执行一个围绕自身不断旋转的动画:
public class LoadingView extends View {
private RotateAnimation animation;
private void init(){
animation = new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, .5f, Animation.RELATIVE_TO_SELF, .5f);
animation.setRepeatMode(Animation.RESTART);
animation.setInterpolator(new LinearInterpolator());
animation.setRepeatCount(-1);
animation.setDuration(1250);
setBackgroundResource(R.drawable.loading_frame);
}
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
super.onVisibilityChanged(changedView, visibility);
if(visibility == View.VISIBLE){
if (!animation.hasStarted()||animation.hasEnded()) {
startAnimation(animation);
}
}
}
}
然后在布局文件中使用LoadingView,最外层使用FrameLayout:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LoadingView
android:id="@+id/progress"
android:layout_width="42dp"
android:layout_height="42dp"
android:layout_gravity="center"
/>
FrameLayout>
最后在Activity中调用Gone:
mProgressView.setVisibility(GONE)
结果loading依旧在那里转圈。
查看LoadingView的属性,visibility是gone:
width和height也有值。
看似一个问题,背后却隐藏着至少两处细节:
1、View动画和绘制
2、主流布局中对gone的处理
在View的官方注释中,将绘制分为如下几步:
第四步绘制子视图,在ViewGroup里有这样一段代码:
protected void dispatchDraw(Canvas canvas) {
for (int i = 0; i < childrenCount; i++) {
...
if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
more |= drawChild(canvas, child, drawingTime);
}
}
}
代码表明,如果子视图可见,或者存在动画,会调用子视图的绘制逻辑。
因此,存在动画的前提下,使用gone是无法隐藏View的。
View被隐藏了,通常我们认为这个View的widht和height都为0。
实际在布局测量过程中,跳过了visibility属性是gone的子View。
# FrameLayout
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (mMeasureAllChildren || child.getVisibility() != GONE) {
if (measureMatchParentChildren) {
if (lp.width == LayoutParams.MATCH_PARENT ||
lp.height == LayoutParams.MATCH_PARENT) {
mMatchParentChildren.add(child);
}
}
}
}
}
# LinearLayout
void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
...
for (int i = 0; i < count; ++i) {
...
if (child.getVisibility() == GONE) {
i += getChildrenSkipCount(child, i);
continue;
}
...
}
}
因此,属性为gone子View没有经过重新测量,它们的大小仍是上次被测量的尺寸。
查明了上述问题,又产生了另外一个疑问:
设置了gone同时执行动画的子View是可见的,那么可以接收Touch事件吗?
打了log后,发现是可以的。
对此,可以在源码中找到原因。
子View的Touch事件是由父控件传递的,在ViewGroup的事件分发方法中,有如下代码:
#ViewGroup
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
}
如果child.canReceivePointerEvents()
返回false
,会跳过分发。
canReceivePointerEvents
的代码如下:
#View
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
虽然设置了gone,但因为animation不为空,因此可以接收Touch事件。
即使在一开始View被设置了gone,用户看不见View,View也会不断执行动画,消耗系统性能。
所以使用动画时要谨慎,及时调用clearAnimation
关闭动画。
clearAnimation
就解决了。但是无法弄懂其中的缘由,下次遇到类似的问题就没招了。为了弄明白原理,一分钟可以解决的问题,用了不到一天的时间,个人感觉值得。参考资料:
界面是如何刷新的流程
View动画运行原理