我所学到的任何有价值的知识都是由自学中得来的。——达尔文
在Android 3.0引入了属性动画,并新增了属性的get/set函数,比如setAlpha(),setTranslateX(),setScaleX()等。很明显,ObjectAnimator不仅能设置这些属性,还能设置自定义属性,但在使用中,这些默认的属性动画确实很常见的。Android开发团队也意识到这一点,没有为View的动画操作提供一种更加便捷的用法确实有点不太人性化,于是在Android 3.1 中补充了ViewPropertyAnimator这个机制。
我们先来回顾之前的用法,比如,我们想要让一个TextView 从常规状态变成透明状态
private void objectAnimator() {
ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(ballIv, "alpha", 0);
objectAnimator.start();
}
看上去不太复杂,确实不太容易理解。
ViewPropertyAnimator提供了更加易懂,更加面向对象的API,如下所示:
ballIv.animate().alpha(0);
非常简单,除此之外,还可以非常容易将多个动画结合起来。比如,将控件引动到(50,250)
ballIv.animate().x(50).y(250);

-
animate 整个系统从调用View 的这个叫做 animate() 的新函数开始,这个函数会返回一个ViewPropertyAnimator对象,可以通过调用这个对象的函数来设置需要实现动作的属性。
-
自动开始:我们并没有显示调用过start()函数,在新的API中,启动动画是隐式的,在声明完成后,动画就开始了。这里有一个细节,就是这些动画实际上会在下一次界面刷新的时候启动,ViewPropertyAnimator正是通过这个机制来讲所有动画结合在一起的。如果你继续在声明动画,它就会继续将这些动画添加到 下一帧开始的动画列表中。而当你声明完毕并结束对UI线程的控制之后,时间队列机制开始起作用,动画也就开始了。
属性设置
这里小结一下ViewPropertyAnimator常用的用于设置属性的函数,如下所示
函数号 | 含义 |
---|---|
alpha(float value) | 设置透明度 |
scaleY(float value) | 设置Y轴方向的缩放大小 |
scaleX(float value) | 设置X轴方向的缩放大小 |
translationX(float value) | 设置X轴方向的移动值 |
translationY(float value) | 设置Y轴方向的移动值 |
rotation(float value) | 设置Z轴旋转度数 |
rotationX(float value) | 设置X轴旋转度数 |
rotationY(float value) | 设置Y轴旋转度数 |
x(float value) | 相对父容器左上角坐标X轴方向最终位置 |
y(float value) | 相对父容器左上角坐标Y轴方向最终位置 |
alphaBy(float value) | 设置透明度增量 |
rotationXBy(float value) | 设置绕X轴旋转增量 |
rotationYBy(float value) | 设置绕Y轴旋转增量 |
translationXBy(float value) | 设置X轴方向的移动值增量 |
translationYBy(float value) | 设置Y轴方向的移动值增量 |
scaleXBy(float value) | 设置X轴方向的缩放大小增量 |
scaleYBy(float value) | 设置Y轴方向的缩放大小增量 |
xBy(float value) | 相对父容器左上角坐标X轴方向位置增量 |
yBy(float value) | 相对父容器左上角坐标Y轴方向位置增量 |
setInterpolator(TimeInterpolator interpolator) | 设置插值器 |
setStartDelay(long startDelay) | 设置开始延迟 |
setDuration(long duration) | 设置动画时长 |
关于alpha ,rotaion,scale属性,在讲解ObjectAnimator中已经提到,这里就不再赘述。而x(flaot value) 和 y(flaot value) 对应的是在Android 3.0中新增的setX(float value) ,setY(float value)函数,用于将控件移动到指定位置,而这个位置是以控件的父窗口左上角坐标为原点的。与 translationX(float value)和 translationY(float value)不同的是,translation是偏移距离,是相对于当前控件的坐标。
上面的属性设置函数都对应着一个以By结尾的函数,比如scaleY(float value) 与scaleYBy(float value) ,它们的区别与View中的scrollTo()和scrollBy()函数区别一样。scrollTo表示滚动到某个顶点,scrollBy() 表示在当前位置在滚动一段距离,这里的scaleY(float value) 表示将视图放大到一定值,scaleYBy(float value) 表示在当前点在增加value。
下面举个例子说明,有两个TextView ,当点击按钮时,两个TextView分别调用scaleY()和scaleYBy()函数
先来看下布局文件
<?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="match_parent"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="100dp"
android:text="Start Anim"
android:textAllCaps="false" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="100dp"
android:background="#FF5722"
android:padding="10dp"
android:text="TextView 1"
android:textColor="@android:color/white" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginTop="100dp"
android:background="#FF5722"
android:padding="10dp"
android:text="TextView 2"
android:textColor="@android:color/white" />
</LinearLayout>
很简单,截个图

我们再来点击按钮后这个TextView
public class ViewPropertyAnimatorActivity extends AppCompatActivity {
private AppCompatTextView tv1;
private AppCompatTextView tv2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_property_animator);
AppCompatButton btnStart = findViewById(R.id.btnStart);
tv1 = findViewById(R.id.tv1);
tv2 = findViewById(R.id.tv2);
btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
tv1.animate().scaleY(2);
tv2.animate().scaleYBy(2);
}
});
}
}
点击按钮多次,tv1 在Y轴方向始终放大到当前控件的2倍,tv2在Y轴方向点击一次增量放大2倍。

从效果图中可以明显的看出这两个函数的区别。
设置监听器
虽然ViewPropertyAnimator并非派生自Animator,但它仍允许我们设置Animator.AnimatorListener监听器
tv1.animate().scaleX(2).scaleY(2).setListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
Log.d(TAG, "----onAnimationStart");
}
@Override
public void onAnimationEnd(Animator animation) {
Log.d(TAG, "----onAnimationEnd");
}
@Override
public void onAnimationCancel(Animator animation) {
Log.d(TAG, "----onAnimationCancel");
}
@Override
public void onAnimationRepeat(Animator animation) {
Log.d(TAG, "----onAnimationRepeat");
}
});
打印结果
06-15 16:35:05.377 9508-9508/com.as.propertyanimator D/ViewPropertyAnimatorActivity: ----onAnimationStart
06-15 16:35:05.701 9508-9508/com.as.propertyanimator D/ViewPropertyAnimatorActivity: ----onAnimationEnd
ViewPropertyAnimator 用起来非常方便,这是它的很大优势。
ViewGroup 内的组件添加动画
前面一直讲述的ValueAnimator,ObjectAnimator,AnimatorSet 都只能针对一个控件做动画。如果我们相对ViewGroup 内部控件做同意入场动画,出场动画等,比如ListView 中的每个Item 在入场时添加动画,在出场时添加动画,在数据变更时添加动画,那么使用ValueAnimator,ObjectAnimator,AnimatorSet 时无法实现的。
为ViewGroup 内组件添加动画,Android 共提供了4中方法
-
1 layoutAnimation标签与LayoutAnimationController
layoutAnimation标签在API 1时就已经引入了,它是专门针对ListView 添加入场动画所使用的。LayoutAnimationController 是它的代码实现,它可以实现ListView创建时对其中的每个Item 添加入场动画,而且动画可以自定义。然而,ListView 创建完成后,如果在添加数据,则新添加的数据是不会有入场动画的。 -
2 gridLaygoutAnimation 标签 与 GridLaygoutAnimationController
gridLaygoutAnimation标签也是在API 1 时引入的,它是专门针对GridView 添加入场动画的,GridLaygoutAnimationController是它的代码实现。它可以实现GridView创建时对其中的每个Item 添加入场动画,而且动画可以自定义。与layoutAnimation标签相同的事,动画只会在GridView 初次创建时出现,GridView 创建完成后,如果在添加数据,则新添加的数据是不会有入场动画的。 -
3 android:animateLayoutChanges属性
在API 11 之后,Android 为了支持ViewGroup 类控件,在添加或者移除其中控件时自动添加动画,提供了一个非常简单的属性android:animateLayoutChanges="true/false",所有派生自ViewGroup 类的控件都具有此属性。而且只要在XML中添加这个属性,就能实现在添加/删除其中的控件时带有默认动画,遗憾的是,动画不能自定义。 -
4 LayoutTransition
LayoutTransition 在API 11之后引入,可以实现ViewGroup 动态添加或删除其中的控件时指定动画,动画可以自定义。对比以上4中方法,LayoutTransition 是最强大的,这也是本节的重点;而有关layoutAnimation 和 gridLaygoutAnimation 标签,由于自身存在缺陷,新增数据时无法添加动画,这里就不在讲解。
下面看个例子
1 布局代码
<?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="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/addBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加控件" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/delBtn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除控件" />
</LinearLayout>
<LinearLayout
android:id="@+id/llContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:animateLayoutChanges="true"
android:orientation="vertical" />
</LinearLayout>

布局代码中有两个按钮,而位于代码底部的是一个LinearLayout标签,作为动态添加btn的容器,需要注意的时,这里给它添加了android:animateLayoutChanges="true"属性,也就是说,它内部控件在添加或者删除时会带有默认动画。
package com.as.propertyanimator;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import java.util.Locale;
public class ViewGroupAnimatorActivity extends AppCompatActivity {
private int btnCount = 0;
private LinearLayout llContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_group_animator);
AppCompatButton addBtn = findViewById(R.id.addBtn);
AppCompatButton delBtn = findViewById(R.id.delBtn);
llContainer = findViewById(R.id.llContainer);
addBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addButtonView();
}
});
delBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
removeButtonView();
}
});
}
private void addButtonView() {
btnCount++;
Button button = new Button(this);
button.setText("button " + btnCount);
LinearLayout.LayoutParams layoutParams =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(layoutParams);
llContainer.addView(button, 0);
}
private void removeButtonView() {
btnCount--;
if (btnCount < 0) {
return;
}
llContainer.removeViewAt(0);
}
}

单击添加按钮,我们动态创建一个按钮实例,并将其添加到LinearLayout容器的第一个元素。在单机删除控件时,总是将LinearLayout 容器中的第一个元素删除。
通过对实例的讲解,可以发现,只需在ViewGroup 的xml中添加一行代码,android:animateLayoutChanges="true"即可实现内部控件在添加/删除时都带有默认动画,但是通过android:animateLayoutChanges="true"属性添加的动画效果不可以自定义。
LayoutTransition
要使用LayoutTransition是非常容易的,只需要三步
private void addButtonView() {
//第一步
LayoutTransition layoutTransition = new LayoutTransition();
//第二步
ObjectAnimator rotation = ObjectAnimator.ofFloat(null, "rotation", 0, 90, 0);
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, rotation);
//第三步
llContainer.setLayoutTransition(layoutTransition);
btnCount++;
Button button = new Button(this);
button.setText("button " + btnCount);
LinearLayout.LayoutParams layoutParams =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(layoutParams);
llContainer.addView(button, 0);
}
在第二步中, layoutTransition.setAnimator设置动画函数声明如下
public void setAnimator(int transitionType, Animator animator)
其中,int transitionType表示当前动画的应用范围
- APPEARING : 元素在容器中出现时所定义的动画
- DISAPPEARING : 元素在容器中消失时所定义的动画
- CHANGE_APPEARING :由于容器中要显示一个新的元素,其它变化的元素所应用的动画。
- CHANGE_DISAPPEARING :容器中当消失一个新的元素,其它变化的元素所应用的动画。
Animation animation :表示当前所选控件所使用的动画。
LayoutTransition.APPEARING 与 LayoutTransition.DISAPPEARING
LayoutTransition.APPEARING 用于指定当前控件被添加时的动画,LayoutTransition.DISAPPEARING 则用于控件被删除时的动画。下面举例说明其用法与效果!
我们来看看代码
package com.as.propertyanimator;
import android.animation.LayoutTransition;
import android.animation.ObjectAnimator;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.AppCompatButton;
import java.util.Locale;
public class ViewGroupAnimatorActivity extends AppCompatActivity {
private int btnCount = 0;
private LinearLayout llContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_view_group_animator);
AppCompatButton addBtn = findViewById(R.id.addBtn);
AppCompatButton delBtn = findViewById(R.id.delBtn);
llContainer = findViewById(R.id.llContainer);
//第一步
LayoutTransition layoutTransition = new LayoutTransition();
//入场动画:视图出现时显示的动画
ObjectAnimator animInRotationY = ObjectAnimator.ofFloat(null, "RotationY", 0, 180, 0);
layoutTransition.setAnimator(LayoutTransition.APPEARING, animInRotationY);
//出场动画:视图消失时的动画
ObjectAnimator animOutRotation = ObjectAnimator.ofFloat(null, "Rotation", 0, 90, 0);
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOutRotation);
//第三步
llContainer.setLayoutTransition(layoutTransition);
addBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addButtonView();
}
});
delBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
removeButtonView();
}
});
}
private void addButtonView() {
btnCount++;
Button button = new Button(this);
button.setText("button " + btnCount);
LinearLayout.LayoutParams layoutParams =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
button.setLayoutParams(layoutParams);
llContainer.addView(button, 0);
}
private void removeButtonView() {
btnCount--;
if (btnCount < 0) {
return;
}
llContainer.removeViewAt(0);
}
}

其中addButtonView()和removeButtonView()函数代码很简单,为ViewGroup添加控件,只需将创建好的LayoutTransition变量设置为ViewGroup容器即可。这里设置LayoutTransition的过程就是本小节提到的三个步骤。
第一步
LayoutTransition layoutTransition = new LayoutTransition();
第二步,创建动画并进行设置
//入场动画:视图出现时显示的动画
ObjectAnimator animInRotationY = ObjectAnimator.ofFloat(null, "RotationY", 0, 180, 0);
layoutTransition.setAnimator(LayoutTransition.APPEARING, animInRotationY);
//出场动画:视图消失时的动画
ObjectAnimator animOutRotation = ObjectAnimator.ofFloat(null, "Rotation", 0, 90, 0);
layoutTransition.setAnimator(LayoutTransition.DISAPPEARING, animOutRotation);
上述代码的含义是:当一个控件被插入时,这个被插入的控件,所使用的动画绕Y轴旋转180度再回来,当第一个控件被移除,这个被移除的控件所使用的动画,既绕Z轴旋转90度再回来。
LayoutTransition.CHANGE_APPEARING
用于指定在容器中添加控件时,其它已有控件需要移动时的动画。LayoutTransition.CHANGE_DISAPPEARING 用于指定在容器中删除控件时,其它已有控件需要移动时的动画。
核心代码如下
PropertyValuesHolder pvLeft = PropertyValuesHolder.ofInt("left", 0, 0);
PropertyValuesHolder pvTop = PropertyValuesHolder.ofInt("top", 0, 0);
PropertyValuesHolder pvScaleX = PropertyValuesHolder.ofFloat("scaleX", 1,0, 1);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(llContainer, pvLeft, pvTop, pvScaleX);
layoutTransition.setAnimator(LayoutTransition.CHANGE_APPEARING, objectAnimator);
//第三步
llContainer.setLayoutTransition(layoutTransition);
在这个示例中,只添加了LayoutTransition.CHANGE_APPEARING 效果,这里有几个注意项,LayoutTransition.CHANGE_APPEARING 和LayoutTransition.CHANGE_DISAPPEARING 必须使用PropertyValuesHolder所构造的动画才会有效。也就是,ObjectAnimator构造的动画,在这里不会有效果。
在构造PropertyValuesHolder动画时,left,top属性的变化时必须写的。如果不需要变动,则直接写为:
PropertyValuesHolder pvLeft = PropertyValuesHolder.ofInt("left", 0, 0);
PropertyValuesHolder pvTop = PropertyValuesHolder.ofInt("top", 0, 0);
LayoutTransition.CHANGE_DISAPPEARING
核心代码如下:
PropertyValuesHolder outLeft = PropertyValuesHolder.ofInt("left", 0, 0);
PropertyValuesHolder outTop = PropertyValuesHolder.ofInt("top", 0, 0);
Keyframe keyframe0 = Keyframe.ofFloat(0, 0);
Keyframe keyframe1 = Keyframe.ofFloat(0.1f, -20);
Keyframe keyframe2 = Keyframe.ofFloat(0.2f, 20);
Keyframe keyframe3 = Keyframe.ofFloat(0.3f, -20);
Keyframe keyframe4 = Keyframe.ofFloat(0.4f, 20);
Keyframe keyframe5 = Keyframe.ofFloat(0.5f, -20);
Keyframe keyframe6 = Keyframe.ofFloat(0.6f, 20);
Keyframe keyframe7 = Keyframe.ofFloat(0.7f, -20);
Keyframe keyframe8 = Keyframe.ofFloat(0.8f, 20);
Keyframe keyframe9 = Keyframe.ofFloat(0.9f, -20);
Keyframe keyframe10 = Keyframe.ofFloat(1f, 0);
PropertyValuesHolder rotationHolder = PropertyValuesHolder.ofKeyframe("rotation",
keyframe0, keyframe1, keyframe2,
keyframe3, keyframe4, keyframe5, keyframe6,
keyframe7, keyframe8, keyframe9, keyframe10);
ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(llContainer, outLeft, outTop, rotationHolder);
layoutTransition.setAnimator(LayoutTransition.CHANGE_DISAPPEARING, objectAnimator);
//第三步
llContainer.setLayoutTransition(layoutTransition);

第一步,有left,top 属性是必须的,但我们在做响铃时不需要left,top属性变化,所以将它们设置为无效值。
第二步 用Keyframe 构造PropertyValuesHolder 动画。PropertyValuesHolder 动画由4个构造方法:ofInt(),ofFloat(),ofObject(),ofKeyframe(),这里使用的时ofKeyframe()。有关PropertyValuesHolder动画的方法在上一篇博客已经讲过了,这里就不再讲解了。
第三步,设置LayoutTransition.CHANGE_DISAPPEARING动画。
LayoutTransition 还有其它函数,我么一起看一下
// 所有动画完成所需要的时长
public void setDuration(long duration)
// 针对单个Type设置动画时长
public void setDuration(int transitionType, long duration)
// 针对单个Type设置插值器
// transitionType 有 APPEARING ,DISAPPEARING , CHANGE_APPEARING,CHANGE_DISAPPEARING,
public void setInterpolator(int transitionType, TimeInterpolator interpolator)
// 针对单个Type设置动画延迟时间
public void setStartDelay(int transitionType, long delay)
// 针对单个Type 设置每个子item 动画时间间隔
public void setStagger(int transitionType, long duration)
这些函数有兴趣动手!
网友评论