《Android开发艺术探索》笔记6:动画的深入分析

1,View动画

1.1,View的分类

View动画分为平移动画,对应的xml标签,Java类为TranslateAnimation;缩放动画,对应的xml标签,Java类为ScaleAnimation;旋转动画,对应的xml标签,Java类为RotateAnimation;透明度动画,对应的xml标签,Java类为AlphaAnimation。以下是xml中的定义:




  
      android:toAlpha="float"/>
  
      android:toXScale="float"
      android:fromYScale="float"
      android:toYScale="float"
      android:pivotX="float"
      android:pivotY="float"/>
  
      android:fromYDelta="float"
      android:toXDelta="float"
      android:toYDelta="float"/>
  
      android:toDegrees="float"
      android:pivotX="float"
      android:pivotY="float"/>
  
  ...
  


此外,还有
android:duration:动画的持续时长,ms。
android:fillAfter:动画结束以后View是否停留在结束位置。
其中标签表示动画集合,对应AnimationSet,它主要有2个属性如下:
android:interpolator
表示动画集合采用的插值器,插值器会影响动画的速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。默认值“@android:anim/accelerate_decelerate_interpolator”,即加速减速插值器。
android:shareInterpolator
表示集合中的动画是否和集合共享同一个插值器。如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或者使用默认值。

1.2,View的使用

1,引用XML

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

2,Java代码啊创建

AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(2000);
button.startAnimation(alphaAnimation);

3,View动画过程监听

 public static interface AnimationListener {
      void onAnimationStart(Animation animation);
      void onAnimationEnd(Animation animation);
      void onAnimationRepeat(Animation animation);
}
1.3,自定义View动画

自定义View继承自Animation类,重写它的initialize和applyTranslation方法,在initialize方法中做初始化工作,在applyTranslation方法中进行矩阵变换。实用的例子可以参考ApiDemos下的动画,例如Rotate3dAnimation。

1.4,帧动画

帧动画是顺序播放的一组图片,类似于电影播放。下面是帧动画的使用方式:



    
    
    

Button button = (Button) findViewById(R.id.button);
button.setBackgroundResource(R.drawable.frame_animation);
AnimationDrawable drawable = (AnimationDrawable) button.getBackground();
drawable.start();

以上就是帧动画的使用方式,非常简单,但是图片使用过多,会造成OOM。

2,View动画的特殊使用场景

2.1,LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样当它的子元素出场时就会具有这个动画效果,例如ListView的item的动画,通常使用LayoutAnimation制造。以下是LayoutAnimation的使用步骤:
1,定义LayoutAnimation:


android:delay
表示子元素开始动画的时间延迟,比如子元素入场动画的时间周期为300ms,那么0.5表示每个子元素都需要延迟150ms才能播放入场动画。也就是说,第一个子元素延迟150ms播放动画,第二个子元素延迟300ms播放动画,以此类推。
android:animationOrde
表示子元素动画的顺序,有三种:normal、reverse和random,其中normal表示顺序显示,即排在前面的子元素先开始动画;reverse表示逆向播放,即排在后面的子元素先开始动画;random则是随机播放入场动画。
android:animation
为子元素指定具体的动画。
2,为子元素指定具体的入场动画:




    

    


3,为ViewGroup指定android:layoutAnimation属性,android:layoutAnimation="@anim/anim_layout"。对于ListView来说,这样item就具有了入场动画了。


除了在XML中定义,还可以通过LayoutAnimationController实现:

ListView listView = (ListView) findViewById(R.id.button);
Animation animation = AnimationUtils.loadAnimation(this, R.anim.anim_item);
LayoutAnimationController controller = new LayoutAnimationController(animation);
controller.setDelay(0.5f);
controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
listView.setLayoutAnimation(controller);
2.2,Activity切换动画

Activity切换动画通过overridePendingTransition(int enterAnim, int exitAnim)方法实现:
Fragment切换动画可以通过FragmentTransaction中的setCustomAnimations()方法来实现。


2,属性动画

属性动画可以对任意对象的属性进行动画而不仅仅是View,动画的默认时间间隔为300ms,默认帧率10ms/帧。可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,因此,属性动画几乎无所不能,只要对象有这个属性,它都是实现动画效果。常用的属性动画类有:ValueAnimator、ObjectAnimator和AnimatorSet。

2.1,使用属性动画

下面是是对ObjectAnimator、ValueAnimator和AnimatorSet的简单玩法:
(1)改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一个距离:它的高度。动画使用默认的时间间隔,插值器。

ObjectAnimator.ofFloat(myObject, "translationY", -myObject.getHeight());

(2)改变一个对象的背景色,下面动画可以让View在3s内实现从0xFFFF8080到0xFF8080FF的渐变,并且无限循环+反转。

Button button = (Button) findViewById(R.id.button);
ValueAnimator colorAnim = ObjectAnimator.ofInt(button, "backgroundColor",
        0xFFFF8080, 0xFF8080FF);
colorAnim.setDuration(3000);
colorAnim.setEvaluator(new ArgbEvaluator());
colorAnim.setRepeatCount(ValueAnimator.INFINITE);
colorAnim.setRepeatMode(ValueAnimator.REVERSE);
colorAnim.start();

(3)动画集合,5秒内对View的旋转、平移、缩放和透明度都进行改变。

Button button = (Button) findViewById(R.id.button);
AnimatorSet set = new AnimatorSet();
set.playTogether(
       ObjectAnimator.ofFloat(button, "rotationX", 0, 360),
       ObjectAnimator.ofFloat(button, "rotationY", 0, 180),
       ObjectAnimator.ofFloat(button, "rotation", 0, -90),
       ObjectAnimator.ofFloat(button, "translationX", 0, 90),
       ObjectAnimator.ofFloat(button, "translationY", 0, 90),
       ObjectAnimator.ofFloat(button, "scaleX", 1, 1.5f),
       ObjectAnimator.ofFloat(button, "scaleY", 1, 0.5f),
      ObjectAnimator.ofFloat(button, "alpha", 1, 0.25f, 1)
);
set.setDuration(5000).start();

属性动画除了可以用Java代码描述,还可以在XML中定义,属性动画需要定义在res/animator/目录下,它的语法结构如下:




    
        android:propertyName="string"
        android:repeatCount="int"
        android:repeatMode="restart|reverse"
        android:startOffset="int"
        android:valueFrom="float|int|color"
        android:valueTo="float|int|color"
        android:valueType="colorType|intType|floatType|pathType" />

    
      
       
            ...
      

上面XML的标签表示属性动画集合AnimatorSet,表示的是ObjectAnimator,表示的是ValueAnimator。在Java代码引用XML:

AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(context, 
        R.animator.);
set.setTarget(button);
set.start();
2.2,插值器和估值器

TimeInterpolator插值器的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有

图1、系统内置的插值器

以上图来自Google Develop.
常用的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等。

TypeEvaluator估值器的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有

图2、系统内置的估值器

以上图来自Google Develop.

下面的图来自Google官方,表示插值器的工作原理


图3、线性插值器的工作原理

上图表示的是线性插值器的流程,即匀速动画流程,表示在0-40ms内,将对象的x属性从0到40增加,我们截取t=20ms的这一帧来分析,因为属性动画的默认刷新率是10ms,那么时间流逝的百分比为(20/40)0.5,说明时间过去了一半,那么x具体会变化多少呢?我们先看一下LinearInterpolator的源码:

public class LinearInterpolator implements Interpolator, 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方法,就是返回的x的值,即0.5,而且是输入值和输出值是一样的,此时需要看看估值器的源码,才能知道x的具体数值了:

public class IntEvaluator implements TypeEvaluator {

    public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
        int startInt = startValue;
        return (int)(startInt + fraction * (endValue - startInt));
    }
}

根据估值器的算法,参数中fraction代表的是估值小数,startValue起始值,endValue终点值,对应的值分别是0.5,0,40,带入公式得0+0.5*(40 - 0)=20。就是图中t=20ms,x=20的由来。
注意:属性动画要求对象的属性有set方法和get方法(可选)。插值器和估值器除了以上系统提供的以为,还可以自定义。自定义插值器,需要派生一个累实现Interpolator或者TimeInterpolator,自定义估值器需要派生一个类实现TypeEvaluator接口。

2.3,监听属性动画

属性动画和View动画一样,系统也提供了接口用于监听属性动画的过程和状态,主要会用到2个接口,AnimatorListener 和AnimatorUpdateListener

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

AnimatorListener 接口实现属性动画的开始、结束、取消和重复的监听,然而并不是所有的方法都是我们感兴趣的,系统给我们提供了一个简化的监听AnimatorUpdateListener,如下:

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

AnimatorUpdateListener 会监听整个动画过程,动画是由许多帧组成的,每播放一帧,onAnimationUpdate方法就会被调用一次。
此外,我们还可以通过AnimatorListenerAdapter 类来选择监听某一个或者多个过程。AnimatorListenerAdapter 的代码如下:

public abstract class AnimatorListenerAdapter implements Animator.AnimatorListener,
        Animator.AnimatorPauseListener {

    @Override
    public void onAnimationCancel(Animator animation) {
    }

    @Override
    public void onAnimationEnd(Animator animation) {
    }

    @Override
    public void onAnimationRepeat(Animator animation) {
    }

    @Override
    public void onAnimationStart(Animator animation) {
    }

    @Override
    public void onAnimationPause(Animator animation) {
    }

    @Override
    public void onAnimationResume(Animator animation) {
    }
}

可以看到AnimatorListenerAdapter 实现了AnimatorListener 和AnimatorPauseListener 接口的抽象类,可以取到属性动画的各种状态下的回调方法,如果我们只是想监听动画结束状态的话,可以如下:

anim.addListener(new AnimatorListenerAdapter() {  
  
    @Override  
    public void onAnimationEnd(Animator animation) {  
        // TODO Auto-generated method stub  
        super.onAnimationEnd(animation);  
    }  
  
});
2.4,对任意属性做动画

属性动画的原理:属性动画要求动画作用的对象提供该属性的set和get方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。总结一下,我们对object的属性abc做动画,如果想要动画生效,那么必须同时满足如下2个条件:
(1)object必须要提供setAbc方法,如果动画的时候没有传递初始值,那么还要提供getAbc方法,因为系统要去取abc属性的初始值(如果这条不满足,程序直接Crash)。
(2)object的setAbc对属性abc所做的改变必须能够通过某种方法反映出来,比如会带来UI上的改变之类的(如果这条不满足,动画无效但不会Crash)。

针对上面所说的问题,如果一个对象并不同时满足以上2个条件的话,Google官方给我们的建议是:
(1)给对象加上set和get方法,如果你有权限的话
(2)用一个类去包装原始对象,间接的提供set和get方法

private void performAnimate() {
     ViewWrapper wrapper = new ViewWrapper(mButton);
     ObjectAnimator.ofInt(wrapper, "width", 500).setDuration(5000).start();
}

@Override
public void onClick(View v) {
     if (v == mButton) {
         performAnimate();
     }
}

private static class ViewWrapper {
     private View mTarget;

     public ViewWrapper(View target) {
          this.mTarget = target;
     }

     public int getWidth() {
         return mTarget.getLayoutParams().width;
     }

    public void setWidth(int width) {
         mTarget.getLayoutParams().width = width;
         mTarget.requestLayout();
    }

}

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

@Override
public void onWindowFocusChanged(boolean hasFocus) {
      super.onWindowFocusChanged(hasFocus);
      if (hasFocus) {
          Button button = (Button)findViewById(R.id.button1);
          performAnimate(button, button.getWidth(), 500);
      }
}

private void performAnimate(final View target, final int start, final int end) {
      ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
      valueAnimator.addUpdateListener(new AnimatorUpdateListener() {

          // 持有一个IntEvaluator对象,方便下面估值的时候使用
          private IntEvaluator mEvaluator = new IntEvaluator();

          @Override
          public void onAnimationUpdate(ValueAnimator animator) {
              // 获得当前动画的进度值,整型,1-100之间
              int currentValue = (Integer) animator.getAnimatedValue();
              Log.d(TAG, "current value: " + currentValue);

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

      valueAnimator.setDuration(5000).start();
}

3,属性动画的工作原理

属性动画的原理:属性动画要求动画作用的对象提供该属性的set和get方法,属性动画根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。
下面从ObjectAnimator的start方法为入口,查看一下ObjectAnimator的源码:

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

上述代码的作用很简单,注意anim.cancel()和3个循环,意思就是判断如果当前动画、等待动画(Pending)和延迟动画(Delay)中有和当前动画相同的动画,那么就把相同的动画取消掉,然后打印一堆的Log,再调用父类ValueAnimator的start方法,所以接下来需要参谋参谋ValueAnimator的start方法:

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;
    updateScaledDuration(); // in case the scale factor has changed since creation time
    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.start(),animationHandler是一个Runnable,通过各种追踪代码,一直会调用到JNI层,最后还是会回调回来,一直到ValueAnimator的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方法,接下来就看看animateValue方法:

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中可以看出,get方法其实是通过反射来调用的:

private void setupValue(Object target, Keyframe kf) {
    if (mProperty != null) {
        Object value = convertBack(mProperty.get(target));
        kf.setValue(value);
    }
    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());
    }
}

当动画的下一帧到来的时候,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());
        }
    }
}

ps:各位,《Android开发艺术探索》真的是android进阶的一本好书,虽然很多概念在我这里已经很清晰并不算什么难点,但是该书还是帮我重新系统性回顾了一遍android相关知识,我的笔记做的笔记详细,但是还是建议购买一本正版《Android开发艺术探索》方便学习(ps:绝非打广告啊)。

你可能感兴趣的:(《Android开发艺术探索》笔记6:动画的深入分析)