【Android】Animation和Gone不得不说的故事:分析动画对View的影响

背景

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依旧在那里转圈。
【Android】Animation和Gone不得不说的故事:分析动画对View的影响_第1张图片

查看LoadingView的属性,visibility是gone:

【Android】Animation和Gone不得不说的故事:分析动画对View的影响_第2张图片

width和height也有值。

【Android】Animation和Gone不得不说的故事:分析动画对View的影响_第3张图片

看似一个问题,背后却隐藏着至少两处细节:

1、View动画和绘制
2、主流布局中对gone的处理

View动画和绘制

在View的官方注释中,将绘制分为如下几步:
【Android】Animation和Gone不得不说的故事:分析动画对View的影响_第4张图片

  1. 绘制背景
  2. 如有必要,保存画布的图层以准备渐变
  3. 绘制视图的内容
  4. 绘制子视图
  5. 如有必要,绘制渐变边并恢复图层
  6. 绘制装饰(例如滚动条)

第四步绘制子视图,在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的

主流布局中对gone的处理

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没有经过重新测量,它们的大小仍是上次被测量的尺寸。

Touch事件

查明了上述问题,又产生了另外一个疑问:

设置了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关闭动画。

总结

  1. 一开始遇到这个问题,调用一下clearAnimation就解决了。但是无法弄懂其中的缘由,下次遇到类似的问题就没招了。为了弄明白原理,一分钟可以解决的问题,用了不到一天的时间,个人感觉值得。
  2. 提前掌握事件分发的知识,在分析Touch事件的时候有的放矢。
  3. 熟悉Choreographer以及界面刷新流程,能够很快了解动画的执行过程。
  4. 重在积累,有些知识可能当下看不出用途,用的时候才知道真舒服。

参考资料:
界面是如何刷新的流程
View动画运行原理

你可能感兴趣的:(android,android)