• 【Android】Animation和Gone不得不说的故事


    背景

    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);
                }
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    然后在布局文件中使用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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    最后在Activity中调用Gone:

    mProgressView.setVisibility(GONE)
    
    • 1

    结果loading依旧在那里转圈。
    在这里插入图片描述

    查看LoadingView的属性,visibility是gone:

    在这里插入图片描述

    width和height也有值。

    在这里插入图片描述

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

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

    View动画和绘制

    在View的官方注释中,将绘制分为如下几步:
    在这里插入图片描述

    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);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    代码表明,如果子视图可见,或者存在动画,会调用子视图的绘制逻辑

    因此,存在动画的前提下,使用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);
                  }
              }
          }
      }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    # LinearLayout
    
    void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
    	...
    	for (int i = 0; i < count; ++i) {
    		...
            if (child.getVisibility() == GONE) {
                i += getChildrenSkipCount(child, i);
                continue;
            }
         	...
    	 }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    因此,属性为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;
       }
    	...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果child.canReceivePointerEvents()返回false,会跳过分发。

    canReceivePointerEvents的代码如下:

    #View
    
    protected boolean canReceivePointerEvents() {
        return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    虽然设置了gone,但因为animation不为空,因此可以接收Touch事件。

    其他

    即使在一开始View被设置了gone,用户看不见View,View也会不断执行动画,消耗系统性能。

    所以使用动画时要谨慎,及时调用clearAnimation关闭动画。

    总结

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

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

  • 相关阅读:
    java计算机毕业设计springboot+vue员工管理系统
    ZYNQ从放弃到入门(十二)- AMP — Zynq 上的非对称多核处理器
    【字典合集】SecLists-更全面的渗透测试字典 v2024.1
    16 “count(*)“ 和 “count(1)“ 和 “count(field1)“ 的差异
    如何优雅的卸载linux上的todesk
    物联网安全概述
    VS2022ide下使用C++实现简谐振动,C语言程序设计简谐运动的模拟,C语言课程设计简谐振动实验的模拟。
    Hadoop知识点全面总结
    Web 后端的一生之敌:分页器
    SpringBoot 05 静态资源导入、热部署
  • 原文地址:https://blog.csdn.net/qq_23049111/article/details/127531493