美文网首页
补间动画原理

补间动画原理

作者: gczxbb | 来源:发表于2018-04-24 22:31 被阅读94次

    补间动画:设置动画初始与结束状态,中间状态由系统计算并控制。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方法。

    补间动画基本流程图。 补间动画基本流程图.jpg

    任重而道远

    相关文章

      网友评论

          本文标题:补间动画原理

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