Android 动画深入

动画的简单使用------略!

本章内容先介绍一些动画中常用的概念,然后再分别对补间动画和属性动画的原理进行分析。

一.

1.插值器:插值器在动画中的作用就是影响动画的速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。

在安卓中使用的插值器是:TimeInterpolator(时间插值器),它的作用是根据时间流逝的百分比来计算当前属性值改变的百分比(动画完成度的百分比)。

那根据时间的流逝如何计算当前属性值改变的百分比呢?

系统给出的有:LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateinterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢),插值器的实现就是在动画执行的这段时间内选择一个因素例如加、减速度等搞一套算法影响它的执行进度。

注意:TimeInterpolator(接口)是最大父类,然后是Interpolator继承TimeInterpolator,BaseInterpolator实现了Interpolator接口

,然后才是LinearInterpolator、AccelerateDecelerateinterpolator这些继承BaseInterpolator

下面我们通过LinearInterpolator(线性插值器:匀速动画)和DecelerateInterpolator(减速插值器:动画越来越慢)的源码对比来体验一下插值器:

LinearInterpolator:

public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {

    public LinearInterpolator() {
    }

    public LinearInterpolator(Context context, AttributeSet attrs) {
    }

    public float getInterpolation(float input) {
        return input;
    }

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createLinearInterpolator();
    }
}

主要是看:getInterpolation(float input)方法,这个方法的参数是时间的百分比,返回值是当前动画完成的百分比,可以看到输入值就是返回值,就是说在完成这个动画的过程中是匀速的。

DecelerateInterpolator:

public class DecelerateInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {
    public DecelerateInterpolator() {
    }

    /**
     * Constructor
     *
     * @param factor Degree to which the animation should be eased. Setting factor to 1.0f produces
     *        an upside-down y=x^2 parabola. Increasing factor above 1.0f makes exaggerates the
     *        ease-out effect (i.e., it starts even faster and ends evens slower)
     */
    public DecelerateInterpolator(float factor) {
        mFactor = factor;
    }

    public DecelerateInterpolator(Context context, AttributeSet attrs) {
        this(context.getResources(), context.getTheme(), attrs);
    }

    /** @hide */
    public DecelerateInterpolator(Resources res, Theme theme, AttributeSet attrs) {
        TypedArray a;
        if (theme != null) {
            a = theme.obtainStyledAttributes(attrs, R.styleable.DecelerateInterpolator, 0, 0);
        } else {
            a = res.obtainAttributes(attrs, R.styleable.DecelerateInterpolator);
        }

        mFactor = a.getFloat(R.styleable.DecelerateInterpolator_factor, 1.0f);
        setChangingConfiguration(a.getChangingConfigurations());
        a.recycle();
    }

    public float getInterpolation(float input) {
        float result;
        if (mFactor == 1.0f) {
            result = (float)(1.0f - (1.0f - input) * (1.0f - input));
        } else {
            result = (float)(1.0f - Math.pow((1.0f - input), 2 * mFactor));
        }
        return result;
    }

    private float mFactor = 1.0f;

    /** @hide */
    @Override
    public long createNativeInterpolator() {
        return NativeInterpolatorFactoryHelper.createDecelerateInterpolator(mFactor);
    }
}

主要还是看getInterpolation(float input)这个方法,明显看到和上面算法不一样了,具体就不分析了估计就是一个减速度不断的减缓动画完成的速度,通过这个比较大家了解了插值器的实质就是getInterpolation(float input)这个方法的算法,之后自己也可以写了。

2.估值器:TypeEvaluator它的作用是根据当前属性改变的百分比(动画完成度的百分比)来计算改变后的属性值。

系统预置的有IntEvaluator(针对整形属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。

public class IntEvaluator implements TypeEvaluator {

    /**
     * This function returns the result of linearly interpolating the start and end values, with
     * fraction representing the proportion between the start and end values. The
     * calculation is a simple parametric calculation: result = x0 + t * (v1 - v0),
     * where x0 is startValue, x1 is endValue,
     * and t is fraction.
     *
     * @param fraction   The fraction from the starting to the ending values
     * @param startValue The start value; should be of type int or
     *                   Integer
     * @param endValue   The end value; should be of type int or Integer
     * @return A linear interpolation between the start and end values, given the
     *         fraction parameter.
     */
    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

就是把属性改变的百分比(动画完成的百分比)换算成一个值:主要是evaluate方法,它的三个参数分别是估值小数(百分比)、开始值和结束值。

二:对两大动画的分析

1.View动画又叫补间动画

一些细节:

(1)在xml文件中使用动画合集set的时候,有一个shareInterpolator的属性,这个属性的作用是表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值

(2)在xml文件中使用动画,标签中有轴点这个概念:android:pivotX或者android:pivotY

(3)view动画的特殊使用场景:

a.LayoutAnimation作用于ViewGrope,为ViewGrope 指定一个动画,这样当他的子元素出场时都会具有这种动画效果

b.activity切换的效果

原理:https://www.jianshu.com/p/48317612c164

原理补充:大家都知道在View 动画中,如果使用Translation动画后,点击事件仍旧会在原来的位置发生,而属性动画没有这个问题,因为属性动画是因为通过反射修改了view的真实属性。我好奇的的是View 动画中到底发生了什么,视图变化了,但感觉View自身的位置并没有变化。那位移动画又是怎么产生的呢?

答:view在绘制的时候,只是根据动画后的matrix,绘制了对应区域:

if (transformToApply != null) {
if (concatMatrix) {
if (drawingWithRenderNode) {
renderNode.setAnimationMatrix(transformToApply.getMatrix());
} else {
// Undo the scroll translation, apply the transformation matrix,
// then redo the scroll translate to get the correct result.
canvas.translate(-transX, -transY);
canvas.concat(transformToApply.getMatrix());
canvas.translate(transX, transY);
}
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}

float transformAlpha = transformToApply.getAlpha();
if (transformAlpha < 1) {
alpha *= transformAlpha;
parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
}
}

2.属性动画:

(1)自己的理解:属性动画的原理就是通过时间插值器来改变一个实例的属性,通过这个实例的get和set方法来改变属性所以如果想要实现自己想达到的效果不仅我们改变的属性要有get和set方法而且get和set方法源码必须是改变我们想要改变的属性(因为有的get或set方法源码操作的不是我们想改变的)。注意:属性动画实质就是属性值的渐变,所以不一定是view,其他的对象只要满足有get和set方法就行,只不过view有invalidate和requestlayout方法让我们直观的看到了他的改变。

(2)一些容易忘的小点:

a.在xml中用属性动画,动画集合标签的android:ordering属性有两个值可以选:together和sequentially,together是表示动画集合中的子动画同时播放,sequentially表示动画集合中的子动画按次序播放,android:ordering的默认属性值是together。

b.相比,前者比后者少了一个android:propertyName属性,其他都一样,下面我们简单说一下的属性:

android:propertyName:表示属性动画的作用对象的属性的名称

android:duration:表示动画的时长

android:valueFrom:表示属性的起始值

android:valueTo:表示属性的结束值

android:startOffset:表示动画的延迟时间,表示动画开始后,需要延迟多少毫秒才会真正播放此动画

android:repeatCount:表示动画重复的次数

android:repeatMode:表示重复的模式

andriod:valueType:表示android:propertyName所指定的属性的类型,有"intType"和"floatType"两个选项,分别表示属性的类型为整形和浮点型。另外如果所指定的属性是颜色,那么不需要指定andriod:valueType,系统会自动对颜色类型的属性做处理。

注意:repeatCount它的默认值为0,-1表示无限循环

(3)属性动画的监听器

a.Animator.AnimatorListener (是一个接口,如果实现他必须重写所有的方法)可以监听器来监听动画播放过程中的重要事件:

onAnimationStart() - 当动画开始的时候调用.
onAnimationEnd() - 动画结束时调用.
onAnimationRepeat() - 动画重复时调用.
onAnimationCancel() - 动画取消时调用.取消动画也会调用onAnimationEnd,它不会关系动画是怎么结束的。

        new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                
            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        };

b.如果你不想实现Animator.AnimatorListener中的所有接口,你可以通过继承AnimatorListenerAdapter而不是去实现Animator.AnimatorListener接口AnimatorListenerAdapter类为所有的方法提供了一个空实现,所以你可以根据需要实现你需要的,覆盖AnimatorListenerAdapter总原来的方法

用这个适配器好处就是不用实现所有的方法,想实现哪个就实现哪个,因为他是一个类不是接口

        AnimatorListenerAdapter animatorListenerAdapter = new AnimatorListenerAdapter(){


        };


        AnimatorListenerAdapter animatorListenerAdapter1 = new AnimatorListenerAdapter(){


            @Override
            public void onAnimationStart(Animator animation) {
                super.onAnimationStart(animation);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }

            @Override
            public void onAnimationCancel(Animator animation) {
                super.onAnimationCancel(animation);
            }





        };

c.ValueAnimator.AnimatorUpdateListener(接口)

作用:动画中每一帧更新的时候调用,监听这个接口可以使用动画播放过程中由ValueAnimator计算出来的值。

首先科普个东西:两种属性动画实现方式的不同

一个是用ObjectAnimator的一个是ValueAnimator的,首先ObjectAnimator继承自ValueAnimator所以ObjectAnimator比ValueAnimator更有细节,这个细节就是在设置属性动画的时候ObjectAnimator是直接对一个对象的属性进行插值变化操作的是我们设置对象的属性它会直观的体现出来,而ValueAnimator在设置属性动画的时候只是对一个数值区间进行插值变化,并没有涉及到我们自己的对象仅仅是一个算法变化过程,如果我们想实现直观效果需要我们去在AnimatorUpdateListener中去自己获取当前的动画执行进度然后设置相应的效果。

例:objectAnimator的不用自己在AnimatorUpdateListener中去实现动画效果,因为objectAnimator已经控制了属性的变化

        final ImageView iv_launcher = findViewById(R.id.iv_launcher);
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(iv_launcher,"translationX",0,1000);
        objectAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {

                float fraction = animation.getAnimatedFraction();
                int value = (int) animation.getAnimatedValue();

                Log.d("allen","fraction:"+fraction+"*******"+"value:"+value);

            }
        });

        objectAnimator.setDuration(500).start();

ValueAnimator的必须在AnimatorUpdateListener中实现动画效果,因为ValueAnimator只是完成了一个值的区间的渐变

        final LinearLayout lin = findViewById(R.id.lin);
        final  int startX = 0;
        final  int deltaX = 1000;

        ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                lin.scrollTo(-Math.abs(startX + (int)(deltaX*fraction)),0);

            }
        });

        animator.start();

(4)从一个小例子来体会一下属性动画的实质:

a.在布局文件中加一个button,注意不要把width设置成wrap_content,如果设置了wrap_content就达不到效果了,为啥达不到效果之后再说

    

b.在java代码中给width设置属性动画:

        final Button button = findViewById(R.id.button);


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                ObjectAnimator.ofInt(button,"width",1000).setDuration(5000).start();

            }
        });

c.你会发现并没有我们想要看到width渐渐变大的效果

为什么呢?

上面说道属性动画的实质就是对属性逐渐改变直到达到某个值,如何改变呢?就是靠这个属性的get、set方法,如果这个属性没有get和set方法则会crash,如果set方法中的代码不是在改变我们想要效果的属性则会没有效果或者效果和我们预期的不同,上面这个button的width属性的set方法就是这样:

button没有自己的setWidth方法,所以我们在button的父类textView中找它的setWidth方法:

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

        requestLayout();
        invalidate();
    }

从上面的代码可以看到,setWidth方法是TextView自己新添加的,View并没有setWidth方法,TextView的setWidth方法并不是改变View的宽度而是设置View的最大宽度和最小宽度的,不是设置View的宽度的,我们想要改变的宽度对应的是在布局文件中android:layout_width属性,而不是button的width属性因为这个width 并不是代表View的宽度。

 

在做上述实验中,发现了一个问题,当把button的layout_width属性值设置为wrap_content时,虽然属性动画改变的是minWidth

但是照样出现了button渐渐变大的动画效果,后来在看button的源码实际上是TextView的源码的时候看到了这段:

   // Check against our minimum width
            width = Math.max(width, getSuggestedMinimumWidth());

            if (widthMode == MeasureSpec.AT_MOST) {
                width = Math.min(widthSize, width);
            }

发现,像TextView和Imageview这类的在widthMode == MeasureSpec.AT_MOST的时候width都和minWidth有关系,所以在动画执行时改变minwidth属性时大小就会跟随着改变,所以要注意不能设置button的属性为wrap_content,否则就达不到我们想要的错误的效果。

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

(1)Object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。

(2)Object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI的改变之类的(如果这条不满足,动画无效果但不会Crash,就像上面button的小例子一样)

针对上述问题,官方文档告诉我们有3种方法:

(1)给你的对象加上get和set方法,如果你有权限的话;

(2)用一个类来包装原始对象,间接为其提供get和set方法;

(3)采用ValueAnimator,监听动画过程,自己实现属性的改变。

针对上面提出的三种解决方法,下面给出具体的介绍。

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

这个意思很好理解,如果你有权限的话,加上get和set方法就搞定了。但是很多时候我们没有权限去这么做。比如之前的button小例子,我们无法给button加上一个合乎要求的setWidth方法,因为这是Android SKD内部实现的。这个方法最简单,但是往往是不可行的,这里就不对其进行更多的分析了。

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

这是一个很有用的解决方法,是大家最常用的,下面通过一个具体的例子来介绍它。

     

上述代码在5s内让Button的宽度从当前宽度增加到了1000,为了达到这个效果,我们提供了ViewWrapper类专门用于包装View,具体到本例是包装Button。然后我们对ViewWrapper的width属性做动画,并且在setWidth方法中修改其内部的target的宽度,而且target实际上就是我们包装的button。这样一个间接属性动画就搞定了,上述代码同样适用于一个对象的其他属性。

 

下面详细且正式的介绍一下ValueAnimator,和ValueAnimator与估值器使用的一个例子

首先介绍什么是ValueAnimator,ValueAnimator本身不作用任何对象,也就是说直接使用他没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程,在动画过程中修改我们的对象的属性值,这样也就相当于我们的对象做了动画。下面用例子来说明:

        final Button button = findViewById(R.id.button);


        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                ValueAnimator valueAnimator = ValueAnimator.ofInt(1,100);
                valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                    private IntEvaluator mEvaluator = new IntEvaluator();
                    @Override
                    public void onAnimationUpdate(ValueAnimator animation) {
                        //我感觉不用估值器自己算也行
                        //获得当前动画的进度值,整型,1~100之间
                        int currentValue = (int) animation.getAnimatedValue();
                        Log.d("ValueAnimator","currentValue:"+currentValue);

                        //获得当前进度占整个动画过程的比例,浮点型,0~1之间
                        float fraction = animation.getAnimatedFraction();
                        //直接调用整形估值器,通过比例计算出宽度,然后再设给Button
                        button.getLayoutParams().width = mEvaluator.evaluate(fraction,button.getWidth(),1000);
                        button.requestLayout();
                    }
                });

                valueAnimator.setDuration(5000).start();

            }
        });

上述代码他会在5000ms内讲一个数从1变到100(调用默认的插值器),然后动画的每一帧会回调onAnimationUpdate方法。在这个方法里,我们可以获取当前的值(1~100)和当前值所占的比例,我们可以计算出Button现在的宽度应该是多少。比如时间过了一半,当前值是50,比例是0.5,假设Button的起始宽度是100px,最终宽度是500px,那么button增加的宽度也应该占总增加宽度的一半,总增加宽度为:500-100=400,所以这个时候button应该增加的宽度是400*0.5=200,那么当前的宽度应该为初始宽度+增加宽度(100+200=300)。上述计算过程很简单,其实他就是整型估值器intEvaluator内部实现,所以我们不用自己写。

 

使用动画的注意事项:

1.oom问题

这个问题主要出现在帧动画中,当图片数量较多且图片较大时就极易出现oom,这个在实际开发中要尤其注意,尽量避免使用帧动画。

2.内存泄漏

在属性动画中有一类无限循环的动画,这类动画需要在Activity退出时及时停止,否则将导致Activity无法释放从而造成内存泄漏,通过验证后发现View动画并不存在此问题。

3.兼容性问题

动画在3.0以下的系统上有兼容性问题,在某些特殊场景可能无法正常工作,因此要做好适配。

4.View动画的问题

View动画是对View的影像做动画,并不是真正的改变View的状态,因此有时候会出现动画完成后View无法隐藏的现象,即setVisibility(View.GONE)失效了,这个时候只要调用view.clearAnimation()清除View动画即可解决此问题

5.不要使用px

在进行动画过程中,尽量使用dp,使用px会导致在不同的设备上有不同的效果

6.动画元素的交互

将view移动(平移)后,在Android3.0以前的系统上,不管是View动画还是属性动画,新位置均无法触发单击事件,同时,老位置仍然可以触发单击事件。尽管View已经在视觉上不存在了,将View移动回原位置以后,原位置的单击事件继续生效。从3.0开始,属性动画的单击事件触发位置为移动后的位置,但是View动画仍然在原位置。

7.硬件加速

使用动画的过程中,建议开启硬件加速,这样会提高动画的流畅性

 

 

你可能感兴趣的:(安卓进阶)