android View动画---设计理念

本章内容: 了解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);
    }

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

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

以上内容, 仅代表个人观点, 如有不同意见, 欢迎指出一起讨论.
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容