Android动画二:使用属性动画
前言
Android系统支持原生动画,这为应用开发者开发绚丽的界面提供了极大的方便,有时候动画是很必要的,当你想做一个滑动的特效的时候,如果苦思冥想都搞不定,那么你可以考虑下动画,说不定动画轻易就搞定了。下面再简单回顾下Android中的动画,本文后面会介绍如何用属性动画做个稍微复杂点的动画。
Android动画系列:
动画分类
分类 | 描述 |
---|---|
View动画 | 也叫渐变动画,针对View的动画,主要支持平移、旋转、缩放、透明度 |
Drawable动画 | 也叫帧动画,主要是设置View的背景,可以以动画的形式为View设置多张背景 |
对象属性动画 | 可以对对象的属性进行动画而不仅仅是View,动画默认时间间隔300ms,默认帧率10ms/帧。其可以达到的效果是:在一个时间间隔内完成对象从一个属性值到另一个属性值的改变,因此,属性动画几乎是无所不能的,只要对象有这个属性,它都能实现动画效果。 |
属性动画
比较常用的几个动画类是:ValueAnimator、ObjectAnimator和AnimatorSet,其中ObjectAnimator继承自ValueAnimator,AnimatorSet是动画集,可以定义一组动画。使用起来也是及其简单的,下面举个小栗子。
栗子:实现的稍微复杂点的动画,先上效果图
布局xml如下:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<Button
android:id="@+id/menu"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="0" />
<Button
android:id="@+id/item1"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="1"
android:visibility="gone" />
<Button
android:id="@+id/item2"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="2"
android:visibility="gone" />
<Button
android:id="@+id/item3"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="3"
android:visibility="gone" />
<Button
android:id="@+id/item4"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="4"
android:visibility="gone" />
<Button
android:id="@+id/item5"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="5"
android:visibility="gone" />
<Button
android:id="@+id/item6"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="6"
android:visibility="gone" />
<Button
android:id="@+id/item7"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="7"
android:visibility="gone" />
<Button
android:id="@+id/item8"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="8"
android:visibility="gone" />
<Button
android:id="@+id/item9"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="9"
android:visibility="gone" />
<Button
android:id="@+id/item10"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="10"
android:visibility="gone" />
<Button
android:id="@+id/item11"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="11"
android:visibility="gone" />
<Button
android:id="@+id/item12"
android:layout_gravity="center"
android:layout_width="50dp"
android:layout_height="50dp"
android:text="12"
android:visibility="gone" />
</FrameLayout>
代码如下:
public class AnimationActivity extends Activity implements View.OnClickListener {
private static final String TAG = "AnimationActivity";
private Button mMenuButton;
private Button mItemButton1;
private Button mItemButton2;
private Button mItemButton3;
private Button mItemButton4;
private Button mItemButton5;
private Button mItemButton6;
private Button mItemButton7;
private Button mItemButton8;
private Button mItemButton9;
private Button mItemButton10;
private Button mItemButton11;
private Button mItemButton12;
private boolean mIsMenuOpen = false;
private int menuNum = 12;
private int range = 360; // 显示角度范围: 0 - 360
private float centerX = 0;
private float centerY = 0;
private float radius = 400;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.animation_layout);
initView();
}
private void initView() {
mMenuButton = (Button) findViewById(R.id.menu);
mMenuButton.setOnClickListener(this);
mItemButton1 = (Button) findViewById(R.id.item1);
mItemButton1.setOnClickListener(this);
mItemButton2 = (Button) findViewById(R.id.item2);
mItemButton2.setOnClickListener(this);
mItemButton3 = (Button) findViewById(R.id.item3);
mItemButton3.setOnClickListener(this);
mItemButton4 = (Button) findViewById(R.id.item4);
mItemButton4.setOnClickListener(this);
mItemButton5 = (Button) findViewById(R.id.item5);
mItemButton5.setOnClickListener(this);
mItemButton6 = (Button) findViewById(R.id.item6);
mItemButton6.setOnClickListener(this);
mItemButton7 = (Button) findViewById(R.id.item7);
mItemButton7.setOnClickListener(this);
mItemButton8 = (Button) findViewById(R.id.item8);
mItemButton8.setOnClickListener(this);
mItemButton9 = (Button) findViewById(R.id.item9);
mItemButton9.setOnClickListener(this);
mItemButton10 = (Button) findViewById(R.id.item10);
mItemButton10.setOnClickListener(this);
mItemButton11 = (Button) findViewById(R.id.item11);
mItemButton11.setOnClickListener(this);
mItemButton12 = (Button) findViewById(R.id.item12);
mItemButton12.setOnClickListener(this);
}
@Override
public void onClick(View v) {
if (v == mMenuButton) {
if (!mIsMenuOpen) {
mIsMenuOpen = true;
doAnimateOpen(mItemButton1, 0, menuNum);
doAnimateOpen(mItemButton2, 1, menuNum);
doAnimateOpen(mItemButton3, 2, menuNum);
doAnimateOpen(mItemButton4, 3, menuNum);
doAnimateOpen(mItemButton5, 4, menuNum);
doAnimateOpen(mItemButton6, 5, menuNum);
doAnimateOpen(mItemButton7, 6, menuNum);
doAnimateOpen(mItemButton8, 7, menuNum);
doAnimateOpen(mItemButton9, 8, menuNum);
doAnimateOpen(mItemButton10, 9, menuNum);
doAnimateOpen(mItemButton11, 10, menuNum);
doAnimateOpen(mItemButton12, 11, menuNum);
} else {
mIsMenuOpen = false;
doAnimateClose(mItemButton1, 0, menuNum);
doAnimateClose(mItemButton2, 1, menuNum);
doAnimateClose(mItemButton3, 2, menuNum);
doAnimateClose(mItemButton4, 3, menuNum);
doAnimateClose(mItemButton5, 4, menuNum);
doAnimateClose(mItemButton6, 5, menuNum);
doAnimateClose(mItemButton7, 6, menuNum);
doAnimateClose(mItemButton8, 7, menuNum);
doAnimateClose(mItemButton9, 8, menuNum);
doAnimateClose(mItemButton10, 9, menuNum);
doAnimateClose(mItemButton11, 10, menuNum);
doAnimateClose(mItemButton12, 11, menuNum);
}
} else {
Toast.makeText(this, "你点击了" + v.toString(), Toast.LENGTH_SHORT).show();
}
}
/**
* 打开菜单的动画
* @param view 执行动画的view
* @param index view在动画序列中的顺序
* @param total 动画序列的个数
*/
private void doAnimateOpen(View view, int index, int total) {
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
}
float angle = index * (range/total);
double degree = Math.PI * (angle/180);
int translationX = (int) (radius * Math.cos(degree));
int translationY = (int) (radius * Math.sin(degree));
Log.d(TAG, String.format("degree=%f, translationX=%d, translationY=%d",
degree, translationX, translationY));
AnimatorSet set = new AnimatorSet();
//包含平移、缩放、透明度、旋转和倒转显示动画
set.playTogether(
ObjectAnimator.ofFloat(view, "translationX", 0, translationX),
ObjectAnimator.ofFloat(view, "translationY", 0, translationY),
ObjectAnimator.ofFloat(view, "scaleX", 0f, 1f),
ObjectAnimator.ofFloat(view, "scaleY", 0f, 1f),
ObjectAnimator.ofFloat(view, "alpha", 0f, 1),
ObjectAnimator.ofFloat(view, "rotation", 0, 360)
//need sdk 21
// ObjectAnimator.ofArgb((Button)view, "textColor", 0x000, 0xFFFF00),
// ObjectAnimator.ofArgb(view, "backgroundColor", 0x000, 0xFF0000)
);
// add TimeInterpolator
// set.setInterpolator(new LinearInterpolator());
// set.setInterpolator(new AccelerateDecelerateInterpolator());
// set.setInterpolator(new DecelerateInterpolator());
//动画周期为500ms
set.setDuration(1 * 2000).start();
}
/**
* 关闭菜单的动画
* @param view 执行动画的view
* @param index view在动画序列中的顺序
* @param total 动画序列的个数
*/
private void doAnimateClose(final View view, int index, int total) {
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
}
float angle = index * (range/total);
double degree = Math.PI * (angle/180);// index / ((total - 1) * (180/angle));
int translationX = (int) (radius * Math.cos(degree));
int translationY = (int) (radius * Math.sin(degree));
Log.d(TAG, String.format("degree=%f, translationX=%d, translationY=%d",
degree, translationX, translationY));
AnimatorSet set = new AnimatorSet();
//包含平移、缩放、透明度、旋转和倒转显示动画
set.playTogether(
ObjectAnimator.ofFloat(view, "translationX", translationX, 0),
ObjectAnimator.ofFloat(view, "translationY", translationY, 0),
ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f),
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f),
ObjectAnimator.ofFloat(view, "rotation", 360, 0)
//need sdk 21
// ObjectAnimator.ofArgb((Button)view, "textColor", 0xFFFF00, 0x000),
// ObjectAnimator.ofArgb(view, "backgroundColor", 0xFF0000, 0x000)
);
//为动画加上事件监听,当动画结束的时候,我们把当前view隐藏
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
}
@Override
public void onAnimationRepeat(Animator animator) {
}
@Override
public void onAnimationEnd(Animator animator) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationCancel(Animator animator) {
}
});
// add TimeInterpolator
// set.setInterpolator(new LinearInterpolator());
// set.setInterpolator(new AccelerateDecelerateInterpolator());
// set.setInterpolator(new DecelerateInterpolator());
set.setDuration(1 * 2000).start();
}
}
运行后,就会出现上图的效果,你可以用动画属性做出更为复杂的动画效果。说到属性动画,就不得不提到插值器(TimeInterpolator)和估值算法(TypeEvaluator),下面介绍。
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)做动画,你必须要自定义类型估值算法。
之前做的动画加上TimeInterpolator的效果如下:
AccelerateDecelerateInterpolator效果 DecelerateInterpolator效果 LinearInterpolator效果自定义TimeInterpolator
在实际开发中,系统预置的时间差值器肯定是不能满足我们的需求的,这就需要自定义了。这里自定义一个简单的时间插值器,使其具有这样的曲线特征:
基于中心点正负振幅逐渐趋于0的时间插值器,数学表达式:
pow(2, -10 * x) * sin((x - factor / 4) * (2 * PI) / factor) + 1
自定义曲线的特征:
image
上面已经说过,自定义非常简单,只要实现接口就OK,实现如下:
public class MyInterpolator implements Interpolator {
private static final float DEFAULT_FACTOR = 0.3f;
// 因子数值越小振动频率越高
private float mFactor;
public MyInterpolator() {
this(DEFAULT_FACTOR);
}
public MyInterpolator(float factor) {
mFactor = factor;
}
@Override
public float getInterpolation(float input) {
// pow(2, -10 * input) * sin((input - factor / 4) * (2 * PI) / factor) + 1
return (float) (Math.pow(2, -10 * input) * Math.sin((input - mFactor / 4.0d) * (2.0d * Math.PI) / mFactor) + 1);
}
}
效果:
OK,属性动画就学习到这里,下一节进行属性动画的深入分析
Demo源码:https://github.com/yufenfenGH/Android-Animation.git
参考: https://blog.csdn.net/singwhatiwanna/article/details/17639987