美文网首页
Android动画深入分析

Android动画深入分析

作者: Vinson武 | 来源:发表于2020-02-26 23:40 被阅读0次

    Android的动画可以分为三种:View动画、帧动画和属性动画。(其实帧动画也可以算属于View动画,只是表现形式不同而已)

    View动画

    View动画的作用对象是View,它支持4中动画效果,分别是平移、缩放、旋转和透明度。

    View动画的种类

    View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation。这四种动画可以通过XML来定义也可以通过代码动态创建。(建议XML来定义,可读性更好)

    要使用View动画,首先创建XML文件,文件路径为res/anim/xx.xml(即在res/anim文件夹下建xml文件)

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="300"
        android:interpolator="@android:anim/accelerate_interpolator" //指定插值器
        android:shareInterpolator="true" //是否共用一个插值器
        android:fillAfter = "true" //动画结束后是否停在结束位置>
    
        <alpha
            android:fromAlpha="0.0"
            android:toAlpha="1.0" />
    
        <translate
            android:fromXDelta="500"
            android:toXDelta="0" />
        <rotate
            android:duration="500" //动画时间
            android:fromDegress="0" //旋转起始角度
            android:toDegress="180" //旋转结束角度
            android:pivotX="100" //旋转轴点的X坐标
            android:pivotX="100" //旋转轴点的Y坐标/>
        <set>
        ...
        </set>
    
    </set>
    

    从上面的定义可以看出,View动画可以是单个动画,也可以由一系列动画组成。

    <set>标签表示动画集合,对应AnimationSet类,属性说明:

    • android:interpolator,指定动画集合采用的插值器,影响动画速度。可以不指定,默认为@android:anim/accelerate_decelerate_interpolator.
    • android:shareInterpolator,表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那子动画就需要单独指定所需的插值器或者使用默认值。

    应用xml定义动画

    Button btn =(Button)findViewById(R.id.btn);
    Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test);
    btn.startAnimation(animation);
    

    代码使用动画

    AlphaAnimation alphaAnima = new AlphaAnimation(0, 1);
    alphaAnima.setDuration(300);
    btn.startAnimation(alphaAnima);
    
    添加View动画过程监听

    Animation的setAnimationListener方法可以给View动画添加过程监听

    public static interface AnimationListener {
    
            void onAnimationStart(Animation animation);
    
            void onAnimationEnd(Animation animation);
    
            void onAnimationRepeat(Animation animation);
        }
    

    自定义View动画

    自定义View动画是一件既简单又复杂的事。简单在于,自定义View动画只需继承Animation这个抽象类,然后重写它的initialize和applyTransformation方法,在initialize做一些初始化工作,在applyTransformation进行相应的矩阵变换(可以采用Camera类来简化变换过程)。复杂在于矩阵变换是数学概念,如果不熟悉会比较吃力。
    实际开发中很少自定义View动画。

    帧动画

    帧动画是顺序播放一组预先定义好的图片,系统提供来AnimationDrawable类来使用帧动画。

    首先在XML中定义一个AnimationDrawable

    res/drawable/frame_anim.xml

    <?xml version="1.0" encoding="utf-8"?>
    <animation-list xmlns:android="http://schemas.android.com/apk/res/android" 
    android:oneshot="false">
    
        <item android:drawable="@drawable/image1" android:duration="500" />
    
    </animation-list>
    

    然后将上述Drawable作为View的背景并通过Drawable来播放动画

    Button btn = (Button)findViewById(R.id.btn);
    btn.setBackGroundResource(R.drawable.frame_anim);
    AnimationDrawable drawable = (AnimationDrawable)btn.getBackgraoud();
    drawable.start();
    

    帧动画容易引起OOM,尽量避免使用尺寸较大的图片。

    View动画的特殊使用场景

    View动画还可以在ViewGroup中控制子元素的出场效果,在Activity中可以实现Activity之间的切换效果。

    LayoutAnimation

    LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时都会具有这种动画效果。比如用在ListView上,让item都以某种动画出现。

    定义LayoutAnimation

    res/anim/anim_layout.xml

    <layoutAnimation
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:delay="0.5"
        android:animationOrder="reverse" //播放顺序
        android:animation="@anim/anim_item"/> //item动画效果
    
    

    res/anim/anim_item.xml

    <?xml version="1.0" encoding="utf-8"?>
    <set xmlns:android="http://schemas.android.com/apk/res/android"
        android:duration="300"
        android:interpolator="@android:anim/accelerate_interpolator"
        android:shareInterpolator="true" >
    
        <alpha
            android:fromAlpha="0.0"
            android:toAlpha="1.0" />
    
        <translate
            android:fromXDelta="500"
            android:toXDelta="0" />
    
    </set>
    

    为ViewGroup指定属性android:layoutAnimation="@anim/anim_layout"

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical" >
    
        <ListView
            android:id="@+id/list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layoutAnimation="@anim/anim_layout"
            android:background="#fff4f7f9"
            android:cacheColorHint="#00000000"
            android:divider="#dddbdb"
            android:dividerHeight="1.0px"
            android:listSelector="@android:color/transparent" />
    
    </LinearLayout>
    

    除了在XML中指定LayoutAnimation外,还可以通过LayoutAnimationController来实现

    Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(0.5f);
    listView.setLayoutAnimation(controller);
    

    Activity的切换效果

    Activity有默认的切换效果,我们也可以自定义切换效果,主要用到overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在startActivity(Intent)或者finish()之后被调用才能生效

    • enterAnim:Activity被打开时,所需的动画资源id
    • exitAnim:Activity被暂停时,所需的动画资源id

    (在5.0以上,Material Design有很多炫酷的过度效果)

    Intent intent = new Intent(this, TestActivity.class);
    startActivity(intent);
    overridePendingTransition(R.anim.enter_anim, R.anim.exit_anim);
    

    属性动画

    属性动画可以对任何对象做动画,甚至还可以没有对象。属性动画中又ValueAnimator、ObjectAnimator和AnimatorSet等概念,通过他们可以实现绚丽的动画效果。

    使用属性动画

    属性动画默认时间间隔300ms,默认帧率10ms/帧。

    代码使用

    • 改变一个对象的translationY属性,让其沿着Y轴平移
    ObjectAnimation.ofFloat(myview, "translationY", -myview.getHeight()).start();
    
    • 改变一个对象的背景色
    ValueAnimation colorAnim = ObjectAnimation.ofInt(this, "backgroundColor",0xFFFF8080, 0xFF8080FF);
    colorAnim.setDuration(2000);
    colorAnim.setEvaluator(new ArgbEvaluator());
    colorAnim.start();
    
    • 动画集合
    AnimatorSet set = new AnimatorSet();
    set.playTogether(
    ObjectAnimation.ofFloat(myview, "rotationX", 0, 360));
    ObjectAnimation.ofFloat(myview, "scaleX", 1, 1.5f));
    set.setDuration(3000).start();
    

    属性动画除了用代码实现,同样可以用XML定义。属性动画需要定义在res/animator/目录下(View动画是在res/anim).

    <set
        android:ordering="together">
        <objectAnimator
            android:propertyName="x"
            android:duration = "400"
            android:valueTo="200"
            android:valueType="intType"
        />
    </set>
    
    AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(myContext, R.anim.test);
    set.setTarget(nButton);
    set.start();
    

    理解插值器和估值器

    • 时间插值器TimeInterpolator:根据时间的流逝的百分比来计算出当前属性值改变的百分比。系统预置有线性插值器LinearInterpolator等。
    • 类型估值器TypeEvaluator:根据当前属性改变的百分比来计算改变后的属性值,系统预置有IntEvaluator、FloatEvaluator、ArgbEvaluator(针对颜色)

    看下线性插值器源码

    public class LinearInterpolator implements Interpolator {
    
        public LinearInterpolator() {
        }
        
        public LinearInterpolator(Context context, AttributeSet attrs) {
        }
        //实现接口方法
        public float getInterpolation(float input) {
            return input;
        }
    }
    

    非常简单,输入值和返回值一样,也就是说输入时间流逝百分比是多少,属性值改变百分比就是多少。

    再看下整型估值器源码

    public class IntEvaluator implements TypeEvaluator<Integer> {
    //实现接口方法
        public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
            int startInt = startValue;
            return (int)(startInt + fraction * (endValue - startInt)); //根据属性改变百分比和初始/结束值计算属性值
        }
    }
    
    自定义插值器和估值器

    除了系统提供的插值器和估值器外,我们也可以自定义。自定义
    通过上面代码其实也可以看出,实现自定义插值器只需实现Interpolator或TimeInterpolator接口,实现public float getInterpolation(float input)接口方法即可;实现自定义估值器则是实现TypeEvaluator<T>接口,实现public T evaluate(float fraction, T startValue, T endValue);接口方法即可。

    属性动画的监听器

    属性动画提供来监听器用于监听动画播放过程,主要有如下两个接口:

    public static interface AnimatorListener {
    
            void onAnimationStart(Animator animation);
    
            void onAnimationEnd(Animator animation);
    
            void onAnimationCancel(Animator animation);
    
            void onAnimationRepeat(Animator animation);
        }
    
    public static interface AnimatorUpdateListener{
        void onAnimationUpdate(ValuaAnimator animation);
    }
    

    其中AnimatorUpdateListener会监听整个动画过程,动画是有许多帧组成的,没播放一帧,onAnimationUpdate就会被调用一次。

    对任意属性做动画

    属性动画不是随便传一个属性都会有动画效果的,要想动画生效要同时满足两个条件:

    1. 对象必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去去abc属性的初始值。(如果这条不满足,直接Crash)
    2. 对象的setAbc方法对属性abc所做的修改必须能够通过某种方式反映出来,比如改变UI之类的。(如果这条不满足,动画无效果,不会Crash)

    比如,用属性动画让按钮Button的宽度变到500px

    ObjectAnimator.ofInt(mButton, "width",500).setDuration(2000).start();
    

    不会有效果。因为Button内部虽然提供来getWidth和setWidth 方法,但这个setWidth方法并不是改变视图的大小,它是TextView新加的方法(Button继承TextView所以也有了这个方法)。TextView的setWidth是对应android:width属性,而宽度是由android:layout_width决定的

    public void setWidth(int pixels) {
            mMaxWidth = mMinWidth = pixels;
            mMaxWidthMode = mMinWidthMode = PIXELS;
    
            requestLayout();
            invalidate();
        }
    

    getWidth是获取宽度,但setWidth不是设置宽度,两个干的不是同件事。通过setWidth无法改变控件宽度,所以对width做属性动画无效。

    解决办法

    1. 如果有权限的话,给对象加上正确的get和set方法

    除非是自定义的View,否则系统sdk内部实现的,我们没办法改。不太能用到。

    1. 用一个类来包装原始对象,间接为其提供get和set方法。

    比较方便是实现方法,比如上面button的例子可以通过这个方法解决

    private static class ViewWrapper{
        private View targer;
        public ViewWrapper(View target){
            this.target = target;
        }
        public int getWidth(){
            return target.getLayoutParams().width;
        }
        public void setWidth(int width){
            target.getLayoutParams().width = width;
            target.requestLayout();
        }
    }
    
    1. 采用ValueAnimator,监听动画过程,自己实现属性的改变
    private void performAnimator(final View target, final int start, final int end){
        VulueAnimator animator = VulueAnimator.ofInt(1,100); //将一个数从1变到100
        animator.addUpdateListener(new AnimatorUpdateListener(){
            private IntEvaluator evaluator = new IntEvaluator();
            
            @Override
            public void onAnimatorUpdate(VulueAnimator animator){
                int currentVal = (Integer)animator.getAniamtedValue();//获取当前动画进度值,1-100之间
                
                float fraction = animator.getAnimatedFraction(); //获取当前进度占动画过程的比例
                targer.getLayoutParams.width = evaluator.evaluate(fraction,start,end);
                targer.requestLayout();
            }
        });
        animator.setDuration(2000).start();
    }
    

    VulueAnimator本身不作用于任何对象,也就是说直接使用它没有任何动画效果。它可以对一组值做动画,然后监听动画过程,在动画过程中我们可以改变我们的对象的属性值。

    属性动画工作原理

    属性动画要求动画作用的对象提供该属性的set方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次去调用set方法。每次传递给set方法的值不同,随着时间推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去获取属性的初始值。

    源码分析

    先从入口start方法开始,先看ObjectAnimator的start方法

     @Override
        public void start() {
            // See if any of the current active/pending animators need to be canceled
            AnimationHandler handler = sAnimationHandler.get();
            if (handler != null) {
                int numAnims = handler.mAnimations.size();
                for (int i = numAnims - 1; i >= 0; i--) {
                    if (handler.mAnimations.get(i) instanceof ObjectAnimator) {
                        ObjectAnimator anim = (ObjectAnimator) handler.mAnimations.get(i);
                        if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                            anim.cancel();
                        }
                    }
                }
                numAnims = handler.mPendingAnimations.size();
                for (int i = numAnims - 1; i >= 0; i--) {
                    if (handler.mPendingAnimations.get(i) instanceof ObjectAnimator) {
                        ObjectAnimator anim = (ObjectAnimator) handler.mPendingAnimations.get(i);
                        if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                            anim.cancel();
                        }
                    }
                }
                numAnims = handler.mDelayedAnims.size();
                for (int i = numAnims - 1; i >= 0; i--) {
                    if (handler.mDelayedAnims.get(i) instanceof ObjectAnimator) {
                        ObjectAnimator anim = (ObjectAnimator) handler.mDelayedAnims.get(i);
                        if (anim.mAutoCancel && hasSameTargetAndProperties(anim)) {
                            anim.cancel();
                        }
                    }
                }
            }
            if (DBG) {
                Log.d("ObjectAnimator", "Anim target, duration: " + mTarget + ", " + getDuration());
                for (int i = 0; i < mValues.length; ++i) {
                    PropertyValuesHolder pvh = mValues[i];
                    ArrayList<Keyframe> keyframes = pvh.mKeyframeSet.mKeyframes;
                    Log.d("ObjectAnimator", "   Values[" + i + "]: " +
                        pvh.getPropertyName() + ", " + keyframes.get(0).getValue() + ", " +
                        keyframes.get(pvh.mKeyframeSet.mNumKeyframes - 1).getValue());
                }
            }
            super.start();
        }
    

    做的事很简单,首先会判断当前动画、等待动画和延迟对内搞活中有和当前动画相同的动画,那么将相同的动画取消掉。最后调用super.start().因为ObjectAnimator继承来ValueAnimator,接下来看ValueAnimator的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");
            }
            mPlayingBackwards = playBackwards;
            mCurrentIteration = 0;
            mPlayingState = STOPPED;
            mStarted = true;
            mStartedDelay = false;
            mPaused = false;
            AnimationHandler animationHandler = getOrCreateAnimationHandler();
            animationHandler.mPendingAnimations.add(this);
            if (mStartDelay == 0) {
                // This sets the initial value of the animation, prior to actually starting it running
                setCurrentPlayTime(0);
                mPlayingState = STOPPED;
                mRunning = true;
                notifyStartListeners();
            }
            animationHandler.start();
        }
    
    

    可以看出属性动画要运行在有Looper的线程中。这个方法将动画加入AnimationHandler(一个Runnable)中,然后会调用它的start方法。经过JNI后,最终调到doAnimationFrame

    final boolean doAnimationFrame(long frameTime) {
            if (mPlayingState == STOPPED) {
                mPlayingState = RUNNING;
                if (mSeekTime < 0) {
                    mStartTime = frameTime;
                } else {
                    mStartTime = frameTime - mSeekTime;
                    // Now that we're playing, reset the seek time
                    mSeekTime = -1;
                }
            }
            if (mPaused) {
                if (mPauseTime < 0) {
                    mPauseTime = frameTime;
                }
                return false;
            } else if (mResumed) {
                mResumed = false;
                if (mPauseTime > 0) {
                    // Offset by the duration that the animation was paused
                    mStartTime += (frameTime - mPauseTime);
                }
            }
            // 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);
            return animationFrame(currentTime);
        }
    
    

    最后调 animationFrame,而animationFrame内部调用 animateValue(fraction);

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

    上面calculateValue方法计算每帧动画对应的属性值。

    下面看哪里调用属性的get和set方法。初始时,如果没提供初始值,会调用get,看PropertyValuesHolder的setupValue

    private void setupValue(Object target, Keyframe kf) {
            if (mProperty != null) {
                kf.setValue(mProperty.get(target));
            }
            try {
            //反射
                if (mGetter == null) {
                    Class targetClass = target.getClass();
                    setupGetter(targetClass);
                    if (mGetter == null) {
                        // Already logged the error - just return to avoid NPE
                        return;
                    }
                }
                kf.setValue(mGetter.invoke(target));
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
    

    可以发现get方法是通过反射来调用的。

    当动画下一帧来到事,看PropertyValuesHolder的setAnimatedValue方法会将新的属性值设置给对象,调用其set方法。set方法也是通过反射来调用的

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

    使用动画注意事项

    1. OOM问题:主要在帧动画中,但图片较大较大时,尽量避免使用帧动画
    2. 内存泄漏:在属性动画中无限循环动画,要在Activity退出时及时停止。
    3. View动画的问题:动画后View无法隐藏,即setVisibility(View.GONE)失效,这个时候调view.clearAnimatio()清除View动画即可。
    4. 不要用px:尽量使用dp,使用px不同设备效果不同。
    5. 动画元素交互:view动画之后,view的事件触发位置还在原处;而属性动画在3.0以前也有这个问题,3.0之后则不会。
    6. 硬件加速:建议开启硬件加速提高动画流畅性。

    参考《Adroid开发艺术探究》第7章

    相关文章

      网友评论

          本文标题:Android动画深入分析

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