Android动画

176 阅读5分钟

android动画主要分成两类(view动画和属性动画),Animation是view动画的基类,Animatior是属性动画的基类。

1 View动画

使用view动画的使用:调用View当中的startAnimation(),设置animation开始时间,设置当前view关联的动画,然后重绘view。

/**
 * Start the specified animation now.
 *
 * @param animation the animation to start now
 */
public void startAnimation(Animation animation) {
    animation.setStartTime(Animation.START_ON_FIRST_FRAME);
    setAnimation(animation);
    invalidateParentCaches();
    invalidate(true);
}

1.1 Animation

Animation在android.view.animation包下,animation的start()方法,在setStartTime中初始化参数。当开始时间设置为 START_ON_FIRST_FRAME 时,动画将在第一次调用 getTransformation(long, Transformation) 时开始。传递给此方法的时间应通过调用 AnimationUtils.currentAnimationTimeMillis() 而不是 System.currentTimeMillis() 来获取。参数:startTimeMillis - 以毫秒为单位的开始时间

/**
 * Convenience method to start the animation the first time
 * {@link #getTransformation(long, Transformation)} is invoked.
 */
public void start() {
    setStartTime(-1);
}

public void setStartTime(long startTimeMillis) {
    mStartTime = startTimeMillis;
    mStarted = mEnded = false;
    mCycleFlip = false;
    mRepeated = 0;
    mMore = true;
}

获取要在指定时间点的动画,如果动画仍在运行则为返回ture。

public boolean getTransformation(long currentTime, Transformation outTransformation) {
    if (mStartTime == -1) {
        mStartTime = currentTime;
    }
    //动画开始的时间,之后动画必须开始,当start offset > 0时,动画的开始时间为startTime + startOffset
    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;
    }
    //如果normalized大于1或者cancel()结束动画
    final boolean expired = normalizedTime >= 1.0f || isCanceled();
    mMore = !expired;
    mFillEnabled指示是否应考虑 fillBefore, fillBefore:动画停止后是否保留在第一帧。
    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(this);
            fireAnimationStart();
            mStarted = true;
            //使用预加载容器将静态初始化隔离到内部类中,这允许 Animation 及其子类在编译时进行初始化
            if (NoImagePreloadHolder.USE_CLOSEGUARD) {
                guard.open("cancel or detach or getTransformation");
            }
            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);
            //当动画以 REVERSE 模式重复时,由 getTransformation(long, Transformation) 设置。
            if (mCycleFlip) {
                normalizedTime = 1.0f - normalizedTime;
            }

            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
            //应用动画,动画执行的关键方法
            applyTransformation(interpolatedTime, outTransformation);
            
            //...之后是判断动画是否结束,是否重复
        }
     }

applyTransformation是Animation的抽象方法,看TranslateAnimation中的实现,

protected void applyTransformation(float interpolatedTime, Transformation t) {
}

Transformation,应用在动画的某个时间点的transfortion,主要用于记录动画信息

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);
}

在view的draw中,获取到Animation

boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
    final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();
   
    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();
        //.....
    } 
private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
        Animation a, boolean scalingRequired) {
        boolean more = a.getTransformation(drawingTime, t, 1f);

从整个Animation的执行过程中,可以看出View动画并没有改变view的top、bottom...等,使用Anmimation动画只是把显示的位置改变了,view.getTop()、getBottom()的值并没有改变,即View动画没有改变View的真正位置。

1.2 Animator

Animator是为动画提供基本支持父类,位于android.animation包下,Animator中主要定义了一些公共方法和接口,start()、cancel()都为空。

Animator类
/**
 * Starts this animation. If the animation has a nonzero startDelay, the animation will start
 * running after that delay elapses. A non-delayed animation will have its initial
 * value(s) set immediately, followed by calls to
 * {@link AnimatorListener#onAnimationStart(Animator)} for any listeners of this animator.
 */
public void start() {
}
public void cancel() {
}
ValueAnimator类
public void start() {
    start(false);
}

/**
 *@param playBackwards 是否反向播放动画
 */
private void start(boolean playBackwards) {
 
    mStarted = true;
    mPaused = false;
    mRunning = false;
    mAnimationEndRequested = false;
    // Resets mLastFrameTime when start() is called, so that if the animation was running,
    // calling start() would put the animation in the
    // started-but-not-yet-reached-the-first-frame phase.
    mLastFrameTime = -1;
    mFirstFrameTime = -1;
    mStartTime = -1;
    //添加动画回调,关键方法
    addAnimationCallback(0);

    if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
        // If there's no start delay, init the animation and notify start listeners right away
        // to be consistent with the previous behavior. Otherwise, postpone this until the first
        // frame after the start delay.
        startAnimation();
        if (mSeekFraction == -1) {
            // No seek, start at play time 0. Note that the reason we are not using fraction 0
            // is because for animations with 0 duration, we want to be consistent with pre-N
            // behavior: skip to the final value immediately.
            setCurrentPlayTime(0);
        } else {
            setCurrentFraction(mSeekFraction);
        }
    }
}

/**
 * Called internally to start an animation by adding it to the active animations list. Must be
 * called on the UI thread.
 */
private void startAnimation() {

    mAnimationEndRequested = false;
    initAnimation();
    mRunning = true;
    if (mSeekFraction >= 0) {
        mOverallFraction = mSeekFraction;
    } else {
        mOverallFraction = 0f;
    }
    if (mListeners != null) {
        notifyStartListeners();
    }
}

/**
 * Register to get a callback on the next frame after the delay.
 */
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    if (mAnimationCallbacks.size() == 0) {
        getProvider().postFrameCallback(mFrameCallback);
    }
    if (!mAnimationCallbacks.contains(callback)) {
        mAnimationCallbacks.add(callback);
    }

    if (delay > 0) {
        mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
}

addAnimationFrameCallBack()中获取到AnimationFrameCallbackProvider,添加mFragmentCallback,当收到屏幕刷新信号后,在回调中执行动画doAnimationFrame()

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        doAnimationFrame(getProvider().getFrameTime());
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};
private void doAnimationFrame(long frameTime) {
    long currentTime = SystemClock.uptimeMillis();
    final int size = mAnimationCallbacks.size();
    for (int i = 0; i < size; i++) {
        //取出AnimationCallback,ValueAnimator实现了该接口
        final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
        if (callback == null) {
            continue;
        }
        if (isCallbackDue(callback, currentTime)) {
            //调用ValueAnimatior的doAnimationFrame
            callback.doAnimationFrame(frameTime);
            if (mCommitCallbacks.contains(callback)) {
                getProvider().postCommitCallback(new Runnable() {
                    @Override
                    public void run() {
                        commitAnimationFrame(callback, getProvider().getFrameTime());
                    }
                });
            }
        }
    }
    cleanUpList();
}

ValueAnimator的doAnimationFrame()

public final boolean doAnimationFrame(long frameTime) {

    mLastFrameTime = frameTime;
    // The frame time might be before the start time during the first frame of
    // an animation.  The "current time" must always be on or after the start
    // time to avoid animating frames at negative time intervals.  In practice, this
    // is very rare and only happens when seeking backwards.
    final long currentTime = Math.max(frameTime, mStartTime);
    //
    boolean finished = animateBasedOnTime(currentTime);

    if (finished) {
        endAnimation();
    }
    return finished;
}
boolean animateBasedOnTime(long currentTime) {
    boolean done = false;
    if (mRunning) {
        mOverallFraction = clampFraction(fraction);
        float currentIterationFraction = getCurrentIterationFraction(
                mOverallFraction, mReversing);
        animateValue(currentIterationFraction);
    }
    return done;
}
void animateValue(float fraction) {
    fraction = mInterpolator.getInterpolation(fraction);
    mCurrentFraction = fraction;
    int numValues = mValues.length;
    for (int i = 0; i < numValues; ++i) {
        //mValues是PropertyValuesHolder
        mValues[i].calculateValue(fraction);
    }
    if (mUpdateListeners != null) {
        int numListeners = mUpdateListeners.size();
        for (int i = 0; i < numListeners; ++i) {
            //动画进度更新回调
            mUpdateListeners.get(i).onAnimationUpdate(this);
        }
    }
}

valueAnimtor只是计算动画执行的进度和位置,属性动画的运行机制是通过不断地对值进行操作来实现的,动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果

/**
 * This class provides a simple timing engine for running animations
 * which calculate animated values and set them on target objects.
 */
public class ValueAnimator extends Animator implements AnimationHandler.AnimationFrameCallback {
    
  ` 构造并返回一个在 int 值之间进行动画处理的 ValueAnimator`
    public static ValueAnimator ofInt(int... values) {
        ValueAnimator anim = new ValueAnimator();
        anim.setIntValues(values);
        return anim;
    }

ValueAnimator的ofInt(int...values)、ofFloat(float...values)传入可变参数,比如int(0,100)表示动画从0到100.OfInt中调用setIntValues().

/**
 * @param values A set of values that the animation will animate between over time.
 * values 一组值,动画将随着时间的推移在这些值之间设置动画
 */
public void setFloatValues(float... values) {
    if (values == null || values.length == 0) {
        return;
    }
    //PropertyValuesHolder[] mValues--记录属性值的holder
    if (mValues == null || mValues.length == 0) {
        setValues(PropertyValuesHolder.ofFloat("", values));
    } else {
        PropertyValuesHolder valuesHolder = mValues[0];
        valuesHolder.setFloatValues(values);
    }
    // New property/values/target should cause re-initialization prior to starting
    mInitialized = false;
}

public void setValues(PropertyValuesHolder... values) {
    int numValues = values.length;
    mValues = values;
    mValuesMap = new HashMap<String, PropertyValuesHolder>(numValues);
    for (int i = 0; i < numValues; ++i) {
        PropertyValuesHolder valuesHolder = values[i];
        mValuesMap.put(valuesHolder.getPropertyName(), valuesHolder);
    }
    // New property/values/target should cause re-initialization prior to starting
    mInitialized = false;
}

总结

属性动画和view动画都通过矩阵变化来实现,在view.drawChild()中调用canvas.connat()移动view。

为什么属性动画移动一个控件后,目标位置仍然能响应用户事件?

我们知道,调用View的translationXX方法之后,虽然在屏幕上的位置是变了,但是它的[left,top,right,bottom]是不会变的。属性动画改变了translationXX的值,getleft、top...在重新布局的时候才会改变。

来捋一遍ViewGroup分派事件的大致流程:

当手指按下时,触摸事件会经过ViewGroup中的dispatchTouchEvent方法筛选符合条件(手指在边界范围内)的子View进行分派事件(如果未被onInterceptTouchEvent拦截的话)。在分发事件时,判断是否进行了动画平移,将点击事件也转换到对应View位置。

调用链ViewGroup类dispatchTouchEventdispatchTransformedTouchEvent

image.png 那么,如果某个子View刚好应用了translation属性动画,在ViewGroup筛选子View时,直接判断触摸点是否在[left,top,right,bottom]范围内,是肯定不行的。

那它是怎么判断的呢?
  1. 它会先调用子View的hasIdentityMatrix方法来判断这个View是否应用过位移、缩放、旋转之类的属性动画。
  2. 如果应用过的话,那接下来还会把触摸点映射到该子View的逆矩阵上(getInverseMatrix)。
  3. 判断处理后的触摸点,是否在该子View的边界范围内。
那么为什么只有属性动画可以这样,补间动画就不行呢?

View在draw的时候,会检测是否设置了Animation(补间动画),

如果有的话,会获取这个动画当前的值(旋转或位移或缩放,透明度等),应用到canvas上,然后把东西draw出来。

比如设置了位移动画,当前值是向右移动了100,那么效果就等于这样:

Matrix matrix = new Matrix();

matrix.setTranslate(100, 0);

canvas.setMatrix(matrix);

它的作用只会在draw的时候有效。

虽然大家都是操作Matrix,但是Matrix的对象不一样(属性动画操作的Matrix,是View的mRenderNode所对应的Matrix),

所以在ViewGroup筛选的时候,应用属性动画的View会被正确找到,而补间动画的不行。

3 Transition转场动画

Transition位于android.transition下 Transition 包含有关在场景更改期间将在其目标上运行的动画的信息。这个抽象类的子类可以编排几个子转换(TransitionSet)或者它们可以自己执行自定义动画。任何转换都有两个主要工作:(1)捕获属性值,以及(2)根据捕获的属性值的变化播放动画。自定义transition 知道它对 View 对象的哪些属性值感兴趣,也知道如何对这些值的更改进行动画处理。例如,Fade transition 跟踪对可见性相关属性的更改,并能够构建和运行使项目淡入淡出的动画或基于对这些属性的更改.