美文网首页面试理论
android View动画---设计理念

android View动画---设计理念

作者: liys_android | 来源:发表于2022-07-31 00:17 被阅读0次

    本章内容: 了解View动画的总体设计理念,关键是思想, 而非代码细节.

    一. 如何让View动起来.

    1. 首先要了解View是如何展示到屏幕上的?
    ①. 先确定View的位置, 如下图:

    View的位置.png

    ②. 在View上面绘制内容, 如下图:


    View绘制内容.png

    2. 得出两种让View运动的方案:
    ①. layout() 改变布局位置
    ②. draw() 改变 绘制内容的位置

    二. 系统采用的时哪种方案呢?

    答:第2种, draw() 绘制时,改变绘制内容的位置.
    这样做的好处:无论如何运动, 保留了原始位置 (相对父控件的位置), 如下图:

    View位置.png
    三. View中,setScrollX、setScrollY如何实现滚动的?

    从设计者的角度看: 为了降低耦合度, 我们应该把需要滚动的信息单独记录下来,然后在draw()绘制的时候 ,加上需要滚动的坐标, 最终的出新的绘制坐标, 如下图:

    View.Scroll滚动设计图.png

    从源码实现的角度看:
    以setScrollY为例:

    1. 给mScrollY 变量赋值
        protected int mScrollY;
    
        public void setScrollY(int value) {
            scrollTo(mScrollX, value);
        }
    
        public void scrollTo(int x, int y) {
            if (mScrollX != x || mScrollY != y) {
                mScrollX = x;
                mScrollY = y;
                if (!awakenScrollBars()) {
                    postInvalidateOnAnimation();
                }
            }
        }
    
    1. draw()绘制, 关键代码(伪代码)
        public void draw(Canvas canvas) {
            int left = mScrollX + paddingLeft;
            int right = left + mRight - mLeft - mPaddingRight - paddingLeft;
            int top = mScrollY + getFadeTop(offsetRequired);
            int bottom = top + getFadeHeight(offsetRequired);
    
            canvas.saveUnclippedLayer(left、top、right、位置信息);
            canvas.drawRect(left、top、right、bootm 位置信息)
    }
    
    四. 补间动画 和属性动画 如何实现的?

    从设计者的角度看: 原理和上面类似, 先把动画信息先用单独对象保存起来,然后在draw()绘制的时候,进行矩阵变化, 如下图:

    补间动画/属性动画.png
    从源码的角度看:

    ①. 补间动画保存,其实就是保存到一个变量中 mCurrentAnimation

    View.java
    
        protected Animation mCurrentAnimation = null;
    
        public void startAnimation(Animation animation) {
            ...
            setAnimation(animation); 
            invalidate(true);
        }
    
        public void setAnimation(Animation animation) {
            mCurrentAnimation = animation;
        }
    

    ②. 属性动画保存, 以setTranslationX为例,其实就是把值保存到一个变量中mRenderNode

    View.java
    
        final RenderNode mRenderNode;
    
        public View(Context context) {
              mRenderNode = RenderNode.create(getClass().getName(), new ViewAnimationHostBridge(this));
        }
    
    
        public void setTranslationX(float translationX) {
            if (translationX != getTranslationX()) {
                mRenderNode.setTranslationX(translationX);
                invalidateViewProperty(false, true);
            }
    

    ③. 绘制过程

    View.java
    
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
            Transformation transformToApply = null;
            //获取补间动画
            final Animation a = getAnimation();
            if (a != null) {
                //根据时间, 计算出需要的矩阵变化信息,并用Transformation包起来
                applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
                //这时transformToApply里面已经有变换矩阵的信息
                transformToApply = parent.getChildTransformation();
            }
    
          //canvas 矩阵变换
          if (transformToApply != null) {
              canvas.concat(transformToApply.getMatrix());
          }
        
          //获取属性动画的矩阵变化信息,canvas 矩阵变换
          if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                canvas.concat(getMatrix());
         }
    
          //开始常见的draw绘制
          draw(canvas);
    }
    
    

    ④. 补间动画,如何计算Matrix()信息 (矩阵变换信息)
    Matrix()信息, 实际上是包裹起来的,结构如下图:


    Matrix()信息.png
    View.java
    
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
    }
    
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime, Animation a, boolean scalingRequired) {
          Transformation t = parent.getChildTransformation();
          a.getTransformation(drawingTime, t, 1f);
    }
    
    Animation.java
    
        public boolean getTransformation(long currentTime, Transformation outTransformation,float scale) {
            return getTransformation(currentTime, outTransformation);
        }
    
        public boolean getTransformation(long currentTime, Transformation outTransformation) {
                //估值器处理, 不是本章重点
                final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
                applyTransformation(interpolatedTime, outTransformation);
        }
        
        //获取Matrix()信息,  
        protected void applyTransformation(float interpolatedTime, Transformation t) {
        }
    

    可以看到, Animation类中具体的算法applyTransformation()是空实现, 需要交给子类来实现, 如果需要自定义动画算法, 关键是重写这个方法, 例如系统提供的平移动画:TranslateAnimation.java

        @Override
        protected void applyTransformation(float interpolatedTime, Transformation t) {
            float dx = mFromXDelta;
            float dy = mFromYDelta;
            if (mFromXDelta != mToXDelta) {
                dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
            }
            if (mFromYDelta != mToYDelta) {
                dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
            }
            t.getMatrix().setTranslate(dx, dy);
        }
    

    ⑤. 属性动画,如何计算Matrix()信息 (矩阵变换信息)
    mRenderNode就是前面保存的属性动画信息,将matrix传到RenderNode进行赋值, 最后跑到native里面处理了,具体赋值的算法这里就不深究了.

    View.java
    
        boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
          canvas.concat(getMatrix());
        }
    
        public Matrix getMatrix() {
            final Matrix matrix = mTransformationInfo.mMatrix;
            mRenderNode.getMatrix(matrix);  // 传进去后, 在里面给matrix赋值
            return matrix;
        }
      
    
    RenderNode.java
        public void getMatrix(@NonNull Matrix outMatrix) {
            nGetTransformMatrix(mNativeRenderNode, outMatrix.native_instance);
        }
    
          @CriticalNative
        private static native void nGetTransformMatrix(long renderNode, long nativeMatrix);
    
    五. 动画是如何做到一帧一帧运动的?
    1. 每次draw()的时候,都是根据当前时间, 来获取动画对应的坐标.
    2. 如果动画没到结束时间,调用invalidate(),等待下一次垂直同步信号, 会继续执行draw(), 详细的可以去了解 屏幕渲染机制.
    六. 补间动画 和 属性动画 对于点击触摸事件有什么不同?

    动画只是位置上有所变化, 所以我们只需要 事件 对于位置是如何判定的就可以了,源码如下:

    ViewGroup.java
    
    public boolean dispatchTouchEvent(MotionEvent ev) {
        for (int i = childrenCount - 1; i >= 0; i--) {
                View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
                 //判断触摸的坐标 是否在child内
                 if (!isTransformedTouchPointInView(x, y, child, null)) {
                      continue;
                 }
        }
    }
    
    protected boolean isTransformedTouchPointInView(float x, float y, View child,PointF outLocalPoint) {
        //关键:原始坐标+属性动画 ---> 新的坐标 
        transformPointToViewLocal(point, child);  //执行完这一句之后,point就已经算上动画后的坐标了
        final boolean isInView = child.pointInView(point[0], point[1]);
        return isInView;
    }
    
        public void transformPointToViewLocal(float[] point, View child) {
            if (!child.hasIdentityMatrix()) {
                child.getInverseMatrix().mapPoints(point); //重新计算point坐标
            }
        }
    
    View.java 
        //获取逆矩阵
        public final Matrix getInverseMatrix() {
            final Matrix matrix = mTransformationInfo.mInverseMatrix;
            mRenderNode.getInverseMatrix(matrix);
            return matrix;
        }
    
     /*package*/ final boolean pointInView(float localX, float localY) {
            return pointInView(localX, localY, 0);
        }
        
        //最终计算位置
        public boolean pointInView(float localX, float localY, float slop) {
            return localX >= -slop && localY >= -slop && localX < ((mRight - mLeft) + slop) &&
                    localY < ((mBottom - mTop) + slop);
        }
    

    结论: 事件处理计算坐标的时候,只是把属性动画考虑进去了, 并没有把补间动画算进去, 所以属性动画运动后,点击触摸事件是可以触发的,补间动画则不行.

    思考: 如果补间动画也需要处理点击触摸事件, 那怎么办呢?能看懂本章内容的话, 那应该很好解决了.

    以上内容, 仅代表个人观点, 如有不同意见, 欢迎指出一起讨论.

    相关文章

      网友评论

        本文标题:android View动画---设计理念

        本文链接:https://www.haomeiwen.com/subject/ymcoirtx.html