Android 动画分析与优化

原理

在我们现实的生活中,时间是线性匀速地一秒一秒逝去,在设置完动画运动时间后,动画也是按时间线性匀速化进行的,但如果现在想让动画加速或者减速前进的话,我们就需要插值器-Interpolator帮忙了,它的任务就是修改动画进行的节奏,也就是说Interpolator的作用是控制动画过程速度快慢,这也就是Interpolator存在的意义了,当然单有Interpolator还是不足以完成任务的,我们还需要TypeEvaluator的协助,那么TypeEvaluator又是干嘛的呢?其实TypeEvaluator的作用是控制整个动画过程的运动轨迹,通俗地讲就是这条路往哪里走由TypeEvaluator说了算。 走进绚烂多彩的属性动画-Property Animation之TimeInterpolator和TypeEvaluator

Interpolator这个东西很难进行翻译,直译过来的话是补间器的意思,它的主要作用是可以控制动画的变化速率,比如去实现一种非线性运动的动画效果。那么什么叫做非线性运动的动画效果呢?就是说动画改变的速率不是一成不变的,像加速运动以及减速运动都属于非线性运动。Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法

系统提供了许多已经实现OK的插值器,具体如下:

java类描述
AccelerateInterpolator加速,开始时慢中间加速
DecelerateInterpolator减速,开始时快然后减速
AccelerateDecelerateInterolator先加速后减速,开始结束时慢,中间加速
AnticipateInterpolator反向 ,先向相反方向改变一段再加速播放
AnticipateOvershootInterpolator反向加超越,先向相反方向改变,再加速播放,会超出目的值然后缓慢移动至目的值
BounceInterpolator跳跃,快到目的值时值会跳跃,如目的值100,后面的值可能依次为85,77,70,80,90,100
CycleIinterpolator循环,动画循环一定次数,值的改变为一正弦函数:Math.sin(2 * mCycles * Math.PI * input)
LinearInterpolator线性,线性均匀改变
OvershottInterpolator超越,最后超出目的值然后缓慢改变到目的值
TimeInterpolator一个接口,允许你自定义interpolator,以上几个都是实现了这个接口

TimeInterpolator和TypeEvaluator

  • TimeInterpolator中文翻译为时间插值器,它的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画)、AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快)和DecelerateInterpolator(减速插值器:动画越来越慢)等;
  • TypeEvaluator的中文翻译为类型估值算法,它的作用是根据当前属性改变的百分比来计算改变后的属性值,系统预置的有IntEvaluator(针对整型属性)、FloatEvaluator(针对浮点型属性)和ArgbEvaluator(针对Color属性)。可能这么说还有点晦涩,没关系,下面给出一个实例就很好理解了。

看上述动画,很显然上述动画是一个匀速动画,其采用了线性插值器和整型估值算法,在40ms内,View的x属性实现从0到40的变换,由于动画的默认刷新率为10ms/帧,所以该动画将分5帧进行,我们来考虑第三帧(x=20 t=20ms),当时间t=20ms的时候,时间流逝的百分比是0.5 (20/40=0.5),意味这现在时间过了一半,那x应该改变多少呢,这个就由插值器和估值算法来确定。拿线性插值器来说,当时间流逝一半的时候,x的变换也应该是一半,即x的改变是0.5,为什么呢?因为它是线性插值器,是实现匀速动画的,下面看它的源码:

public class LinearInterpolator implements Interpolator {  
  
    public LinearInterpolator() {  
    }  
      
    public LinearInterpolator(Context context, AttributeSet attrs) {  
    }  
      
    public float getInterpolation(float input) {  
        return input;  
    }  
}  

很显然,线性插值器的返回值和输入值一样,因此插值器返回的值是0.5,这意味着x的改变是0.5,这个时候插值器的工作就完成了。

具体x变成了什么值,这个需要估值算法来确定,我们来看看整型估值算法的源码:

public class IntEvaluator implements TypeEvaluator<Integer> {  
  
    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方法(可选);插值器和估值算法除了系统提供的外,我们还可以自定义,实现方式也很简单,因为插值器和估值算法都是一个接口,且内部都只有一个方法,我们只要派生一个类实现接口就可以了,然后你就可以做出千奇百怪的动画效果。具体一点就是:自定义插值器需要实现Interpolator或者TimeInterpolator,自定义估值算法需要实现TypeEvaluator。还有就是如果你对其他类型(非int、float、color)做动画,你必须要自定义类型估值算法。

TimeInterplator

time interplator定义了属性值变化的方式,如线性均匀改变,开始慢然后逐渐快等。在Property Animation中是TimeInterplator,在View Animation中是Interplator,这两个是一样的,在3.0之前只有Interplator,3.0之后实现代码转移至了 TimeInterplator。Interplator继承自TimeInterplator,内部没有任何其他代码。

Frame Animation

Frame动画是一帧一帧的绘制出来的,是一系列图片按照一定的顺序展示的过程,和放电影的机制很相似,我们称为逐帧动画。Frame动画可以被定义在XML文件中,也可以完全编码实现。

正如上面所说,帧动画就像电影一样,每一帧就是一张图片,因此创建一个自定义帧动画的首要条件就是建立图片序列。我们可以用它来模拟简单的 gif 动图。

帧动画有两种实现方式,一种是使用XML文件,另一种是使用 AnimationDrawable 类,在代码里面实现动画。

AnimationDrawable常见api:

  • void start() 开始播放动画
  • void stop() 停止播放动画
  • addFrame(Drawable frame, int duration) - 添加一帧,并设置该帧显示的持续时间
  • void setOneShoe(boolean flag) - false为循环播放,true为仅播放一次
  • boolean isRunning() 是否正在播放

XML实现方式

XML实现方式又分两种,一种是 Animation-list,另一种是 Animated-selector。

咱们先来看第一种 Animation-list 的方式,我们在工程 res 目录下新建 drawable 文件夹(注意,animation-list不是放在 anim 文件夹下),然后新建一个 animation_list_test.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/a1"
        android:duration="150"></item>
    <item
        android:drawable="@mipmap/a2"
        android:duration="150"></item>
    <item
        android:drawable="@mipmap/a3"
        android:duration="150"></item>
    <item
        android:drawable="@mipmap/a1"
        android:duration="150"></item>
</animation-list>

MainActivity.java:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private final String TAG = MainActivity.class.getSimpleName();

    private ImageView mIVTest;
    private AnimationDrawable mAnimDrawable;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIVTest = (ImageView) findViewById(R.id.iv_test);
        mAnimDrawable = (AnimationDrawable) mIVTest.getBackground();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_start_animation:
                mAnimDrawable.start();
                break;
            case R.id.btn_stop_animation:
                mAnimDrawable.stop();
                break;
        }
    }
}

布局文件 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.meizu.rxjava.example.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <ImageView
        android:id="@+id/iv_test"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:layout_centerInParent="true"
        android:background="@drawable/animator_selector" />

    <Button
        android:id="@+id/btn_start_animation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:onClick="onClick"
        android:text="开始动画" />

    <Button
        android:id="@+id/btn_stop_animation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/btn_start_animation"
	android:layout_marginTop="16dp"
        android:onClick="onClick"
        android:text="结束动画" />
</RelativeLayout>

最后呈现的动画如下图,它是由 4 张图片,即 4 帧组成的:

接着来看第二种实现,即 Animated-Selector 方式,在 drawable 目录下新建一个 animator_selector.xml 文件,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<animated-selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/state_on"
        android:drawable="@mipmap/a1"
        android:state_checked="false" />

    <item
        android:id="@+id/state_off"
        android:state_checked="true"
        android:drawable="@mipmap/a2" />

    <transition
        android:fromId="@id/state_on"
        android:toId="@id/state_off">
        <animation-list>
            <item
                android:drawable="@mipmap/a1"
                android:duration="150" />
            <item
                android:drawable="@mipmap/a2"
                android:duration="150" />
            <item
                android:drawable="@mipmap/a3"
                android:duration="150" />
            <item
                android:drawable="@mipmap/a4"
                android:duration="150" />
        </animation-list>
    </transition>
</animated-selector>

MainActivity.java:

private ImageView mIVTest;
    private static final int[] STATE_CHECKED = new int[]{android.R.attr.state_checked};
    private static final int[] STATE_UNCHECKED = new int[]{};
    private boolean mIsChecked;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIVTest = (ImageView) findViewById(R.id.iv_test);
        mIVTest.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mIsChecked) {
                    mIVTest.setImageState(STATE_UNCHECKED, true);
                    mIsChecked = false;
                } else {
                    mIVTest.setImageState(STATE_CHECKED, true);
                    mIsChecked = true;
                }
            }
        });
    }

点击图片,我们就可以看到动画了。

动态代码实现

动态实现就是将资源文件里面的图片按顺序的以帧的形式添加到 AnimationDrawable 中,然后播放。

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private final String TAG = MainActivity.class.getSimpleName();

    private ImageView mIVTest;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIVTest = (ImageView) findViewById(R.id.iv_test);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btn_start_animation:
                playAnimation();
                break;
            case R.id.btn_stop_animation:
                break;
        }
    }

    /**
     * 动态代码制作帧动画
     */
    private void playAnimation(){
        AnimationDrawable ad = new AnimationDrawable();
        for(int i = 1; i < 5; i++){
            int resId = getResources().getIdentifier("a" + i, "mipmap", getPackageName());
            Drawable drawable = getResources().getDrawable(resId);
            ad.addFrame(drawable, 150);//添加帧
        }
        ad.setOneShot(false);//动画重复运行
        mIVTest.setImageDrawable(ad);
        ad.start();
    }
}

Tween Animation

是对某个View进行一系列的动画的操作,包括淡入淡出(Alpha),缩放(Scale),平移(Translate),旋转(Rotate)四种模式。原理是给出两个关键帧,通过一些算法将给定的属性值在给定的时间内在两个关键帧之间渐变。需要注意的就是该动画只能应用于View对象。

Android Tween 动画就是通过ParentView 来不断调整 ChildView 的画布坐标系来实现的,下面以平移动画来做示例,假设在动画开始时 ChildView 在 ParentView 中的初始位置在 (100,200) 处,这时 ParentView 会根据这个坐标来设置 ChildView 的画布,在 ParentView的 dispatchDraw 中它发现 ChildView 有一个平移动画,而且当前的平移位置是 (100, 200),于是它通过调用画布的函数traslate(100, 200) 来告诉 ChildView 在这个位置开始画,这就是动画的第一帧。如果 ParentView 发现 ChildView 有动画,就会不断的调用 invalidate() 这个函数,这样就会导致自己会不断的重画,就会不断的调用 dispatchDraw 这个函数,这样就产生了动画的后续帧,当再次进入 dispatchDraw 时,ParentView 根据平移动画产生出第二帧的平移位置 (500, 200),然后继续执行上述操作,然后产生第三帧,第四帧,直到动画播完。

补间动画还有一个致命的缺陷,就是它只是改变了View的显示效果而已,而不会真正去改变View的属性。什么意思呢?比如说,现在屏幕的左上角有一个按钮,然后我们通过补间动画将它移动到了屏幕的右下角,现在你可以去尝试点击一下这个按钮,点击事件是绝对不会触发的,因为实际上这个按钮还是停留在屏幕的左上角,只不过补间动画将这个按钮绘制到了屏幕的右下角而已。

Tween Animation 中主要包含以下几个类:

  • ScaleAnimation 尺寸大小缩放
  • AlphaAnimation 渐变透明度
  • RotateAnimation 画面旋转
  • TranslateAnimation 位置移动
  • AnimationSet 动画集

ScaleAnimation

  • fromXScale 属性为动画起始时 X 坐标上的伸缩尺寸
  • toXScale 属性为动画结束时 X 坐标上的伸缩尺寸
  • fromYScale 属性为动画起始时 Y 坐标上的伸缩尺寸
  • toYScale 属性为动画结束时 Y 坐标上的伸缩尺寸

还可以设置伸缩模式pivotXType、pivotYType, 伸缩动画相对于x,y 坐标的开始位置pivotXValue、pivotYValue等。

1、代码实现

View view = new View(this);
Animation scaleAnimation = new ScaleAnimation(0.5f, 1.0f, 0.5f, 1.0f);
scaleAnimation.setDuration(500);
view.startAnimation(scaleAnimation);

2、xml文件实现

<scale xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fillAfter="false"
    android:fromXScale="0.5"
    android:fromYScale="0.5"
    android:interpolator="@android:anim/accelerate_decelerate_interpolator"
    android:pivotX="50%"
    android:pivotY="50%"
    android:repeatCount="1"
    android:toXScale="1.0"
    android:toYScale="1.0" />

//代码加载
View view = new View(this);
Animation scaleAnimation = AnimationUtils.loadAnimation(this, R.anim.scale_animation);
view.startAnimation(scaleAnimation);

AlphaAnimation

第一个参数 fromAlpha 表示动画起始时的透明度, 第二个参数toAlpha表示动画结束时的透明度

1、代码实现

View view = new View(this);
Animation alphaAnimation = new AlphaAnimation(0f, 1.0f);
alphaAnimation.setDuration(500);
view.startAnimation(alphaAnimation);

2、xml文件实现

<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fromAlpha="0"
    android:toAlpha="1.0" />

RotateAnimation

  • fromDegrees 为动画起始时物件的角度,说明:
    • 当角度为负数——表示逆时针旋转
    • 当角度为正数——表示顺时针旋转
    • (负数 from——to 正数:顺时针旋转)
    • (负数 from——to 负数:逆时针旋转)
    • (正数 from——to 正数:顺时针旋转)
    • (正数 from——to 负数:逆时针旋转)
  • toDegrees 属性为动画结束时物件旋转的角度 可以大于360度
  • pivotX pivotY 为动画相对于物件的 X、Y 坐标的开始位 说明:
    • 以上两个属性值 从 0%-100% 中取值
    • 50%为物件的X或Y方向坐标上的中点位置

1、代码实现

View view = new View(this);
Animation rotateAnimation = new RotateAnimation(0f, 180f);
rotateAnimation.setDuration(500);
view.startAnimation(rotateAnimation);

2、xml文件实现

<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:repeatCount="2"
    android:fromDegrees="0"
    android:toDegrees="180"
    android:pivotX="50%"
    android:pivotY="50%"
    android:duration="500" />

TranslateAnimation

  • fromXDelta、toXDelta 动画起始、结束时X坐标
  • fromYDelta、toYDelta 动画起始、结束时Y坐标

1、代码实现

View view = new View(this);
Animation transAnimation = new TranslateAnimation(50f, 100f, 50f, 180f);
transAnimation.setDuration(500);
view.startAnimation(transAnimation);

2、xml文件实现

<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:fromXDelta="50"
    android:fromYDelta="50"
    android:toXDelta="100"
    android:toYDelta="180"
    android:duration="500" />

AnimationSet

动画集合,顾名思义,就是多个相同或者不相同种类的动画集合在一起,然后播放,也就是动画的叠加。

1、代码实现

View view = new View(this);
Animation transAnimation = new TranslateAnimation(50f, 100f, 50f, 180f);
transAnimation.setDuration(500);
view.startAnimation(transAnimation);

Animation rotateAnimation = new RotateAnimation(0f, 180f);
rotateAnimation.setDuration(500);
view.startAnimation(rotateAnimation);

AnimationSet animationSet = new AnimationSet(true);
animationSet.addAnimation(transAnimation);
animationSet.addAnimation(rotateAnimation);
animationSet.setStartOffset(1000);//一秒后再执行动画 = 等待1秒后执行动画
animationSet.setFillAfter(true);//设置动画执行后保持最后状态
view.startAnimation(animationSet);

2、xml文件实现

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:anim/accelerate_interpolator"
    android:shareInterpolator="true">
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1" />
    <translate
        android:fromXDelta="50"
        android:fromYDelta="50"
        android:toXDelta="100"
        android:toYDelta="180" />
    <rotate
        android:fromDegrees="0"
        android:pivotX="50"
        android:pivotY="50"
        android:toDegrees="180" />
    <!--<set>
        //还可以在这里添加子动画集合
    </set>-->
</set>

Property Animation

ValueAnimator

ValueAnimator 是计算动画过程中变化的值,包含动画的开始值,结束值,持续时间等属性。但并没有把这些计算出来的值应用到具体的对象上面,所以也不会有什么的动画显示出来。要把计算出来的值应用到对象上,必须为 ValueAnimator 注册一个监听器 ValueAnimator.AnimatorUpdateListener,该监听器负责更新对象的属性值。

ValueAnimator 它是调用 TypeEvaluator 来完成计算 view 在动画中的属性值的。那 TypeEvaluator 的作用到底是什么呢?简单来说,就是告诉动画系统如何从初始值过度到结束值,即它基于插值函数、开始值、结束值计算你在动画的属性的值。

比如说 FloatEvaluator,它实现了 TypeEvaluator 接口:

/**
     * This evaluator can be used to perform type interpolation between <code>float</code> values.
     */
    public class FloatEvaluator implements TypeEvaluator<Number> {

        /**
         * @param fraction   用于表示动画的完成度的,根据它来计算当前动画的值应该是多少
         * @param startValue 动画的初始值
         * @param endValue   动画的结束值
         * @return A linear interpolation between the start and end values, given the
         *         <code>fraction</code> parameter.
         */
        public Float evaluate(float fraction, Number startValue, Number endValue) {
            float startFloat = startValue.floatValue();
            //初始值加上已完成的动画的值,即为当前动画的值
            return startFloat + fraction * (endValue.floatValue() - startFloat);
        }
    }

我们还可以根据自己的需求自定义 TypeEvaluator,完成自己想要的动画效果。

class PointEvaluator implements TypeEvaluator<Point> {

    /**
     * @param fraction   动画变化中的浮点参数,0-1
     * @param startValue 动画开始时的Point对象
     * @param endValue   动画结束时的Point对象
     * @return 动画过程中通过计算获取半径并返回一个新的Point对象
     */
    @Override
    public Point evaluate(float fraction, Point startValue, Point endValue) {
        int startRadius = startValue.getRadius();
        int endRadius = endValue.getRadius();
        int newRadius = (int) (startRadius + fraction * (endRadius - startRadius));
        return new Point(newRadius);
    }
}
//使用的时候就是
 ValueAnimator animator = ValueAnimator.ofObject(new PointEvaluator(),new Point(0), new Point(minValue * 2 / 5));

ObjectAnimator

相比于 ValueAnimator,ObjectAnimator 可能才是我们最常接触到的类,因为 ValueAnimator 只不过是对值进行了一个平滑的动画过渡,但我们实际使用到这种功能的场景好像并不多。而 ObjectAnimator 则就不同了,它是可以直接对任意对象的任意属性进行动画操作的,比如说 View 的 alpha 属性。

不过虽说 ObjectAnimator 会更加常用一些,但是它其实是继承自 ValueAnimator 的,底层的动画实现机制也是基于 ValueAnimator 来完成的,因此 ValueAnimator 仍然是整个属性动画当中最核心的一个类。那么既然是继承关系,说明 ValueAnimator 中可以使用的方法在 ObjectAnimator 中也是可以正常使用的,它们的用法也非常类似。

AnimatorSet

是针对视图属性的动画集合。它支持设置组中动画的时序关系,如同时播放,顺序播放等。AnimatorSet 就比 AnimationSet 功能强大很多了, AnimatorSet 可以使用 playSequentially、playTogether 两个方法,来让一些列的动画串行和并行。

1、代码实现

View view = new View(this);
ObjectAnimator animatorA = ObjectAnimator.ofFloat(view, "TranslationX", -300, 300, 0);
ObjectAnimator animatorB = ObjectAnimator.ofFloat(view, "scaleY", 0.5f, 1.5f, 1f);
ObjectAnimator animatorC = ObjectAnimator.ofFloat(view, "rotation", 0, 270, 90, 180, 0);
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animatorA, animatorB, animatorC);//并行播放
//animatorSet.playSequentially(animatorA, animatorB, animatorC);//顺序播放
animatorSet.start();

2、xml文件实现

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

    <objectAnimator
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:propertyName="alpha"
        android:duration="3000"
        android:valueType="floatType"
        android:valueFrom="0"
        android:valueTo="1"/>

    <objectAnimator
        android:interpolator="@android:anim/anticipate_overshoot_interpolator"
        android:propertyName="scaleX"
        android:duration="3000"
        android:valueType="floatType"
        android:valueFrom="0.5"
        android:valueTo="1.5" />

</set>

//代码加载
View view = new View(this);
AnimatorSet animatorSet = (AnimatorSet) AnimatorInflater.loadAnimator(MainActivity.this, R.animator.animator_set);
animatorSet.setTarget(view);
animatorSet.start();

PropertyValuesHolder

多属性动画同时工作管理类。有时候我们需要同时修改多个属性,那就可以用到此类,具体如下:

PropertyValuesHolder a1 = PropertyValuesHolder.ofFloat("alpha", 0f, 1f);  
PropertyValuesHolder a2 = PropertyValuesHolder.ofFloat("translationY", 0, viewWidth);  
......
ObjectAnimator.ofPropertyValuesHolder(view, a1, a2, ......).setDuration(1000).start();

ViewPropertyAnimator

zejian的博客-属性动画-Property Animation之ViewPropertyAnimator 你应该知道的一切

(1)概述

属性动画的推出已不再是针对于View而进行设计的了,而是一种对数值不断操作的过程,我们可以将属性动画对数值的操作过程设置到指定对象的属性上来,从而形成一种动画的效果。虽然属性动画给我们提供了ValueAnimator类和ObjectAnimator类,在正常情况下,基本都能满足我们对动画操作的需求,但ValueAnimator类和ObjectAnimator类本身并不是针对View对象的而设计的,而我们在大多数情况下主要都还是对View进行动画操作的,Android 3.0 之后,Google给View增加了animate方法来直接驱动属性动画。

  • 专门针对View对象动画而操作的类。
  • 提供了更简洁的链式调用设置多个属性动画,这些动画可以同时进行的。
  • 拥有更好的性能,多个属性动画是一次同时变化,只执行一次UI刷新(也就是只调用一次invalidate,而n个ObjectAnimator就会进行n次属性变化,就有n次invalidate)。
  • 在使用ViewPropertyAnimator时,我们无需调用start()方法,因为新的接口中使用了隐式启动动画的功能,只要我们将动画定义完成之后,动画就会自动启动。并且这个机制对于组合动画也同样有效,只要我们不断地连缀新的方法,那么动画就不会立刻执行,等到所有在ViewPropertyAnimator上设置的方法都执行完毕后,动画就会自动启动。当然如果不想使用这一默认机制的话,我们也可以显式地调用start()方法来启动动画。

(2)常规使用

1)、单一动画

之前我们要设置一个View控件旋转360的代码是这样:

ObjectAnimator.ofFloat(btn,"rotation",360).setDuration(200).start();

而现在我们使用ViewPropertyAnimator后是这样:

btn.animate().rotation(360).setDuration(200);

再比如转动xy坐标:

                ObjectAnimator.ofFloat(view, "translationX", 0, translationX).setDuration(200).start();
                ObjectAnimator.ofFloat(view, "translationY", 0, translationY).setDuration(200).start()

而现在我们使用ViewPropertyAnimator后是这样:

btn.animate().x(500).y(500).setDuration(200); 

2)、同时设置一个动画集

之前我们是这样写:

AnimatorSet set = new AnimatorSet();
set.playTogether( ObjectAnimator.ofFloat(btn,"alpha",0.5f),
        ObjectAnimator.ofFloat(btn,"rotation",360),
        ObjectAnimator.ofFloat(btn,"scaleX",1.5f),
        ObjectAnimator.ofFloat(btn,"scaleY",1.5f),
        ObjectAnimator.ofFloat(btn,"translationX",0,50),
        ObjectAnimator.ofFloat(btn,"translationY",0,50)
);
set.setDuration(5000).start();

而现在我们使用ViewPropertyAnimator后是这样:

btn.animate().alpha(0.5f).rotation(360).scaleX(1.5f).scaleY(1.5f)
              .translationX(50).translationY(50).setDuration(5000);

ViewPropertyAnimator简单用法讲完了,这里小结一下ViewPropertyAnimator的常用方法:

MethodDiscription
alpha(float value)设置透明度,value表示变化到多少,1不透明,0全透明。
scaleY(float value)设置Y轴方向的缩放大小,value表示缩放到多少。1表示正常规格。小于1代表缩小,大于1代表放大。
scaleX(float value)设置X轴方向的缩放大小,value表示缩放到多少。1表示正常规格。小于1代表缩小,大于1代表放大。
translationY(float value)设置Y轴方向的移动值,作为增量来控制View对象相对于它父容器的左上角坐标偏移的位置,即移动到哪里。
translationX(float value)设置X轴方向的移动值,作为增量来控制View对象相对于它父容器的左上角坐标偏移的位置。
rotation(float value)控制View对象围绕支点进行旋转, rotation针对2D旋转
rotationX (float value)控制View对象围绕X支点进行旋转, rotationX针对3D旋转
rotationY(float value)控制View对象围绕Y支点进行旋转, rotationY针对3D旋转
x(float value)控制View对象相对于它父容器的左上角坐标在X轴方向的最终位置。
y(float value)控制View对象相对于它父容器的左上角坐标在Y轴方向的最终位置
void cancel()取消当前正在执行的动画
setListener(Animator.AnimatorListener listener)设置监听器,监听动画的开始,结束,取消,重复播放
setUpdateListener(ValueAnimator.AnimatorUpdateListener listener)设置监听器,监听动画的每一帧的播放
setInterpolator(TimeInterpolator interpolator)设置插值器
setStartDelay(long startDelay)设置动画延长开始的时间
setDuration(long duration)设置动画执行的时间
withLayer()设置是否开启硬件加速
withStartAction(Runnable runnable)设置用于动画监听开始(Animator.AnimatorListener)时运行的Runnable任务对象
withEndAction(Runnable runnable)设置用于动画监听结束(Animator.AnimatorListener)时运行的Runnable任务对象

布局动画

zejian的博客-Android布局动画之animateLayoutChanges与LayoutTransition

关于布局动画是针对ViewGroup而言的,意指ViewGroup在增加子View或者删除子View时其子View的过渡动画,在Android官网有这么一个简单的例子,其效果如下,接下来我们就通过这个例子来入门

事实上,实现上面ViewGroup的布局动画非常简单,我们只要给子View所在的ViewGroup的xml中添加下面的属性即可:

android:animateLayoutChanges="true"

LayoutAnimation

除了上面的布局动画外,有时我们可能需要第一次加载ListView或者GridView的时候能有个动画的过度效果,以便达到更好的体验,如下ListView加载子View的效果:

事实上实现这种效果也比较简单,我们只需要在ListView的布局文件中添加android:layoutAnimation=”@anim/layout_animation”属性即可。

left_into.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:duration="500"
        android:fromXDelta="100%"
        android:fromYDelta="0"
        android:toXDelta="0"
        android:toYDelta="0" />
    <alpha
        android:duration="500"
        android:fromAlpha="0"
        android:toAlpha="1" />
</set>

接着创建layout_animation.xml

<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
    android:animation="@anim/left_into"
    android:animationOrder="normal"
    android:delay="0.5"
    />

这里简单介绍一下layoutAnimation标签属性:

属性名含义
android:delaydelay的单位为秒,表示子View进入的延长时间
android:animationOrder子类的进入方式 ,其取值有,normal 0 默认 ,reverse 1 倒序 ,random 2 随机
android:animation子view要执行的具体动画的文件,自定义即可

后设置给listView控件即可,activity_listview.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ListView
        android:id="@+id/listView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        <!-- 设置在这里 ⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️ -->
        android:layoutAnimation="@anim/layout_animation"
        >
    </ListView>
</LinearLayout>

当然除了通过xml设置外,我们还可以通过代码动态设置,代码范例如下,这里就不演示了。

//通过加载XML动画设置文件来创建一个Animation对象,子View进入的动画
Animation animation=AnimationUtils.loadAnimation(this, R.anim.left_into);
//得到一个LayoutAnimationController对象;
LayoutAnimationController lac=new LayoutAnimationController(animation);
//设置控件显示的顺序;
lac.setOrder(LayoutAnimationController.ORDER_REVERSE);
//设置控件显示间隔时间;
lac.setDelay(0.5f);
//为ListView设置LayoutAnimationController属性;
listView.setLayoutAnimation(lac);

LayoutTransition

前面我们说过ViewGroup在设置android:animateLayoutChanges="true"后在添加或者删除子view时可以启用系统带着的动画效果,但这种效果无法通过自定义动画去替换。不过还好android官方为我们提供了LayoutTransition类,通过LayoutTransition就可以很容易为ViewGroup在添加或者删除子view设置自定义动画的过渡效果了。

LayoutTransition类用于当前布局容器中需要View添加,删除,隐藏,显示时设置布局容器子View的过渡动画。也就是说利用LayoutTransition,可以分别为需添加或删除的View对象在移动到新的位置的过程添加过渡的动画效果。我们可以通过setLayoutTransition()方法为布局容器ViewGroup设置LayoutTransition对象,代码如下:

//初始化容器动画
LayoutTransition mTransitioner = new LayoutTransition();
container.setLayoutTransition(mTransitioner);

一般地,Layout中的子View对象有四种动画变化的形式,如下:

属性值含义
LayoutTransition.APPEARING子View添加到容器中时的过渡动画效果。
LayoutTransition.CHANGE_APPEARING子View添加到容器中时,其他子View位置改变的过渡动画
LayoutTransition.DISAPPEARING子View从容器中移除时的过渡动画效果。
LayoutTransition.CHANGE_DISAPPEARING子View从容器中移除时,其它子view位置改变的过渡动画
LayoutTransition.CHANGING子View在容器中位置改变时的过渡动画,不涉及删除或者添加操作

LayoutTransition的一些常用函数:

函数名称说明
setAnimator(int transitionType, Animator animator)设置不同状态下的动画过渡,transitionType取值为, APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING 、CHANGING
setDuration(long duration)设置所有动画完成所需要的时长
setDuration(int transitionType, long duration)设置特定type类型动画时长,transitionType取值为, APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING 、CHANGING
setStagger(int transitionType, long duration)设置特定type类型动画的每个子item动画的时间间隔 ,transitionType取值为: APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING、CHANGING
setInterpolator(int transitionType, TimeInterpolator interpolator)设置特定type类型动画的插值器, transitionType取值为: APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING、CHANGING
setStartDelay(int transitionType, long delay)设置特定type类型动画的动画延时 transitionType取值为, APPEARING、DISAPPEARING、CHANGE_APPEARING、CHANGE_DISAPPEARING 、CHANGING
addTransitionListener(TransitionListener listener)设置监听器TransitionListener

举个例子:

/**
 * Created by zejian
 * Time 16/9/17.
 * Description:
 */
public class LayoutAnimationActivity extends Activity {


    private int i = 0;
    private LinearLayout container;
    private LayoutTransition mTransitioner;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_layout_animation);

        container = (LinearLayout) findViewById(R.id.container);
        //构建LayoutTransition
        mTransitioner = new LayoutTransition();
        //设置给ViewGroup容器
        container.setLayoutTransition(mTransitioner);
        setTransition();
    }


    private void setTransition() {
        /**
         * 添加View时过渡动画效果
         */
        ObjectAnimator addAnimator = ObjectAnimator.ofFloat(null, "rotationY", 0, 90,0).
                setDuration(mTransitioner.getDuration(LayoutTransition.APPEARING));
        mTransitioner.setAnimator(LayoutTransition.APPEARING, addAnimator);

        /**
         * 移除View时过渡动画效果
         */
        ObjectAnimator removeAnimator = ObjectAnimator.ofFloat(null, "rotationX", 0, -90, 0).
                setDuration(mTransitioner.getDuration(LayoutTransition.DISAPPEARING));
        mTransitioner.setAnimator(LayoutTransition.DISAPPEARING, removeAnimator);

        /**
         * view 动画改变时,布局中的每个子view动画的时间间隔
         */
        mTransitioner.setStagger(LayoutTransition.CHANGE_APPEARING, 30);
        mTransitioner.setStagger(LayoutTransition.CHANGE_DISAPPEARING, 30);


        /**
         *LayoutTransition.CHANGE_APPEARING和LayoutTransition.CHANGE_DISAPPEARING的过渡动画效果
         * 必须使用PropertyValuesHolder所构造的动画才会有效果,不然无效!使用ObjectAnimator是行不通的,
         * 发现这点时真特么恶心,但没想到更恶心的在后面,在测试效果时发现在构造动画时,”left”、”top”、”bottom”、”right”属性的
         * 变动是必须设置的,至少设置两个,不然动画无效,问题是我们即使这些属性不想变动!!!也得设置!!!
         * 我就问您恶不恶心!,因为这里不想变动,所以设置为(0,0)
         *
         */
        PropertyValuesHolder pvhLeft =
                PropertyValuesHolder.ofInt("left", 0, 0);
        PropertyValuesHolder pvhTop =
                PropertyValuesHolder.ofInt("top", 0, 0);
        PropertyValuesHolder pvhRight =
                PropertyValuesHolder.ofInt("right", 0, 0);
        PropertyValuesHolder pvhBottom =
                PropertyValuesHolder.ofInt("bottom", 0, 0);


        /**
         * view被添加时,其他子View的过渡动画效果
         */
        PropertyValuesHolder animator = PropertyValuesHolder.ofFloat("scaleX", 1, 1.5f, 1);
        final ObjectAnimator changeIn = ObjectAnimator.ofPropertyValuesHolder(
                this, pvhLeft,  pvhBottom, animator).
                setDuration(mTransitioner.getDuration(LayoutTransition.CHANGE_APPEARING));
        //设置过渡动画
        mTransitioner.setAnimator(LayoutTransition.CHANGE_APPEARING, changeIn);


        /**
         * view移除时,其他子View的过渡动画
         */
        PropertyValuesHolder pvhRotation =
                PropertyValuesHolder.ofFloat("scaleX", 1, 1.5f, 1);
        final ObjectAnimator changeOut = ObjectAnimator.ofPropertyValuesHolder(
                this, pvhLeft, pvhBottom, pvhRotation).
                setDuration(mTransitioner.getDuration(LayoutTransition.CHANGE_DISAPPEARING));

        mTransitioner.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, changeOut);
    }


    public void addView(View view) {
        i++;
        Button button = new Button(this);
        button.setText("布局动画_" + i);
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
        container.addView(button, Math.min(1, container.getChildCount()), params);
    }

    public void removeView(View view) {
        if (i > 0)
            container.removeViewAt(0);
    }
}

简单分析一下,LayoutTransition.APPEARING和LayoutTransition.DISAPPEARING的情况下直接使用属性动画来设置过渡动画效果即可,而对于LayoutTransition.CHANGE_APPEARING和LayoutTransition.CHANGE_DISAPPEARING必须使用PropertyValuesHolder所构造的动画才会有效果,不然无效,真特么恶心,但没想到更恶心的在后面,在测试效果时发现在构造动画时,”left”、”top”、”bottom”、”right”属性的变动是必须设置的,至少设置两个,不然动画无效,最坑爹的是我们即使这些属性不想变动!!!如果不想动,这时只要传递的可变参数都一样就行如下面的(0,0)也可以是(100,100)即可。

PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top",0,0);  
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("left",100,100);  
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("bottom",0,0);  
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("right",0,0);  

还有点需要注意的是在使用的ofInt,ofFloat中的可变参数值时,第一个值和最后一个值必须相同,不然此属性将不会有动画效果,比如下面首位相同是有效的

PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top",100,0,100);
PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("top",0,100,0);

但是如果是下面的设置就是无效的:

PropertyValuesHolder pvhTop = PropertyValuesHolder.ofInt("left",0,100);

最后我们通过setAniamtor的方法设置LayoutTransition的5种状态下的过渡动画,最后运行一下程序,效果如下:

自定义动画

动画优化

优化点1

具体请参考:http://blog.csdn.net/rentee/article/details/52251829

  • 使用 PropertyValuesHolder

想必大家都这样写过:

ObjectAnimator animX = ObjectAnimator.ofFloat(myView, "x", 50f);
ObjectAnimator animY = ObjectAnimator.ofFloat(myView, "y", 100f);
AnimatorSet animSetXY = new AnimatorSet();
animSetXY.playTogether(animX, animY);
animSetXY.start();

但是这样写会产生两个 ObjectAnimator 对象,效率较低,官方建立这样写:

PropertyValuesHolder pvhX = PropertyValuesHolder.ofFloat("x", 50f);
PropertyValuesHolder pvhY = PropertyValuesHolder.ofFloat("y", 100f);

ObjectAnimator.ofPropertyValuesHolder(myView, pvhX, pvyY).start();

这里使用了 PropertyValuesHolder,只产生一个 ObjectAnimator 对象,更加高效。

一个 view 同时发生多种效果时,建议这种写法。

  • 使用 Keyframe

同样,我们肯定也这样写过:

AnimatorSet animSet = new AnimatorSet();

ObjectAnimator transYFirstAnim = ObjectAnimator.ofFloat(mView, "translationY", 0, 100);
ObjectAnimator transYSecondAnim = ObjectAnimator.ofFloat(mView, "translationY", 100, 0);

animSet.playSequentially(transYFirstAnim, transYSecondAnim);

产生两个 ObjectAnimator 对象,一个 AnimatorSet,代码繁琐,对象冗杂。
这种情况建议使用 Keyframe 编写其行为。从词义上来理解 Keyframe 是关键帧,下面是使用关键帧的例子:

Keyframe k0 = Keyframe.ofFloat(0f, 0); //第一个参数为“何时”,第二个参数为“何地”
Keyframe k1 = Keyframe.ofFloat(0.5f, 100);
Keyframe k2 = Keyframe.ofFloat(1f, 0);

PropertyValuesHolder p = PropertyValuesHolder.ofKeyframe("translationY", k0, k1, k2);

ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mView, p);
objectAnimator.start();

所以效果就是:
开始时位置为 0;
动画开始 1/2 时位置为 100;
动画结束时位置为 0。

一个 view 的单个属性先后发生一系列变化时,建议使用 Keyframe 达到效果。

总结就是,如果是同一个 view 的一系列动画,均可使用以上组合方式达到只使用一个 ObjectAnimator 的效果 。多个 view 的动画用 AnimatorSet 进行动画组合和排序,代码架构和执行效率基本能达到最优化。

  • 使用 AnimatorListenerAdapter 代替 AnimatorListener

由于 AnimatorListener 是接口,所以实现它得实现它所有的方法,而我们有时只用到它的个别回调(如:onAnimationStart),使用它会导致代码看起来非常冗杂。而 AnimatorListenerAdapter 是默认实现了 AnimatorListener 的一个抽象类,你可以按需要重写其中的方法,代码会优雅一点。

  • 使用 ViewPropertyAnimator

属性动画的机制已经不是再针对于 View 而进行设计的了,而是一种不断地对值进行操作的机制,它可以将值赋值到指定对象的指定属性上。
但是,在绝大多数情况下,还是对 View 进行动画操作的。Android 开发团队也是意识到了这一点,于是在 Android 3.1 系统当中补充了 ViewPropertyAnimator 这个机制。

ObjectAnimator animator = ObjectAnimator.ofFloat(textview, “alpha”, 0f); animator.start(); 
//等同 
textview.animate().alpha(0f);

animate() 方法就是在 Android 3.1 系统上新增的一个方法,这个方法的返回值是一个 ViewPropertyAnimator 对象。(简明方便)

textview.animate().x(500).y(500).setDuration(5000).setInterpolator(new BounceInterpolator());
  • 属性动画其他一些实用方法:(暂时只讲了API 14—Android 4.0及以下的部分)

    • setStartDelay():可以设置动画开始前的延迟时间。注意:此方法 API 14 加入;如果给动画添加了 AnimatorListener,Listener 的 onAnimationStart 方法会在动画最开始的时候回调,而不是 delay 一段时间后回调。
    • isStarted()(API 14)与 isRunning():这个得与 setStartDelay() 放在一起讲,也就是说,当动画在 delay 中并没有真正开始时,isStarted 返回 false,而 isRunning 返回 true。也就是,isRunning 的周期是大于 isStarted 的。
    • cancel() 与 end():cancel 方法会立即停止动画,并且停留在当前帧。end 方法会立即停止动画,并且将动画迅速置到最后一帧的状态。
    • setupStartValues() 与 setupEndValues():设置动画进行到的当前值为开始值(结束值)。

以下这个示例中,当 view 移动了 2.5s 的时候,cancel 当前动画,并重新设定起始位置为当前位置,结束位置为 400,再开始此动画。

final ObjectAnimator o = ObjectAnimator.ofFloat(mTextView, "x", 0, 500);
o.setDuration(5000);
o.start();

mTextView.postDelayed(new Runnable() {
      @Override
      public void run() {
           if (o.isRunning()) {
                o.cancel();
            }
            o.setupStartValues();
            o.setFloatValues(400);
            o.start();
      }
},2500);

优化点2

目的还是尽量减少 ObjectAnimator 的创建。

举个例子,我们希望对一个点的 XY 坐标同时使用动画,那我们一般会使用下面的方法,这样就会创建两个 ObjectAnimator:

ObjectAnimator aPaintX = ObjectAnimator.ofFloat(this, "translationX", 0, 100);
		//PaintY
		ObjectAnimator aPaintY = ObjectAnimator.ofFloat(this, "translationY", 0, 100);
		//AnimatorSet
		AnimatorSet set = new AnimatorSet();
		set.playTogether(aPaintX, aPaintY);
		set.start();

我们使用了 AnimatorSet 来管理动画集,但我们看 AnimatorSet 中的源码可以知道,其最后也是启动每一个的属性动画进行播放的。所以如果我们想同时改变对象的多个属性,就得创建多个 ObjectAnimator。我们换个思路,是不是可以封装一个属性集合类的实例,然后就可以只使用一个属性动画了。

我们回想一下,前面有提到过 TypeEvaluator,属性动画的值就是在它里面做修改的。通过翻看 ObjectAnimator 的源码,发现它是提供了传入自定义 TypeEvaluator 的方法:

public static ObjectAnimator ofObject(Object target, String propertyName,
            TypeEvaluator evaluator, Object... values) {
        ObjectAnimator anim = new ObjectAnimator(target, propertyName);
        anim.setObjectValues(values);
        anim.setEvaluator(evaluator);
        return anim;
    }

那OK,可以这样来优化。实现自定义 TypeEvaluator,使用它的 evaluate 函数来实现自己的属性变更逻辑。

public static class PaintEvaluator implements TypeEvaluator {
        private static final PaintEvaluator sInstance = new PaintEvaluator();
        public static PaintEvaluator getInstance() {
	return sInstance;
        }
        public Object evaluate(float fraction, Object startValue, Object endValue) {
	ViewProperty start = (ViewProperty) startValue;
	ViewProperty end = (ViewProperty) endValue;
	float x = start.paintX + fraction * (end.paintX - start.paintX); //根据自己定义的算法,分别计算 x y 值,然后返回
	float y = start.paintY + fraction * (end.paintY - start.paintY);
	return new ViewProperty(x, y); //不限可以是 xy 坐标值,还可以根据自己需求自定义其它属性,例如 alpha 值等等
        }
    }

优化点3

解决animation循环中停止时卡顿一下的问题。

<?xml version="1.0" encoding="utf-8"?>  
<set xmlns:android="http://schemas.android.com/apk/res/android">  
    <rotate  
        android:fromDegrees="0"  
        android:toDegrees="359"  
        android:duration="500"  
        android:repeatCount="-1"  
        android:pivotX="50%"  
        android:pivotY="50%" />  
</set>  

含义表示从0到359度开始循环旋转,0-359(若设置成360在停止时会出现停顿现象)度旋转所用时间为500ms,旋转中心距离view的左顶点为50%距离,距离view的上边缘为50%距离,即正中心。

优化点4

当界面的绘制和动画比较复杂,计算量比较大的情况,就不再适合使用 View 的属性动画了,因为每次属性的变更都需要多次调用 onMeasure、onLayout、onDraw,重新绘制 View 本身。

我们知道,每个 View 中都有 Canvas 可以用来绘制动画,只需要在这个 View 中重载onDraw()方法就可以。那用什么来绘制动画呢?我们可以考虑使用 SurfaceView,它能够在非 UI 线程中进行图形绘制,释放了 UI 线程的压力。

Demo参考xxss0903

public class MainActivity extends Activity {  
	@Override  
    protected void onCreate(Bundle savedInstanceState) {  
        super.onCreate(savedInstanceState);  
        setContentView(new Circle(this)); // 添加自定义的SurfaceView  
    }  
  
    @Override  
    public boolean onCreateOptionsMenu(Menu menu) {  
        // Inflate the menu; this adds items to the action bar if it is present.  
        getMenuInflater().inflate(R.menu.main, menu);  
        return true;  
    }  
}

public class Circle extends SurfaceView implements SurfaceHolder.Callback {

    private OnCircleAnimationListener mListener;

    private int mWidth;
    private int mHeight;
    private SurfaceHolder mHolder;
    private static final String TEXT_CIRCLE = "跳过";

    private int mDegree = 0;

    private long mDelay = 3000; // 默认运行时间3秒
    private boolean mStop = false;
    private Paint mClearPaint;

    public Circle(Context context) {
        super(context);

        //
        mHolder = getHolder();
        mHolder.addCallback(this);
    }

    public Circle(Context context, AttributeSet attrs) {
        super(context, attrs);
        mHolder = getHolder();
        mHolder.addCallback(this);
        // 下面的代码会能够去除掉圆圈之外的黑色背景
        setZOrderOnTop(true);
        mHolder.setFormat(PixelFormat.TRANSLUCENT);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//        setMeasuredDimension(100, 100);
    }

    @Override
    public void surfaceCreated(SurfaceHolder surfaceHolder) {
        // 获取宽高
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        if (mListener != null) {
            mListener.onAnimationPrepared(Circle.this);
        }
    }

    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
        Log.e("John", "Circle" + " # " + "surface changed");
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder surfaceHolder) {

    }

    public void setCircleAnimationListener(OnCircleAnimationListener listener) {
        mListener = listener;
    }

    public void startCircleAnimation() {
        new DrawThread().start();
    }

    public void setAnimationDelay(long delay) {
        mDelay = delay;
    }

    public void stopAnimation() {
        mStop = true;
    }

    public class DrawThread extends Thread {

        private Paint mPaint;
        private Paint mTextPaint;
        private Paint mArcPaint;

        public DrawThread() {
            mPaint = new Paint();
            mPaint.setAntiAlias(true);
            mTextPaint = new TextPaint();
            mTextPaint.setTextSize(mHeight / 2-5);
            mTextPaint.setColor(Color.WHITE);
            mPaint.setColor(Color.RED);
            mArcPaint = new Paint();
            mArcPaint.setStrokeWidth((float) (mWidth / 2 * 0.1));
            mArcPaint.setColor(Color.BLUE);
            mArcPaint.setAntiAlias(true);
            mArcPaint.setStyle(Paint.Style.STROKE);

            //这是定义橡皮擦画笔
            mClearPaint = new Paint();
            mClearPaint.setAntiAlias(true);
            mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

        }

        @Override
        public void run() {
            mListener.onAnimationStart(Circle.this);
            while (!mStop) {
                drawCircle();
                if (mDegree >= 360) {
                    mListener.onAnimationFinished(Circle.this);
                    break;
                }
                // 把总的动画时间分成360份
                long animation_duration = mDelay / 360;
                SystemClock.sleep(animation_duration);
                mDegree++;
            }
        }

        // 绘制圆圈
        private void drawCircle() {
            mListener.onAnimationRunning(Circle.this);
            Canvas canvas = mHolder.lockCanvas();
            if (canvas == null) {
                return;
            }
            canvas.drawRect(0, 0, mWidth, mHeight, mClearPaint);
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

            canvas.save();
            canvas.scale(0.9f, 0.9f, mWidth * 1f / 2, mHeight * 1f / 2);
            canvas.drawCircle(mWidth * 1.0f / 2, mHeight * 1.0f / 2, mWidth * 1.0f / 2, mPaint);
            float centerY = mHeight * 1f / 2;
            // 这个是获取每个文字的宽度,那么怎么知道有几个文字呢?
            float widths[] = new float[2];
            mTextPaint.getTextWidths(TEXT_CIRCLE, widths);
            canvas.drawText(TEXT_CIRCLE, 5, centerY + (widths[0] * 1f / 2) - 10, mTextPaint);
            canvas.restore();

            // 旋转坐标画圆圈
            canvas.save();
            canvas.rotate(-90, mWidth / 2, mHeight / 2);
            RectF rectF = new RectF((float) (mWidth / 2 * 0.1) / 2, (float) (mWidth / 2 * 0.1) / 2, mWidth - (float) (mWidth / 2 * 0.1) / 2, mHeight - (float) (mWidth / 2 * 0.1) / 2);
            canvas.drawArc(rectF, 0, mDegree, false, mArcPaint);
            canvas.restore();

            mHolder.unlockCanvasAndPost(canvas);
        }
    }

    public interface OnCircleAnimationListener {
        void onAnimationPrepared(View view);

        void onAnimationFinished(View view);

        void onAnimationRunning(View view);

        void onAnimationStart(View view);
    }
}  

明天待续…

参考

文章目录
  1. 1. 原理
    1. 1.1. TimeInterpolator和TypeEvaluator
    2. 1.2. TimeInterplator
  2. 2. Frame Animation
    1. 2.1. XML实现方式
    2. 2.2. 动态代码实现
  3. 3. Tween Animation
    1. 3.1. ScaleAnimation
    2. 3.2. AlphaAnimation
    3. 3.3. RotateAnimation
    4. 3.4. TranslateAnimation
    5. 3.5. AnimationSet
  4. 4. Property Animation
    1. 4.1. ValueAnimator
    2. 4.2. ObjectAnimator
    3. 4.3. AnimatorSet
    4. 4.4. PropertyValuesHolder
    5. 4.5. ViewPropertyAnimator
  5. 5. 布局动画
    1. 5.1. LayoutAnimation
    2. 5.2. LayoutTransition
  6. 6. 自定义动画
  7. 7. 动画优化
    1. 7.1. 优化点1
    2. 7.2. 优化点2
    3. 7.3. 优化点3
    4. 7.4. 优化点4
  8. 8. 参考
|