学习总结--属性动画

关于属性动画的学习,郭大婶已经有过很经典的分享,这里只是对自己这些天学习属性动画的总结,传送门:郭大婶属性动画分享

这篇总结分为以下几个方面:

  1. 基本使用
  2. 自定义属性
  3. 实例

基本使用

属性动画,用的较多的就是 ObjectAnimator、ValueAnimator、AnimatorSet 这三个,这里就不浪费口水了,看这:Android属性动画完全解析(上),初识属性动画的基本用法

这里补充一些常用的可以直接使用的属性动画的属性值:

来自 Android 群英传:

  • translationX 和 translationY:这两个属性作为一种增量来控制着 View 对象从它的布局器的左上角坐标开始的位置。
  • rotation、rotationX 和 rotationY:这三个属性控制 View 对象围绕支点进行 2D 和 3D 旋转。
  • scaleX 和 scaleY:这两个属性控制着 View 对象围绕它的支点进行 2D 缩放。
  • pivotX 和 pivotY:这两个属性控制着 View 对象的支点位置,围绕这个支点进行旋转和缩放变换处理。默认情况下,该支点的位置就是 View 对象的中心点。
  • x 和 y:这是两个简单实用的属性,它描述了 View 对象在它的容器中的最终位置,它是最初的左上角坐标与translationX、translationY 值的累计和。
  • alpha:它表示 View 对象的 alpha 透明度。默认值是 1(不透明), 0 代表完全透明(不可见)。

自定义属性

系统提供的动画属性值足以完成很多功能了,但是如果想要些更炫的效果,系统的那些属性值就不够用了,只能自己来实现咯。这里提供两种方案:

1、通过自定义一个属性类或者包装类

如果想要通过动画来实现某个 View 宽度的变化该怎么做呢?系统提供的属性动画的属性中并没有 width 这个属性,那么只能自己来定义一个 width 属性咯。首先新建一个类,在这个类中提供一个构造方法,并且提供想要定义的属性的 get、set 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class WrapperView {

private View mTarget;

public WrapperView(View mTarget) {
this.mTarget = mTarget;
}

/**
* 获取宽度
* @return
*/
public int getWidth() {
return mTarget.getLayoutParams().width;
}

/**
* 设置宽度
* @param width
*/
public void setWidth(int width) {
mTarget.getLayoutParams().width = width;
// 重新刷新
mTarget.requestLayout();
}
}

这个包装类还是很简单的,目的是改变某个 View 的 width。接下来就可以拿来用了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ObjectActivity extends AppCompatActivity {

private Button button;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_object);

button = (Button) findViewById(R.id.button);
// 自定义属性的属性动画
WrapperView view = new WrapperView(button);
ObjectAnimator.ofInt(view, "width", 500).setDuration(5000).start();
}
}

这个都没什么难度的,布局文件就只是添加了个 Button。附上布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_object"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="cn.ljuns.androidgrowing.practice.ObjectActivity">

<Button
android:text="Button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true"
android:id="@+id/button" />
</RelativeLayout>

最终的效果是这样的:



2、通过 ValueAnimator 来实现

如果想要点击一个 View 来实现另一个 View 的展示和隐藏,设置 View 的 visibility 为 GONE 或 VISIBLE 就可以实现,但是这样的话显示和隐藏都是瞬间完成的,如何让它在显示和隐藏时增加一个动画效果呢?
先来看看布局文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_fade"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="cn.ljuns.androidgrowing.example.FadeActivity">

<LinearLayout
android:id="@+id/click"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorAccent"
android:gravity="center_vertical"
android:orientation="horizontal">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click Me" />
</LinearLayout>

<LinearLayout
android:id="@+id/hide"
android:layout_width="match_parent"
android:layout_height="50dp"
android:background="@color/colorPrimary"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher" />

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="I am hide" />
</LinearLayout>
</LinearLayout>

这里有两个 LinearLayout,第二个默认为不可见。点击第一个 LinearLayout 实现第二个 LinearLayout 的显示和隐藏,接下来看看 Acitvity 的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class FadeActivity extends AppCompatActivity {

private LinearLayout mClick;
private LinearLayout mHide;
private int mLayoutHeight;
private float mDensity;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_fade);

mDensity = getResources().getDisplayMetrics().density;
mLayoutHeight = (int) (mDensity * 50 + 0.5);

mHide = (LinearLayout) findViewById(R.id.hide);
mClick = (LinearLayout) findViewById(R.id.click);
mClick.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (mHide.getVisibility() == View.GONE) {
// 展开
OpenAnim(mHide);
} else {
// 隐藏
CloseAnim(mHide);
}
}
});
}

/**
* 展开动画
*/
private void OpenAnim(View view) {
Toast.makeText(this, "open", Toast.LENGTH_SHORT).show();
view.setVisibility(View.VISIBLE);
ValueAnimator animator = createAnimaor(view, 0, mLayoutHeight);
animator.start();
}

/**
* 隐藏动画
*/
private void CloseAnim(final View view) {
Toast.makeText(this, "close", Toast.LENGTH_SHORT).show();
int height = view.getHeight();
ValueAnimator animator = createAnimaor(view, height, 0);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
}
});
animator.start();
}

/**
* 创建动画
* @param view
* @param start:起始位置
* @param end:结束位置
* @return
*/
private ValueAnimator createAnimaor(final View view, int start, int end) {
ValueAnimator animator = ValueAnimator.ofFloat(start, end);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
// 改变高度来实现动态显示
float value = (float) valueAnimator.getAnimatedValue();
ViewGroup.LayoutParams params = view.getLayoutParams();
params.height = (int) value;
view.setLayoutParams(params);
}
});
animator.setDuration(3000);
return animator;
}
}

这里有个小技巧:给动画添加一个监听事件,在事件中通过 valueAnimator.getAnimatedValue() 获取到变化的值再赋值给 View 的高度,这样就可以实现 View 是通过动画慢慢出现的。效果图:



其实这个例子和上面那个例子很相似,上面是通过动画显示 View 的宽度,这里是通过动画显示和隐藏 View 的高度,两种方案都可以实现一样的效果。这里再给一个郭大婶属性动画系列中对 ValueAnimator 的高级用法:[Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法]

实例

最后这里拿个小例子来对属性动画的学习进行结尾,要实现的效果是这样子的:




其实这种效果真的很简单,就是将所有图片放在一起,然后给最上面的图片设置点击事件,通过动画来显示和隐藏其他图片。下面先来看看布局文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/activity_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="cn.ljuns.androidgrowing.example.MenuActivity">

<ImageView
android:id="@+id/img_b"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/b" />

<ImageView
android:id="@+id/img_c"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/c" />

<ImageView
android:id="@+id/img_d"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/d" />

<ImageView
android:id="@+id/img_e"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/e" />

<ImageView
android:id="@+id/img_a"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@mipmap/a" />
</RelativeLayout>

布局文件就只是在 RelativeLayout 中放了几个要作为菜单显示的图片,接下来是 Activity :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class MenuActivity extends AppCompatActivity implements
View.OnClickListener{
// 图片集合
private int[] mRes = {R.id.img_a, R.id.img_b, R.id.img_c, R.id.img_d, R.id.img_e};
private List<ImageView> mImageView;
private boolean isOpen;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_menu);

init();
}

/**
* 初始化
*/
private void init() {
isOpen = false;
mImageView = new ArrayList<>();
for (int i = 0; i < mRes.length; i++) {
ImageView imageView = (ImageView) findViewById(mRes[i]);
// 给每个 ImageView 设置点击事件
imageView.setOnClickListener(this);
mImageView.add(imageView);
}
}

/**
* 点击事件
* @param view
*/
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.img_a:
if (isOpen) {
closeMenu();
isOpen = false;
} else {
openMenu();
isOpen = true;
}
break;
default:
Toast.makeText(this, "子菜单", Toast.LENGTH_SHORT).show();
break;
}
}

/**
* 打开菜单
*/
private void openMenu() {
ObjectAnimator animator0 =
ObjectAnimator.ofFloat(mImageView.get(0), "alpha", 1f, 0.5f);
ObjectAnimator animator1 =
ObjectAnimator.ofFloat(mImageView.get(1), "translationX", 0, -200f);
ObjectAnimator animator2 =
ObjectAnimator.ofFloat(mImageView.get(2), "translationX", 0, 200f);
ObjectAnimator animator3 =
ObjectAnimator.ofFloat(mImageView.get(3), "translationY", 0, -200f);
ObjectAnimator animator4 =
ObjectAnimator.ofFloat(mImageView.get(4), "translationY", 0, 200f);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator0, animator1, animator2, animator3, animator4);
set.setInterpolator(new BounceInterpolator());
set.setDuration(1000);
set.start();
}

/**
* 关闭菜单
*/
private void closeMenu() {
ObjectAnimator animator0 =
ObjectAnimator.ofFloat(mImageView.get(0), "alpha", 0.5f, 1f);
ObjectAnimator animator1 =
ObjectAnimator.ofFloat(mImageView.get(1), "translationX", -200f, 0);
ObjectAnimator animator2 =
ObjectAnimator.ofFloat(mImageView.get(2), "translationX", 200, 0);
ObjectAnimator animator3 =
ObjectAnimator.ofFloat(mImageView.get(3), "translationY", -200, 0);
ObjectAnimator animator4 =
ObjectAnimator.ofFloat(mImageView.get(4), "translationY", 200, 0);
AnimatorSet set = new AnimatorSet();
set.playTogether(animator0, animator1, animator2, animator3, animator4);
set.setInterpolator(new BounceInterpolator());
set.setDuration(1000);
set.start();
}
}

初始化的时候把图片装进 list 集合,并给每个图片设置点击事件。打开和关闭的时候给每个图片设置一个动画,再用 AnimatorSet 来统一执行。打开和关闭的动画不同点就是起始的位置和结束的位置刚好相反,有兴趣可以根据这个原理自己计算角度设置更炫的效果。源码:属性动画相关

通过几天的学习,属性动画算是告一段落了,加紧时间不断学习提升自己,争取每周都来一篇总结。

坚持原创技术分享,您的支持将鼓励我继续创作!