浅析Android动画(二),属性动画高级实例探究
转载请注明出处!http://www.cnblogs.com/wondertwo/p/5312482.html
ObjectAnimator实现属性动画
为了写好Android动画这几篇博客,在动笔之前我是下过很大决心的,我对自己的要求是尽量把一个小知识点写清楚写明白,说白了就是相对于大而全的长篇大论,我更倾向于去写小而美的博客!为了保证在高产的同时能坚持每篇博客质量上让我满意,我翻阅了很多大牛的博客,这其中尤以郭霖大神的博客我印象最为深刻,也给我带来了很多启发,在此表示感谢并在博客的最后贴了出来供大家参考!另外值得一提的是,在这篇博客的第三部分,会对两个非常酷炫的属性动画实例进行分析,分别是桌面弹球动画和仿Win10系统开机小圆点旋转动画,为了激发大家阅读的兴趣,我们先来看一下效果如何?
下面言归正传,要了解属性动画我还是习惯先去翻看谷歌的API文档介绍,相对视图动画Added in API level 1
,属性动画Added in API level 11
,那我们可能会纠结既然已经有了视图动画,为什么还要加入属性动画呢?仔细来看属性动画虽然叫做动画,但是其意义已经不仅仅局限于实现炫酷的动画啦,借助于插值器(Interpolator
)和估值器(TypeEvaluator
),我们可以更具体的描述他:一种按照一定变化率对属性值进行操作的机制,变化率就是依赖Interpolator
控制,而值操作则是TypeEvaluator
控制!由此可见,属性动画不是一般的强大,与视图动画的区别主要在以下几点:
- 属性动画作用的对象可以是任何一个
Object
对象,也就是说我们完全可以给任意Object
对象设置属性动画,而这个对象可以不是一个View组件,也不管这个对象是否是可见的,而视图动画的作用对象只能是一个View对象,这是最大的不同; - 视图动画的一个致命缺陷就是,通过视图动画将一个View对象(比如一个
TextView
,Button
)位置改编后,该对象的触摸事件的焦点依然在原位置,而这在实际的开发中是不能容忍的,属性动画就很好的解决了这一缺陷; - 属性动画可以控制动画执行过程中的任意时刻的任意属性值,这么说可能不好理解,但是大家肯定知道,我在第一篇博客 [浅析Android动画(一),View动画高级实例 http://www.cnblogs.com/wondertwo/p/5295976.html ] 中也提及,视图动画从本质上来说是一种补间动画,他只对动画的起始值和结束值进行赋值,而动画中间执行过程中的属性值则是系统帮我们计算的。那我们怎样自己用代码控制动画执行过程中的属性值呢?属性动画就提供了很好地解决方案,就是自定义估值器;
那么属性动画是怎样完美的解决上述问题的呢?下面就开始学习属性动画的基本用法,我们来看属性动画的继承关系,如下如所示:
显然关注的焦点应该是ValueAnimator
,ObjectAnimator
这两个类啦,ObjectAnimator
继承自ValueAnimator
,是属性动画中非常重要的一个实现类,通过ObjectAnimator
类的静态欧工厂方法来创建ObjectAnimator
对象,这些静态工厂方法包括:ObjectAnimator.ofFloat()
,ObjectAnimator.ofInt()
等等,当然最为重要的一个静态工厂方法是ObjectAnimator.ofObject()
,可以接收一个Object对象并为其设置属性动画,瞬间高大上了有木有?这些静态工厂方法接收的参数分别是:
- 要设置动画的目标对象;
- 动画的属性类型;
- 一个或多个属性值;当只指定一个属性值,系统默认此值为结束值;当指定两个属性值,系统默认分别为起始值和结束值;当指定三个或三个以上时,系统默认线性插值;
ValueAnimator
是整个属性动画机制当中最核心的一个类,前面我们已经提到了,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator
这个类来负责计算的,ValueAnimator
对过渡动画值的计算依靠一个时间因子fraction
,而这个时间因子fraction
是系统由setDuration()
方法设置的动画执行时间通过计算得来的,所以ValueAnimator
还负责管理动画的持续时间、播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。ValueAnimator
使用起来也很简单,会在本篇博客的的二部分详细讲解!下面看一个实例:通过ValueAnimator
的子类ObjectAnimator
实现影子效果,类似于西游记中的元神出窍,哈哈是不是很好玩?先看效果如下:
那这个效果是怎么实现的呢?你所看到的四个影子并不是真实的影子,而是把四张相同的图片设置了半透明效果,并同时向四个不同的方向做位移变换。布局很简单,就是在根布局RelativeLayout
中放置五个ImageView
,src
值都引用同一个图片的资源id
,这样五张图片就会重叠在一起,因此造成了你看到的只有一张图片的假象,动画实现类EffectAni
的代码如下:
package com.wondertwo.effect;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.view.animation.BounceInterpolator;
import android.widget.ImageView;
import android.widget.Toast;
import com.wondertwo.R;
import java.util.ArrayList;
/**
* 属性动画PropertyAni
*
* 常用的属性动画的属性值有:
* - translationX、translationY----控制view对象相对其左上角坐标在X、Y轴上偏移的距离
* - rotation、rotationX、rotationY----控制view对象绕支点进行2D和3D旋转
* - scaleX、scaleY----控制view对象绕支点进行2D缩放
* - pivotX、pivotY----控制view对象的支点位置,这个位置一般就是view对象的中心点。围绕这个支点可以进行旋转和缩放处理
* - x、y----描述view对象在容器中的最终位置,是最初的左上角坐标和translationX、translationY值的累计和
* - alpha----表示view对象的透明度。默认值是1(完全透明)、0(不透明)
*
* Created by wondertwo on 2016/3/11.
*/
public class EffectAni extends AppCompatActivity implements View.OnClickListener {
// ImageView组件id数组
private int[] mRes = new int[]{R.id.iv_a, R.id.iv_b, R.id.iv_c, R.id.iv_d, R.id.iv_e};
// ImageView对象集合
private ArrayList<ImageView> mImViews = new ArrayList<>();
private boolean flag = true;// 启动动画、关闭动画的标记位
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_effect);
// for循环创建ImageView对象,并添加到集合中
for (int i = 0; i < mRes.length; i++) {
ImageView iv_a = (ImageView) findViewById(mRes[i]);
iv_a.setOnClickListener(this);
mImViews.add(iv_a);
}
}
// 按钮点击事件
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.iv_a:
if (flag) {
startAnim();
} else {
closeAnim();
}
break;
default:
Toast.makeText(EffectAni.this, "" + v.getId(), Toast.LENGTH_SHORT).show();
break;
}
}
// 关闭动画
private void closeAnim() {
// 创建ObjectAnimator属性对象,参数分别是动画要设置的View对象、动画属性、属性值
ObjectAnimator animator0 = ObjectAnimator.ofFloat(mImViews.get(0),
"alpha",
0.5F,
1F);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mImViews.get(1),
"translationY",
200F,
0);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mImViews.get(2),
"translationX",
200F,
0);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mImViews.get(3),
"translationY",
-200F,
0);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(mImViews.get(4),
"translationX",
-200F,
0);
AnimatorSet aniSet = new AnimatorSet();
aniSet.setDuration(4000);
aniSet.setInterpolator(new BounceInterpolator());// 弹跳效果的插值器
aniSet.playTogether(animator0,
animator1,
animator2,
animator3,
animator4);// 同时启动5个动画
aniSet.start();
// 重置标记位
flag = true;
}
// 启动动画
private void startAnim() {
// 创建ObjectAnimator属性对象,参数分别是动画要设置的View对象、动画属性、属性值
ObjectAnimator animator0 = ObjectAnimator.ofFloat(
mImViews.get(0),
"alpha",
1f,
0.5f);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(
mImViews.get(1),
"translationY",
200f);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(
mImViews.get(2),
"translationX",
200f);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(
mImViews.get(3),
"translationY",
-200f);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(
mImViews.get(4),
"translationX",
-200f);
AnimatorSet aniSet = new AnimatorSet();
aniSet.setDuration(4000);
aniSet.setInterpolator(new BounceInterpolator());// 弹跳效果的插值器
aniSet.playTogether(animator0,
animator1,
animator2,
animator3,
animator4);// 同时启动5个动画
aniSet.start();
// 重置标记位
flag = false;
}
}
上面代码看起来挺长,但是实现属性动画的代码也没几行代码,属性动画的逻辑就是startAnim()
和closeAnim()
这两个方法,先来看启动动画的方法startAnim()
,上面也提到过,首先是通过ObjectAnimator.ofFloat()
静态工厂方法创建ObjectAnimator
对象,可以看到上面一共创建了5个ObjectAnimator
对象,传入的第一个参数就是我们布局文件中的五个ImageView
,但是注意了,给他们设置的属性动画却是各不相同的,对应的第二个参数就表示要设置的属性对应的字符串,系统会自动解析它们对应的是哪个属性,我们分别传入了"alpha"
,"translationY"
,"translationX"
,你肯定很清楚是用来设置透明度、Y轴方向平移、X轴方向平移的。除了上述几个,还可以传入"rotation"
,"rotationY"
,"rotationX"
,"rotationZ"
,"scaleY"
,"scaleX"
等等,而关闭动画方法closeAnim()
的作用正好相反,把刚才变化的动画移回原位。最后我们注册了点击事件,判断一个布尔型的标记位的值,使响应事件分别调用开启动画和关闭动画这两个方法。
以上介绍的这些就是ObjectAnimator
的最基本的用法,同时也是Android开发者进阶必须掌握的内容,当然作为一个很有上进心的开发者,要求当然不能是只是会用基本用法这么低,我们再继续来学习属性动画的监听和ValueAnimator
的用法,这就比上面的要高级很多了。
ValueAnimator和属性动画的监听
上面在学习ObjectAnimator
的使用方法的同时,已经对ValueAnimator
的继承关系进行了初步的介绍,下面更进一步,我们一起来更深入的学习ValueAnimator
,如果要用一句话来概括ValueAnimator
的特性,可以这样概括,在属性动画的执行过程中,不断计算并修改动画的属性值。这样说可能比较晦涩难懂,但接下来就会有一个实例,看完这个实例你就明白为什么会这样描述ValueAnimator
。
考虑这样一个场景,我们希望在6秒内把一个view控件的背景颜色从从红色渐变到蓝色,我们该怎样去实现它呢?不用想了,前面学过的视图动画完成不了这个需求,就是这样一个简单的不能再简单的需求,就能看出视图动画的局限性了。对的,那我们还有属性动画呢,用属性动画就可以轻松实现上面的场景啦,不过在此之前我们还需要了解一下属性动画的原理到底是什么?
属性动画需要不断改变对象的某个属性值,从而达到动画的效果,那么问题来了,既然是改变对象的属性值,比如上面所说的view控件的背景颜色这一属性的值,那么背景颜色这个属性一定是要不断地反复的被赋值并在手机屏幕上显示出来的,注意上面的这句话,我们首先应该画出的关键字是“不断地被赋值”这几个字,说到赋值,Android系统最常见的取值、赋值方法自然是setter
、getter
方法啦,所以自然而然的,属性动画要求定义的属性必须有setter
、getter
方法,就算没有getter
方法在某些特殊的情况下是允许的,但是所有情况下setter
方法必须要有,如果系统没有提供那就需要我们自己动手去写setter
方法啦!其次对于上面那句话我们画出的关键字,你是否注意到有“不断地”这三个字?不断地赋值那么这些值是怎么来的呢?我可以告诉你有两种方式来不断得到这些值:
- 为
ValueAnimator
对象设置动画监听,代码如下所示:valueAnimator.addUpdateListener()
,需要传入一个AnimatorUpdateListener
对象,一般我们传入的是AnimatorUpdateListener
的匿名对象,即:valueAnimator.addUpdateListener(new AnimatorUpdateListener(){...})
,需要重写它的onAnimationUpdate()
方法,那么上述值的计算逻辑就放在onAnimationUpdate()
方法体内; - 重写
TypeEvaluator
,TypeEvaluator
这个词直译过来就是类型值算法,也被译作估值器,我觉得这个叫法很形象,因为他就是用来计算属性动画某个时刻的属性值的具体值的,关于估值器和插值器我会在下一篇博客中详细介绍;
现在继续来完成上面提到的这个场景,这也是我认为本篇博客最值得一看的地方:在6秒内把一个view控件的背景颜色从从红色渐变到蓝色。先来看效果图如下,这里我们实现的是把一个按钮的背景颜色从蓝色渐变到红色,并且同时做缩放动画,效果还是很明显的。
布局文件很简单,在RelativeLayout
中定义了一个Button
,我们来看主要代码,实现这种效果的代码BuleToRed.java
我贴出来,下面会详细分析实现的细节:
package com.wondertwo.propertyanime;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
/**
* BuleToRed实现目标对象背景色的渐变
* Created by wondertwo on 2016/3/23.
*/
public class BuleToRed extends Activity {
private Button targetView;
private int mCurrentRed = -1;
private int mCurrentGreen = -1;
private int mCurrentBlue = -1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_blue_to_red);
targetView = (Button) findViewById(R.id.tv_color_backgroound);
/**
* 注册点击事件,展示效果
*/
targetView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
displayResult(targetView, "#0000ff", "#ff0000");
}
});
}
/**
* displayResult()展示结果
*/
private void displayResult(final View target, final String start, final String end) {
// 创建ValueAnimator对象,实现颜色渐变
ValueAnimator valueAnimator = ValueAnimator.ofFloat(1f, 100f);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 获取当前动画的进度值,1~100
float currentValue = (float) animation.getAnimatedValue();
Log.d("当前动画值", "current value : " + currentValue);
// 获取动画当前时间流逝的百分比,范围在0~1之间
float fraction = animation.getAnimatedFraction();
// 直接调用evaluateForColor()方法,通过百分比计算出对应的颜色值
String colorResult = evaluateForColor(fraction, start, end);
/**
* 通过Color.parseColor(colorResult)解析字符串颜色值,传给ColorDrawable,创建ColorDrawable对象
*/
/*LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) target.getLayoutParams();*/
ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
// 把ColorDrawable对象设置为target的背景
target.setBackground(colorDrawable);
target.invalidate();
}
});
valueAnimator.setDuration(6 * 1000);
// 组装缩放动画
ValueAnimator animator_1 = ObjectAnimator.ofFloat(target, "scaleX", 1f, 0.5f);
ValueAnimator animator_2 = ObjectAnimator.ofFloat(target, "scaleY", 1f, 0.5f);
ValueAnimator animator_3 = ObjectAnimator.ofFloat(target, "scaleX", 0.5f, 1f);
ValueAnimator animator_4 = ObjectAnimator.ofFloat(target, "scaleY", 0.5f, 1f);
AnimatorSet set_1 = new AnimatorSet();
set_1.play(animator_1).with(animator_2);
AnimatorSet set_2 = new AnimatorSet();
set_2.play(animator_3).with(animator_4);
AnimatorSet set_3 = new AnimatorSet();
set_3.play(set_1).before(set_2);
set_3.setDuration(3 * 1000);
// 组装颜色动画和缩放动画,并启动动画
AnimatorSet set_4 = new AnimatorSet();
set_4.play(valueAnimator).with(set_3);
set_4.start();
}
/**
* evaluateForColor()计算颜色值并返回
*/
private String evaluateForColor(float fraction, String startValue, String endValue) {
String startColor = startValue;
String endColor = endValue;
int startRed = Integer.parseInt(startColor.substring(1, 3), 16);
int startGreen = Integer.parseInt(startColor.substring(3, 5), 16);
int startBlue = Integer.parseInt(startColor.substring(5, 7), 16);
int endRed = Integer.parseInt(endColor.substring(1, 3), 16);
int endGreen = Integer.parseInt(endColor.substring(3, 5), 16);
int endBlue = Integer.parseInt(endColor.substring(5, 7), 16);
// 初始化颜色的值
if (mCurrentRed == -1) {
mCurrentRed = startRed;
}
if (mCurrentGreen == -1) {
mCurrentGreen = startGreen;
}
if (mCurrentBlue == -1) {
mCurrentBlue = startBlue;
}
// 计算初始颜色和结束颜色之间的差值
int redDiff = Math.abs(startRed - endRed);
int greenDiff = Math.abs(startGreen - endGreen);
int blueDiff = Math.abs(startBlue - endBlue);
int colorDiff = redDiff + greenDiff + blueDiff;
if (mCurrentRed != endRed) {
mCurrentRed = getCurrentColor(startRed, endRed, colorDiff, 0, fraction);
} else if (mCurrentGreen != endGreen) {
mCurrentGreen = getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction);
} else if (mCurrentBlue != endBlue) {
mCurrentBlue = getCurrentColor(startBlue, endBlue, colorDiff,
redDiff + greenDiff, fraction);
}
// 将计算出的当前颜色的值组装返回
String currentColor = "#" + getHexString(mCurrentRed)
+ getHexString(mCurrentGreen) + getHexString(mCurrentBlue);
return currentColor;
}
/**
* 根据fraction值来计算当前的颜色。
*/
private int getCurrentColor(int startColor, int endColor, int colorDiff,
int offset, float fraction) {
int currentColor;
if (startColor > endColor) {
currentColor = (int) (startColor - (fraction * colorDiff - offset));
if (currentColor < endColor) {
currentColor = endColor;
}
} else {
currentColor = (int) (startColor + (fraction * colorDiff - offset));
if (currentColor > endColor) {
currentColor = endColor;
}
}
return currentColor;
}
/**
* 将10进制颜色值转换成16进制。
*/
private String getHexString(int value) {
String hexString = Integer.toHexString(value);
if (hexString.length() == 1) {
hexString = "0" + hexString;
}
return hexString;
}
}
乍一看起来似乎代码很长很繁琐,有170多行,但是别急,都是你见过的知道的东西,所以分析起来会很简单!在BuleToRed.java
类中,首先我们在onCreate()
方法中拿到目标对象,也就是我们定义的Button
对象,可以看到属性动画的启动入口就是Button
的点击事件中displayResult()
方法,再往下看我们发现displayResult(final View target, final String start, final String end)
方法接收三个参数,分别是:
- 要设置颜色渐变的目标对象的实例,这里我们直接传入了
Button
按钮的实例对象; - 颜色起始值,我们传入
"#0000ff"
,即蓝色; - 颜色结束值,我们传入
"#ff0000"
,即红色;
在displayResult()
方法中,先是创建ValueAnimator
对象用于实现颜色渐变的动画效果,我们为ValueAnimator
对象valueAnimator
设置了监听器ValueAnimator.AnimatorUpdateListener()
,动画的执行过程中会不断回调AnimatorUpdateListener()
中的onAnimationUpdate(ValueAnimator animation)
方法,所以要实现背景颜色的渐变效果,则控制颜色渐变的逻辑必须要放在onAnimationUpdate()
中,紧接着我们在onAnimationUpdate()
中通过animation.getAnimatedValue()
拿到监听的数值,代码如下:
// 获取当前动画的进度值,1~100
float currentValue = (float) animation.getAnimatedValue();
Log.d("当前动画值", "current value : " + currentValue);
并获取当前时间流逝所占的百分比参数fraction
,接着调用evaluateForColor(fraction, start, end)
方法,这个方法就是专门负责计算当前对应的颜色值,需要传入我们刚才计算出来的fraction
参数,代码如下:
// 获取动画当前时间流逝的百分比,范围在0~1之间
float fraction = animation.getAnimatedFraction();
// 直接调用evaluateForColor()方法,通过百分比计算出对应的颜色值
String colorResult = evaluateForColor(fraction, start, end);
很自然的,我们用String colorResult
来接受evaluateForColor()
方法返回的颜色值,是一个十六进制的字符串,到这里你可能会纠结我们怎么才能把这个字符串解析出来并设置给目标对象呢?代码如下:
/**
* 通过Color.parseColor(colorResult)解析字符串颜色值,传给ColorDrawable,创建ColorDrawable对象
*/
ColorDrawable colorDrawable = new ColorDrawable(Color.parseColor(colorResult));
// 把ColorDrawable对象设置为target的背景
target.setBackground(colorDrawable);
target.invalidate();
给目标对象设置背景target.setBackground(colorDrawable)
,setBackground()
方法接收一个Drawable
对象,顺水推舟我们很容易就会联想到ColorDrawable
,ColorDrawable
是Drawable
接口的一个实现类,我们只需要创建一个ColorDrawable
对象并把它传给setBackground()
方法就OK,而ColorDrawable
的构造方法需要接收一个int
类型的颜色值,这个好办,我们用Color
类的静态工厂方法parseColor()
把字符串颜色值colorResult
解析成int
类型的颜色值传进去就好,代码是这样的:Color.parseColor(colorResult)
,到这里displayResult()
方法就讲完了,因为后面的那几行代码已经出现过好多次了,就是在颜色渐变的同时给目标对象再加一个缩放的动画效果。
接下来我还想再补充一下,把evaluateForColor()
方法计算颜色值的具体过程在这里分析一下,其实计算颜色值的逻辑也不复杂,首先计算出红绿蓝三种颜色的对应的初始值和结束值,然后根据初始值和结束值之间的差值来计算当前对应的颜色值,getCurrentColor(startGreen, endGreen, colorDiff, redDiff, fraction)
就是完成这个计算的逻辑所在,接着把计算得到的三种颜色值的int
型数据转换为十六进制字符串数据,并把它们组装在一起后返回,而getHexString()
方法就负责将int
型颜色值数据转换为十六进制数据。
桌面弹球和Win10开机小圆点旋转动画的实例探究
我们先来分析简单一点的Win10开机小圆点旋转动画,用过Win10系统的同学都应该知道,Win10开机系统初始化的时候会显示一圈环形小圆点旋转的动画,相信这个动画效果我一说你肯定历历在目记忆犹新,先来看一下最终的效果图如下:
作为对ObjectAniumator
的用法的高级探究,其实他还是很简单的,布局文件先定义了4个小圆点ImageView
,把每个小圆点ImageView
都放在了一个LinearLayout
中,这很简单!说到绘制小圆点,我比较推荐的一种做法是在res/drawable
目录下直接通过xml定义shape资源文件,这样定义的好处是可以避免使用图片资源造成不必要的内存占用。这里我把我的小圆点定义代码贴一下:
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@android:color/holo_red_dark" />
</shape>
实际上我们定义的只是一个椭圆,要显示出小圆点我们需要指定它的宽高相等,即android:layout_width
和android:layout_height
的值要相等,否则就会显示成椭圆。然后只需要像引用图片资源一样,在drawable目录下引用它就好,比如:
<LinearLayout
android:id="@+id/ll_point_circle_4"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/shape_point" />
</LinearLayout>
下面是完整的布局文件,仅供参考:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/start_ani_2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:text="@string/start_ani" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_centerVertical="true">
<LinearLayout
android:id="@+id/ll_point_circle_1"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/shape_point"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_point_circle_2"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/shape_point"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_point_circle_3"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/shape_point"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/ll_point_circle_4"
android:layout_width="wrap_content"
android:layout_height="240dp"
android:layout_centerInParent="true"
android:orientation="vertical">
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/shape_point"
/>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
接着把CircleProgress
属性动画类的代码贴出来,并在CircleProgress
属性动画类中拿到上面4个小圆点的对象,动画实现的细节会在代码后面详细讲解:
package com.wondertwo.propertyanime;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.Button;
import android.widget.LinearLayout;
/**
* ObjectAnimator高级实例探究
* Created by wondertwo on 2016/3/22.
*/
public class CircleProgress extends Activity {
private LinearLayout mPoint_1;
private LinearLayout mPoint_2;
private LinearLayout mPoint_3;
private LinearLayout mPoint_4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_circle_progress);
mPoint_1 = (LinearLayout) findViewById(R.id.ll_point_circle_1);
mPoint_2 = (LinearLayout) findViewById(R.id.ll_point_circle_2);
mPoint_3 = (LinearLayout) findViewById(R.id.ll_point_circle_3);
mPoint_4 = (LinearLayout) findViewById(R.id.ll_point_circle_4);
Button startAni = (Button) findViewById(R.id.start_ani_2);
startAni.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
beginPropertyAni();
}
});
}
/**
* 开启动画
*/
private void beginPropertyAni() {
ObjectAnimator animator_1 = ObjectAnimator.ofFloat(
mPoint_1,
"rotation",
0,
360);
animator_1.setDuration(2000);
animator_1.setInterpolator(new AccelerateDecelerateInterpolator());
ObjectAnimator animator_2 = ObjectAnimator.ofFloat(
mPoint_2,
"rotation",
0,
360);
animator_2.setStartDelay(150);
animator_2.setDuration(2000 + 150);
animator_2.setInterpolator(new AccelerateDecelerateInterpolator());
ObjectAnimator animator_3 = ObjectAnimator.ofFloat(
mPoint_3,
"rotation",
0,
360);
animator_3.setStartDelay(2 * 150);
animator_3.setDuration(2000 + 2 * 150);
animator_3.setInterpolator(new AccelerateDecelerateInterpolator());
ObjectAnimator animator_4 = ObjectAnimator.ofFloat(
mPoint_4,
"rotation",
0,
360);
animator_4.setStartDelay(3 * 150);
animator_4.setDuration(2000 + 3 * 150);
animator_4.setInterpolator(new AccelerateDecelerateInterpolator());
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(animator_1).with(animator_2).with(animator_3).with(animator_4);
animatorSet.start();
}
}
代码确实不长只有80多行,但是麻雀虽小五脏俱全,很显然beginPropertyAni()
方法就是启动动画的方法,调用ObjectAnimator.ofFloat()
静态工厂方法创建ObjectAnimator
对象我就不解释了,很容易看懂!重点来了,Win10开机小圆点旋转动画的难点不在旋转,如果我们把旋转的最高点看作是旋转的起始点,小圆点的旋转是一个先加速后减速的过程,这恰好符合高中物理的规律,小球内切圆环轨道做圆周运动,不知道我这样解释是不是很形象呢?那么控制旋转的加速度很好办,只要设置一个AccelerateDecelerateInterpolator()
插值器就OK,但是我们发现,这不是一个小球在旋转,而是有4个同时在旋转,而且旋转还不同步,这又该如何解决呢?你只要从第二个小球开始,每个小球设置固定时间间隔的延时启动,就能完美解决上面的问题。代码是这样的:
animator_2.setStartDelay(150);
animator_3.setStartDelay(2 * 150);
animator_4.setStartDelay(3 * 150);
写到这里已经三万字了,最后一起来学习一个桌面弹球动画,这也是这篇博客的收尾工作。老习惯我们还是先展示桌面弹球动画的酷炫效果吧:
在动画中可以清晰的看到小球下落过程中的加速运动,碰到桌面(手机屏幕的底部)后的变形压扁,以及小球弹起的动画,非常形象生动!先贴代码后面再做分析:
package com.wondertwo.propertyanime;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.Shader;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.LinearLayout;
import java.util.ArrayList;
/**
* 小球下落动画加强版XBallsFallActivity,增加了小球桌底时的压扁、回弹动画
* Created by wondertwo on 2016/3/20.
*/
public class XBallsFallActivity extends Activity {
static final float BALL_SIZE = 50f;// 小球直径
static final float FULL_TIME = 1000;// 下落时间
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_x_ball_fall);
LinearLayout xContainer = (LinearLayout) findViewById(R.id.xcontainer);
// 设置要显示的view组件
xContainer.addView(new XBallView(this));
}
/**
* 自定义动画组件XBallView
*/
public class XBallView extends View implements ValueAnimator.AnimatorUpdateListener {
public final ArrayList<XShapeHolder> balls = new ArrayList<>();// 创建balls集合来存储XShapeHolder对象
public XBallView(Context context) {
super(context);
setBackgroundColor(Color.WHITE);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 屏蔽ACTION_UP事件
if (event.getAction() != MotionEvent.ACTION_DOWN && event.getAction() != MotionEvent.ACTION_MOVE) {
return false;
}
// 在ACTION_DOWN事件发生点生成小球
XShapeHolder newBall = addBall(event.getX(), event.getY());
// 计算小球下落动画开始时Y坐标
float startY = newBall.getY();
// 计算小球下落动画结束时的Y坐标,即屏幕高度减去startY
float endY = getHeight() - BALL_SIZE;
// 获取屏幕高度
float h = (float) getHeight();
float eventY = event.getY();
// 计算动画持续时间
int duration = (int) (FULL_TIME * ((h - eventY) / h));
/**
* 下面开始定义小球的下落,着地压扁,反弹等属性动画
*/
// 定义小球下落动画
ValueAnimator fallAni = ObjectAnimator.ofFloat(
newBall,
"y",
startY,
endY);
// 设置动画持续时间
fallAni.setDuration(duration);
// 设置加速插值器
fallAni.setInterpolator(new AccelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
fallAni.addUpdateListener(this);
// 定义小球压扁动画,控制小球x坐标左移半个球宽度
ValueAnimator squashshAni1 = ObjectAnimator.ofFloat(
newBall,
"x",
newBall.getX(),
newBall.getX() - BALL_SIZE / 2);
squashshAni1.setDuration(duration / 4);
squashshAni1.setRepeatCount(1);
squashshAni1.setRepeatMode(ValueAnimator.REVERSE);
squashshAni1.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
squashshAni1.addUpdateListener(this);
// 定义小球压扁动画,控制小球宽度加倍
ValueAnimator squashshAni2 = ObjectAnimator.ofFloat(
newBall,
"width",
newBall.getWidth(),
newBall.getWidth() + BALL_SIZE);
squashshAni2.setDuration(duration / 4);
squashshAni2.setRepeatCount(1);
squashshAni2.setRepeatMode(ValueAnimator.REVERSE);
squashshAni2.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
squashshAni2.addUpdateListener(this);
// 定义小球拉伸动画, 控制小球的y坐标下移半个球高度
ValueAnimator stretchAni1 = ObjectAnimator.ofFloat(
newBall,
"y",
endY,
endY + BALL_SIZE / 2);
stretchAni1.setDuration(duration / 4);
stretchAni1.setRepeatCount(1);
stretchAni1.setRepeatMode(ValueAnimator.REVERSE);
stretchAni1.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
stretchAni1.addUpdateListener(this);
// 定义小球拉伸动画, 控制小球的高度减半
ValueAnimator stretchAni2 = ObjectAnimator.ofFloat(
newBall,
"height",
newBall.getHeight(),
newBall.getHeight() - BALL_SIZE / 2);
stretchAni2.setDuration(duration / 4);
stretchAni2.setRepeatCount(1);
stretchAni2.setRepeatMode(ValueAnimator.REVERSE);
stretchAni2.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
stretchAni2.addUpdateListener(this);
// 定义小球弹起动画
ValueAnimator bounceAni = ObjectAnimator.ofFloat(
newBall,
"y",
endY,
startY);
bounceAni.setDuration(duration);
bounceAni.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
bounceAni.addUpdateListener(this);
// 定义AnimatorSet,按顺序播放[下落、压扁&拉伸、弹起]动画
AnimatorSet set = new AnimatorSet();
//在squashshAni1之前播放fallAni
set.play(fallAni).before(squashshAni1);
/**
* 由于小球弹起时压扁,即宽度加倍,x坐标左移,高度减半,y坐标下移
* 因此播放squashshAni1的同时还要播放squashshAni2,stretchAni1,stretchAni2
*/
set.play(squashshAni1).with(squashshAni2);
set.play(squashshAni1).with(stretchAni1);
set.play(squashshAni1).with(stretchAni2);
// 在stretchAni2之后播放bounceAni
set.play(bounceAni).after(stretchAni2);
// newBall对象的渐隐动画,设置alpha属性值1--->0
ObjectAnimator fadeAni = ObjectAnimator.ofFloat(
newBall,
"alpha",
1f,
0f);
// 设置动画持续时间
fadeAni.setDuration(250);
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
fadeAni.addUpdateListener(this);
// 为fadeAni设置监听
fadeAni.addListener(new AnimatorListenerAdapter() {
// 动画结束
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束时将该动画关联的ShapeHolder删除
balls.remove(((ObjectAnimator) (animation)).getTarget());
}
});
// 再次定义一个AnimatorSet动画集合,来组合动画
AnimatorSet aniSet = new AnimatorSet();
// 指定在fadeAni之前播放set动画集合
aniSet.play(set).before(fadeAni);
// 开始播放动画
aniSet.start();
return true;
}
@Override
protected void onDraw(Canvas canvas) {
for (XShapeHolder xShapeHolder : balls) {
canvas.save();
canvas.translate(xShapeHolder.getX(), xShapeHolder.getY());
xShapeHolder.getShape().draw(canvas);
canvas.restore();
}
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 指定重绘界面
this.invalidate();
}
/**
* addBall()方法返回XShapeHolder对象,ShapeHolder对象持有小球
*/
private XShapeHolder addBall(float x, float y) {
// 创建一个椭圆
OvalShape circle = new OvalShape();
// 设置椭圆宽高
circle.resize(BALL_SIZE, BALL_SIZE);
// 把椭圆包装成Drawable对象
ShapeDrawable drawble = new ShapeDrawable(circle);
// 创建XShapeHolder对象
XShapeHolder holder = new XShapeHolder(drawble);
// 设置holder坐标
holder.setX(x - BALL_SIZE / 2);
holder.setY(y - BALL_SIZE / 2);
// 生成随机组合的ARGB颜色
int red = (int) (Math.random() * 255);
int green = (int) (Math.random() * 255);
int blue = (int) (Math.random() * 255);
// 把red,green,blue三个颜色随机数组合成ARGB颜色
int color = 0xff000000 + red << 16 | green << 8 | blue;
// 把red,green,blue三个颜色随机数除以4得到商值组合成ARGB颜色
int darkColor = 0xff000000 | red / 4 << 16 | green / 4 << 8 | blue / 4;
// 创建圆形渐变效果
RadialGradient gradient = new RadialGradient(
37.5f,
12.5f,
BALL_SIZE,
color,
darkColor,
Shader.TileMode.CLAMP);
// 获取drawble关联的画笔
Paint paint = drawble.getPaint();
paint.setShader(gradient);
// 为XShapeHolder对象设置画笔
holder.setPaint(paint);
balls.add(holder);
return holder;
}
}
}
这次的代码挺长有260多行,如果把它拆分开来,你会觉得代码还是原来的套路,还是很熟悉的有木有?我们首先来看,弹球动画类XBallsFallActivity
中的代码分为两块,一是onCreate()
方法,这是每个Activity都要重写的方法,那我们在onCreate()
方法中干了什么呢?只干了一件事就是拿到LinearLayout
布局的对象,并调用addBall()
方法给它添加XBallView
这个view对象,代码是这样的:
xContainer.addView(new XBallView(this));
那XBallView
对象又是什么鬼呢?一个自定义view组件,也就是实现我们小球的view组件,这也是我们这个动画的难点所在,我们慢慢来分析,代码定位到XBallView
类,第一眼你会发现这个类不仅继承了View类,而且还实现了ValueAnimator.AnimatorUpdateListener
这样一个接口,再仔细一看你又会发现,这个接口怎么听起来这么耳熟呢?没错,这就是上面我们在上面第二部分[ValueAnimator和属性动画的监听]中讲过的AnimatorUpdateListener
类!实现了这个接口就意味着可以在XBallView
中直接调用addUpdateListener(this)
方法对属性动画进行监听,只需要传入this
即可!
那我们再继续往下看看有没有我们要找的定义属性动画的逻辑呢?果然有!XBallView
类中一共定义了7个动画和两个AnimatorSet
动画集合,我把这段代码摘录出来:
/**
* 下面开始定义小球的下落,着地压扁,反弹等属性动画
*/
// 定义小球下落动画
ValueAnimator fallAni = ObjectAnimator.ofFloat(
newBall,
"y",
startY,
endY);
// 设置动画持续时间
fallAni.setDuration(duration);
// 设置加速插值器
fallAni.setInterpolator(new AccelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
fallAni.addUpdateListener(this);
// 定义小球压扁动画,控制小球x坐标左移半个球宽度
ValueAnimator squashshAni1 = ObjectAnimator.ofFloat(
newBall,
"x",
newBall.getX(),
newBall.getX() - BALL_SIZE / 2);
squashshAni1.setDuration(duration / 4);
squashshAni1.setRepeatCount(1);
squashshAni1.setRepeatMode(ValueAnimator.REVERSE);
squashshAni1.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
squashshAni1.addUpdateListener(this);
// 定义小球压扁动画,控制小球宽度加倍
ValueAnimator squashshAni2 = ObjectAnimator.ofFloat(
newBall,
"width",
newBall.getWidth(),
newBall.getWidth() + BALL_SIZE);
squashshAni2.setDuration(duration / 4);
squashshAni2.setRepeatCount(1);
squashshAni2.setRepeatMode(ValueAnimator.REVERSE);
squashshAni2.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
squashshAni2.addUpdateListener(this);
// 定义小球拉伸动画, 控制小球的y坐标下移半个球高度
ValueAnimator stretchAni1 = ObjectAnimator.ofFloat(
newBall,
"y",
endY,
endY + BALL_SIZE / 2);
stretchAni1.setDuration(duration / 4);
stretchAni1.setRepeatCount(1);
stretchAni1.setRepeatMode(ValueAnimator.REVERSE);
stretchAni1.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
stretchAni1.addUpdateListener(this);
// 定义小球拉伸动画, 控制小球的高度减半
ValueAnimator stretchAni2 = ObjectAnimator.ofFloat(
newBall,
"height",
newBall.getHeight(),
newBall.getHeight() - BALL_SIZE / 2);
stretchAni2.setDuration(duration / 4);
stretchAni2.setRepeatCount(1);
stretchAni2.setRepeatMode(ValueAnimator.REVERSE);
stretchAni2.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
stretchAni2.addUpdateListener(this);
// 定义小球弹起动画
ValueAnimator bounceAni = ObjectAnimator.ofFloat(
newBall,
"y",
endY,
startY);
bounceAni.setDuration(duration);
bounceAni.setInterpolator(new DecelerateInterpolator());
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
bounceAni.addUpdateListener(this);
// 定义AnimatorSet,按顺序播放[下落、压扁&拉伸、弹起]动画
AnimatorSet set = new AnimatorSet();
//在squashshAni1之前播放fallAni
set.play(fallAni).before(squashshAni1);
/**
* 由于小球弹起时压扁,即宽度加倍,x坐标左移,高度减半,y坐标下移
* 因此播放squashshAni1的同时还要播放squashshAni2,stretchAni1,stretchAni2
*/
set.play(squashshAni1).with(squashshAni2);
set.play(squashshAni1).with(stretchAni1);
set.play(squashshAni1).with(stretchAni2);
// 在stretchAni2之后播放bounceAni
set.play(bounceAni).after(stretchAni2);
// newBall对象的渐隐动画,设置alpha属性值1--->0
ObjectAnimator fadeAni = ObjectAnimator.ofFloat(
newBall,
"alpha",
1f,
0f);
// 设置动画持续时间
fadeAni.setDuration(250);
// 添加addUpdateListener监听器,当ValueAnimator属性值改变时会激发事件监听方法
fadeAni.addUpdateListener(this);
// 为fadeAni设置监听
fadeAni.addListener(new AnimatorListenerAdapter() {
// 动画结束
@Override
public void onAnimationEnd(Animator animation) {
// 动画结束时将该动画关联的ShapeHolder删除
balls.remove(((ObjectAnimator) (animation)).getTarget());
}
});
// 再次定义一个AnimatorSet动画集合,来组合动画
AnimatorSet aniSet = new AnimatorSet();
// 指定在fadeAni之前播放set动画集合
aniSet.play(set).before(fadeAni);
// 开始播放动画
aniSet.start();
逻辑很简单,动画fallAni
控制小球下落,动画squashshAni1
控制小球压扁时小球x坐标左移半个球宽度,动画squashshAni2
控制小球压扁时小球宽度加倍,动画stretchAni1
,控制小球拉伸动画时小球的y坐标下移半个球高度,动画stretchAni2
控制小球水平拉伸时控制小球的高度减半,动画bounceAni
定义小球弹起动画,接着用一个AnimatorSet
动画集合把这六个动画先组装起来,下落动画fallAni
之后是squashshAni1
、squashshAni2
、stretchAni1
、stretchAni2
这四个动画同时播放,这也是小球落地瞬间的完美诠释,再之后是小球弹起bounceAni
。最后还有一个fadeAni
渐隐动画控制小球弹回起始高度后消失,接着再用一个AnimatorSet
动画集合把前面的那个动画集合和第七个fadeAni
渐隐动画组装起来,整个桌面弹球动画就大功告成了!
需要注意的是,在addBall()
方法中,返回的是一个XShapeHolder
类型的对象,那么XShapeHolder
是什么呢?XShapeHolder
包装了ShapeDrawable
对象,并且为x,y,width,height,alpha
等属性提供了setter
、getter
方法,代码如下:
package com.wondertwo.propertyanime;
import android.graphics.Paint;
import android.graphics.RadialGradient;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.Shape;
/**
*
* Created by wondertwo on 2016/3/20.
*/
public class XShapeHolder {
private float x = 0, y = 0;
private ShapeDrawable shape;
private int color;
private RadialGradient gradient;
private float alpha = 1f;
private Paint paint;
public XShapeHolder(ShapeDrawable shape) {
this.shape = shape;
}
public float getWidth() {
return shape.getShape().getWidth();
}
public void setWidth(float width) {
Shape s = shape.getShape();
s.resize(width, s.getHeight());
}
public float getHeight() {
return shape.getShape().getHeight();
}
public void setHeight(float height) {
Shape s = shape.getShape();
s.resize(s.getWidth(), height);
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public ShapeDrawable getShape() {
return shape;
}
public void setShape(ShapeDrawable shape) {
this.shape = shape;
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public RadialGradient getGradient() {
return gradient;
}
public void setGradient(RadialGradient gradient) {
this.gradient = gradient;
}
public float getAlpha() {
return alpha;
}
public void setAlpha(float alpha) {
this.alpha = alpha;
}
public Paint getPaint() {
return paint;
}
public void setPaint(Paint paint) {
this.paint = paint;
}
}
博客的最后再放上郭霖大神的两篇博客,供大家参考!
- Android属性动画完全解析(上),初识属性动画的基本用法 http://blog.csdn.net/guolin_blog/article/details/43536355
- Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 http://blog.csdn.net/guolin_blog/article/details/43816093
- Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法 http://blog.csdn.net/guolin_blog/article/details/44171115
在最后附上浅析Android动画系列的三篇文章:
- 浅析Android动画(一),View动画高级实例探究 http://www.cnblogs.com/wondertwo/p/5295976.html
- 浅析Android动画(二),属性动画与高级实例探究 http://www.cnblogs.com/wondertwo/p/5312482.html
- 浅析Android动画(三),自定义Interpolator与TypeEvaluator http://www.cnblogs.com/wondertwo/p/5327586.html
如果觉得不错,请继续关注我哦!