《Android开发艺术探索》第7章- Android 动画深入分析读书笔记

1. View 动画

1.1 Android 动画的分类有哪些?

总共有两类动画:View Animation(视图动画) 和 Property Animation(属性动画)。其中,View Animation 又包括 Tween Animation(补间动画)和 Frame Animation(逐帧动画);Property Animation 又包括 ValueAnimator 和 ObjectAnimator。

Android动画
ViewAnimation-视图动画-API1引入
PropertyAnimation-属性动画-API1引入
TweenAnimation-补间动画
FrameAnimation-逐帧动画
ValueAnimator
ObjectAnimator

1.2 Android 动画的特点是什么?

动画特点
Tween Animation(补间动画)通过对控件不断地执行图像变换(平移、缩放、旋转、透明度)从而产生动画,是一种渐进式动画,支持自定义
Frame Animation(逐帧动画)通过顺序地播放一系列图像从而产生动画效果
ValueAnimator不会对控件执行任何操作,只会在监听回调中返回值得渐变过程
ObjectAnimator通过动态地改变控件得属性从而达到动画效果

1.3 Tween Animation 补间动画中的轴点是什么作用?

在缩放动画和旋转动画中有轴点的概念,默认情况下轴点是 View 的中心点。

对于缩放动画来来说,如果轴点是 View 的中心点,在水平方向上会导致 View 向左右两个方向同时缩放;如果把轴点设为 View 的右边界,那么 View 只会向左边进行缩放。

对于旋转动画来说,View 是围绕着轴点进行旋转的,如果轴点是 View 的中心点,那么 View 的旋转看起来就是稳定地旋转,否则会形成一种偏心旋转效果。

1.4 自定义 View 动画的步骤是什么?

  1. 集成 Animation 抽象类;
  2. 重写它的 initializeapplyTransformation 方法;
  3. initialize 方法中做一些初始化工作;
  4. applyTransformation 方法中进行相应的矩阵变换。

这里展示一个来自 ApiDemos 里的翻转动画效果:

public class FlipAnimation extends Animation {
    private float mStartDegree;
    private float mEndDegree;
    private float mCenterX;
    private float mCenterY;
    private Camera mCamera;
    private boolean mFlag;
    private float mZDistance = 400f;
    public FlipAnimation(float startDegree, float endDegree, float centerX, float centerY, boolean flag) {
        mStartDegree = startDegree;
        mEndDegree = endDegree;
        mCenterX = centerX;
        mCenterY = centerY;
        mFlag = flag;
    }

    @Override
    public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mCamera = new Camera();
    }

    @Override
    protected void applyTransformation(float interpolatedTime, Transformation t) {
        super.applyTransformation(interpolatedTime, t);
        final float startDegree = mStartDegree;
        final float endDegree = mEndDegree;
        final float zDistance = mZDistance;
        final float centerX = mCenterX;
        final float centerY = mCenterY;
        float currDegree = startDegree + interpolatedTime * (endDegree - startDegree);
        Camera camera = mCamera;
        Matrix matrix = t.getMatrix();
        camera.save();
        if (mFlag) {
            camera.translate(0, 0, interpolatedTime * zDistance);
        } else {
            camera.translate(0,0, (1 - interpolatedTime) * zDistance);
        }
        camera.rotateX(currDegree);
        camera.getMatrix(matrix);
        camera.restore();
        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

页面布局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    android:id="@+id/rl"
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:gravity="center"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ImageView
        android:id="@+id/cab"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@drawable/junk_cab"
        android:layout_centerHorizontal="true" />

    <ImageView
        android:id="@+id/junk_ok"
        android:visibility="invisible"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:src="@drawable/ok"
        android:layout_below="@id/cab"
        android:layout_centerHorizontal="true" />

    <ImageView
        android:id="@+id/bin"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="8dp"
        android:src="@drawable/junk_bin"
        android:layout_below="@id/cab"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

页面代码如下:

public class CustomFlipAnimationActivity extends Activity {

    private RelativeLayout mRl;
    private ImageView mIvCab;
    private ImageView mIvJunkOk;
    private ImageView mIvBin;

    public static void start(Context context) {
        Intent starter = new Intent(context, CustomFlipAnimationActivity.class);
        context.startActivity(starter);
    }

    private boolean mInvisToVis = true;
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom_flip_animation);

        initViews();

        mRl.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Rect rect = new Rect();
                mRl.getHitRect(rect);
                final float centerX = rect.centerX();
                final float centerY = rect.centerY();
                FlipAnimation flipAnimation = new FlipAnimation(0.0F, 90.0F, centerX, centerY, true);
                flipAnimation.setDuration(600L);
                flipAnimation.setInterpolator(new AccelerateInterpolator());
                flipAnimation.setAnimationListener(new Animation.AnimationListener() {
                    @Override
                    public final void onAnimationStart(Animation paramAnonymousAnimation) {
                    }

                    @Override
                    public final void onAnimationEnd(Animation paramAnonymousAnimation) {
                        if (mInvisToVis) {
                            mIvJunkOk.setVisibility(View.VISIBLE);
                            mIvCab.setVisibility(View.INVISIBLE);
                            mIvBin.setVisibility(View.INVISIBLE);
                        } else {
                            mIvJunkOk.setVisibility(View.INVISIBLE);
                            mIvCab.setVisibility(View.VISIBLE);
                            mIvBin.setVisibility(View.VISIBLE);
                        }
                        mInvisToVis = !mInvisToVis;
                        FlipAnimation localb = new FlipAnimation(270.0F, 360.0F, centerX, centerY, false);
                        localb.setDuration(600L);
                        localb.setFillAfter(true);
                        localb.setInterpolator(new DecelerateInterpolator());
                        localb.setAnimationListener(null);
                        mRl.startAnimation(localb);
                    }

                    @Override
                    public final void onAnimationRepeat(Animation paramAnonymousAnimation) {
                    }
                });
                mRl.startAnimation(flipAnimation);
            }
        });
    }

    private void initViews() {
        mRl = (RelativeLayout) findViewById(R.id.rl);
        mIvCab = (ImageView) findViewById(R.id.cab);
        mIvJunkOk = (ImageView) findViewById(R.id.junk_ok);
        mIvBin = (ImageView) findViewById(R.id.bin);
    }
}

翻转效果如下:
在这里插入图片描述

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

2.1 如何控制 ViewGroup 中子元素的出场效果?

使用 <layoutAnimation>,一般用于 ListView

2.2 如何自定义 Activity 的切换效果?

使用 overridePendingTransition(int enterAnim, int exitAnim) 方法,这个方法必须在 startActivity(Intent intent) 或者 finish() 之后被调用才有效。

当启动一个 Activity 时,overridePendingTransition(int enterAnim, int exitAnim) 方法中的 enterAnim 表示给新打开的 Actiivty 添加入场动画效果,exitAnim 表示给当前的 Activity 添加出场动画效果。

当关闭一个 Activity 时,overridePendingTransition(int enterAnim, int exitAnim) 方法中的 enterAnim 表示恢复到前台的 Actiivty 添加入场动画效果,exitAnim 表示给关闭的 Activity 添加出场动画效果。

也就是说,overridePendingTransition(int enterAnim, int exitAnim) 为即将到来的 Activity 添加入场动画效果,为即将退出的 Activity 添加出场动画效果。

需要注意的是,enterAnimexitAnim 都是定义在 anim 目录下的动画资源文件的 id。如果不希望有任何页面切换效果,可以把这个参数都传入 0 即可。

2.3 如何自定义 Fragment 的切换效果?

使用 setCustomAnimations(@AnimRes int enter, @AnimRes int exit) 这个方法,这里同样是定义在 anim 目录下的动画资源文件的 id。

3. 属性动画

3.1 插值器和估值器的作用分别是什么?

插值器(或者说时间插值器)需要实现 Interpolator 接口或者 TimeInterpolator 接口:

/**
 * A time interpolator defines the rate of change of an animation. This allows animations
 * to have non-linear motion, such as acceleration and deceleration.
 */
public interface TimeInterpolator {

    /**
     * Maps a value representing the elapsed fraction of an animation to a value that represents
     * the interpolated fraction. This interpolated value is then multiplied by the change in
     * value of an animation to derive the animated value at the current elapsed animation time.
     *
     * @param input A value between 0 and 1.0 indicating our current point
     *        in the animation where 0 represents the start and 1.0 represents
     *        the end
     * @return The interpolation value. This value can be more than 1.0 for
     *         interpolators which overshoot their targets, or less than 0 for
     *         interpolators that undershoot their targets.
     */
    float getInterpolation(float input);
}
public interface Interpolator extends TimeInterpolator {
    // A new interface, TimeInterpolator, was introduced for the new android.animation
    // package. This older Interpolator interface extends TimeInterpolator so that users of
    // the new Animator-based animations can use either the old Interpolator implementations or
    // new classes that implement TimeInterpolator directly.
}

需要说明的是:

getInterpolation 方法的 input 参数与我们设定的任何值都没有关系,只与时间有关,随着时间的推移,动画的进度也自然地增加,这个值也会从 0 增加到 1.0。所以,这个值的取值范围是 [0,1.0],其中 0 表示动画刚开始,1.0 表示动画结束了,0.5 表示动画进行到一半了。

getInterpolation 方法的返回值表示当前实际想要显示的进度,这个值可以超过 1.0,也可以小于 0。超过 1.0 表示超过了目标值,小于 0 表示小于开始位置。

通过 setInterpolator(TimeInterpolator value) 方法设置插值器,这是策略模式的应用了。

估值器要实现 TypeEvaluator 接口:

/**
 * Interface for use with the {@link ValueAnimator#setEvaluator(TypeEvaluator)} function. Evaluators
 * allow developers to create animations on arbitrary property types, by allowing them to supply
 * custom evaluators for types that are not automatically understood and used by the animation
 * system.
 *
 * @see ValueAnimator#setEvaluator(TypeEvaluator)
 */
public interface TypeEvaluator<T> {

    /**
     * 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 * (x1 - x0)</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.
     * @param endValue   The end value.
     * @return A linear interpolation between the start and end values, given the
     *         <code>fraction</code> parameter.
     */
    public T evaluate(float fraction, T startValue, T endValue);

}

根据当前实际显示的进度,动画开始属性值,动画结束属性值,来计算出当前要显示的属性值。其中,fraction 的值就是插值器的 getInterpolation 方法的返回值。

3.2 如何使用属性动画对任意属性做动画?

  1. 尝试给控件提供该属性的 get 和 set 方法,这样属性动画会根据外界传递的该属性的初始值和最终值,以动画的效果多次去调用 set 方法,每次传递给 set 方法的值都不一样,这样随着时间的推移,所传递的值越来越接近最终值。

    需要说明的是,set 方法是必须的,get 方法只有在动画没有传递初始值的时候才需要,因为此时系统需要通过 get 方法去获取初始值,如果没有 get 方法则会取动画参数类型的默认值作为初始值,当无法获取动画参数类型的默认值时,则会直接崩溃。

  2. 如果不能给直接给控件添加该属性的 get 和 set 方法,则可以使用一个类来包装原始的控件对象,间接地为其提供 get 和 set 方法;

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

下面使用具体的例子来说明:

使用动画在 5s 内把 Button 的宽度增加到 500 px。

我们先采用继承 Button 来自定义一个 MyButton 来实现:

public class MyButton extends Button {

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

使用属性动画来实现动画效果,代码如下:

ObjectAnimator.ofInt(mBtn5, "breadth", 500).setDuration(2000).start();

效果如下:
在这里插入图片描述
可以看到,动画开始的宽度是从 0 开始的,这是因为我们并没有提供 breadth 属性的 get 方法,而 breadth 参数的默认值是 0,所以动画开始的宽度是 0。那该怎么办呢?添加对应的 get 方法即可。

public class MyButton extends Button {

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

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

    public int getBreadth() {
        return getWidth();
    }
}

代码不用改动,再次查看效果:
在这里插入图片描述
我们还可以使用一个类包装 Button 对象来实现:

private static class ViewWrapper {
    private View mTarget;
    private ViewWrapper(View target) {
        mTarget = target;
    }
    public int getBreadth() {
        return mTarget.getLayoutParams().width;
    }
    public void setBreadth(int width) {
        mTarget.getLayoutParams().width = width;
        mTarget.requestLayout();
    }
}

点击按钮时的代码如下:

ViewWrapper viewWrapper = new ViewWrapper(mBtn3);
ObjectAnimator.ofInt(viewWrapper, "breadth", mBtn3.getWidth(), 500).setDuration(2000).start();

同样可以实现效果。

现在需求变化了:使用动画在 5s 内把 Button 的宽度和高度都增加到 500 px。

public class MyButton extends Button {

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    
    public void setSize(PointF point) {
        getLayoutParams().width = (int) point.x;
        getLayoutParams().height = (int) point.y;
        requestLayout();
    }
}

点击按钮的代码如下:

ObjectAnimator.ofObject(mBtn6, "size", new PointFEvaluator(), new PointF(500F,500F)).setDuration(2000).start();

运行程序,崩溃了,日志如下:

W/PropertyValuesHolder( 6601): Method getSize() with type null not found on target class class com.wzc.chapter_7.MyButton
D/AndroidRuntime( 6601): Shutting down VM
E/AndroidRuntime( 6601): FATAL EXCEPTION: main
E/AndroidRuntime( 6601): Process: com.wzc.chapter_7, PID: 6601
E/AndroidRuntime( 6601): java.lang.NullPointerException: Attempt to read from field 'float android.graphics.PointF.x' on a null object reference
E/AndroidRuntime( 6601): 	at android.animation.PointFEvaluator.evaluate(PointFEvaluator.java:73)
E/AndroidRuntime( 6601): 	at android.animation.PointFEvaluator.evaluate(PointFEvaluator.java:23)
E/AndroidRuntime( 6601): 	at android.animation.KeyframeSet.getValue(KeyframeSet.java:202)
E/AndroidRuntime( 6601): 	at android.animation.PropertyValuesHolder.calculateValue(PropertyValuesHolder.java:1017)
E/AndroidRuntime( 6601): 	at android.animation.ValueAnimator.animateValue(ValueAnimator.java:1561)
E/AndroidRuntime( 6601): 	at android.animation.ObjectAnimator.animateValue(ObjectAnimator.java:987)
E/AndroidRuntime( 6601): 	at android.animation.ValueAnimator.setCurrentFraction(ValueAnimator.java:692)
E/AndroidRuntime( 6601): 	at android.animation.ValueAnimator.setCurrentPlayTime(ValueAnimator.java:655)
E/AndroidRuntime( 6601): 	at android.animation.ValueAnimator.start(ValueAnimator.java:1087)
E/AndroidRuntime( 6601): 	at android.animation.ValueAnimator.start(ValueAnimator.java:1106)
E/AndroidRuntime( 6601): 	at android.animation.ObjectAnimator.start(ObjectAnimator.java:852)
E/AndroidRuntime( 6601): 	at com.wzc.chapter_7.PropertyAnimationActivity$6.onClick(PropertyAnimationActivity.java:104)
E/AndroidRuntime( 6601): 	at android.view.View.performClick(View.java:7509)
E/AndroidRuntime( 6601): 	at android.view.View.performClickInternal(View.java:7486)
E/AndroidRuntime( 6601): 	at android.view.View.access$3600(View.java:841)
E/AndroidRuntime( 6601): 	at android.view.View$PerformClick.run(View.java:28720)
E/AndroidRuntime( 6601): 	at android.os.Handler.handleCallback(Handler.java:938)
E/AndroidRuntime( 6601): 	at android.os.Handler.dispatchMessage(Handler.java:99)
E/AndroidRuntime( 6601): 	at android.os.Looper.loop(Looper.java:236)
E/AndroidRuntime( 6601): 	at android.app.ActivityThread.main(ActivityThread.java:8059)
E/AndroidRuntime( 6601): 	at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime( 6601): 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:656)
E/AndroidRuntime( 6601): 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:967)

从日志可以看到,是因为缺少 getSize 方法。

添加 getSize 方法:

public class MyButton extends Button {

    public MyButton(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public void setSize(PointF point) {
        getLayoutParams().width = (int) point.x;
        getLayoutParams().height = (int) point.y;
        requestLayout();
    }

    public PointF getSize() {
        return new PointF(getWidth(), getHeight());
    }
}

运行程序,效果如下:
在这里插入图片描述

4. 使用动画的注意事项

  1. 帧动画 OOM 问题:对于帧动画来说,当图片数量较多且图片尺寸较大时就很容易出现 OOM 问题,这时我们要减少图片数量,减小图片尺寸,或者采用其他动画来实现。
  2. 无限循环属性动画内存泄漏问题:无限循环的属性动画,在 Activity 退出时要及时取消动画,否则动画会无限循环,从而导致 View 控件无法释放,进一步导致整个 Activity 无法释放,最终引起内存泄漏。作为对比,View 动画不存在这种问题。
  3. View 动画完成后 View 无法隐藏问题:View 动画是对 View 的影像做动画,并不是真正地改变 View 的状态,所以会出现动画完成后设置 setVisibility(View.GONE) 失效的问题,这个时候只要调用 View.clearAnimation() 清除 View 动画即可解决此问题。
  4. 使用 px 动画在不同设备上动画效果不同的问题:尽量使用 dp,而不要使用 px。
  5. View 动画平移后,控件点击事件仍在原位置的问题:可以改用属性动画来实现。
  6. 动画不流畅问题:建议开启硬件加速,这样可以提供动画的流畅性。
  7. onAnimationEnd没有回调的问题onAnimationEnd 可能因各种异常没被回调,建议加上超时保护或者通过 postDelay 替代 onAnimationEnd,参考:onAnimationEnd is not getting called, onAnimationStart works fine

参考

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

willwaywang6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值