android属性动画爬坑之路

android属性动画爬坑之路

1.什么是Android动画

Android的动画一般分为3种,分别是帧动画(Frame Animation),补间动画(
Tween Animation)和属性动画(Property Animation)。所谓帧动画就是每一个画面都是一帧,然后很多张图片连续播放形成动画,好处是变化比较自由,缺点是因为每一帧都是一张图片,占用体积很大。补间动画就是有制定开始和结束的状态,然后由系统去计算和绘制中间状态。优点是设置简单,基本兼容安卓所有版本,缺点是补间动画操作的属性非常有限,只有平移、旋转、缩放、透明度。而且,补间动画只是改变了View的绘制的位置,并没有真正的改变View,所以实际上View还在原来的位置上。例如给一个View设置了一个点击事件,然后View做一个平移动画。View的位置虽然改变了,但是它的点击事件还在原来的位置上。正是由于这些问题Google爸爸在Android 3.0(API11)以后,加入了属性动画。

2.属性动画的优点

属性动画顾名思义就是改变了View的属性,而不仅仅是绘制的位置。属性动画可以操作的属性相比于补间动画大大增加,除了常用的平移、旋转、缩放、透明度还有颜色等,基本上能通过View.setXX来设置的属性,属性动画都可以操作,这大大增加了我们在使用动画时的灵活性。一般来说我们设置属性动画,常用的就是ValueAnimator和ObjectAnimator,这里我们主要讲这两个。

3.ValueAnimation

例如我们要写一个水平移动的动画

    public void horizontalMove(View view) {  
        ValueAnimator animator = ValueAnimator.ofFloat(0, 1000);  
        animator.setDuration(1000).start();  
    }  

如果你写过补间的动画,你肯定一眼就可以发现问题。这竟然没有设定动画要改变的属性,这也能跑?当然不能,所谓值动画就是只是改变值,并不改变任何属性的动画。如果需要改变具体的属性,例如我们要它水平移动,我们还需要加上以下代码:

public void horizontalMove(View view) {  
    ValueAnimator animator = ValueAnimator.ofFloat(0, 1000);  
    anim.addUpdateListener(new AnimatorUpdateListener() {  
        @Override  
        public void onAnimationUpdate(ValueAnimator animation) {                
        mView.setTranslationX(animation.getAnimatedValue());
        }  
    }); 
    animator.setDuration(1000).start();  
} 

简单的来说我们这里做的就是把改变的值拿过了,赋值给我们需要的对象和属性。这样就比Tween Animation要高明的多,一个值的监听就可以赋值给任意的属性,使用起来非常的灵活。但是,我们也发现了一个问题,我们只是做了一个view的水平移动,结果就写了这么多代码,如果我们的动画比较复杂,是不是就要写一堆的监听,然后改变一堆的值?当然不是,Google爸爸还为我们准备另一个好东西—ObjectAnimator。

4.ObjectAnimation

同样的,我们来实现一个平移动画。

    ObjectAnimator animator=ObjectAnimator.ofFloat (mView,"translationX",0,1000);
    animator.setDuration (1000);
    animator.start ();

我们可以看到,我们只用了简单的几行就实现了,感觉比ValueAnimator简单多了,很多初学者也会比较多的使用ObjectAnimator而不是ValueAnimator。但是实际上ValueAnimator要更加强大和灵活,因为它直接操作的对象就是要改变的值,我们可以根据一组值,做出我们要实现的任何东西。他们两个的关系就像是厨房和微波加热的食物,微波炉食物虽然方便快捷但是如果家里来了客人,我们要做一桌菜,微波炉实现起来就很困难,但是厨房就可以轻易的做到,当然前提是你会的话。

5.动画监听

其实我们之前的对ValueAnimator的值进行监听也是一种动画监听,但是那个我们都会了。这里我们要说的是动画监听(AnimatorListener)。如下:

animator.addListener (new Animator.AnimatorListener () {
        @Override
        public void onAnimationStart (Animator animator) {
            Log.d ("ObjectAnimator", "onAnimationStart: ");
        }

        @Override
        public void onAnimationEnd (Animator animator) {
            Log.d ("ObjectAnimator", "onAnimationEnd: ");
        }

        @Override
        public void onAnimationCancel (Animator animator) {
            Log.d ("ObjectAnimator", "onAnimationCancel: ");
        }

        @Override
        public void onAnimationRepeat (Animator animator) {
            Log.d ("ObjectAnimator", "onAnimationRepeat: ");
        }
    });

这里有4个方法,分别对应动画的开始、结束、取消、重复。如果我们需要根据动画的进度来完成一些操作的话就可以在这里进行,但是我们也看到这里有4个方法需要重写,但是我们往往并不需要这么多的方法,这里就有一个动画监听的适配器AnimatorListenerAdapter,通过它我们可以只重写我们需要的方法,而不需要写这么多无效的代码。

常用属性和设置

属性动画的设置其实和补间动画差不多,常用的属性有duration,repeatMode,repeatCount,Interpolator,startDelay等等。这里我要提的一点就是这个startDelay。通过这个方法我们可以设置动画开始的延迟时间,可以达做到很多炫酷的动画。比如说之前我们要做一个文字的位移动画,但是我们有3行文本,我希望这3行文本的位移有一定的延迟,1先开始,然后2开始,最后3开始。如果要用补间动画的话,我们能设置的播放顺序只有同步和依次。但是通过startDelay我们就可以方便的设置一些不同步的动画效果。而且设置完延迟后我们依然可以使用同时启动,并不会忽略delay的时间。

AnimatorSet

属性动画的animatorSet和补间动画的animationSet类似,都是可以同时控制多个动画。AnimatorSet除了提供一般的playTogether()和playAfter()和playBefore()这些方法见名知义,使用并不困难,就不在详细叙述。

动画差值器

前面我们说过,值动画就是根据一系列值的变化来改变属性,从而做出各种各样的动画效果。但是在实际开发中我们的动画效果可能并不是线性的,这就需要我们给我们的动画设置一些差值器,来改变值的变化速率。android本身给我们提供了一些基本的差值器:
1. AccelerateInterpolator 加速插值器
2. DecelerateInterpolator 减速插值器
3. AccelerateDecelerateInterpolator 加速减速插值器
4. LinearInterpolator 线性插值器
5. BounceInterpolator 弹跳插值器
6. AnticipateInterpolator 回荡秋千插值器
7. AnticipateOvershootInterpolator
8. CycleInterpolator 正弦周期变化插值器
9. OvershootInterpolator
这几个差值器中 我们常用的主要是线性,加速,减速,正选和弹跳。
这几个差值器的使用都比较简单,具体信息和使用方法可以移步https://my.oschina.net/banxi/blog/135633
这里我想补充的是另一种相对少见的差值器类型:贝塞尔差值器
public class EaseCubicInterpolator implements Interpolator {

private final static int ACCURACY = 4096;
private int mLastI = 0;
private final PointF mControlPoint1 = new PointF();
private final PointF mControlPoint2 = new PointF();

/**
 * 设置中间两个控制点.
* 在线工具: http://cubic-bezier.com/
* * @param x1 * @param y1 * @param x2 * @param y2 */ public EaseCubicInterpolator(float x1, float y1, float x2, float y2) { mControlPoint1.x = x1; mControlPoint1.y = y1; mControlPoint2.x = x2; mControlPoint2.y = y2; } @Override public float getInterpolation(float input) { float t = input; // 近似求解t的值[0,1] for (int i = mLastI; i < ACCURACY; i++) { t = 1.0f * i / ACCURACY; double x = MathUtil.cubicCurves(t, 0, mControlPoint1.x, mControlPoint2.x, 1); if (x >= input) { mLastI = i; break; } } double value = MathUtil.cubicCurves(t, 0, mControlPoint1.y, mControlPoint2.y, 1); if (value > 0.999d) { value = 1; mLastI = 0; } return (float) value; }

}
这就是一个简单的贝塞尔差值器的代码,原理就是根据贝塞尔曲线来控制值的变化,我们平常的工作组一般二阶的贝塞尔曲线就足够满足我们的需求了,这里给大家一个可以图形化生成贝塞尔控制点的网址http://cubic-bezier.com/ ,在这里可以方便的生成贝塞尔控制点。https://www.desmos.com/calculator这个网站可以帮我们生成函数图形,也能方便我们进行自定义差值器的工作。

动画的内存泄露问题

如果我们的项目中动画的应用比较多,那么我们为了控制动画,通常会对动画进行监听,如果页面的逻辑比较复杂,我们甚至会和handler进行结合。

private void createCircleClockwiseAccelerateAnimator() {
        mCircleClockwiseAccelerateAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
        mCircleClockwiseAccelerateAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                mRotation = value * 720;
                mIvConnectedAnimCircle1.setRotation(mRotation);
                mIvConnectedAnimCircle2.setRotation(mRotation *0.2f);
            }
        });
        mCircleClockwiseAccelerateAnimator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                if (mHandler != null) {
                    mRotation = 0;
                    mHandler.sendEmptyMessage(MESSAGE_CIRCLE_ROTATE_ACCELERATE_START);
                    mIvConnectedAnimCircle1.setVisibility(View.VISIBLE);
                    mIvConnectedAnimCircle2.setVisibility(View.VISIBLE);
                    mIvMiddleImage.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (mHandler != null) {
                    mHandler.sendEmptyMessage(MESSAGE_CIRCLE_ROTATE_ACCELERATE_END);
                    mIvConnectedAnimCircle1.setVisibility(View.INVISIBLE);
                    mIvConnectedAnimCircle2.setVisibility(View.INVISIBLE);
                    mIvConnectedAnimCircle3.setRotation(mRotation);
                    mIvConnectedAnimCircle3.setVisibility(View.VISIBLE);
                    mIvMiddleImage.setVisibility(View.INVISIBLE);
                    Logger.d("onAnimationEnd","======"+mRotation);

                }
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });
        mCircleClockwiseAccelerateAnimator.setInterpolator(new LinearInterpolator());
        mCircleClockwiseAccelerateAnimator.setRepeatCount(ValueAnimator.INFINITE);
        mCircleClockwiseAccelerateAnimator.setDuration(6000);
    }

这是我们项目中的一个简单的动画,这里看到我们动画的持续时间是ValueAnimator.INFINITE,但是我们在动画的生命周期里又引用了handler,这就很容易导致整个View层的泄露。
为了避免这种情况,在activity或者fragment的onDestory方法中我们要及时的解除动画,移除handler。

 @Override
    protected void onDestroy() {
        stopAnim();
        if (mHandler != null) {
            mHandler.removeCallbacksAndMessages(null);
            mHandler = null;
        }
       if (mPresenter!=null){
           mPresenter.stopDetect();
       }
        super.onDestroy();
    }

private void stopAnim() {
        if (mCircleAntiClockwiseAccelerateAnimator != null) {
            mCircleAntiClockwiseAccelerateAnimator.cancel();                   mCircleAntiClockwiseAccelerateAnimator.removeAllUpdateListeners();                       mCircleAntiClockwiseAccelerateAnimator.removeAllListeners();
      mCircleAntiClockwiseAccelerateAnimator = null;
      } 
 }

相对应的animatorSet也有自己的removeListener方法,但是为了确保安全,我们在处理animatorSet的时候最好还是遍历一下,然后移除每一个动画的监听,再移除集合的监听,最后把集合置空。

补充:

解决动画被遮挡的问题:

android:clipChildren="false"
android:clipToPadding="false"

有些朋友在做动画的时候尤其是动画的移动范围比较大的时候,会发现我的动画竟然你会被父控件遮挡,一开始我以为是层级的问题,于是修改布局,发现动画依然会被遮挡。经过反复的检查发现我漏掉了上面两个属性。上面两个属性默认情况下都是true,在为true的情况下,子控件在做动画的时候会被padding和父控件遮挡,结果就是你只能子啊父控件的范围内做动画,这让人很不爽。但是有两点要注意:1,这两个属性只有配置在根布局才会有效。2,这两个属性一旦配置,就会应用于整个页面。
动画性能优化:
随着android的发展和对用户体验的重视,android应用中的动画越来越多。但是过多的动画也给我们的机器带来很大的负担。就以我们上面说的ValueAnimator为例,我们一般是监控值的变化,来进行动画。如果我们在log里面打印值的变化的时候我们会发现,这些值的变化往往非常小,这也保证了我们动画的流畅度,但是这也意味着我们在onAnimationUpdate中的代码会被调用很多遍,很容易产生一些性能的浪费,所以如果我们页面的动画过多而onAnimationUpdate中的代码又比较多的话我们可以对值进行过滤,具体的代码就不贴了,思路就是判断这一次的值和上一次的值如果变化过小就不再响应(已知问题:如果UI线程卡顿,有一定几率出现动画跳跃。具体情况,可以自己在log中查看值的变化)。其实动画的优化可以做的有很多,远不止上面的值过滤,常用的还有动画暂停,动画合并等等。但是现在android硬件大多性能足够,很多时候并不需要做太多处理。这个可以根据自己的项目进行酌情优化。

暂时就这么多,有空再来补充。

你可能感兴趣的:(动画)