美文网首页
一文了解android动画

一文了解android动画

作者: 不做android | 来源:发表于2019-12-18 10:47 被阅读0次

    动画分类

    最常见的动画种类

    • 帧动画
    • tween 动画
    • PropertyAnimation 属性动画
    • layoutAnimation 布局动画(用的是tween动画,作用在viewgroup上)
    • 转场动画(基于 transition)
    • vector动画

    tween动画

    Tween Animation,即补间动画,它提供了淡入淡出alpha、缩放scale、旋转rotate、移动translate等效果。它涉及的主要类如下

    • Animation
    • AlphaAnimation
    • ScaleAnimation
    • RotateAnimation
    • TranslateAnimation
    • AnimationSet
      其中后几个均为Animation的子类,具体实现不做讲解

    PropertyAnimation 属性动画

    Android3.0之后推出的属性动画,旨在替换Tween动画,凡是Tween动画可以实现的效果都可以通过属性动画实现。主要原理就是通过改变对象的某一属性达到动画的效果。

    Android官网对属性动画的特征描述如下:

    • Duration: You can specify the duration of an animation. The default length is 300 ms.
    • 可以指定动画的持续时间,默认长度为300ms
    • Time interpolation: You can specify how the values for the property are calculated as a function of the animation's current elapsed time.
    • 时间差值:可以指定如何计算属性值作为动画当前已用时间的函数。
    • Repeat count and behavior: You can specify whether or not to have an animation repeat when it reaches the end of a duration and how many times to repeat the animation. You can also specify whether you want the animation to play back in reverse. Setting it to reverse plays the animation forwards then backwards repeatedly, until the number of repeats is reached.
    • 重复计数和行为:可以指定是否在达到了动画结束时继续重复动画以及重复的次数,也可以指定是否反转动画播放。设置为反向向前播放动画然后反复播放,知道达到设置的重复次数。
    • Animator sets: You can group animations into logical sets that play together or sequentially or after specified delays.
    • 动画设置:可以将动画分组为一起或按顺序或在指定延迟后播放的逻辑集合。
    • Frame refresh delay: You can specify how often to refresh frames of your animation. The default is set to refresh every 10 ms, but the speed in which your application can refresh frames is ultimately dependent on how busy the system is overall and how fast the system can service the underlying timer.
    • 帧刷新延迟:可以指定刷新动画帧的频率。默认设置为每10ms刷新一次,但应用程序刷新帧的速度最终取决于系统整体的繁忙程度以及系统为基础计时器提供服务的速度。

    属性动画涉及的核心类

    • ValueAnimator
    • ObjectAnimator

    接下来通过具体实例来分析一下属性动画的实现原理:
    一个简单的动画:

     val roateAnimator = ObjectAnimator.ofFloat(tv_hello, "rotation", 0f, 360f)
     rotateAnimator.duration = 1000;
     rotateAnimator.start()
    

    可以看到一个动画的创建到执行是很简单的,接下来详细介绍每一步的过程:
    从ObjectAnimator.ofFloat()开始:

     /**
         *  构建一个返回值为 float 的 ObjectAnimator 的实例
         * @param target The object whose property is to be animated. This object should 
         * have a public method on it called <code>setName()</code>, where <code>name</code> is
         * the value of the <code>propertyName</code> parameter.
         * @param propertyName The name of the property being animated.
         * @param values A set of values that the animation will animate between over time.
         * @return An ObjectAnimator object that is set up to animate between the given values.
         */
        public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
            ObjectAnimator anim = new ObjectAnimator(target, propertyName);
            anim.setFloatValues(values);
            return anim;
        }
    
    

    该方法的作用就是创建了一个ObjectAnimator的实例,并为该实例设置values,接下来看ObjectAnimator做了什么。

    private ObjectAnimator(Object target, String propertyName) {
            setTarget(target);
            setPropertyName(propertyName);
        }
    

    可以看到分别调用了setTarget()和setPropertyName()方法,接下来分别介绍一下这两个方法做了什么。

    setTarget()

    @Override
        public void setTarget(@Nullable Object target) {
            final Object oldTarget = getTarget();
            if (oldTarget != target) {
                if (isStarted()) {
                    cancel();
                }
                mTarget = target == null ? null : new WeakReference<Object>(target);
                // New target should cause re-initialization prior to starting
                mInitialized = false;
            }
        }
    

    从代码可以看出,先获取旧动画对象,并判断旧对象是否和新的对象是一致的,如果不一致的话,要看旧对象是否是动画开始了的状态,如果是,先取消动画,并将动画对象保存在一个弱引用中。

    setPropertyName()

    public void setPropertyName(@NonNull String propertyName) {
            // mValues could be null if this is being constructed piecemeal. Just record the
            // propertyName to be used later when setValues() is called if so.
            if (mValues != null) {
                PropertyValuesHolder valuesHolder = mValues[0];
                String oldName = valuesHolder.getPropertyName();
                valuesHolder.setPropertyName(propertyName);
                mValuesMap.remove(oldName);
                mValuesMap.put(propertyName, valuesHolder);
            }
            mPropertyName = propertyName;
            // New property/values/target should cause re-initialization prior to starting
            mInitialized = false;
        }
    

    该方法的主要是记录propertyName,用HashMap来保存propertyName和PropertyValuesHolder。
    初始化方法看完后接下来看setFloatValues() 方法做了什么:

    setFloatValues()

    @Override
        public void setFloatValues(float... values) {
            if (mValues == null || mValues.length == 0) {
                // No values yet - this animator is being constructed piecemeal. Init the values with
                // whatever the current propertyName is
                if (mProperty != null) {
                    setValues(PropertyValuesHolder.ofFloat(mProperty, values));
                } else {
                    setValues(PropertyValuesHolder.ofFloat(mPropertyName, values));
                }
            } else {
                super.setFloatValues(values);
            }
        }
    

    values有值的时候会调super.setFloatValues(values),跟进看一下:

    if (values == null || values.length == 0) {
                return;
            }
            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;
    

    至此可以看到,values最终会构建PropertyValuesHolder,最后存放在HashMap中,接下来看一下PropertyValuesHolder.ofFloat("", values)这个方法做了什么。

    PropertyValuesHolder.ofFloat()

    public static PropertyValuesHolder ofFloat(String propertyName, float... values) {
            return new FloatPropertyValuesHolder(propertyName, values);
        }
    

    构造了一个FloatPropertyValuesHolder对象并返回,再看一下FloatPropertyValuesHolder ()方法做了什么

    public FloatPropertyValuesHolder(String propertyName, float... values) {
                super(propertyName);
                setFloatValues(values);
            }
    

    做了两步操作,传递propertyName给父类,调用setFloatValues()方法。

    FloatPropertyValuesHolder#setFloatValues()

     @Override
            public void setFloatValues(float... values) {
                super.setFloatValues(values);
                mFloatKeyframes = (Keyframes.FloatKeyframes) mKeyframes;
            }
    

    这里又调了父类的setValues()

    PropertyValuesHolder#setValues()

    public void setFloatValues(float... values) {
            mValueType = float.class;
            mKeyframes = KeyframeSet.ofFloat(values);
        }
    

    在这里初始化了动画的关键帧,通过KeyframeSet.ofFloat(values)来构建关键帧,KeyframeSetKeyframes的一个实现类,可以看一下这个方法做了什么。

    KeyframeSet#ofFloat()

    public static KeyframeSet ofFloat(float... values) {
            boolean badValue = false;
            //首先获取关键帧数
            int numKeyframes = values.length;
            //关键帧至少要2帧
            FloatKeyframe keyframes[] = new FloatKeyframe[Math.max(numKeyframes,2)];
            //构造关键帧
            if (numKeyframes == 1) {
                keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f);
                keyframes[1] = (FloatKeyframe) Keyframe.ofFloat(1f, values[0]);
                if (Float.isNaN(values[0])) {
                    badValue = true;
                }
            } else {
                keyframes[0] = (FloatKeyframe) Keyframe.ofFloat(0f, values[0]);
                for (int i = 1; i < numKeyframes; ++i) {
                    keyframes[i] =
                            (FloatKeyframe) Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]);
                    if (Float.isNaN(values[i])) {
                        badValue = true;
                    }
                }
            }
            if (badValue) {
                Log.w("Animator", "Bad value (NaN) in float animator");
            }
            return new FloatKeyframeSet(keyframes);
        }
    

    构建关键帧的函数Keyframe.ofFloat()

    Keyframe#ofFloat()

    public static Keyframe ofFloat(float fraction, float value) {
            return new FloatKeyframe(fraction, value);
        }
    

    两个参数fractionvalue,fraction可以看作序号,value对应值,最终所有的关键帧构造一个集合返回给PropertyValuesHolder.
    至此一个ObjectAnimator创建完成。

    接下来看一下setDuration()做了什么

    @NonNull
        public ObjectAnimator setDuration(long duration) {
            super.setDuration(duration);
            return this;
        }
    @Override
        public ValueAnimator setDuration(long duration) {
            if (duration < 0) {
                throw new IllegalArgumentException("Animators cannot have negative duration: " +
                        duration);
            }
            mDuration = duration;
            return this;
        }
    

    可以看到只是对mDuration做了一个赋值。

    动画的执行

    通过调用start()方法

    AnimationHandler.getInstance().autoCancelBasedOn(this);
            if (DBG) {
                Log.d(LOG_TAG, "Anim target, duration: " + getTarget() + ", " + getDuration());
                for (int i = 0; i < mValues.length; ++i) {
                    PropertyValuesHolder pvh = mValues[i];
                    Log.d(LOG_TAG, "   Values[" + i + "]: " +
                        pvh.getPropertyName() + ", " + pvh.mKeyframes.getValue(0) + ", " +
                        pvh.mKeyframes.getValue(1));
                }
            }
            super.start();
    

    AnimationHandler.getInstance().autoCancelBasedOn(this)这个方法将现有动画取消,接着调用父类的start();

    @Override
        public void start() {
            start(false);
        }
    

    父类方法调用自己的私有方法

    private void start(boolean playBackwards) {
            if (Looper.myLooper() == null) {
                throw new AndroidRuntimeException("Animators may only be run on Looper threads");
            }
            mReversing = playBackwards;
            mSelfPulse = !mSuppressSelfPulseRequested;
            // Special case: reversing from seek-to-0 should act as if not seeked at all.
            if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
                if (mRepeatCount == INFINITE) {
                    // Calculate the fraction of the current iteration.
                    float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                    mSeekFraction = 1 - fraction;
                } else {
                    mSeekFraction = 1 + mRepeatCount - mSeekFraction;
                }
            }
            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);
                }
            }
        }
    
    • playBackwards参数代表动画是否是逆向
    • mSelfPulse = !mSuppressSelfPulseRequested;设置脉冲为true
    • mStarted = true;
      mPaused = false;
      mRunning = false;
      mAnimationEndRequested = false;
      mLastFrameTime = -1;
      mFirstFrameTime = -1;
      mStartTime = -1;各种状态重置
    • addAnimationCallback(0);添加回调
    • startAnimation();开始执行动画
      可以看到主要的调用方法是addAnimationCallback(0)startAnimation()setCurrentPlayTime(0)

    addAnimationCallback(0)

    private void addAnimationCallback(long delay) {
            if (!mSelfPulse) {
                return;
            }
            getAnimationHandler().addAnimationFrameCallback(this, delay);
        }
    

    可以看到是在AnimationHander中添加一个回调,代码如下:

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

    最终会被添加到mAnimationCallbacks这个集合当中。

    startAnimation()

    private void startAnimation() {
            if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
                Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                        System.identityHashCode(this));
            }
    
            mAnimationEndRequested = false;
            initAnimation();
            mRunning = true;
            if (mSeekFraction >= 0) {
                mOverallFraction = mSeekFraction;
            } else {
                mOverallFraction = 0f;
            }
            if (mListeners != null) {
                notifyStartListeners();
            }
        }
    

    这里需要关注的方法只有initAnimation()

    void initAnimation() {
            if (!mInitialized) {
                int numValues = mValues.length;
                for (int i = 0; i < numValues; ++i) {
                    mValues[i].init();
                }
                mInitialized = true;
            }
        }
    

    可以看到这里是对mValues的初始化,而mValues在上面说到过是PropertyValuesHolder的数组,最终这里是对PropertyValuesHolder的初始化,来看一下init()方法做了什么。

    void init() {
            if (mEvaluator == null) {
                // We already handle int and float automatically, but not their Object
                // equivalents
                mEvaluator = (mValueType == Integer.class) ? sIntEvaluator :
                        (mValueType == Float.class) ? sFloatEvaluator :
                        null;
            }
            if (mEvaluator != null) {
                // KeyframeSet knows how to evaluate the common types - only give it a custom
                // evaluator if one has been set on this class
                mKeyframes.setEvaluator(mEvaluator);
            }
        }
    
    

    这里主要就是给关键帧设置了估值器,至此startAnimation()的工作做完了,接下来看setCurrentPlayTime()

    setCurrentPlayTime()

    public void setCurrentPlayTime(long playTime) {
            float fraction = mDuration > 0 ? (float) playTime / mDuration : 1;
            setCurrentFraction(fraction);
        }
    

    可以看到这个方法中传进来的playTime为0,mDuration我们之前设置过值,所以可以得到fraction的值为0,接下来看setCurrentFraction(fraction)方法。

    public void setCurrentFraction(float fraction) {
            initAnimation();
            fraction = clampFraction(fraction);
            mStartTimeCommitted = true; // do not allow start time to be compensated for jank
            if (isPulsingInternal()) {
                long seekTime = (long) (getScaledDuration() * fraction);
                long currentTime = AnimationUtils.currentAnimationTimeMillis();
                // Only modify the start time when the animation is running. Seek fraction will ensure
                // non-running animations skip to the correct start time.
                mStartTime = currentTime - seekTime;
            } else {
                // If the animation loop hasn't started, or during start delay, the startTime will be
                // adjusted once the delay has passed based on seek fraction.
                mSeekFraction = fraction;
            }
            mOverallFraction = fraction;
            final float currentIterationFraction = getCurrentIterationFraction(fraction, mReversing);
            animateValue(currentIterationFraction);
        }
    

    看到该方法再次调用了initAnimation(),但是之前已经调用过,没什么影响。clampFraction(fraction)方法跟进

    private float clampFraction(float fraction) {
            if (fraction < 0) {
                fraction = 0;
            } else if (mRepeatCount != INFINITE) {
                fraction = Math.min(fraction, mRepeatCount + 1);
            }
            return fraction;
        }
    

    可以看到这个方法只是重新调整了fraction的范围,保证其范围在 [0, mRepeatCount + 1]。再回到setCurrentFraction()方法中,关键函数为animateValue(currentIterationFraction)

    void animateValue(float fraction) {
            fraction = mInterpolator.getInterpolation(fraction);
            mCurrentFraction = fraction;
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].calculateValue(fraction);
            }
            if (mUpdateListeners != null) {
                int numListeners = mUpdateListeners.size();
                for (int i = 0; i < numListeners; ++i) {
                    mUpdateListeners.get(i).onAnimationUpdate(this);
                }
            }
        }
    

    该方法在父类ValueAnimator中。做了以下几件事,获取时间插值并赋值给估值器,最后通知回调。注意到在子类ObjectAnimator当中对animateValue()的调用。

    void animateValue(float fraction) {
            final Object target = getTarget();
            if (mTarget != null && target == null) {
                // We lost the target reference, cancel and clean up. Note: we allow null target if the
                /// target has never been set.
                cancel();
                return;
            }
    
            super.animateValue(fraction);
            int numValues = mValues.length;
            for (int i = 0; i < numValues; ++i) {
                mValues[i].setAnimatedValue(target);
            }
        }
    

    在子类调用了父类的animateValue(),也就是说父类做了时间插值和估值器的计算,接下来就要修改target的属性值。

    setAnimatedValue()

    void setAnimatedValue(Object target) {
                if (mIntProperty != null) {
                    mIntProperty.setValue(target, mIntAnimatedValue);
                    return;
                }
                if (mProperty != null) {
                    mProperty.set(target, mIntAnimatedValue);
                    return;
                }
                if (mJniSetter != 0) {
                    nCallIntMethod(target, mJniSetter, mIntAnimatedValue);
                    return;
                }
                if (mSetter != null) {
                    try {
                        mTmpValueArray[0] = mIntAnimatedValue;
                        mSetter.invoke(target, mTmpValueArray);
                    } catch (InvocationTargetException e) {
                        Log.e("PropertyValuesHolder", e.toString());
                    } catch (IllegalAccessException e) {
                        Log.e("PropertyValuesHolder", e.toString());
                    }
                }
            }
    

    这里可以看到通过反射来修改属性值。至此动画一帧关键帧的执行结束。
    那动画的完整执行是靠什么实现的呢?这里靠的是Android的重绘来实现的。具体如下
    在最开始的start()方法中,我们曾注册了AnimationHandler.AnimationFrameCallback,在这个callback中有一个方法叫doAnimationFrame,在子类的实现如下

    public final boolean animateBasedOnTime(long currentTime) {
           boolean finished = animateBasedOnTime(currentTime);
            if (finished) {
                endAnimation();
            }
            return finished;
        }
    

    可以看到调用了关键函数animateBasedOnTime ()

    boolean animateBasedOnTime(long currentTime) {
            boolean done = false;
            if (mRunning) {
                .....
                float currentIterationFraction = getCurrentIterationFraction(
                        mOverallFraction, mReversing);
                animateValue(currentIterationFraction);
            }
            return done;
        }
    

    主要做的就是计算出currentIterationFraction,然后再调用animateValue(currentIterationFraction)来执行动画。也就是说doAnimationFrame ()被不断调用就会不断的产生关键帧。而是谁再调用doAnimationFrame ()呢?最终发现在AnimationHandler中有对它的调用。

    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
            @Override
            public void doFrame(long frameTimeNanos) {
                doAnimationFrame(getProvider().getFrameTime());
                if (mAnimationCallbacks.size() > 0) {
                    getProvider().postFrameCallback(this);
                }
            }
        };
    

    可以看到AnimationHandler实现了Choreographer.FrameCallback,这个函数控制的就是android的重绘。Choreographer会在每秒产生60个
    vsync来通知 view tree 进行 view 的重绘。在每次产生vsync后就会通知回调。从而就会导致doAnimationFrame ()被不断的调用,最终达到动画的效果。
    以上,对于属性动画的了解至此为止。

    layoutAnimation布局动画

    基本没有人用的一个动画,简单介绍下几个概念:

    • layoutAnimation
    • gridLayoutAnimation
    • animateLayoutChanges
    • LayoutTransition

    前两种较容易理解,一种是作用于ViewGroup上,ViewGroup首次创建后会执行的动画。第二种作用于网格布局上。这两种动画只能用tween动画。
    animateLayoutChanges作用于ViewGroup创建显示后,内容改变时对于新的内容做动画,配合layoutAnimation使用。
    LayoutTransition比layoutAnimation稍微高级一点,用属性动画实现布局的动画。

    看一个简单的demo

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout
            xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:tools="http://schemas.android.com/tools"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            android:layoutAnimation="@anim/layout_animation"
            tools:context=".MainActivity">
    
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Hello World!"
                android:textSize="30dp"
                android:layout_gravity="center"
        />
    
    
    </LinearLayout>
    

    可以看到只需要在根结点加入android:layoutAnimation="@anim/layout_animation"一行代码,对应的动画文件为

    <?xml version="1.0" encoding="utf-8"?>
    <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
                     android:animation="@anim/animation_enter"
                     android:animationOrder="random"
                     android:delay="0.5"
    />
    

    <?xml version="1.0" encoding="utf-8"?>
    <translate xmlns:android="http://schemas.android.com/apk/res/android"
               android:duration="1000"
               android:fromYDelta="100%p"
               android:toYDelta="0"
    />
    

    layoutAnimation中几个参数的解释:

    • animation 指定的动画
    • animationOrder 是子元素动画的顺序,三种选项:nomal,reverserandom,其中normal表示顺序显示,reverse表示逆向显示,random表示随机播放入场动画。
    • delay 是子元素开始动画的时间延迟.

    当然layoutAnimation也可以通过代码动态添加,这里就不做过多的介绍了。

    转场动画

    转场动画分为两个部分,第一个部分是5.0以前的常规transition动画,另一部分则是5.0以后的transition动画。
    5.0以前传统实现在这里不做过多介绍,主要看以下5.0之后的实现方式。
    5.0以上几个transition的默认实现有:

    • Explode :爆炸效果
    • Slide 侧滑效果
    • Fade 淡入效果
    • Share Element 共享元素
      使用方法:
      通过设置window的状态:
    • setExitTransition() :当前页面退出时view对应的动画。
    • setEnterTransition() :打开新页面时新页面view对应的动画。
    • setReturnTransition():当前页面返回时上级页面时,当前页面中view执行的动画。
    • setReenterTransition():当前页面返回时上级页面时,上级页面中view执行的动画。

    同样可以在theme中定义:

    • android:windowExitTransition
    • android:windowEnterTransition
    • android:windowReturnTransition
    • android:windowReenterTransition
     <item name="android:windowEnterAnimation">@anim/animation_enter</item>
     <item name="android:windowExitAnimation">@anim/animation_enter</item>
     <item name="android:windowReturnTransition">@anim/animation_enter</item>
     <item name="android:windowReenterTransition">@anim/animation_enter</item>
    

    需要知道的是,转场动画是对页面根结点视图中所有view都执行的动画。

    Explode动画

    explode.gif

    代码如下

    在activityA中
                val intent = Intent()
                val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this).toBundle()
                intent.setClass(this, ExplodeAnimationActivity().javaClass)
                startActivity(intent, options)
    
    在activityB中
        val explode= Explode()
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            window.enterTransition=explode
            window.exitTransition=explode
            setContentView(R.layout.activity_explode_animation)
    
        }
    

    其余两种动画的实现方式和Explode一样,可以看到这三种动画的使用方式还是很简单的。

    相关文章

      网友评论

          本文标题:一文了解android动画

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