文章目录
要点提炼|开发艺术之Animation
Android 属性动画:这是一篇全面 & 详细的 属性动画 总结&攻略
自定义View
1.空气质量
可以看出该自定义View包括:(1)顶层文字Text(2)中间的文字Text(3)中间的数字Text(动画形式)(4)两个有角度的不同颜色的圆,第一个圆默认画至一定角度,第二个圆根据空气质量数值转化成角度。
我们设定空气质量为500时是满格,空气质量的数字是Text是动画效果增长至最终值,而顶层圆也是动画效果到达对应的角度。提供一个方法当获取到天气信息后调用,在updateIndex(int value,String middleText)进行动画效果,
主要遇到的问题是drawText中的baseline选取。
layout中使用:
<com.example.aaa.coolweather.View.CircleIndexView
android:id="@+id/circleindexview"
android:layout_width="170dp"
android:layout_height="170dp"
app:topText="污染指数"
/>
2.日出日落
主要思路:绘制一个封闭的半圆,在半圆的左下角与右下角绘制日出时间与日落时间。根据日出日落时间可以计算出总时间。用当前时间-日出时间,可以计算出现在太阳应走过的时间。用应走过的时间/总时间就可以得到当前时间对应的角度。
计算出当前时间对应的最终角度的代码如下,之后会通过动画的形式来将太阳图片移动到对应角度。
mStartTime = startTime;
mEndTime = endTime;
mCurrentTime = currentTime;
mTotalMinute = calculateTime(mStartTime, mEndTime);//计算总时间,单位:分钟
mNeedMinute = calculateTime(mStartTime, mCurrentTime);//计算当前所给的时间 单位:分钟
mPercentage = formatTime(mTotalMinute, mNeedMinute);//当前时间的总分钟数占日出日落总分钟数的百分比
mCurrentAngle = (180 * mPercentage);
setAnimation(0, mCurrentAngle, 5000);
太阳对应的x座标位置与y座标位置如下,这里的mCurrentAngle是动画中当前帧的角度,而不是最终角度。可以通过sin函数和cos函数计算出对应的xy座标值,因为该自定义View具有属性radius来代表圆的半径。
注意:(1)要记得减去这个图标自己的宽度/2,否则图片就不在线的正中间了。
(2)同时要注意Math.cos和sin传入的都是弧度制。
(3)marginTop是用于将圆弧距离顶部一定位置,防止图片到上面就显示不全了。
//当太阳在最高点时不会有部分消失
private int marginTop = 30;//离顶部的高度
private int mRadius; //圆的半径
...
positionX=mWidth/2-(float)(mRadius*Math.cos((mCurrentAngle)*Math.PI/180))-mSunIcon.getWidth()/2;
positionY=marginTop+mRadius-(float)(mRadius*Math.sin((mCurrentAngle)*Math.PI/180))-mSunIcon.getHeight()/2;
在layout中的具体使用,除了宽高以外,还定义了字体大小,字体颜色,圆的颜色以及圆的半径(最重要)
<com.example.aaa.coolweather.View.SunView
android:id="@+id/sunview"
android:layout_width="match_parent"
android:layout_height="150dp"
app:sun_circle_color="#ffff"
app:sun_circle_radius="120"
app:sun_font_size="10px"
app:sun_font_color="#ffff"
/>
自定义View+动画的整体流程总述
自定义View过程
(1-6是自定义View,7-9是属性动画的使用)
- 写一个MyView类,继承自View。
- 在values文件夹下创建attrs.xml文件,在
<declare-styleable name="XXXView">
标签下声明自定义的属性(会生存R.styleable.name的数组,幷包含R.styleable.name_attrname的索引)。 - 在构造函数中使用TypedArray获取在布局中定义的属性,并使用。
- 重写onMeasure以支持wrap_content属性。定义wrap_content下的宽高值,并当SpecMode(不论宽高)==MeasureSpec.AT_Most,就调用setDimension(定义的宽与高);
- 重写onDraw来绘制我们想要绘制的东西。
- 在layout布局中使用自定义属性,注意开头要使用app,而非android。
动画过程
- 创建ValueAnimator对象或ObjectAnimator对象,设置动画持续时间与开始停止位置;
- 设置动画从开始到停止的执行逻辑(设置插值器与估值器,比如线性与非线性的动画效果等等,线性不需要设置)
注:(1)(2)过程都是在ValueAnimator.ofObject/ofInt/ofFloat中执行的
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
- 设置AnimatorUpdateListener监听器,当动画每播放一帧时会调用其中的onAnimationUpdate方法,如果是ValueAnimator就需要在其中手动设置该对象的属性,否则则不需要。最后调用invalidate方法让View重绘,调用onDraw方法。
- (如果需要的话)使用AnimatorSet让两个动画同时进行,使用
playTogether(Animator A,Animator B)
方法。 - 如果使用AnimatorSet就要调用其start方法,如果没有使用,就要调用ValueAnimator对象的start方法。
- 之后动画会按照要求执行,直到结束。
自定义View过程
- 写一个MyView类,继承自View。
- 在values文件夹下创建attrs.xml文件,声明自定义的属性。
- 在构造函数中使用TypedArray获取在布局中定义的属性,并使用。
- 重写onMeasure以支持wrap_content属性
- 重写onDraw来绘制我们想要绘制的东西。
- 在layout布局中使用自定义属性,注意开头要使用app,而非android。
1.构造函数
只带有Context的构造函数是在代码中使用的,而带有context和AttributeSet(属性集)的是layout文件中使用的。在最后的构造函数中,我们要初始化参数(将layout中的参数加载进来),以及初始化画笔(我们要通过画笔绘制Text与圆)。
public CircleIndexView(Context context)
{
this(context, null);
}
public CircleIndexView(Context context, AttributeSet attrs)
{
this(context, attrs, -1);
}
public CircleIndexView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
initParams(context, attrs);
initPaint();
}
2.创建attrs.xml文件,并声明自定义的属性
需要在values文件夹下创建attrs.xml文件声明自定义的属性。在resource标签下,写一个declare-styleable的标签,name一定要写自定义View的类名。具体属性使用<attr name="middleText" format="string"/>
,name表示名称,format代表类型。类型包括:string、dimension(尺寸)、color、integer等等。
declare-styleable 不是必要的,但是styleable可以帮我们节省开发工作量,帮我们生成数组的索引常量。每个declare-styleable产生一个R.styleable.name数组,外加每个属性的R.styleable.name_attrname 对应属性的下标,可以通过这个下标取出对应属性存储的值。
<resources>
<declare-styleable name="CircleIndexView">
<attr name="middleText" format="string"/>
<attr name="middleTextSize" format="dimension"/>
<attr name="middleTextColor" format="color"/>
<attr name="topText" format="string"/>
<attr name="topTextSize" format="dimension"/>
<attr name="topTextColor" format="color"/>
<attr name="numberTextSize" format="dimension"/>
<attr name="numberColor" format="color"/>
<attr name="outCircleColor" format="color"/>
<attr name="inCircleColor" format="color"/>
<attr name="duration" format="integer"/>
</declare-styleable>
</resources>
3.构造函数中使用TypedArray获取在布局中定义的属性,并使用。
TypedArray帮我们实现了获取attrs中的id与解析id的功能,因此我们可以根据这个id在TypedArray中获取想要的属性值。
(1)取出数组名为R.styleable.CircleIndexView的TypeArray,之后读取该TypedArray中的属性。
(2)最后,recycle()
方法的作用就是关闭了这个TypedArray。
可以看到下标为R.styleable.CircleIndexView_middleText,因此可知declare-styleable为我们创建了数组,同时可以让我们通过TypedArray对应下标去查询属性。
另外注意一点TypedValue.applyDimension()
可以将非标准尺寸(dp,sp等等)转成Android中的标准尺寸(px像素)。
/**
* 初始化View参数
* 获取布局(xml)中的属性以及使用布局(xml)中的属性,
*
*
* @param context
* @param attrs
*/
private void initParams(Context context, AttributeSet attrs)
{
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.CircleIndexView);
middleText = ta.getString(R.styleable.CircleIndexView_middleText);
setMiddleText(middleText);
...
//这里其实是一般在layot里没有去写这个属性,因此这里直接将默认值转为px以便使用
topTextSize = ta.getDimension(R.styleable.CircleIndexView_topTextSize,
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 15, context.getResources().getDisplayMetrics()));
ta.recycle();
}
初始化画笔就比较简单了,初始化画笔的颜色,粗细等等
4.重写onMeasure以支持wrap_content属性
常规操作:
1. 首先通过当前的MeasureSpec来获取宽高测量模式和大小
2. 设定WrapContent模式下的宽高
3. 当测量模式为AT_MOST且布局参数为wrap_content时,直接调用setMeasuredDimension(mWidth, mHeight);
,去设置宽高。
完整代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽-测量规则的模式和大小
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高-测量规则的模式和大小
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 设置wrap_content的默认宽 / 高值
// 默认宽/高的设定并无固定依据,根据需要灵活设置
// 类似TextView,ImageView等针对wrap_content均在onMeasure()对设置默认宽 / 高值有特殊处理,具体读者可以自行查看
int mWidth = 400;
int mHeight = 400;
// 当布局参数设置为wrap_content时,设置默认值
if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, mHeight);
// 宽 / 高任意一个布局参数为= wrap_content时,都设置默认值
} else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(mWidth, heightSize);
} else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
setMeasuredDimension(widthSize, mHeight);
}
}
而本自定义View中为了好看,默认wrap_content和match_parent都为宽高200dp,除非为固定大小,我们才更改size为固定的大小,注意这里不管是200dp还是固定大小,都要将dp转成px。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
setMeasuredDimension(measureWidth(widthMeasureSpec),
measureHeight(heightMeasureSpec));
}
5.重写onDraw来绘制我们想绘制的东西
(1)绘制圆(canvas.drawArc)
RectF的四个参数分别是左上右下,可以通过创建RectF来让canvas通过canvas.drawArc(mRectF,startAngle,sweepAngle,false,outPaint);
来画圆(其中传入参数是圆的左上右下四个点的位置,开始的角度,停止的角度),稍微注意一下内圆圈的角度是会变的,所以这里使用getInSweepAngle()
方法来获取。
(2)绘制文字(canvas.drawText)
canvas.drawText(text, x, y, paint)方法,其中x是这个字符串的左边在屏幕的位置(也就是最左边那条竖线的座标),如果设置了paint.setTextAlign(Paint.Align.CENTER);那就是字符串的中心;y是指定这个字符串baseline在屏幕上的位置,而不是字符居中的位置。
baseline问题:
FontMetrics可以理解为字体度量,可以获取到top,ascent,descent与bottom距离base的距离。而值向下增加,值向上减少,也就是说top、ascent的值都是负的。
fontMetrics=middleTextPaint.getFontMetrics();
在设计baseline时,我们想让Text内容居中在某个位置(比如说整个View的正中心)。那么我们就要让baseline的位置为正中心高度middle+(base距离正中心高度的距离)。
我们可以推出这个距离设base距离正中心高度为x,因为是正中心(top距离middle和bottom距离middle相等),因此top距离base的距离-x = bottom距离base的距离+x,可以求得x=(dtop-dbottom)/2,而已知top的值是负的,因此x就等于(-top-bottom)/2,将该值加上正中心高度middle即可。
baseline=(getCircleHeight()-fontMetrics.bottom-fontMetrics.top)/2;
protected void onDraw(Canvas canvas){
//200dp:middleTextSize:60,middleNumberSize:100,bottomTextSize:45
super.onDraw(canvas);
mCenter = getCircleWidth()/2;
//mRadius = getCircleWidth() / 2 - 50;
mRadius = getCircleWidth()/2-50;
//左,上,右,下
mRectF =new RectF(mCenter-mRadius,mCenter-mRadius+getTopTextSize(),
mCenter+mRadius,mCenter+mRadius+getTopTextSize());
//外圆圈
canvas.drawArc(mRectF,startAngle,sweepAngle,false,outPaint);
//内圆圈
canvas.drawArc(mRectF,startAngle,getInSweepAngle(),false,inPaint);
//中心文字(etc. 良)
middleTextPaint.setColor(getMiddleTextColor());
middleTextPaint.setTextSize(getMiddleTextSize());
Paint.FontMetrics fontMetrics=middleTextPaint.getFontMetrics();
float baseline=(getCircleHeight()-getNumberTextSize()-getMiddleTextSize()-20-fontMetrics.bottom-fontMetrics.top)/2+getTopTextSize();
//想让他在居中的位置(getCircleHeight()/2-getNumberTextSize()/2-getMiddleTextSize()/2-20)
canvas.drawText(getMiddleText(), getCircleWidth() / 2, baseline , middleTextPaint);
//中心数字(etc)
middleTextPaint.setColor(getNumberColor());
middleTextPaint.setTextSize(getNumberTextSize());
fontMetrics=middleTextPaint.getFontMetrics();
baseline=(getCircleHeight()-fontMetrics.bottom-fontMetrics.top)/2+getTopTextSize();
canvas.drawText(getIndexValue()+"",getCircleWidth()/2,
baseline,middleTextPaint);
//顶部文字(etc. 空气污染指数)
middleTextPaint.setColor(getTopTextColor());
middleTextPaint.setTextSize(getTopTextSize());
fontMetrics=middleTextPaint.getFontMetrics();
baseline=(getCircleHeight()-2*mRadius-fontMetrics.bottom-fontMetrics.top)/2;
//canvas.drawText(getBottomText(), getCircleWidth() / 2, getCircleHeight() - 50, middleTextPaint);
//让这个与圆的最顶端间隔20px
canvas.drawText(getTopText(), getCircleWidth() / 2,baseline ,middleTextPaint);
}
6.layout文件中使用
注意我们自己定义的属性,要使用app:attrname。同时要在前面定义 :xmlns:app=“http://schemas.android.com/apk/res-auto”。
动画
空气质量的使用
这是空气质量动画暴露给外界使用的函数,当获取到天气信息后,会通过updateIndex去更新当前空气质量的数值与空气质量的文字,其中使用了动画。
注意调用invalidate会导致View重绘,最终会调用onDraw。因此在invalidate之前要通过set方法设置当前的角度与当前的空气质量值。
/**
*
* @param value 空气质量数值
* @param middleText 空气质量(良)
*/
public void updateIndex(int value,String middleText){
setMiddleText(middleText);
invalidate();
//当前角度
float inSweepAngle = sweepAngle * value / 500;
//角度由0f到当前角度变化
ValueAnimator angleAnim = ValueAnimator.ofFloat(0f, inSweepAngle);
//动画持续时间
angleAnim.setDuration(getDuration());
//数值由0到value变化
ValueAnimator valueAnim = ValueAnimator.ofInt(0,value);
valueAnim.setDuration(getDuration());
//注册监听器
angleAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
//当angleAnim变化回调/
public void onAnimationUpdate(ValueAnimator valueAnimator)
{
float currentValue = (float) valueAnimator.getAnimatedValue();
//将当前的角度值赋给inSweepAngle
setInSweepAngle(currentValue);
//通知view改变,调用这个函数后,会返回到onDraw();
invalidate();
}
});
valueAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator)
{
int currentValue = (int) valueAnimator.getAnimatedValue();
setIndexValue(currentValue);
invalidate();
}
});
//让两个动画同时进行。
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.setInterpolator(new DecelerateInterpolator());
animatorSet.setStartDelay(150);
animatorSet.playTogether(angleAnim, valueAnim);
animatorSet.start();
}
日出日落动画的使用
setTimes是暴露给外部使用的方法,用于更新当前的时间。
public void setTimes(String startTime, String endTime, String currentTime)
{
mStartTime = startTime;
mEndTime = endTime;
mCurrentTime = currentTime;
mTotalMinute = calculateTime(mStartTime, mEndTime);//计算总时间,单位:分钟
mNeedMinute = calculateTime(mStartTime, mCurrentTime);//计算当前所给的时间 单位:分钟
mPercentage = formatTime(mTotalMinute, mNeedMinute);//当前时间的总分钟数占日出日落总分钟数的百分比
mCurrentAngle = (180 * mPercentage);
setAnimation(0, mCurrentAngle, 5000);
}
private void setAnimation(float startAngle,float currentAngle,int duration){
ValueAnimator sunAnimator= ValueAnimator.ofFloat(startAngle,currentAngle);
sunAnimator.setDuration(duration);
sunAnimator.setTarget(currentAngle);
sunAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mCurrentAngle=(float)animation.getAnimatedValue();
invalidateView();
}
});
//千万别忘了这个
sunAnimator.start();
}
private void invalidateView(){
//绘制太阳的x座标与y座标
//记得要减去太阳图标的大小。(让太阳始终在半圆上)
positionX=mWidth/2-(float)(mRadius*Math.cos((mCurrentAngle)*Math.PI/180))-mSunIcon.getWidth()/2;
positionY=marginTop+mRadius-(float)(mRadius*Math.sin((mCurrentAngle)*Math.PI/180))-mSunIcon.getHeight()/2;
invalidate();
}
动画总结
View动画
主要包括translate平移动画、scale缩放动画、rotate旋转动画、alpha透明度动画等四种。
View动画的View移动只是视觉效果,并不能真正的改变view的位置。
xml的使用:
首先创建xml文件,地址为res/anim/xxx.xml,可以看到动画可以仅是一个动画,也可以是一堆动画的组合。其中<set>
标签代表AnimatorSet,<translate>
代表平移动画,<scale>
代表缩放动画,<rotate>
代表旋转动画,<alpha>
代表透明动画(改变View的透明度)
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="true"
android:fillAfter="true">
<translate
android:fromXDelta="float"
android:toXDelta="float"
android:fromYDelta="float"
android:toYDelta="float"/>
<scale
android:fromXScale="float"
android:toXScale="float"
android:fromYScale="float"
android:toYScale="float"
android:pivotX="float"
android:pivotY="float"/>
<rotate
android:fromDegrees="float"
android:toDegrees="float"
android:pivotY="float"
android:pivotX="float"/>
<alpha
android:fromAlpha="float"
android:toAlpha="float"/>
</set>
帧动画
帧动画也是View动画的一种,它会按照顺序播放一组预先定义好的图片。对应类AnimationDrawable。
通过xml定义:
该xml文件创建在res/drawable/ 下,其中根节点<animation-list>
,属性android:oneshot
表示是否执行一次;子节点<item>
下可设置轮播的图片资源id和持续时间。例如:
<?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/xxx1" android:duration="500"/>
<item android:drawable="@drawable/xxx2" android:duration="500"/>
<item android:drawable="@drawable/xxx3" android:duration="500"/>
<item android:drawable="@drawable/xxx4" android:duration="500"/>
</animation-list>
在xml声明好之后,将它作为View的背景并通过AnimationDrawable来播放。
mView.setBackgroundResource(R.drawable.XXX);
AnimationDrawable animationDrawable = (AnimationDrawable)mView.getBackground();
animationDrawable.start();
属性动画
与View动画不同之处主要在于:(1)View动画通过不断图形变换,而属性动画通过动态改变对象属性;(2)作用对象分别是View和任何对象;(3)如果是通过xml使用的话,存放位置不同:res/anim和res/animator(4)View动画没有真正改变View的位置,而属性动画真正改变了View的位置。
为第四点作补充:
补间动画只是改变了View的视觉效果,而不会真正去改变View的属性。
如,将屏幕左上角的按钮 通过补间动画 移动到屏幕的右下角
点击当前按钮位置(屏幕右下角)是没有效果的,因为实际上按钮还是停留在屏幕左上角,补间动画只是将这个按钮绘制到屏幕右下角,改变了视觉效果而已。
ValueAnimator与ObjectAnimator
ValueAnimator
ValueAnimator是ObjectAnimator的父类,它继承自Animator。ValueAnimaotor同样提供了ofInt、ofFloat、ofObject等静态方法,传入的参数是动画过程的开始值、(中间值可以没有)、结束值来构造动画对象。
可以将ValueAnimator看成一个值变化器,通过不断控制值的变化,再不断手动赋给对象的属性,从而实现动画效果。
ObjectAnimator
直接对对象(自动)的属性值进行改变操作,从而实现动画效果,和ValueAnimator相比是直接更改属性的值。使用ObjectAnimator必须要实现set方法。
自动赋值的思路如下:(1)如果没有传递初值会通过get值来获取;(2)在动画执行的过程中通过set更改属性的值
插值器与估值器
时间插值器:TimeInterpolator。**根据时间流逝的百分比来计算当前的属性值改变的百分比。**包括:线性插值器(LinearInterpolator 匀速动画)、加速减速插值器(AccelerateDecelerateInterpolator 动画两头慢中间快)、减速插值器(DecelerateInterpolator 动画持续减速);
类型估值器(TypeEvaluator):根据当前属性改变的百分比来计算改变后的属性值。包括针对整形、浮点型、颜色的估值器。
以下图为例,View的x属性需要在40ms内从0到40,结合时间插值器和类型估值器来完成。
以线性插值器为例,由于动画默认是10ms一帧,那么当运行到第二帧是t=20ms,因此时间流逝了0.5,因此属性值也应该改变0.5。时间插值器计算出0.5并返回,具体属性改变多少由类型估值器计算。估值器通过估值小数*(属性终点-属性起点)
来计算出改变后的属性值。
当然插值器与估值器都可以自定义,尤其是在当前类型的估值器没有的情况下就必须自定义类型估值器。
监听器
属性动画提供了AnimatorLinstenr用于在动画开始结束取消重复播放时进行回调,
public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}
AnimatorUpdateListener :监听整个动画过程。每播放一帧onAnimationUpdate()就会被调用一次,如下:
public interface AnimatorUpdateListener {
void onAnimationUpdate(ValueAnimator var1);//在属性动画的属性值变化是回调。
}
工作流程
(1)创建ValueAnimator或ObjectAnimator,并设置动画持续时间与开始停止位置;
(2)设置动画从开始到停止的执行逻辑(设置插值器与估值器,比如线性与非线性的动画效果等等,线性不需要设置)
注:(1)(2)过程都是在ValueAnimator.ofObject/ofInt/ofFloat中执行的
ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
(3)设置AnimatorUpdateListener监听器,当动画每播放一帧时会调用其中的onAnimationUpdate方法,如果是ValueAnimator就需要在其中手动设置该对象的属性,否则则不需要。最后调用invalidate方法让View重绘,调用onDraw方法。
在这里插入代码片
(4)(如果需要的话)使用AnimatorSet让两个动画同时进行,使用playTogether(Animator A,Animator B)
方法。
(5)如果使用AnimatorSet就要调用其start方法,如果没有使用,就要调用ValueAnimator对象的start方法。
(6)之后动画会按照要求执行,直到结束。