[Android开发艺术探索阅读笔记]第7章 Android 动画深入分析

Android 动画分为 3 种,View 动画、帧动画、属性动画。在本章中,首先简单介绍View动画以及自定义View动画的方式,接着介绍View动画的一些特殊的使用场景,最后对属性动画做一个全面性的介绍,另外还介绍使用动画的一些注意事项。

View 动画

作用对象是 View。支持 4 种 动画效果。

  1. 平移动画
  2. 缩放动画
  3. 旋转动画
  4. 透明度动画

View 动画的种类

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@[package:]anim/interpolator_resource"
    android:shareInterpolator=["true" | "false"] >
        <alpha
          android:fromAlpha="float"
          android:toAlpha="float"/>
        
        <scale
          android:fromXScale="float"
          android:toXScale="float"
          android:fromYScale="float"
          android:toYScale="float"
          android:pivotX="float"
          android:pivotY="float"/> 
        <translate
          android:fromXDelta="float"
          android:toXDelta="float"
          android:fromYDelta="float"
          android:toYDelta="float"/>  
        <rotate
          android:fromDegrees="float"
          android:toDegrees="float"
          android:pivotX="float"
          android:pivotY="float"/>
    </set>
复制代码

android:interpolater

集合所采用的插值器,默认为@android:anim/accelerate_decelerate_interpolator

android:shareInterpolater

集合中的动画是否和集合共享一个插值器。

自定义 View 动画

继承 Animation

public class CustomAnimation extends Animation {

  @Override public void initialize(int width, int height, int parentWidth, int parentHeight) {
    super.initialize(width, height, parentWidth, parentHeight);
    // 做一些初始化工作
  }

  @Override protected void applyTransformation(float interpolatedTime, Transformation t) {
    super.applyTransformation(interpolatedTime, t);
    // 进行相应的矩阵变化
  }
}

复制代码

帧动画

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

frame_animation.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="@mipmap/ic_launcher"
      android:duration="500"/>

  <item
      android:drawable="@mipmap/ic_launcher_round"
      android:duration="500"/>

  <item
      android:drawable="@mipmap/egg"
      android:duration="500"/>

</animation-list>
复制代码

    textView.setBackgroundResource(R.drawable.frame_animation);
    AnimationDrawable background = (AnimationDrawable) textView.getBackground();
    background.start();
复制代码

帧动画的使用比较简单,但是比较容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。

View 动画的特殊使用场景

LayoutAnimation

作用于 ViewGroup, 为 ViewGroup 指定一个动画,当它的子元素出场时都会具有这种效果。

layout_animation.xml

<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:delay="0.5"
    android:animationOrder="normal"
    android:animation="@anim/anim_item"
    >

</layoutAnimation>
复制代码

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>
复制代码

android:delay

子元素开始动画的时间延迟,如果子元素入场动画的时间周期为300ms,0.5 代表每个子元素都要延迟150ms才能播放入场动画。总体来说,第一个子元素延迟150ms开始播放入场动画,第2个子元素延迟300ms开始播放入场动画,依次类推。

android:animationOrder

normal表示顺序显示,reverse表示逆向显示,random则是随机播放入场动画。

android:animation

为元素指定具体的入场动画

指定android:layoutAnimation属性

  <android.support.v7.widget.RecyclerView
      android:id="@+id/recyclerview"
      android:layoutAnimation="@anim/layout_animation"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      />
复制代码

Activity 的切换效果

Github项目解析(九)-->实现Activity跳转动画的五种方式

    /**
     * Call immediately after one of the flavors of {@link #startActivity(Intent)}
     * or {@link #finish} to specify an explicit transition animation to
     * perform next.
     *
     * <p>As of {@link android.os.Build.VERSION_CODES#JELLY_BEAN} an alternative
     * to using this with starting activities is to supply the desired animation
     * information through a {@link ActivityOptions} bundle to
     * {@link #startActivity(Intent, Bundle)} or a related function.  This allows
     * you to specify a custom animation even when starting an activity from
     * outside the context of the current top activity.
     *
     * @param enterAnim A resource ID of the animation resource to use for
     * the incoming activity.  Use 0 for no animation.
     * @param exitAnim A resource ID of the animation resource to use for
     * the outgoing activity.  Use 0 for no animation.
     */
    public void overridePendingTransition(int enterAnim, int exitAnim) {
        try {
            ActivityManagerNative.getDefault().overridePendingTransition(
                    mToken, getPackageName(), enterAnim, exitAnim);
        } catch (RemoteException e) {
        }
    }
复制代码

在 startActiviy 或者 finish 之后立刻执行。

属性动画

API11 之后加入。 API11 之前使用 nineoldandroids 代替。

使用属性动画

对任意对象的属性进行动画。
默认帧率是 10ms/帧。

常用的动画类:

  1. ValueAnimator
  2. ObjectAnimator : 继承自ValueAnimator
  3. AnimatorSet : 动画集合
  /**
   * 改变一个对象(myObject)的translationY属性,让其沿着Y轴向上平移一段距离:它的高度,该动画在
   * 默认时间内完成,动画的完成时间是可以定义的。
   */
  private void example1(View view) {
    ObjectAnimator.ofFloat(view, "translationY", view.getHeight()).start();
  }

  /**
   * 改变一个对象的背景色属性,典型的情形是改变View的背景色,下面的动画可以让背景色在3秒内实现从0xFFFF8080到
   * 0xFF8080FF的渐变,动画会无限循环而且会有反转的效果。
   */
  private void example2(View view) {
    ObjectAnimator animator = ObjectAnimator.ofInt(view, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
    animator.setDuration(3000);
    animator.setEvaluator(new ArgbEvaluator());
    animator.setRepeatCount(ValueAnimator.INFINITE); // 无限循环
    animator.setRepeatMode(ValueAnimator.REVERSE);
    animator.start();
  }

  /**
   * 动画集合,5秒内对View的旋转、平移、缩放和透明度都进行了改变。
   */
  private void example3(View view) {
    AnimatorSet set = new AnimatorSet();
    set.playSequentially(
        ObjectAnimator.ofFloat(view,"rotationX",0,360),
        ObjectAnimator.ofFloat(view,"rotationY",0,180),
        ObjectAnimator.ofFloat(view,"translationX",0,90),
        ObjectAnimator.ofFloat(view,"translationY",0,90),
        ObjectAnimator.ofFloat(view,"scaleX",1,1.5f),
        ObjectAnimator.ofFloat(view,"scaleY",1,0.5f),
        ObjectAnimator.ofFloat(view,"alpha",1,0.25f,1)
    );

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

复制代码

插值器和估值器

TimeInterpolate: 根据时间流逝的百分比来计算当前的属性值改变的百分比。
系统预置了:

  1. LinearInterpolate (线性插值器:匀速动画)
  2. AccelerateDecelerateInterpolator (加减速插值器,动画两头慢中间快)
  3. DecelerateInterpolater (减速插值器:动画越来越慢)

TypeEvaluator:类型估值算法。根据当前属性改变的百分比来计算改变后的属性值。
系统预置的有

  1. IntEvaluator
  2. FloatEvaluator
  3. ArgbEvaluator (针对Color属性)

android.view.animation.LinearInterpolator


/**
 * An interpolator where the rate of change is constant
 */
@HasNativeInterpolator
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();
    }
}

复制代码

android.animation.IntEvaluator

/**
 * This evaluator can be used to perform type interpolation between <code>int</code> values.
 */
public class IntEvaluator implements TypeEvaluator<Integer> {

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

属性动画要求对象的属性必须有 set 和 get 方法。

自定义插值器需要实现 Interpolator 或者 TimeInterpolater,自定义估值算法需要实现 TypeEvaluator。

如果要对其他类型(非 int 、float、Color)做动画,必须要自定义类型估值算法。

属性动画的监听器

主要有 2 个接口: AnimatorUpdateListenerAnimatorListener

android.animation.Animator.AnimatorListener


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

        void onAnimationCancel(Animator animation);

        void onAnimationRepeat(Animator animation);
    }

复制代码

android.animation.ValueAnimator.AnimatorUpdateListener


    // 监听整个动画过程,每播放一帧就会回调一次。
   public static interface AnimatorUpdateListener {
        /**
         * <p>Notifies the occurrence of another frame of the animation.</p>
         *
         * @param animation The animation which was repeated.
         */
        void onAnimationUpdate(ValueAnimator animation);

    }
复制代码

对任意属性做动画

Q:给Button加一个动画,让这个Button的宽度从当前宽度增加到500px。

A:View动画只支持4种类型:平移(Translate),旋转(Rotate),缩放(Scale),透明度(Alpha)。无法达到需求。

属性动画的原理:需要动画对象提供 get,set 方法,属性动画根据外界传递的该属性的初始值和最终值,以动画效果多次去调用 set 方法。

总结:对 Object 的属性 abc 做动画,需要同时满足 2 个条件:

  1. Object 必须要提供 setAbc() 方法,如果动画的时候没有传递初始值,那么还需要提供 getAbc() 方法。因为系统需要去获取 adc 属性的初始值(若不满足,app 直接 crash)。
  2. Object 的 setAbc() 方法对属性 abc 所做的改变必须能够通过某种方法反映出来,比如会带来 Ui的改变。(若不满足,动画没有效果但不会 crash)。

如果直接对 Button 的 windth 使用属性动画就会没有动画效果。

set方法在 TextView 中

android.widget.TextView#setWidth

// 设置最大宽度和最小宽度
 public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;

        requestLayout();
        invalidate();
    }
复制代码

get方法在 View 中。

android.view.View#getWidth

    // 获取 view 的宽度
    public final int getWidth() {
        return mRight - mLeft;
    }

复制代码

解决方法

  1. 给对象加上 set,get 方法

    内部SDK没有权限。

  2. 包装类

      private class ViewWraper {
    private View mTarget;

    public ViewWraper(View target) {
      mTarget = target;
    }

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

    public void setWidth(int width) {
      mTarget.getLayoutParams().width = width;
      mTarget.requestLayout();
    }
  }
复制代码
  1. 监听动画过程,自行实现属性的改变。

ValueAnimator 本身不作用于任何对象,直接使用它没有任何动画效果。它可以对一个值做动画,然后我们可以监听其动画过程。

属性动画的工作原理

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

android.animation.ObjectAnimator#start

  @Override
    public void start() {
        // 如果当前动画、等待的动画(Pending)和延迟的动画(delay)中有何当前相同的动画,就把相同的动画给取消掉。
        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));
            }
        }
        
        // 调用父类的 start
        super.start();
    }
复制代码

android.animation.ValueAnimator#start

   private void start(boolean playBackwards) {
   
   // 需要运行在有 looper 的线程中
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
            if (mRepeatCount == INFINITE) {
                // Calculate the fraction of the current iteration.
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        // Resets mLastFrameTime when start() is called, so that if the animation was running,
        // calling start() would put the animation in the
        // started-but-not-yet-reached-the-first-frame phase.
        mLastFrameTime = 0;
        AnimationHandler animationHandler = AnimationHandler.getInstance();
        animationHandler.addAnimationFrameCallback(this, (long) (mStartDelay * sDurationScale));

        if (mStartDelay == 0 || mSeekFraction >= 0) {
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            startAnimation();
            if (mSeekFraction == -1) {
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }

复制代码

android.animation.ValueAnimator#doAnimationFrame


    public final void doAnimationFrame(long frameTime) {
        AnimationHandler handler = AnimationHandler.getInstance();
        if (mLastFrameTime == 0) {
            // First frame
            handler.addOneShotCommitCallback(this);
            if (mStartDelay > 0) {
                startAnimation();
            }
            if (mSeekFraction < 0) {
                mStartTime = frameTime;
            } else {
                long seekTime = (long) (getScaledDuration() * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false; // allow start time to be compensated for jank
        }
        mLastFrameTime = frameTime;
        if (mPaused) {
            mPauseTime = frameTime;
            handler.removeCallback(this);
            return;
        } else if (mResumed) {
            mResumed = false;
            if (mPauseTime > 0) {
                // Offset by the duration that the animation was paused
                mStartTime += (frameTime - mPauseTime);
                mStartTimeCommitted = false; // allow start time to be compensated for jank
            }
            handler.addOneShotCommitCallback(this);
        }
        // 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);
        boolean finished = animateBasedOnTime(currentTime);

        if (finished) {
            endAnimation();
        }
    }
复制代码

在初始化的时候,如果属性的初始值没有提供,则get方法会被调用,get方法是用反射来调用的。

android.animation.PropertyValuesHolder#setupValue

 private void setupValue(Object target, Keyframe kf) {
        if (mProperty != null) {
            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());
            }
        }
    }
复制代码

当下一帧到来的时候,PropertyValuesHolder 中的 setAnimatedValue 会将新的属性值设置给对象。set 也是通过反射来调用的。

android.animation.PropertyValuesHolder#setAnimatedValue

  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 退出时及时停止。View 动画不存在此问题。

  3. 兼容性
    在 3.0 以下需要适配。

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

  5. 不要使用 px
    尽量使用 dp

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

  7. 硬件加速
    建议开启,提高动画流程性

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值