在 Android 3.0 之前已有的动画框架 Animation 存在一些局限性—— 动画改变的只是显示,并不能响应事件(相比属性动画,视图动画的一个非常大的缺陷就是不具备交互性,当某个元素发生视图动画后,其响应事件的位置依然在动画前的地方,所以视图动画只能做普通的动画效果,避免交互的发生。它的优点是效率比较高使用方便)。
本文要实现的效果如下:
在这之前先简单的介绍一下属性动画。
1. 属性动画涉及的 API :
(1)Animator :它提供了创建属性动画的基类,基本上不会直接使用该类。通常该类只用于被继承并重写它的相关方法。
(2) ValueAnimator :属性动画主要的时间引擎,它负责计算各个帧的属性值。属性动画主要有两方面组成:
——> 1)计算各帧的相关属性值;
——> 2)为指定对象设置这些计算后的值;
ValueAnimator 只负责第一方面内容,因此程序员必须根据 ValueAnimator 计算并监听值更新来更新对象的相关属性值。使用 ValueAnimator 需要注册 AnimatorUpdateListener 监听器。
(3)ObjectAnimator :它是 ValueAnimator 的子类,允许程序员对指定对象的属性执行动画。在实际应用中,它比 ValueAnimator 使用起来更加简单。使用 ObjectAnimator 就不需要注册 AnimatorUpdateListener 监听器了。
(4) AnimatorSet :它是 Animator 的子类,组合多个 Animator ,并指定多个 Animator 是按次序播放,还是同时播放。
在 Animator 框架中使用最多的就是 AnimatorSet 和 ObjectAnimator ,使用 ObjectAnimator 进行更精细化的控制,只控制一个对象的一个属性值,而使用多个 ObjectAnimator 组合到 AnimatorSet 形成一个动画。属性动画通过调用属性的 get 、set 方法来真实地控制一个 View 的属性值,因此强大的动画框架,基本可以实现所有的动画效果。
2. 使用属性动画的步骤如下:
(1)创建 ObjectAnimator 或 ValueAnimator 对象——即可从 XML 资源文件加载该动画资源,也可以直接调用 ObjectAnimator 或 ValueAnimator 的静态工厂方法来创建动画。
——> 静态方法 ofInt() 、ofFloat() 或者 ofObject();
——> 注意使用 ObjectAnimator 的静态方法创建 ObjectAnimator 对象时,需要指定具体的对象,以及对象的属性名。如: ObjectAnimator animator0 = ObjectAnimator.ofFloat(foo, "alpha", 0.5F, 1F);
(2)根据需要为 Animator 对象设置属性。
(3)如果需要监听 Animator 的动画开始事件、动画结束事件、动画重复事件、动画值改变事件,并根据事件提供相应的处理代码,则应该为 Animator 对象设置事件监听器。
(4)如果有多个动画需要按次序或者同时播放,则应使用 AnimatorSet 组合这些动画。
(5)调用 Animator 对象的 start() 方法启动动画。
ObjectAnimator
ObjectAnimator 参数包括一个对象和对象的属性名,但这个属性必须有 get() 和 set() 函数,内部会通过 Java 反射机制来调用 set 函数修改对象属性。
前文的使用 ObjectAnimator animator0 = ObjectAnimator.ofFloat(foo, "alpha", 0.5F, 1F); 创建一个 ObjectAnimator 对象。其中第一个参数时需要操纵的 View;第二个参数则是操纵的属性;而最后一个参数是一个可变数组参数,需要传进去该属性变化的一个取值过程。
第二个参数的属性值如下:
(1)translationX 和 translationY:这两个属性作为一种增量来控制着 View 对象从它布局容器的左上角坐标偏移的位置。
(2)rotation 、rotationX 和 rotationY:这三个属性控制着 View 对象围绕支点进行 2D 和 3D 旋转。
(3)scaleX 和 scaleY:这两个属性控制着 View 对象围绕它的支点进行 2D 缩放。
(4)pivotX 和 pivotY:这两个属性控制着 View 对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认情况下,该支点的位置就是 View 对象的中心点。
(5)x 和 y:它描述了 View 对象在它的容器中的最终位置,它是最初的左上角坐标和 translationX 、 translationY 值得累计和。
(6)alpha:它表示 View 对象的 alpha 透明度。默认值为 1 (不透明),0 代表完全透明(不可见)。
属性的 get() 和 set() 方法也可以从如下两个方案来解决:
——> 通过自定义一个属性或者包装类,来间接地给这个属性增加 get() 、set() 方法。
——> 通过 ValueAnimator 来实现。
AnimatorSet
对于一个属性同时作用多个属性动画效果,可以使用 PropertyValuesHolder 实现这样的效果,但是通过 AnimatorSet 不仅能实现这样的效果,同行也能实现更为精确的顺序控制。代码如下:
ObjectAnimator animator1 = ObjectAnimator.ofFloat(foo, "translationY", -200F, 0);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(foo, "translationX", -200F, 0);
AnimatorSet set = new AnimatorSet();
set.setDuration(500);
set.setInterpolator(new BounceInterpolator());
set.playTogether(animator1, animator2);
set.start();
在属性动画中, AnimatorSet 正是通过 playTogether()、playSequentially()、animSet.play().with()、before()、after()这些方法来控制多个动画的协同工作方式。
第一个效果代码:
forclick_layout.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView_b"
android:src="@drawable/b"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView_c"
android:src="@drawable/c"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView_d"
android:src="@drawable/d"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView_e"
android:src="@drawable/e"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/imageView_a"
android:src="@drawable/a"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
PropertyTest.java :
package com.imooc.anim;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.BounceInterpolator;
import android.widget.ImageView;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
public class PropertyTest extends Activity implements View.OnClickListener {
private int[] mRes = {R.id.imageView_a, R.id.imageView_b, R.id.imageView_c,
R.id.imageView_d, R.id.imageView_e};
private List<ImageView> mImageViews = new ArrayList<>();
private boolean mFlag = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.forclick_layout);
int sum = mRes.length;
for (int i = 0; i < sum; i++) {
ImageView imageView = (ImageView) findViewById(mRes[i]);
imageView.setOnClickListener(this);
mImageViews.add(imageView);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.imageView_a:
if (mFlag) {
startAnim();
} else {
closeAnim();
}
break;
case R.id.imageView_b:
Toast.makeText(PropertyTest.this, "相机", Toast.LENGTH_SHORT).show();
break;
case R.id.imageView_c:
Toast.makeText(PropertyTest.this, "音乐", Toast.LENGTH_SHORT).show();
break;
case R.id.imageView_d:
Toast.makeText(PropertyTest.this, "定位", Toast.LENGTH_SHORT).show();
break;
case R.id.imageView_e:
Toast.makeText(PropertyTest.this, "夜晚", Toast.LENGTH_SHORT).show();
break;
}
}
private void closeAnim() {
ObjectAnimator animator0 = ObjectAnimator.ofFloat(mImageViews.get(0),
"alpha", 0.5F, 1F);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(mImageViews.get(1),
"translationY", 200F, 0);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(mImageViews.get(2),
"translationX", 200F, 0);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(mImageViews.get(3),
"translationY", -200F, 0);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(mImageViews.get(4),
"translationX", -200F, 0);
AnimatorSet set = new AnimatorSet();
set.setDuration(500);
set.setInterpolator(new BounceInterpolator());
set.playTogether(animator0, animator1, animator2, animator3, animator4);
set.start();
mFlag = true;
}
private void startAnim() {
ObjectAnimator animator0 = ObjectAnimator.ofFloat(
mImageViews.get(0),
"alpha",
1F,
0.5F);
ObjectAnimator animator1 = ObjectAnimator.ofFloat(
mImageViews.get(1),
"translationY",
200F);
ObjectAnimator animator2 = ObjectAnimator.ofFloat(
mImageViews.get(2),
"translationX",
200F);
ObjectAnimator animator3 = ObjectAnimator.ofFloat(
mImageViews.get(3),
"translationY",
-200F);
ObjectAnimator animator4 = ObjectAnimator.ofFloat(
mImageViews.get(4),
"translationX",
-200F);
AnimatorSet set = new AnimatorSet();
set.setDuration(500);
set.setInterpolator(new BounceInterpolator());
set.playTogether(
animator0,
animator1,
animator2,
animator3,
animator4);
set.start();
mFlag = false;
}
}
第二个效果的实现:
drop.xml :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:onClick="llClick"
android:background="@android:color/holo_blue_bright"
android:orientation="horizontal">
<ImageView
android:id="@+id/app_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_launcher" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:gravity="left"
android:text="Click Me"
android:textSize="30sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/hidden_view"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@android:color/holo_orange_light"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:src="@drawable/ic_launcher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<TextView
android:id="@+id/tv_hidden"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:textSize="20sp"
android:text="我是隐藏着的!" />
</LinearLayout>
</LinearLayout>
点击上面的 LinearLayout 时,需要获取到隐藏的 LinearLayout 最终需要达到的一个高度,即是目标值,通过将布局文件文件中的 dp 值转化为像素值即可。
// 获取像素密度
mDensity = getResources().getDisplayMetrics().density;
// 获取布局的高度
mHiddenViewMeasuredHeight = (int) (mDensity * 40 + 0.5);
40 就是在 XML 文件中定义的布局高度。
需要使用 ValueAnimator 来创建一个从 0 到目标值的数值发生器,并由此来改变 View 的布局属性。
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams =
view.getLayoutParams();
layoutParams.height = value;
view.setLayoutParams(layoutParams);
}
});
DropTest.java :
package com.imooc.anim;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
public class DropTest extends Activity {
private LinearLayout mHiddenView;
private float mDensity;
private int mHiddenViewMeasuredHeight;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.drop);
mHiddenView = (LinearLayout) findViewById(R.id.hidden_view);
// 获取像素密度
mDensity = getResources().getDisplayMetrics().density;
// 获取布局的高度
mHiddenViewMeasuredHeight = (int) (mDensity * 40 + 0.5);
}
public void llClick(View view) {
if (mHiddenView.getVisibility() == View.GONE) {
// 打开动画
animateOpen(mHiddenView);
} else {
// 关闭动画
animateClose(mHiddenView);
}
}
private void animateOpen(final View view) {
view.setVisibility(View.VISIBLE);
ValueAnimator animator = createDropAnimator(
view,
0,
mHiddenViewMeasuredHeight);
animator.start();
}
private void animateClose(final View view) {
int origHeight = view.getHeight();
ValueAnimator animator = createDropAnimator(view, origHeight, 0);
animator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
}
});
animator.start();
}
private ValueAnimator createDropAnimator(
final View view, int start, int end) {
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
int value = (Integer) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams layoutParams =
view.getLayoutParams();
layoutParams.height = value;
view.setLayoutParams(layoutParams);
}
});
return animator;
}
}
其中:AnimatorUpdateListener 中监听的是数值变换,从而完成动画的变换;一个完整的动画具有 Start、Repeat、End 、Cancel 四个过程,通过 AnimatorListener 接口可以很方便地监听到这四个事件。AnimatorListener 接口的源码如下:
public interface AnimatorListener {
void onAnimationStart(Animator var1);
void onAnimationEnd(Animator var1);
void onAnimationCancel(Animator var1);
void onAnimationRepeat(Animator var1);
}
但是,大部分的时候,我们都只关心 onAnimationEnd 事件,所以 Android 也提供了一个 AnimatorListenerAdapter 来让我们选择必要的事件进行监听,如文中的代码:
animator.addListener(new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
}
});