Android中的动画详析-kotlin的demo

  Android中的动画可以分为三种,View动画,帧动画,以及属性动画,实际上帧动画也是View动画的一种,只不过二者表现形式不同,View动画是通过不断地对场景里的动画做图像转换从而产生动画效果是一种渐进式的动画,并且View动画支持自定义,帧动画是通过顺序的播放一系列的图像从而产生动画效果,很明显如果图片过大就会造成OOM,而属性动画是通过动态的改变对象的属性从而达到动画效果,低版本无法直接使用,需要通过兼容库。

1.View动画

  view动画的作用对象是View,它支持四种动画效果,分别是平移,旋转,缩放和透明度动画

1.1 View动画的种类

  View动画的四种变换效果对应着Animation的四个子类:TranslateAnimation、ScaleAnimation、RotateAnimation和AlphaAnimation,这四种动画既可以通过Xml形式定义,也可以使用代码动态创建。

动画名称 xml中的标签 子类 效果
平移动画 translate TranslateAnimation 平移效果
缩放动画 scale ScaleAnimation 放大或缩小View
旋转动画 rotate RotateAnimation 旋转View
透明度动画 alpha AlphaAnimation 透明度变化
1.2 View动画的使用

平移 res/anim/view_translate.xml

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:fillAfter="true"
    android:fromXDelta="0.0"
    android:fromYDelta="0.0"
    android:interpolator="@android:anim/anticipate_interpolator"
    android:toXDelta="100.0"
    android:toYDelta="100.0" />
属性 含义
android:fromXDelta 表示x的起始值
android:fromYDelta 表示y的起始值
android:toXDelta 表示x的结束值
android:toYDelta 表示y的结束值
android:interpolator 表示差值器
android:duration 表示动画持续的时间

缩放 res/anim/view_scale.xml

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:fillAfter="true"
    android:fromXScale="1.0"
    android:fromYScale="1.0"
    android:interpolator="@android:anim/anticipate_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="0.0"
    android:toYScale="0.0" />
属性 含义
android:fromXScale 水平方向缩放的起始值
android:fromYScale 竖直方向缩放的起始值
android:toXScale 水平方向缩放的结束值
android:toYScale 竖直方向缩放的结束值
android:pivotX 缩放的轴点的x坐标,会影响缩放的效果
android:pivotY 缩放的轴点的y坐标

轴点:缩放的中心点,默认为View的中心点,如果人为修改至View的右边界,View的会向左边进行缩放,反之亦然,可以自己体会一下

旋转 res/anim/view_rotate.xml

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="1000"
    android:fromDegrees="0"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toDegrees="+360"/>
属性 含义
android:fromDegrees 旋转开始的角度,如0
android:toDegrees 旋转结束的角度,如360,用+,-表示旋转方向
android:pivotX 旋转的轴心点的x坐标
android:pivotY 旋转的轴心点的y坐标

轴心点:旋转的中心点,默认为View的中心点,不同的情况,旋转的轨迹不同,可以自己体会一下

透明度变化

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fromAlpha="1.0"
    android:toAlpha="0.0" />
属性 含义
android:fromAlpha 透明度变化的起始值,如0.1
android:toAlpha 透明度变化的结束值,如1

系列动画

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/anticipate_interpolator"
    android:shareInterpolator="true"
    android:duration="1000"
    android:fillAfter="true">
    <translate android:fromXDelta="0.0"
        android:fromYDelta="0.0"
        android:toXDelta="100.0"
        android:toYDelta="100.0"/>
    <scale android:fromXScale="1.0"
        android:fromYScale="1.0"
        android:toXScale="0.0"
        android:toYScale="0.0"
        android:pivotX="50%"
        android:pivotY="50%"/>
    <rotate android:pivotY="50%"
        android:pivotX="50%"
        android:fromDegrees="0"
        android:toDegrees="+360"/>
    <alpha android:fromAlpha="1.0"
        android:toAlpha="0.0"/>
set>
set标签属性 含义
android:shareInterpolator 表示动画集合是否共享同一个差值器,true为共享
android:fillAfter 是指动画结束是画面停留在此动画的最后一帧
android:fillBefore 是指动画结束时画面停留在此动画的第一帧
android:interpolator 表示动画使用的差值器,其影响动画的速度,可以不指定,默认为@android:anim/accelerate_interpolator

  从上面可以看到,View动画即可以是一个耽搁动画,也可以是一系列的动画组成,标签白哦是动画的集合,对应AnimationSet类,他可以包含若干个动画,并且它的内部可以包含其他的动画集合。

1.3 自定义View动画

  自定义View动画是通过继承Animation这个抽象类,重写他的initialize和applyTransformation方法,在initialize方法中做一些初始化的操作,然后在applyTransformation中进行相应的矩阵变换,很多时候需要通过Camera来简化矩阵变化的过程。这里使用了Android原生的一个3d动画翻转效果作为例子来看,initialize和applyTransformation方法如下:

override fun initialize(width: Int, height: Int, parentWidth: Int, parentHeight: Int) {
        super.initialize(width, height, parentWidth, parentHeight)
        mCamera= Camera()
    }

override fun applyTransformation(interpolatedTime: Float, t: Transformation?) {
        super.applyTransformation(interpolatedTime, t)
        var fromDegrees=mFromDegrees
        var degrees= fromDegrees!! + ((mToDegrees!! - mFromDegrees!!) * interpolatedTime)
        var matix: Matrix?=t!!.matrix
        mCamera!!.save()
        if(mReverse!!){
            mCamera!!.translate(0.0f,0.0f,mDepthZ!!*interpolatedTime)
        }else{
            mCamera!!.translate(0.0f,0.0f,mDepthZ!!*(1.0f-interpolatedTime))
        }
        if (direction!! == DIRECTION.Y) mCamera!!.rotateY(degrees) else {
            mCamera!!.rotateX(degrees)
        }
        mCamera!!.getMatrix(matix)
        mCamera!!.restore()

        matix!!.preTranslate(-mCenterX!!,-mCenterY!!)
        matix!!.postTranslate(mCenterX!!,mCenterY!!)
    }

  上面的代码主要是在initialize中实例化了一个Camera的对象,在applyTransformation中通过计算翻转的角度,实例化矩阵类,然后通过Camera来简化矩阵变化的过程。整个的demo通过kotlin来写的。

1.4 View动画的特殊使用场景

1.4.1 LayoutAnimation

  LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画。这样当他的子元素出场时都会具有这种动画效果。这种效果常常作用在ListView上,首先来看看它的实现过程:
res/anim/anim_layout.xml

 <layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animationOrder="normal"
    android:delay="0.5"
    android:animation="@anim/anim_item"/>

res/anim/anim_item

<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:shareInterpolator="true"
    android:interpolator="@android:anim/accelerate_interpolator">
    <alpha android:toAlpha="0.0"
        android:fromAlpha="1.0"/>
    <translate android:fromYDelta="500"
        android:toYDelta="0"/>
set>

解释一下几个关键的属性:
1. android:animationOrder :表示子元素动画的顺序,normal,reverse,random,顺序,逆向和随机播放入场动画;
2. android:delay :表示子元素开始动画的时间延迟
3. android:animation :指定具体的入场动画

然后给ViewGroup指定android:layoutAnimation属性即可。

"@+id/list_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layoutAnimation="@anim/anim_layout"
        android:cacheColorHint="#00000000"
        android:divider="#dddbdb"
        android:dividerHeight="1.0px"
        android:listSelector="@android:color/transparent"/>

也可以在代码中动态实现:

    var animLayout : Animation = AnimationUtils.loadAnimation(this,R.anim.anim_item)
        var control=LayoutAnimationController(animLayout)
        control!!.delay=0.5f
        control!!.order=LayoutAnimationController.ORDER_NORMAL
        listView!!.layoutAnimation=control
        listView!!.adapter= ArrayAdapter(this,R.layout.list_view_item,date)

1.4.2 Activity的切换效果
  activity有默认的切换效果,但是这个效果我们完全可以通过自定义的方式来z实现,主要用到的是overridePeddingTransition(int enterAnim,int exitAnim)这个方法来实现,这个方法必须在startActivity()或者finish()之后调用才能生效。

overridePendingTransition(R.anim.push_right_in,R.anim.push_right_out)

2.帧动画

  帧动画是顺序的播放一组预先定义好的图片来实现动画效果,不同于View动画,系统提供了AnimationDrawable来使用帧动画,使用起来相对简单,通过一个例子来了解一下:
res/drawable/frame_animation.xml


<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
   android:oneshot="false">
    <item android:duration="50" android:drawable="@drawable/ic2"/>
    <item android:duration="50" android:drawable="@drawable/ic3"/>
    <item android:duration="50" android:drawable="@drawable/ic4"/>
    <item android:duration="50" android:drawable="@drawable/ic5"/>
    <item android:duration="50" android:drawable="@drawable/ic6"/>
    <item android:duration="50" android:drawable="@drawable/ic7"/>
    <item android:duration="50" android:drawable="@drawable/ic8"/>
    <item android:duration="50" android:drawable="@drawable/ic9"/>
animation-list>

在activity中的使用方法:

    iv!!.setBackgroundResource(R.drawable.frame_animation)
    var frameAnim= iv!!.background as AnimationDrawable
    frameAnim.start()

使用帧动画内存溢出解决办法:
  帧动画的原理就是从xml中读取到图片id列表后就去硬盘中找这些图片资源,将图片全部读出来后按顺序设置给ImageView,利用视觉暂留效果实现了动画。一次拿出这么多图片,而系统都是以Bitmap位图形式读取的;而动画的播放是按顺序来的,大量Bitmap就排好队等待播放然后释放,然而这个排队的地方却很小。东西多了没地方存,就会导致OOM,解决方法是:既然来这么多Bitmap,一次却只能拿走一个,那么就翻牌子吧,轮到谁就派个线程去叫谁,bitmap1叫到了得叫上下一位bitmap2做准备,这样更迭效率高一些。为了避免某个bitmap已被叫走了线程白跑一趟的情况,加个Synchronized同步下数据信息,具体的优化代码已在demo的FrameAnimation中

3.属性动画

  属性动画是API11也就是Android3.0之后新加入的特性,他对作用的的对象进行了扩展,属性动画可以对任何对象做动画,甚至可以没有对象,并且作用的效果也得到了加强

2.1 使用属性动画

  属性动画可以对任意对象的属性进行动画而不仅仅是View动画,动画默认时间间隔300ms,默认帧率10ms/帧,其可以达到在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。举几个使用属性动画的小例子:

  • 改变一个对象的translationY属性,让其沿着Y轴向上平移一段距离:
 var ofFloat = ObjectAnimator.ofFloat(view!!,"translationY", (-view!!.height).toFloat()) as ObjectAnimator
        ofFloat!!.start()
  • 改变一个对象的背景色属性,典型的情形是改变View的背景色。
 var value :ValueAnimator = ObjectAnimator.ofInt(view!!,"backgroundColor", Color.RED,Color.BLUE)
        value!!.duration = 500
        value!!.setEvaluator(ArgbEvaluator())
        value!!.repeatMode=ValueAnimator.REVERSE
        value!!.repeatCount=ValueAnimator.INFINITE
        value!!.start()
  • 动画集合,达到一个平移加翻转的效果
 var animSet= AnimatorSet()
        animSet!!.playTogether(
                ObjectAnimator.ofFloat(view!!,"rotationX",0f,360f),
                ObjectAnimator.ofFloat(view!!,"rotationY",0f,180f),
                ObjectAnimator.ofFloat(view!!,"rotation",0f,-90f),
                ObjectAnimator.ofFloat(view!!,"translationX",0f,90f),
                ObjectAnimator.ofFloat(view!!,"translationY",0f,90f),
                ObjectAnimator.ofFloat(view!!,"scaleX",1f,1.5f),
                ObjectAnimator.ofFloat(view!!,"scaleY",1f,0.5f),
                ObjectAnimator.ofFloat(view!!,"alpha",1f,0.25f,1f)
        )
        animSet!!.duration=5*1000
        animSet!!.start()

这是在代码中直接实现,也可以和View动画一样在XMl中实现,定义位置在res/animator/目录下,实现过程较为较为简单,不再赘述。

2.2属性动画中的重要属性介绍
  • android:propertyName –表示属性动画的作用对象的属性的名称
  • android:duration –表示动画的时长
  • android:valueFrom –表示属性的起始值
  • android:valueTo –表示属性的结束值
  • android:startOffset –表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正播放此动画
  • android:repeatCount –表示动画的重复次数 默认为1,-1为无限循环
  • android:repeatMode – 表示动画的重复模式 restart 连续重复,reverse 逆向重复
  • android:valueType – 表示android:propertyName所指定的属性的类型,有整型和浮点型

在实际开发中,通常使用动态的在代码中使用属性动画,更方便和简单。

2.3 插值器和估值器
属 性 TimeInterpolator TypeEvaluator
名称 时间差值器 类型估值算法,也叫估值器
作用 是根据时间的流逝的百分比来计算出当前属性值改变的百分比 根据当前属性改变的百分比来计算改变后的属性值
系统中预置的例子 AccelerateDecelerateInterpolator(加速减速差值器,两头慢中间快),LinearInterpolator(线性差值器)等等 IntEvaluator(针对整型属性),FloatEvaluator(针对浮点型属性),ArgbEvaluator(针对Color属性)等。

  属性动画中二者都是非常重要的,他们是实现非匀速的重要手段。我们看一下TimeInterpolator和以及TypeEvaluator的源码:

public interface TimeInterpolator {

    float getInterpolation(float input);
}
public interface TypeEvaluator {

    public T evaluate(float fraction, T startValue, T endValue);

}

上面的源码只是将注释删掉了,可以看到,我们要是想自己定义差值器和估值器的话,只需要派生一个类实现接口就可以了,然后就可以做出千奇百怪的动画效果。

2.4属性动画的监听器

  属性动画提供了监听器用于监听动画的播放过程,主要有如下两个接口,AnimatorUpdateListener和 AnimatorListener。

  public static interface AnimatorListener {
        /**
         * @param 开始动画
         * @param isReverse 表示动画是否是扭转
         */
        default void onAnimationStart(Animator animation, boolean isReverse) {
            onAnimationStart(animation);
        }
        /**
         * @param 结束动画
         * @param isReverse 表示动画是否是扭转
         */
        default void onAnimationEnd(Animator animation, boolean isReverse) {
            onAnimationEnd(animation);
        }
        void onAnimationStart(Animator animation);
        void onAnimationEnd(Animator animation);
        void onAnimationCancel(Animator animation);
        void onAnimationRepeat(Animator animation);
    }

从这个接口的源码可以看出,他可以监听动画的开始,结束,取消,重复,同时为了方便,系统还提供了了AnimatorListenerAdapter适配器,可以有选择的实现上面的方法。接着来看AnimatorUpdateListener

public static interface AnimatorUpdateListener {
        void onAnimationUpdate(ValueAnimator animation);
    }

AnimatorUpdateListener会监听动画的整个过程,动画是由很多帧组成的,每播放一帧,onAnimationUpdate就会被调用一次。

2.5 对任意的属性做动画

  属性动画的原理:属性动画要求作用的对象提供该属性的set和get方法,然后根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值不一样,确切的来说就是随着时间的推移,所传递的值越来越接近最终值。如果想让动画生效,要满足两个条件

  • object必须要提供set方法,如果动画的时候没有传递初始值,那么还需要提供get方法,因为系统要去取属性的初始值。
  • object的set方法对属性所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的。

上面的条件第一个是必须要满足的,如果不满足,直接Cash,第二个条件不满足,动画会无效果但不会报Crash,针对以上两个条件,官方文档提供了三种解决方式
1. 如果你有权限,给你的对象加上get和set方法
2. 用一个类来包裹原始对象,间接提供get和set方法

    /**
     * 让Button的宽度在2秒钟之内增加到200px
     */
    private fun sample4() {
        var wrapper= ViewWrapper(btn4 as View)
        ObjectAnimator.ofInt(wrapper!!,"width",200).setDuration(2000).start()
    }

private class ViewWrapper constructor(target:View){
        private var mTarget : View?=null
        init {
            this.mTarget=target
        }
        fun  getWidth() : Int{
            return mTarget!!.layoutParams.width
        }
        fun setWidth(width:Int){
            mTarget!!.layoutParams.width=width
            mTarget!!.requestLayout()
        }
    }

由于Button是继承自TextView的,而TextView的setWidth方法的作用并不是设置View的宽度,而是设置TextView的最大宽度和最小宽度的,所以直接给Button添加改变其宽度的属性动画刚开始是没效果的,因为它不满足第二个条件,因此通过用一个 ViewWrapper将其包裹起来,为其增加set和get方法,故而动画开始有效。
3. 采用ValueAnimator,监听动画过程,自己实现属性的改变

 private fun sample5(target: View,start:Int,end:Int) {
        var value=ValueAnimator.ofInt(1,100)
        value.addUpdateListener(ValueAnimator.AnimatorUpdateListener {
            var intEvaluator= IntEvaluator()
            //获得当前的进度值
            var currentValue:Int= it.animatedValue as Int
            Log.e("sample5", "当前的进度$currentValue")
            //获得当前进度占整个动画的比例
            var fraction:Float=it.animatedFraction
            target.layoutParams.width=intEvaluator.evaluate(fraction,start,end)
            target.requestLayout()
        })
        value.setDuration(2000).start()
    }
2.6属性动画的工作原理

  属性动画要求作用的对象提供该属性的set方法,属性动画根据你传递的该属性的初始值和最终值,以动画的效果多次调用set方法,每次传递给set方法的值都不一样,确切的来说是随着时间的推移,所传递的值越来越接近最终值。如果动画没有传递初始值,那么还要提供get方法,因为系统要去获取该属性的初始值。对于属性动画来说,其动画过程所做的就这么多。分析一下源码:
从ObjectAnimator的start方法作为入口:

 @Override
    public void 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);if里面的是log日志,接着就调用了父类的start()方法,所以点进去看一下autoCancelBasedOn方法

void autoCancelBasedOn(ObjectAnimator objectAnimator) {
        for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
            AnimationFrameCallback cb = mAnimationCallbacks.get(i);
            if (cb == null) {
                continue;
            }
            if (objectAnimator.shouldAutoCancel(cb)) {
                ((Animator) mAnimationCallbacks.get(i)).cancel();
            }
        }
    }

这个方法也很简单,首先mAnimationCallbacks是一个存有AnimationFrameCallback的ArrayList的集合,这个方法主要是取出集合中的AnimationFrameCallback,判断是否有和当前动画相同的动画,如果有,将其取消。接着来看ValueAnimator中的start()方法

private void start(boolean playBackwards) {
        //可以看出属性动画需运行在有Looper的线程中
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        ......
        //当start()方法被调用之后重置mLastFrameTime,如果动画正在运行,调用start()让动画处于已经开始但尚未达到第一帧的阶段
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        addAnimationCallback(0);
        //没有开始延迟
        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            //初始化动画,并通知开始监听并与之前的行为一致,否则,推迟初始化直到第一帧开始延迟后
            startAnimation();
            if (mSeekFraction == -1) {
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }

这个方法主要的还是来看一下setCurrentFraction(mSeekFraction);

public void setCurrentFraction(float fraction) {
        //初始化
        initAnimation();
        ...... 省略
        animateValue(currentIterationFraction);
    }

看一下初始化

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

这里我们可以看到mValues通过JNI的方式被init初始化了,而mValues是一个PropertyValuesHolder的数组,这里我们进入PropertyValuesHolder类找到setupValue和setAnimatedValue的方法,我们可以看到我们最关心的set和get通过反射的凡是来调用了。

private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            //属性的初始值没有被提供,get方法将会调用
            Object value = convertBack(mProperty.get(target));
            kf.setValue(value);
        } else {
            try {
                if (mGetter == null) {
                    Class targetClass = target.getClass();
                    setupGetter(targetClass);
                    if (mGetter == null) {
                        // Already logged the error - just return to avoid NPE
                        return;
                    }
                }
                Object value = convertBack(mGetter.invoke(target));
                kf.setValue(value);
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
    }
    //当动画的下一帧来的时候。这个方法会将新的属性值设置给对象,调用其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());
            }
        }
    }

然后接着来看setCurrentFraction中的animateValue(currentIterationFraction);

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

这样整个过程就看完了。附上文中使用的demo地址,有需要的话可以下载下来,自己的微信公众号,偶尔更新文章,生活感悟,好笑的段子,欢迎订阅
Android中的动画详析-kotlin的demo_第1张图片

参考书籍:Android开发艺术探究

你可能感兴趣的:(Android)