Tween Animation原理


补间动画,设置动画初始与结束状态,中间状态由系统计算并控制。Animation是抽象类,它的子类实现动画的具体行为和效果,动画帧的显示与视图关联。四种补间动画类型,平移,旋转,透明度,缩放。

示例介绍

Animation mAnimation = AnimationUtils.loadAnimation(getApplicationContext(), R.anim.tween_anim_sample);
mAnimation.setFillAfter(true);//动画结束后保留结束状态
mAnimation.setAnimationListener(new Animation.AnimationListener() {
    @Override
    public void onAnimationStart(Animation animation) {
    }
    @Override
    public void onAnimationEnd(Animation animation) {
    }
    @Override
    public void onAnimationRepeat(Animation animation) {
    }
});
mTextView.startAnimation(mAnimation);
//xml定义,以缩放为例
<scale
    android:duration="6000"
    android:fromXScale="1.0"
    android:fromYScale="1.0"
    android:pivotX="50%"
    android:pivotY="50%"//缩放的中心点,视图中点
    android:toXScale="0.2"
    android:toYScale="0.2" />

实现一个补间动画,过程很简单,两步就可以。首先创建一个动画对象,可以在xml中定义。然后,对将要进行动画的视图执行startAnimation方法,另外,可以设置动画监听。下面分析一下原理,动画从启动到结束,系统是如何控制的。


动画原理

补间动画基本流程

每个视图都可以实现补间动画,在基类View中定义动画的启动方法。

public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);//值-1
    setAnimation(animation);//为View设置动画
    invalidateParentCaches();
    invalidate(true);//重绘
}

设置Animation内部mStartTime值,(值-1),初始化mStarted和mEnd标志。视图内部mCurrentAnimation赋值,并触发动画#reset方法,动画初始化状态重置。

protected void invalidateParentCaches() {
    if (mParent instanceof View) {
        ((View) mParent).mPrivateFlags |= PFLAG_INVALIDATED;
    }
}

在视图中,找到它的父视图mParent,为父视图增加PFLAG_INVALIDATED标志位。在后面的invalidate方法时,父视图的Canvas将会重建。
invalidate方法,视图重绘,

//invalidateInternal方法代码。
if (invalidateCache) {
    mPrivateFlags |= PFLAG_INVALIDATED;
    mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
}

动画视图增加PFLAG_INVALIDATED标志。
当一个视图执行invalidate方法,硬件渲染时,并非整个树结构视图的Canvas全部重建。从根视图的updateDisplayListIfDirty方法开始,在树结构视图遍历,每个节点都会执行该方法。

//View的updateDisplayListIfDirty方法代码。
if (renderNode.isValid()&& !mRecreateDisplayList) {
    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    dispatchGetDisplayList();//派发子视图
    return renderNode; // no work needed
}

若视图Canvas不需要重建,触发dispatchGetDisplayList方法。

private void recreateChildDisplayList(View child) {
    child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
    child.mPrivateFlags &= ~PFLAG_INVALIDATED;
    child.updateDisplayListIfDirty();
    child.mRecreateDisplayList = false;
}

当动画视图的父视图执行到recreateChildDisplayList方法时,它曾设置过PFLAG_INVALIDATED标志,因此,动画启动后,动画视图与父视图重建Canvas,它的上层视图与动画视图的兄弟视图均不需要重建。第一帧动画重建Canvas,然后除去该标志,后续动画不需要重建。

在父视图的View#updateDisplayListIfDirty方法,Canvas重建,然后,父视图跳过绘制,它自己的onDraw方法不会触发,dispatchDraw方法分发绘制子视图,也就是动画视图。

...
if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
    dispatchDraw(canvas);
    ...
} else {
    draw(canvas);
}

ViewGroup#dispatchDraw方法绘制子视图,包括动画视图与其兄弟视图,触发重载的带三个参数的draw方法,该方法将处理动画事务。该方法是动画视图执行。

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    ...
    final Animation a = getAnimation();
    if (a != null) {//处理动画状态
        more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        transformToApply = parent.getChildTransformation();
    } 
    ...
    if (hardwareAcceleratedCanvas) {
        mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
        mPrivateFlags &= ~PFLAG_INVALIDATED;//除去PFLAG_INVALIDATED标志
    }
    ...
    if (drawingWithRenderNode) {
        renderNode = updateDisplayListIfDirty();
    }
}

首先,判断该视图是否有动画对象,在动画状态下,处理视图上活动的动画,
触发View#applyLegacyAnimation方法。第一帧动画视图重建Canvas,执行onDraw方法,然后,将除去PFLAG_INVALIDATED标志,动画视图后续将不再Canvas重建。

private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
                                     Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
    if (!initialized) {
        //Animation初始化
        a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
        a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
        ...
        //mPrivateFlags加上PFLAG_ANIMATION_STARTED标志
        onAnimationStart();
    }
    //父视图内部Transformation
    final Transformation t = parent.getChildTransformation();
    boolean more = a.getTransformation(drawingTime, t, 1f);
    ...
    if (more) {//成功
        if (!a.willChangeBounds()) {//动画不会改变View的边界,如Alpha动画
            if ((flags & (ViewGroup.FLAG_OPTIMIZE_INVALIDATE | 
                    ViewGroup.FLAG_ANIMATION_DONE)) ==  
                    ViewGroup.FLAG_OPTIMIZE_INVALIDATE) {
                parent.mGroupFlags |= ViewGroup.FLAG_INVALIDATE_REQUIRED;
            } else if ((flags & ViewGroup.FLAG_INVALIDATE_REQUIRED) == 0) {
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                //不改变边界,绘制动画View的区域
                parent.invalidate(mLeft, mTop, mRight, mBottom);
            }
        } else {//动画会改变View的边界,如Scale动画
            if (parent.mInvalidateRegion == null) {
                parent.mInvalidateRegion = new RectF();
            }
            //mInvalidateRegion改变区域
            final RectF region = parent.mInvalidateRegion;
            //该方法的目的就是改变mInvalidateRegion。
            a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                    invalidationTransform);
            parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
            //region是动画过程中的改变区域
            final int left = mLeft + (int) region.left;
            final int top = mTop + (int) region.top;
            parent.invalidate(left, top, left + (int) (region.width() + .5f),
                    top + (int) (region.height() + .5f));
        }
    }
    return more;
}

Animation初始化,入参是动画视图宽高和父视图的宽高(貌似没用到)。
Animation#initializeInvalidateRegion方法将动画视图区域存储在内部的mPreviousRegion对象,动画视图增加PFLAG_ANIMATION_STARTED标志。
获取父视图内部mChildTransformation,将视图改变存储在Transformation的Matrix。
当动画未结束,会继续触发父视图#invalidate(区域)方法重绘。绘制区域包括两种情况,一种是动画未改变视图边界,如透明度动画,另一种是改变视图边界,如Scale动画,这两种情况绘制区域不同。但每一帧动画父视图都会Canvas重建。
willChangeBounds方法,判断边界是否改变,默认改变边界。例如,缩放动画ScaleAnimation会改变边界,透明度动画AlphaAnimation不会改变边界,需重写willChangeBounds方法。
若不改变视图边界,绘制区域是动画视图相对于父视图坐标系的边界坐标(mLeft, mTop, mRight, mBottom)。
若改变视图边界,子视图相对父视图的边界距离不会改变(mLeft/mTop)。子视图宽高不会改变(getHeight与getWidth获取)。
getInvalidateRegion方法,根据视图区域和Transformation变换获取当前需要改变的区域,在父视图中存储。改变的是mInvalidateRegion区域
动画视图在父视图中绘制图。

动画绘制图.png
黄色区域是动画视图在父视图中的位置,当执行放大动画时,逐渐动画扩大成绿色区域(变化),但mLeft和mTop的值不会改变,改变的是mInvalidateRegion区域,子视图相对父视图的边界mLeft/mTop(固定)加上mInvalidateRegion即绿色区域。从图中例子可以看出,放大动画的改变区域left与top都是负值,使得绿色区域相对父视图边界在变小,最终触发父视图#invalidate(绿色区域)绘制,实现动画。动画未结束会一直在绘制。


动画改变分析

public boolean getTransformation(long currentTime, Transformation outTransformation) {
    //若开始时间为-1,说明动画刚开始,设置开始事件为View绘制的当前时间
    if (mStartTime == -1) {
        mStartTime = currentTime;
    }
    final long startOffset = getStartOffset();
    final long duration = mDuration;
    float normalizedTime;
    if (duration != 0) { //经历的时间占总耗时的比例
        normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /(float) duration;
    } else {
        normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
    }
    //比例大于等于1说明动画结束
    final boolean expired = normalizedTime >= 1.0f;
    mMore = !expired;
    ...
    if ((normalizedTime >= 0.0f || mFillBefore) &&
                    (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            fireAnimationStart();
            mStarted = true;
        }
        ...
        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
        applyTransformation(interpolatedTime, outTransformation);
    }
    if (expired) {//动画结束,判断重复
        if (mRepeatCount == mRepeated) {
          
        } else {//重复动画
            if (mRepeatCount > 0) {
                mRepeated++;
            }
            ...
        }
    }
    ...
    return mMore;
}

首先,根据当前时间、开始时间和动画持续时间,计算动画已完成比例,判断动画完成,若比例>=1,说明动画执行已到达持续时间,expired失效,返回结束标志。
normalizedTime时间占比是动画从mStartTime(设置的开始时间)开始计算,已运行时间占总时间的比例。
其次,在动画运行过程中,触发applyTransformation方法,将视图变化写入Transformation内部Matrix,它是一个抽象方法,Animation的子类去实现不同类型动画。插值器Interpolator通过控制时间占比来计算动画运动的变化率。
最后,在动画开始和结束时,根据mStarted标志和重复标志,fireAnimationStart方法和fireAnimationEnd方法,调用动画AnimationListener监听器。若动画重复,自增mRepeated,mStartTime设值-1,重新计时。
下面以子类ScaleAnimation为例,分析实现动画改变的applyTransformation方法。

@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
    float sx = 1.0f;
    float sy = 1.0f;
    //获取Scale因子,在Anmiation类中。
    float scale = getScaleFactor();
    if (mFromX != 1.0f || mToX != 1.0f) {
        sx = mFromX + ((mToX - mFromX) * interpolatedTime);
    }
    if (mFromY != 1.0f || mToY != 1.0f) {
        sy = mFromY + ((mToY - mFromY) * interpolatedTime);
    }
    if (mPivotX == 0 && mPivotY == 0) {
        t.getMatrix().setScale(sx, sy);
    } else {
        t.getMatrix().setScale(sx, sy, scale * mPivotX, scale * mPivotY);
    }
}

视图在x轴Scale的初始比例mFromX,目标比例mToX,当值是1.0f是正常视图。若mFromX和mToX有一个不是1.0f,说明动画正在进行,发生变化。
sx是当前x轴Scale比例值,(mToX-mFromX)是Scale变化差值,interpolatedTime是根据Interpolation计算动画运行进度占比,实现动画速度控制。根据interpolatedTime、开始值(mFromX)和结束值(mToX),计算当前sx和sy。
Matrix#setScale方法,将sx和sy保存在底层Matrix。mPivotX与mPivotY是Scale的中心点,默认值是视图左上角坐标(0,0),中心点不在左上角时,将mPivotX和mPivotY一起保存。
保存Transformation后,getTransformation方法将返回是否动画的标志,接下来继续回到applyLegacyAnimation方法,根据运行标志,计算刷新边界,刷新父视图。下面看一下getInvalidateRegion方法,计算改变的区域。

public void getInvalidateRegion(int left, int top, int right, int bottom,
            RectF invalidate, Transformation transformation) {
    final RectF tempRegion = mRegion;
    final RectF previousRegion = mPreviousRegion;
    //先设置为视图View区域(以View坐标)
    invalidate.set(left, top, right, bottom);
    //底层变换
    transformation.getMatrix().mapRect(invalidate);
    invalidate.inset(-1.0f, -1.0f);
    //invalidate区域值设置成内部mRegion
    tempRegion.set(invalidate);
    //与上次变换的得到的区域合并
    invalidate.union(previousRegion);
    //存储变换后的区域
    previousRegion.set(tempRegion);

    final Transformation tempTransformation = mTransformation;
    final Transformation previousTransformation = mPreviousTransformation;

    tempTransformation.set(transformation);
    transformation.set(previousTransformation);
    //存储此次变换
    previousTransformation.set(tempTransformation);
}

入参是动画视图区域,以视图自己坐标系为标准坐标值(0,0,width,height), RectF区域invalidate在父视图保存,它是上一次动画视图帧的改变区域(变换+合并),将它重新传入,计算这次动画帧的改变区域。

首先,将invalidate设置成动画视图区域(动画视图坐标系),调用Matrix的JNI#native_mapRect(native_instance, dst, src)方法,底层mapRect方法的Matrix变换,将一个src区域(0,0,width,height)转换为一个新的dst目标区域。源区域src和目标区域都是invalidate。因此,转换后的区域保存在invalidate。根据动画运行时间,Matrix转换程度不同。
然后,变换后的目标区域invalidate与之前保存mPreviousRegion区域合并,mPreviousRegion默认初始化动画视图区域,此后,在动画过程中,专门存储每次变换后的区域。tempRegion缓存新dst目标区域,赋值给previousRegion,下一次变换后合并时使用。
最终,invalidate设置的值是以(0,0,width,height)区域进行变换,再+合并的改变区域。

改变区域的本质是动画视图在以自己为坐标系的坐标值(0,0,width,height)区域中,根据当前动画进度计算的Matrix,转换成一个新的坐标区域。
以Scale动画放大视图为例,若中心点是动画视图的中心(不是以左上角为中心),则得到的目标区域left/top是负值。新dst目标区域(-5,-5,width+5,height+5),这个是以动画视图自己坐标系的坐标值。
父视图绘制的区域,invalidate(区域)需要相对父视图的坐标系的坐标值,mLeft/mTop+新dst目标区域,即是动画当前的绘制区域(示意图绿色区域)。


总结

补间动画的核心本质是在一定的持续时间内,不断改变视图Matrix变换,并且不断刷新的过程。

在动画过程中,第一帧刷新父视图和动画视图,后面仅刷新父视图,刷新区域通过计算获取,invalidate(区域),父视图每一帧都会Canvas重建。
在动画启动时,第一帧动画视图Canvas重建,后续动画帧,动画视图不会再Canvas重建。
动画视图的兄弟视图不会Canvas重建,不会触发onDraw方法。


任重而道远

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

推荐阅读更多精彩内容