首页 > 代码库 > Android开发艺术探索——第七章:Android动画深入分析

Android开发艺术探索——第七章:Android动画深入分析

Android开发艺术探索——第七章:Android动画深入分析


Android的动画可以分成三种,view动画,帧动画,还有属性动画,其实帧动画也是属于view动画的一种,,只不过他和传统的平移之类的动画不太一样的是表现形式上有点不一样,view动画是通过对场景的不断图像交换而产生的动画效果,而帧动画就是播放一大段图片,很显然,图片多了会OOM,属性动画通过动态的改变对象的属性达到动画效果,也是api11的新特性,在低版本无法使用属性动画,但是我们依旧有一些兼容库,OK,我们还是继续来看下详细的类别

一.View动画

View动画的作用是view,他支持四种动画,平移,缩放,旋转和透明,帧动画算是特殊的第五种了,我们接下来就一个个的去分析

1.View动画的种类

view动画的变换效果对应着Animation的四个子类,分别是TranslateAnimation,ScaleAnimation,RotateAnimation,AlphaAnimation,这四种动画可以通过xml来定义,也可以代码来实现,如图

技术分享

要使用view动画,首先要创建XML文件,这个文件的路径为res/anim/filename.xml,他的描述语法是固定的,我们来看下

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:shareInterpolator="true">


    <alpha
        android:fromAlpha="10.0"
        android:toAlpha="10.0" />

    <scale
        android:fromXScale="10dp"
        android:fromYScale="10dp"
        android:pivotX="10"
        android:pivotY="10"
        android:toXScale="10dp"
        android:toYScale="10dp" />

    <translate
        android:fromXDelta="10"
        android:fromYDelta="10"
        android:toXDelta="10"
        android:toYDelta="10" />

    <rotate
        android:fromDegrees="10"
        android:pivotX="10"
        android:pivotY="10"
        android:toDegrees="10" />

</set>

从上面的语法可以看出,view的动画既可以是单一的动画,也可以组合在一起,而set标签就是组合动画,对应着AnimationSet,他包含很多个类。我们来看下他的两个属性

android:interpolator

表示动画集合所使用的插值器,插值器影响动画的速度,比如非匀速动画就需要插值器来制作动画的过程,这个属性可以不指定,默认为加速减速插值器,这个会在后面详细描述

android:shareInterpolator

表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独的去指定所需要的插值器了

< translate>标签表示平移动画,对应的TranslateAnimation类,他可以使用一个view在水平和竖直方向完成平移,我们来看下他的属性

  • android:fromXDelta

表示x的起始值,比如0

  • android:fromYDelta

表示y的结束值,比如100

  • android:toXDelta

表示y的起始值

  • android:toYDelta

表示y的结束值

< scale>标签表示的是缩放动画,对应的ScaleAnimation,他可以使view具有放大,缩小的动画效果,他的一系列属性的含义如下

  • android:fromXScale

水平方向缩放的起始值,比如0.5

  • android:fromYScale

竖直方向缩放的起始值

  • android:pivotX

缩放轴点的x坐标,它会影响缩放的效果

  • android:pivotY

缩放轴点的y坐标,它会影响缩放的效果

  • android:toXScale

水平方向缩放的结束值,比如1.2

  • android:toYScale

竖直方向缩放的起始值

在< scale>中提到轴点的这个概念,我举个例子,默认情况下轴点事view的中心点,这个时候水平缩放的话会导致view向左右两个方向进行缩放,但是如果把轴点设置在view的右边界,那么view只会向左缩放,反之,xxx

标签是旋转标签,对应RotateAnimation,他可以让view旋转,他的属性如下

  • android:fromDegrees

旋转开始的角度,比如0

  • android:toDegrees

旋转结束的角度,比如180

  • android:pivotX

旋转轴点的x

  • android:pivotY

旋转轴点的y

在旋转中也有轴的概念,他也会影响到旋转的效果,轴点扮演者旋转轴的角色,view围绕着轴点进行旋转,默认情况下在view的中心,考虑到一种情况,view围绕自己的中心,和围绕左上角进行90度显然是不同的轨迹

< alpha>表示透明动画。对应的AlphaAnimation,我们看下属性

  • android:fromAlpha

表示透明度的起始值,比如0.1

  • android:toAlpha

表示透明度的结束值,比如1

上面都只是很简单的介绍了XM格式,具体的使用方法还是看文档,我们还有一些常用的属性如下

  • android:duration

动画的时间

  • android:fillAfter

动画结束之后是否停留在结束的位置

下面我们再来一个实际的例子

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:fillAfter="true"
    android:zAdjustment="normal">

    <translate
        android:duration="100"
        android:fromXDelta="0"
        android:fromYDelta="0"
        android:toXDelta="100"
        android:toYDelta="100" />

    <rotate
        android:duration="400"
        android:fromDegrees="0"
        android:toDegrees="90" />

</set>

如何使用尼,很简单

private void test1() {
        Animation animation = AnimationUtils.loadAnimation(this, R.anim.animation_test1);
        iv_icon.startAnimation(animation);
 }

我们来运行一下看下实际的效果

技术分享

除了在XML中定义之外,我们还可以通过代码来使用,如下

        AlphaAnimation alpha = new AlphaAnimation(0,1);
        alpha.setDuration(500);
        iv_icon.startAnimation(alpha);

在上面的代码中,我们创建了一个透明动画,这里就不演示了,我们还可以对动画进行监听,来看下代码

      alpha.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
                Log.i("TAG", "动画开始");
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                Log.i("TAG", "动画结束");
            }

            @Override
            public void onAnimationRepeat(Animation animation) {
                Log.i("TAG", "重复动画");
            }
        });

2.自定义View动画

除了系统提供的四种动画以外,我们还可以自定义动画,自定义是一种简单又复杂的工作,说他简单,是因为派生出来只需要继承animation这个抽象类,这里也不打算去讲讲,直接看APIdemo里面3D效果的源码来分析

如果需要地址,可以直接访问 Github for ApiDemo

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.android.apis.animation;

import android.view.animation.Animation;
import android.view.animation.Transformation;
import android.graphics.Camera;
import android.graphics.Matrix;

/**
 * An animation that rotates the view on the Y axis between two specified angles.
 * This animation also adds a translation on the Z axis (depth) to improve the effect.
 */
public class Rotate3dAnimation extends Animation {
    private final float mFromDegrees;
    private final float mToDegrees;
    private final float mCenterX;
    private final float mCenterY;
    private final float mDepthZ;
    private final boolean mReverse;
    private Camera mCamera;

    /**
     * Creates a new 3D rotation on the Y axis. The rotation is defined by its
     * start angle and its end angle. Both angles are in degrees. The rotation
     * is performed around a center point on the 2D space, definied by a pair
     * of X and Y coordinates, called centerX and centerY. When the animation
     * starts, a translation on the Z axis (depth) is performed. The length
     * of the translation can be specified, as well as whether the translation
     * should be reversed in time.
     *
     * @param fromDegrees the start angle of the 3D rotation
     * @param toDegrees the end angle of the 3D rotation
     * @param centerX the X center of the 3D rotation
     * @param centerY the Y center of the 3D rotation
     * @param reverse true if the translation should be reversed, false otherwise
     */
    public Rotate3dAnimation(float fromDegrees, float toDegrees,
            float centerX, float centerY, float depthZ, boolean reverse) {
        mFromDegrees = fromDegrees;
        mToDegrees = toDegrees;
        mCenterX = centerX;
        mCenterY = centerY;
        mDepthZ = depthZ;
        mReverse = reverse;
    }

    @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) {
        final float fromDegrees = mFromDegrees;
        float degrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);

        final float centerX = mCenterX;
        final float centerY = mCenterY;
        final Camera camera = mCamera;

        final Matrix matrix = t.getMatrix();

        camera.save();
        if (mReverse) {
            camera.translate(0.0f, 0.0f, mDepthZ * interpolatedTime);
        } else {
            camera.translate(0.0f, 0.0f, mDepthZ * (1.0f - interpolatedTime));
        }
        camera.rotateY(degrees);
        camera.getMatrix(matrix);
        camera.restore();

        matrix.preTranslate(-centerX, -centerY);
        matrix.postTranslate(centerX, centerY);
    }
}

我们来看下实际效果

技术分享

3.帧动画

帧动画就是顺序的播放一组图片,系统提供了一个AnimationDrawable来实现帧动画,帧动画的使用比较简单,我们看下在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="@drawable/ic_dashboard_black_24dp"
        android:duration="200" />
    <item
        android:drawable="@drawable/ic_dashboard_black_24dp"
        android:duration="200" />
    <item
        android:drawable="@drawable/ic_dashboard_black_24dp"
        android:duration="200" />

</animation-list>

然后将上述的XML作为view的背景并通过drawable来播放就可以了

   private void test2() {??

        iv_icon.setBackgroundResource(R.drawable.animation_test2);
        AnimationDrawable ad = (AnimationDrawable) iv_icon.getBackground();
        ad.start();
    }

帧动画比较简单,但是容易OOM,这个药注意

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

动画的应用场景很多,我们来看下,比如activity的切换动画,又比如viewgroup的子view切换动画等,一起来看下吧

1.LayoutAnimation

LayoutAnimation作用于ViewGroup,为ViewGroup指定一个动画,这样他的子元素出场的时候就会具有这种动画了,这种效果常常在listview上,我们时常会看到一种特殊的listview,他的每一个item都有一个动画,其实这并不是什么高深的技术,它使用的就是LayoutAnimation
,我们来看下具体的实现

(1).定义LayoutAnimation

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

</layoutAnimation>

他们的含义分别是

  • android:delay

表示子元素开始动画的延迟,假设子元素入场动画的周期为300ms,那么0.5表示每一个子元素都需要延迟150ms才能播放入场动画,总体来说,第一个子元素延迟150ms,第二个子元素300ms以此类推

  • android:animationOrder

表示子元素动画的顺序,有三种模式,normal,random,reverse,其中第一个表示顺序执行,第二个表示随机,第三个表示倒叙执行

  • android:animation
    为子元素指定动画,如下

(2)为子元素指定的入场动画

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"
    android:shareInterpolator="true">

    <alpha
        android:fromAlpha="0.1"
        android:toAlpha="1.0" />

    <translate
        android:fromXDelta="500"
        android:fromYDelta="0" />

</set>

(3)为ViewGroup指定layoutanimation属性,对于listview来说,这样item就具有出厂动画了

    <ListView
        android:id="@+id/mListView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layoutAnimation="@anim/anim_layout" />

除了在XML中实现,我们还可以通过LayoutAnimationController来实现

    Animation animation = AnimationUtils.loadAnimation(this,R.anim.anim_layout);
    LayoutAnimationController controller = new LayoutAnimationController(animation);
    controller.setDelay(0.5f);
    controller.setOrder(LayoutAnimationController.ORDER_NORMAL);
    mListView.setLayoutAnimation(controller);

2.Activity的切换效果

activity有默认的切换效果,但是这个效果我们是可以自定义的,主要是利用overridePendingTransition(int enterAnim, int exitAnim)这个方法,这个方法必须在startactivity或者finish之后调用才是有效的,里面的两个参数也很简单,就是进出的动画,让我们启动一个activity的时候,我们可以这样做

startActivity(new Intent(MainActivity.this,OneActivity.class));
//这是activity的跳转动画
overridePendingTransition(R.anim.animation,R.anim.anim_layout);

当activity退出的时候我们也可以为他指定一个切换效果

@Override
    public void onBackPressed() {
        super.onBackPressed();
        overridePendingTransition(R.anim.animation, R.anim.anim_layout);
    }

使用它只需要在后面调用,这个也是注意的地方,其他地方调用无效

在fragment中也是可以的,使用的方法是通过FragmentTransaction中的setCustomAnimations去使用

三.属性动画

属性动画是API11加入的,和view动画不同的是,他对作用对象进行了扩展,属性动画可以对任何对象做动画,甚至没有对象也是可以的,除了作用对象进行了扩展以外,属性动画的效果也得到了加强,不再像view动画一样只支持四中,属性动画有ValueAnimator,ObjectAnimator,AnimatorSet;

1.使用属性动画

属性动画可以对任意对象的属性进行动画而不仅仅是view,动画默认的时间间隔是300ms,默认帧率是10,可以达到的效果是: 在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,但是属性动画是从API11才开始有的,所有这个比较有限制性。当然,网上还是有很多的就兼容库,这个我们就不多说,我们举几个例子:

(1)改变一个对象的translationY属性,让其沿着Y轴向上平移一个时间,该动画在默认的时间完成,好,我们来看下怎么去用

ObjectAnimator.ofFloat(iv_icon, "translationY", -iv_icon.getHeight()).start();

我们看下效果

技术分享

(2)改变一个对象的背景颜色值,典型的就是改变view的背景,下面的动画是让view的背景从0xffff8080到0xff8080ff,动画会无限循环和反转

ValueAnimator valueAnimator =
        ObjectAnimator.ofInt(ll_content, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
valueAnimator.setDuration(3000);
valueAnimator.setEvaluator(new ArgbEvaluator());
valueAnimator.setRepeatCount(ValueAnimator.INFINITE);
valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
valueAnimator.start();

我们看下效果

技术分享

(3)动画集合,5s内对view的旋转,平移,缩放和透明度进行改变

AnimatorSet set = new AnimatorSet();
set.playTogether(
        ObjectAnimator.ofFloat(iv_icon, "rotationX", 0, 360),
        ObjectAnimator.ofFloat(iv_icon, "rotationY", 0, 180),
        ObjectAnimator.ofFloat(iv_icon, "rotation", 0, -90),
        ObjectAnimator.ofFloat(iv_icon, "trabslationX", 0, 90),
        ObjectAnimator.ofFloat(iv_icon, "trabslationY", 0, 90),
        ObjectAnimator.ofFloat(iv_icon, "scaleX", 0, 1.5f),
        ObjectAnimator.ofFloat(iv_icon, "scaleY", 0, 0.5f),
        ObjectAnimator.ofFloat(iv_icon, "alpha", 0, 2.5f, 1)
);
set.setDuration(3000).start();

我们看下效果

技术分享

属性动画还可以用XML来表示的,在res/animator目录下

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:duration="1000"
        android:propertyName="@string/app_name"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:startOffset="15"
        android:valueFrom="0.1dp"
        android:valueTo="1.0"
        android:valueType="colorType" />

    <animator
        android:duration="1000"
        android:repeatCount="infinite"
        android:repeatMode="restart"
        android:startOffset="15"
        android:valueFrom="0.5dp"
        android:valueTo="1.0dp"
        android:valueType="colorType" />

</set>

属性动画的各个参数是比较好理解的,我们简单来说下他们之间的含义

  • android:propertyName:表示属性动画作用对象的属性的名称
  • android:duration:表示动画的时长
  • android:valueFrom:表示属性的起始值
  • android:valueTo:表示属性的结束值
  • android:startOffset:表示动画的延迟时间,当动画开始后,需要延迟多少毫秒才会真正的播放
  • android:repeatCount:表示动画的重复次数
  • android:repeatMode:表示动画的重复模式
  • android:valueType:表示propertyName有两个属性有int和float两个可选项,分别表示属性的类型,和浮点型,另外,如果所制定的是颜色类型,那么就不需要指定propertyName,系统会自动对颜色类型进行处理

对于一个动画来说,有两个属性这里要特别注意一下,一个是count另一个就是mode

下面我们给出一个具体的事例来分析,我们通过XML定义属性动画并且作用在view上

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <objectAnimator
        android:duration="300"
        android:propertyName="x"
        android:valueTo="200"
        android:valueType="intType" />

    <objectAnimator
        android:duration="300"
        android:propertyName="y"
        android:valueTo="300"
        android:valueType="intType" />

</set>

那我们该如何使用尼?

AnimatorSet sets = (AnimatorSet) AnimatorInflater.loadAnimator(MainActivity.this, R.animator.property_animator);
sets.setTarget(iv_icon);
sets.start();

在实际开发当中我还是建议使用代码来实现属性动画,这是因为用代码会比较简单,比如一个view需要从左边移动到右边,但是如果但是XML,是不知道屏幕的宽高的

2.理解插值器和估值器

TimeInterpolator中文翻译是时间插值器的意思,他的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预设的是LinearInterpolator(线性加速器,匀速加速器),加速和减速插值器,TypeEvaluator的中文翻译是类型估值算法,也叫估值器,他的作用是根据当前属性变化的百分比来计算变化后的属性值,系统也预设了针对整型属性,浮点型,和color颜色值,属性动画中的插值器和估值器很重要,他们实现非匀速动画的重要手段,可能说的有点苦涩,我们去通过实际的例子就能很好的理解了

如图,他表示的是一个匀速动画,采用了线性插值器和整形估值算法,在40ms内,x从0-40的变换

技术分享

由于动画的默认刷新率为10ms/帧,所有该动画将分5帧进行,我们来考虑一下第三帧,当时间为20ms的时候,百分比为0.5,意味着时间过去了一半,那x改变了多少?其实x也是0.5,为什么因为他使用了线性的插值器也就是匀速动画,

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

很显然,线性插值器的返回值和输入值是一样的,因为都是0.5,这个估算我们可以看他的源码

/**
 * 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));
    }
}

上述的算法很简单,evaluate的三个参数分别表示估算的小树,开始值和结束值,对于我们的例子而言分别是0.5 0 40 ,所有返回给我们的是20,这就是(x= 20 t = 20ms)的由来

属性动画要求对象的属性有set get方法,插值器和估值器除了系统提供给我们的外,我们还可自定义,实现起来也很简单,因为他们都只是一个接口,而且内部只有一个方法,我们自己手动使用下试试

3.属性动画的监听

属性动画提供了监听器用于监听动画的播放过程,主要有两个接口AnimationUpdateListener和AnimationListener

AnimationListener的定义如下:

    public static interface AnimatorListener {
        /**
         * <p>Notifies the start of the animation.</p>
         *
         * @param animation The started animation.
         */
        void onAnimationStart(Animator animation);

        /**
         * <p>Notifies the end of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.</p>
         *
         * @param animation The animation which reached its end.
         */
        void onAnimationEnd(Animator animation);

        /**
         * <p>Notifies the cancellation of the animation. This callback is not invoked
         * for animations with repeat count set to INFINITE.</p>
         *
         * @param animation The animation which was canceled.
         */
        void onAnimationCancel(Animator animation);

        /**
         * <p>Notifies the repetition of the animation.</p>
         *
         * @param animation The animation which was repeated.
         */
        void onAnimationRepeat(Animator animation);
    }

从他的定义上可以看出,他监听了开始,结束,取消已经重复,同时为了方便开发,系统还提供了AnimatorListenerAdapter这个类,他是适配器,这样我们就可以选择监听了

    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);

    }

AnimatorUpdateListener比较特殊,他会监听整个动画过程,利用这个特性,我们可以做很多的事情

4.对任意属性做动画

这里最一个需求,就是给buttion设置一个动画,让他的宽度从当前的变成500px,这个可以用view动画来搞定,但是你仔细想想,view不能对宽高变化,所有我么可以使用属性动画,我们来试试

ObjectAnimator.ofInt(btn, "width", 500).setDuration(1000).start();

看效果

技术分享

上面的代码是有效果的,但是书中说是没效果,我们来看下作者的分享

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

  • (1)object必须要提供set方法,如果动画的时候没有传递初始值,那么我们还要提供get方法,因为系统要去取abc的属性(如果这条不满意,程序直接Crash)

  • (2)object的set方法对abc所做的改变必须通过某种方法反应,比如带来UI的改变(如果这条不满足,动画无效果但是不会Crash)

以上条件缺一不可,那么为什么我们对button的width属性做动画没有效果,这是因为button内部虽然提供了get/set方法,但是这个set方法并不是改变视图大小,他是textview新添加的方法,view是没有这个setWidth方法的,由于button继承了textview,所有button也就有了set方法,下面看一下这个get/get的源码

    /**
     * Makes the TextView exactly this many pixels wide.
     * You could do the same thing by specifying this number in the
     * LayoutParams.
     *
     * @see #setMaxWidth(int)
     * @see #setMinWidth(int)
     * @see #getMinWidth()
     * @see #getMaxWidth()
     *
     * @attr ref android.R.styleable#TextView_width
     */
    @android.view.RemotableViewMethod
    public void setWidth(int pixels) {
        mMaxWidth = mMinWidth = pixels;
        mMaxWidthMode = mMinWidthMode = PIXELS;

        requestLayout();
        invalidate();
    }

    /**
     * Return the width of the your view.
     *
     * @return The width of your view, in pixels.
     */
    @ViewDebug.ExportedProperty(category = "layout")
    public final int getWidth() {
        return mRight - mLeft;
    }

从上述的源码可以看出,get的确是获取view的宽度,而set是textview的专属方法,他的作用不是设置view的宽度,而是设置textview的最大宽度和最小宽度,这个和textview的宽度不死一个东西,具体来说,textview的宽度对应XML中的android:layout_width,而textview还有一个属性android:width,这个就对应了setwidth,总之textview和button的set/get干的不是同一件事,通过set无法改变控件的宽度,所以对width做属性动画没有效果,对于属性动画的两个条件来说,本例中的动画只满足了第一个条件,我们有三个解决办法:

  • 给你的对象增加set/get方法,前提是你有权限的话
  • 用这个类来包装原始对象,间接提供get/set方法
  • 采用ValueAnimator,监听动画过程自己去实现

我们来具体的实现下这三个解决办法

  • 1.给你的对象增加set/get方法,前提是你有权限的话

这个的意思很好理解,如果你有权限的话,加个set/get方法就搞定了,但是很多时候我们没有权限去这么做,比如本文开头所提到的问题,你无法给button加上一个合乎要求的setwidth方法,因为这个是Android SDK内部实现的,这个方法很简单,但是往往是不可行的,这里就不对其进行更多的分析了

  • 2.用这个类来包装原始对象,间接提供get/set方法

这是一个很有用的解决方法,是笔者最喜欢用的,因为用起来很方便,也好理解,下面我将一个具体的实现来介绍他

    private void performAnimate() {
        ViewWrapper viewWrapper = new ViewWrapper(btn);
        ObjectAnimator.ofInt(viewWrapper, "width", 500).setDuration(1000).start();
    }

    private static class ViewWrapper {

        private View mTarget;

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

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

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

上述代码在1000ms中宽度增加到500,为了达到这个效果我们写了一个包装类去提供方法,这样也就完美的实现了

  • 3.采用ValueAnimator,监听动画过程自己去实现

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

    private void performAnimator(final View target, final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            //持有一个IntEvaluator对象,方便下面估值的时候使用
            private IntEvaluator mEvaluator = new IntEvaluator();

            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                //获得当前动画的进度值,整形1-100之间
                int currentValue = http://www.mamicode.com/(int) animation.getAnimatedValue();
                //获得当前进度占整个动画之间的比例,浮点0-1之间
                float fraction = animation.getAnimatedFraction();
                //直接使用整形估值器,通过比例计算宽度,然后再设置给按钮
                target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
                target.requestLayout();
            }
        });
        valueAnimator.setDuration(5000).start();
    }

上面的代码的效果和刚才的viewwrapper是一样的,关于ValueAnimator还要再说一下,拿上来的例子来说,他会在5s内将一个数1变成100,然后动画的每一帧会回调的每一帧onAnimationUpdate方法,在这个方法里,我们可以获取当前的值和占用的比例我们可以计算出宽度是多少,比如时间过去了一半,当前值是50,比例是0.5,假设起始值为100,最终是500px,那么500-100=400,所有这个时候乘以0.5=200,这些都是内部实现,我们不用自己写,直接用。

5.属性动画的工作原理

属性动画他要求作用的对象必须有set方法根据传递的最终值去不断的更改然后set,而且每次的值都不一样,就这样根据时间的推移形成动画

那我们具体来看下源码是怎么操作的

首先我们找一个入口,可以看他的start方法

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

这段代码别看这么长,其实做的事情很简单,首先判断是否有相同的动画,是的话去掉,接下来就是弗雷调用的start,因为ObjectAnimator继承了ValuesAnimator,所以我们再看他的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();
    }

可以看出属性动画需要运行在Lopper线程中,上述代码终会调用AnimationHandler的start,这个并不是真正的handler,他是一个Runnable,看一下他的代码,通过代码我们发现,很快的调用了JNI层,不过JNI层最终还是调回来的,他的run方法被调用,这个Runnable涉及和底层的调用,我们来看下他的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,看下他的代码

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

上述代码中的calculateValues方法就是计算每一帧动画所对应的属性值,下面我么来看一下到底哪里调用set/get方法

在初始化的时候,如果属性的初始值没有提供,则get方法就会调用,请看PropertyValuesHolder中的setupValue

    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方法,下面是源码,通过反射调用

    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问题

这个问题主要还是帧动画中,当图片过多的时候就OOM了,这个在实际的开发中尤其注意,尽量避免使用帧动画

  • 2.内存泄漏

在属性动画中有一类无限循环的动画,如果你在activity退出后不停止的话,可能就会存在这个问题了

  • 3.兼容性问题

动画在3.0以下的系统上有缺陷,最好做好适配工作

  • 4.View动画的问题

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

  • 5.不要使用PX

在进行动画的过程,要尽量使用dp,使用px会导致适配问题

  • 6.动画元素的交互

将view移动后,在3.0以前的系统,不管是view动画还是属性动画,新位置都无法调用单机事件,同时老位置却可以,从3.0之后,但是事件就是懂后的,但是view动画仍然在原位置。

  • 7.硬件加速

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

Sample下载:点击下载

好的,文章又臭又长,大家就慢慢的看咯

  • 1.通往Android的神奇之旅 555974449 (快满了)
  • 2.Android旅行的路途 484167109
  • 3.Android进阶深度学习群 515171658(此群精品付费)

也欢迎大家关注我的微信公众号

技术分享

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android开发艺术探索——第七章:Android动画深入分析