View 动画的实现原理以及自定义实现

一、View动画的实现原理

View 动画的简单使用,以平移动画为例

TranslateAnimation translateAnimation = new TranslateAnimation(0, 0, 200, 200);
translateAnimation.setDuration(1000);
// 开始动画
view.startAnimation(translateAnimation);

看看View里面的startAnimation()方法

public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidate();
}

public void setAnimation(Animation animation) {
    mCurrentAnimation = animation;
    if (animation != null) {
        animation.reset();
    }
}

先是调用View.setAnimation(Animation)方法给自己设置一个Animation对象,这个对象是View类中的一个名为mCurrentAnimation的成员变量。

然后它调用invalidate()来重绘自己。

调用 invalidate() 方法之后,会让 ViewRootImpl 调用 scheduleTraversals() 发起一个重绘请求,通过 Choreographer 发送一个异步消息,同时在 Choreographer 中处理消息,最终回调到 performTraversals() 执行重绘。

当执行 performTraversals() 方法后,会进行重绘,最终会调用 view 的 draw() 函数进行绘制,在绘制函数中如果发现 getAnimation() 不为 null,将进行动画绘制,执行 applyLegacyAnimation() 方法

下面来看看View.draw(Canvas, ViewGroup, long)

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {

    // ......

    // 得到前面设置的Animation,也就是我们需要执行的动画
    final Animation a = getAnimation();
    if (a != null) {
        // 执行动画
        more = drawAnimation(parent, drawingTime, a, scalingRequired);
        concatMatrix = a.willChangeTransformationMatrix();
        if (concatMatrix) {
            mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
        }
        transformToApply = parent.getChildTransformation();
    } else {
         // ......
    }

    // ......

    return more;
}

下面我们来看看动画的执行流程

private boolean drawAnimation(ViewGroup parent, long drawingTime,
        Animation a, boolean scalingRequired) {
    Transformation invalidationTransform;
    final int flags = parent.mGroupFlags;
    final boolean initialized = a.isInitialized();
    if (!initialized) {
        // 动画的初始化
        a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
        a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
        if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
        onAnimationStart();
    }

    final Transformation t = parent.getChildTransformation();
    boolean more = a.getTransformation(drawingTime, t, 1f);
    if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
        if (parent.mInvalidationTransformation == null) {
            parent.mInvalidationTransformation = new Transformation();
        }
        invalidationTransform = parent.mInvalidationTransformation;
        // 执行动画
        a.getTransformation(drawingTime, invalidationTransform, 1f);
    } else {
        invalidationTransform = t;
    }

    if (more) {
        if (!a.willChangeBounds()) {
            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) {
                // The child need to draw an animation, potentially offscreen, so
                // make sure we do not cancel invalidate requests
                parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                parent.invalidate(mLeft, mTop, mRight, mBottom);
            }
        } else {
            if (parent.mInvalidateRegion == null) {
                parent.mInvalidateRegion = new RectF();
            }
            final RectF region = parent.mInvalidateRegion;
            a.getInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop, region,
                    invalidationTransform);

            // The child need to draw an animation, potentially offscreen, so
            // make sure we do not cancel invalidate requests
            parent.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

            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 的 initialize() 方法以及 getTransformation() 方法 ,在 getTransformation() 中会调用 applyTransformation() 方法。

这个方法TranslateAnimation没有实现,我们看看它的父类Animation的getTransformation()方法。

Animation在其getTransformation函数被调用时会计算一帧动画数据,并且我们需要了解Animation的下面属性。

mStartTime:动画实际开始时间
mStartOffset:动画延迟时间
mFillEnabled:mFillBefore及mFillAfter是否使能
mFillBefore:动画结束之后是否需要进行应用动画
mFillAfter:动画开始之前是否需要进行应用动画
mDuration:单次动画运行时长
mRepeatMode:动画重复模式(RESTART、REVERSE)
mRepeatCount:动画重复次数(INFINITE,直接值)
mInterceptor:动画插间器
mListener:动画开始、结束、重复回调监听器

准备工作完毕,我们来具体看看代码实现。

public boolean getTransformation(long currentTime, Transformation outTransformation,
        float scale) {
    mScaleFactor = scale;
    return getTransformation(currentTime, outTransformation);
}

public boolean getTransformation(long currentTime, Transformation outTransformation) {
    // 若startTime为START_ON_FIRST_FRAME(值为-1)时,将startTime设定为curTime
    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 {
        // time is a step-change with a zero duration
        normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
    }

    final boolean expired = normalizedTime >= 1.0f;
    mMore = !expired;

    if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

    if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
        if (!mStarted) {
            // 若当前为第一帧动画,触发mListener.onAnimationStart
            fireAnimationStart();
            mStarted = true;
            if (USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
        }

        if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if (mCycleFlip) {
            normalizedTime = 1.0f - normalizedTime;
        }
        // 根据插间器mInterpolator调整动画进度
        final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
        // 执行动画
        applyTransformation(interpolatedTime, outTransformation);
    }


    // 是否动画执行完成
    if (expired) {
        // 是否重复执行动画
        if (mRepeatCount == mRepeated) {
            if (!mEnded) {
                mEnded = true;
                guard.close();
                // 触发mListener.onAnimationEnd
                fireAnimationEnd();
            }
        } else {
            if (mRepeatCount > 0) {
                mRepeated++;
            }

            if (mRepeatMode == REVERSE) {
                mCycleFlip = !mCycleFlip;
            }

            mStartTime = -1;
            mMore = true;
            // mListener.onAnimationRepeat
            fireAnimationRepeat();
        }
    }

    if (!mMore && mOneMoreTime) {
        mOneMoreTime = false;
        return true;
    }

    return mMore;
}

从上面,我们知道两点:
1、插值器可以控制动画执行的进度
2、真正的每一帧动画是在applyTransformation() 方法中。

二、View动画的自定义实现

从上面可以看到实现自定义的动画,我们只需要实现applyTransformation()就可以了。

我们首先来说说动画的原理。

实际上我们平常利用 Matrix 来进行 Translate(平移)、Scale(缩放)、Rotate(旋转)的操作,就是在操作着这个矩阵中元素的数值来达到我们想要的效果。

Matrix 是 Android SDK 提供的一个矩阵类,它代表一个 3 X 3 的矩阵

Translate(平移)、Scale(缩放)、Rotate(旋转)的基本矩阵运算。

这里写图片描述

这里写图片描述

这里写图片描述

如上,可以依次看到平移、缩放、旋转的矩阵,其中

x’,y’)表示执行操作后的点的坐标,(x,y)表示执行操作前的点的坐标
tx、ty 分别表示x轴、y轴上平移的距离,Sx、Sy 分别表示x轴、y轴上的缩放比例
θ 则表示旋转角度

计算机图形应用涉及到几何变换,主要包括平移、旋转、缩放。以矩阵表达式来计算这些变换时,平移是矩阵相加,旋转和缩放则是矩阵相乘。为了方便计算,所以引入齐次坐标,将平移的加法合并用乘法表示。所以,上面的2 X 2 的矩阵经过一番变换后,成了下面这样的。

这里写图片描述

这里写图片描述

这里写图片描述

一个 Matrix 共有 9 个元素,那么它每个元素的值发生改变会起到什么作用呢?按照前面所示的齐次坐标转换得到 3 X 3 的矩阵和 Android 文档提供的官方结构相对应,我们不难看出下面的对应关系。

从这我们可以看出这个 Matrix 结构中的每个参数发挥着如下作用:

MTRANS_X、MTRANS_Y 同时控制着 Translate
MSCALE_X、MSCALE_Y 同时控制着 Scale
MSCALE_X、MSKEW_X、MSCALE_Y、MSKEW_Y 同时控制着 Rotate
从名称上看,我们可以顺带看出 MSKEW_X、MSKEW_Y 同时控制着 Skew

理解了上面使用Matrix来进行动画操作之后,我们下面就可以进行动画的自定义了。

public class SimpleCustomAnimation extends Animation {

    private int mWidth, mHeight;

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        this.mWidth = width;
        this.mHeight = height;
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        Matrix matrix = t.getMatrix();
        matrix.preScale(interpolatedTime, interpolatedTime);//缩放
        matrix.preRotate(interpolatedTime * 360);//旋转
        //下面的Translate组合是为了将缩放和旋转的基点移动到整个View的中心,不然系统默认是以View的左上角作为基点
        matrix.preTranslate(-mWidth / 2, -mHeight / 2);
        matrix.postTranslate(mWidth / 2, mHeight / 2);
    }
}

这里写图片描述

Matrix 复合变换中 pre 、 post 等操作与其对应的矩阵发生的左乘、右乘变化。

四、通过调用栈来验证上面分析的执行过程

上面分析了整个执行过程,下面我们可以通过调用栈来看看这个过程是否正确的。方法很简单。

在上面的SimpleCustomAnimation的applyTransformation方法中,我们打印一下栈的信息,这样,我们就可以知道从View.startAnimation()到最终执行动画的一帧applyTransformation()的方法调用过程。

参考文章:
Android Animation运行原理详解

Android动画原理分析

【Android】Android 动画原理深入解析

欢迎关注微信公众号:DroidMind
精品内容独家发布平台


呈现与博客不一样的技术干货

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值