本文讲介绍android在3.0之后推出的一种新的动画机制,属性动画,对动画不了解的同学,可以先去看看动画篇(一)——android动画基础这篇文章。
本人水平有限,文章中如果出现什么不正确或者模糊的地方,还请各位小伙伴留下评论,多多指教 : )
好了,现在我们进入正题。
基本概念
【android传统动画Animation与属性动画Animator的工作原理】
简单来说,传统动画就是不断的去调用系统的onDraw方法,去重新绘制组件,而属性动画则是通过调用属性的get和set方法重新设置控件的属性值,实现动画的效果
【Animator出现的原因】
既然已经存在了可以实现各种动画的方法了,为什么谷歌还要推出新的动画框架呢?为了解释这个问题,我们写一个小栗子。
首先是布局文件,很简单,就是一个imageView
<?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">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
</RelativeLayout>
接下来是java代码,我们找到这个view,为其添加点击事件,这里直接简单的Toast一下,表示view被点击;然后我们为这个view添加一个位移动画。代码如下:
package com.example.dell.myapplication;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.animation.TranslateAnimation;
import android.widget.ImageView;
import android.widget.Toast;
public class MainActivity extends Activity {
private ImageView imageView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView= (ImageView) findViewById(R.id.image);
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"ImageView",Toast.LENGTH_SHORT).show();
}
});
//参数1:X开始位置 参数2:x结束的位置
//参数3:y开始位置 参数4:y结束的位置
TranslateAnimation animation =new TranslateAnimation(0f,500f,0f,0f);
animation.setDuration(2000);
//动画结束后应用动画
animation.setFillAfter(true);
imageView.startAnimation(animation);
}
}
我们来看看演示效果:
可以看到,当我们进入到Activity当中后,动画就已经完成了,大家注意一下,这段代码其实是有问题的,因为我们只看到了动画的结果,而没看到动画的过程,这样一来,动画设置的就毫无意义。为什么会出现这种情况呢?这涉及到了Activity生命周期的内容,答案将在本节最后揭晓。
现在这时我先点的imageview完成动画后的位置(0f,100f)发现并没有弹出Toast,然后我们再去点击imageView的初始位置(0f,0f),有意思的事情发生了,Toast被弹出来了
那么这些说明了什么呢?
说明了我们在使用Animation的时候,虽然可以改变动画在界面上显示的位置,但是却不能改变点击事件所在的位置。
到这里,传统Animation一个很大的局限性,它只是重绘了动画,改变了显示的位置,但是真正事件响应的位置,却没有发生任何改变。所以,Animation并不适合制作具有交互的动画效果。它只能用来完成一些显示性的效果。
那么下面,我们就来列一下传统Animation的局限性
【Animation的局限性】
- 第一点就是上面所说的内容,不在啰嗦了。
- 因为Animation的工作原理,上面介绍了,是不断调用系统的onDraw()方法去绘制图像,那么必然十分耗费GPU,效率不高
- Animation提供了位移、旋转、透明度、缩放这四种动画,虽然经过组合,可以创造出很多的动画效果,但是有时候依然无法制作出复杂好看的动画,即存在动画效果上的局限。
为解决以上的局限性,Google在android3.0后推出了属性动画。那么接下来我们就正式进入属性动画的内容。
在这之前,我们似乎还有一个小问题没有解决,那就是为什么上一个栗子的动画没有显示出来呢?
答案是,动画的启动写在了onCreate()方法当中,而在这之后,还有onStart(),onResume()方法,而用户真正看到界面的时候,onCreate方法早已经调用完毕了,此时如果动画的持续时间过短,那么用户看到界面时动画自然已经结束了。解决的办法也非常简单,只需要把动画的启动逻辑放在onResume() (活动准备好和用户进行交互的时候 )方法当中即可
补充:经过测试,发现在onResume()方法当中也会出现一些延时,这因为机器性能的问题,启动一个活动的时间长短不一,即便调用了onResume()方法,距离Activity的启动可能还有一段时间。但是这种写法肯定要比在onCreate()方法中启动动画要好一些。
属性动画——ObjectAnimator
【“translationX”】
现在我们在上面栗子的基础上增加一个button,将动画的启动逻辑放在里面,这样就能避免刚才动画显示的问题。
更改后的布局
<?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">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@mipmap/ic_launcher"/>
<Button
android:id="@+id/bt_move"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="move"
android:layout_centerVertical="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
接下来是java代码
public class MainActivity extends Activity {
private ImageView imageView;
private TranslateAnimation animation;
private Button bt_move;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
imageView= (ImageView) findViewById(R.id.image);
bt_move= (Button) findViewById(R.id.bt_move);
//view的点击事件
imageView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Toast.makeText(MainActivity.this,"ImageView",Toast.LENGTH_SHORT).show();
}
});
//button的点击事件
bt_move.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//使用ObjectAnimator
//第一个参数是被操纵的view对象;第二个参数是被操作的属性
//第三,第四个参数是可变长的数,表示熟悉所变化的范围
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"translationX",0f,200f);
//设置显示时长
objectAnimator.setDuration(2000);
//让动画开始
objectAnimator.start();
}
});
}
}
我们看一下效果
这里很明显,view的点击事件也发生了相应的变化。
【“translationY”】
想要改变Y上的属性,也很简单,代码如下:
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"translationY",0f,200f);
//设置显示时长
objectAnimator.setDuration(2000);
//让动画开始
objectAnimator.start();
看到这里,有的小伙伴可能会有疑问,到底那些属性是我们可以去设置的呢?其实只要是Google定义了的可以通过set和get去操纵的属性,我们都可以在属性动画中对其进行设置。
【“X和Y”】
前面已经讲了translationX和translationY,那么现在出现的X和Y又是什么东西呢?
translationX是指的是物体的偏移量,而X是指物体最后到达的一个绝对值,即具体移动到的坐标
【“rotation”】
旋转属性,旋转360度
ObjectAnimator objectAnimator= ObjectAnimator.ofFloat(imageView,"rotation",0f,360f);
//设置显示时长
objectAnimator.setDuration(2000);
//让动画开始
objectAnimator.start();
以此类推,可以操作的属性还有很多,凡是可以通过get或者set设置的属性,都可以设置成属性动画。
【组合动画】
现在我们使用ObjectAnimator来进行多个动画效果的组合
ObjectAnimator.ofFloat(imageView,"translationY",0f,100f).setDuration(1000).start();
ObjectAnimator.ofFloat(imageView,"translationX",0f,100f).setDuration(1000).start();
ObjectAnimator.ofFloat(imageView,"rotation",0f,360f).setDuration(1000).start();
效果图:
可以发现,这三个动画的效果是同时完成的,因为在调用start()方法之后,实际上是一个异步的过程。
实际上,我们还有更好的方法去实现这种组合效果,我们对上面的写法进行优化:
PropertyValuesHolder p1=PropertyValuesHolder.ofFloat("translationY",0f,100f);
PropertyValuesHolder p2=PropertyValuesHolder.ofFloat("translationX",0f,100f);
PropertyValuesHolder p3=PropertyValuesHolder.ofFloat("rotation",0f,360f);
//通过ObjectAnimator来调用ofPropertyValuesHolder()方法
//第一个参数传递view,后续的参数为可变长的数组
ObjectAnimator.ofPropertyValuesHolder(imageView,p1,p2,p3).setDuration(1000).start();
这里我们使用了一个PropertyValuesHolder的容器来容纳3个动画效果,然后在最后调用ObjectAnimator的ofPropertyValuesHolder()方法来加载之前定义的三个holder。那么这样写的好处是什么呢?Google在PropertyValuesHolder这个类中对动画进行了一些优化,这些优化使得我们在使用多个动画属性的时候能够更加有效率,更加节省系统资源。
在前面的动画基础当中,有一个动画集合的概念,那么在属性动画当中其实也有这么一个集合的概念。下面我们就用AnimatorSet来实现上述的效果。
代码如下:
AnimatorSet animatorSet=new AnimatorSet();
//接下来将单个动画添加到AnimatorSet当中
animatorSet.playTogether(animator1,animator2,animator3);
animatorSet.setDuration(1000);
animatorSet.start();
当然在AnimatorSet方法当中我们还有更多的选择去控制动画。从animatorSet.playTogether()这个方法的名字中就能看出,该方法是让所有的动画同时起效果,我们才看到了和刚才几种方法实现的同样的效果。这里google还提供了playSequentially()方法,该方法则是按顺序去播放动画。大家可以试一下。
我们还可以使用play()方法,这里我们实现view在X和Y轴上同时平移,结束之后再旋转360度。
代码如下:
ObjectAnimator animator1= ObjectAnimator.ofFloat(imageView,"translationY",0f,100f);
ObjectAnimator animator2= ObjectAnimator.ofFloat(imageView,"translationX",0f,100f);
ObjectAnimator animator3= ObjectAnimator.ofFloat(imageView,"rotation",0f,360f);
AnimatorSet animatorSet=new AnimatorSet();
//animator1和Animator2同时播放
animatorSet.play(animator1).with(animator2);
//Animator3在Animator1或者Animator2结束后播放
animatorSet.play(animator3).after(animator2);
animatorSet.setDuration(1000);
animatorSet.start();
这样一来就实现了对每个动画更加细致的控制。通过play(),with(),after(),before()方法,我们就能做到对一个属性集合的详细的顺序控制。这种方式,也是属性动画框架中使用最多的一种配和。
接下来总结一下
(1)通过ObjectAnimator进行更精细的控制,只控制一个对象的一个属性。
(2)同时多个ObjectAnimator组合到AnimatorSet当中,可以形成一个完整的动画效果。
(3)而且AnimatorSet可以自动驱动,可以去调用play(),with(),after(),before,playTogether(),playSequentially()实现更为丰富的动画效果。
动画事件监听
顾名思义,和一般的点击事件差不多,我们为ObjectAnimator设置监听事件,以满足实际开发当中的需求。
bt_move.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
………………
ObjectAnimator bt_animator=ObjectAnimator.ofFloat(view,"alpha",0f,1f);
bt_animator.setDuration(1000);
bt_animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animator) {
//动画开始前调用
}
@Override
public void onAnimationEnd(Animator animator) {
//动画结束后调用,为简单起见,这里我们只简单的提示一行字
Toast.makeText(MainActivity.this,"anim is end",Toast.LENGTH_SHORT).show();
}
@Override
public void onAnimationCancel(Animator animator) {
//动画被取消后
}
@Override
public void onAnimationRepeat(Animator animator) {
//动画重复时调用
}
});
bt_animator.start();
…………
}
});
我们在上一个栗子的基础上为我们的Button添加一个alpha动画,让其从透明变成不透明,然后在这个动画里面加入了一个监听事件,在监听事件当中,我们看到了4个需要重写的方法,通过这4个方法,我们就可以监听动画在不同事件段所需要完成的操作。
演示效果:
现在我们在考虑一种情况,那就是如果我们并不需要重写那么多的方法该怎么办呢?这时可以使用android系统提供的一个更方便的接口AnimatorListenerAdapter(),大家可以发现,系统帮我们实现了很多方法,这里我们只需要添加需要重写的方法即可
代码如下:
bt_animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Toast.makeText(MainActivity.this,"anim is end",Toast.LENGTH_SHORT).show();
}
});
这样我们就实现了对某一个事件的监听,而不需要写出所有的事件。
总结一下
看到出来动画监听事件还是比较简单的,我们只需要调用ObjectAnimator的addListener()方法,就能为我们的Animator增加一个监听事件,接着我们可以通过 AnimatorListenerAdapter这个类,去有选择的去选取我们需要监听的事件。
小栗子
这里的栗子来自于慕课网的课程,有兴趣的小伙伴可以去看看。
在开始写这个栗子之前,请各位小伙伴去下载一下素材,其中a.png的尺寸有点问题,改为58*58即可
演示效果:
接下来让让我们看看布局文件
<?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">
<ImageView
android:id="@+id/view_b"
android:src="@drawable/b"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_c"
android:src="@drawable/c"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_d"
android:src="@drawable/d"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_e"
android:src="@drawable/e"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_f"
android:src="@drawable/f"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_g"
android:src="@drawable/g"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_h"
android:src="@drawable/h"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<ImageView
android:id="@+id/view_a"
android:src="@drawable/a"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
里面定义了我们需要用到的imageView
接下来是比较关键的java代码:
public class MainActivity extends Activity implements View.OnClickListener {
//定义每一个图片资源
private int[] res = {R.id.view_a, R.id.view_b, R.id.view_c, R.id.view_d, R.id.view_e, R.id.view_f, R.id.view_g,
R.id.view_h};
//存储viewd的list集合
private List<ImageView> imageViewList = new ArrayList<>();
//设置一个flag来标示列表是否展开
private boolean flag = true;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_my);
for (int i = 0; i < res.length; i++) {
//实例化imageView
ImageView imageView = (ImageView) findViewById(res[i]);
//添加点击事件
imageView.setOnClickListener(this);
//将每一个ImageView添加到list当中
imageViewList.add(imageView);
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.view_a:
//点击最上面的图片,显示展开动画
//动画方法
if (flag) {
//列表未展开
startAnimation();
flag = false;
} else {
//列表已展开
stopAnimation();
flag = true;
}
break;
default:
//其他按钮
Toast.makeText(this, "clicked", Toast.LENGTH_SHORT).show();
break;
}
}
private void stopAnimation() {
for (int i = 1; i < res.length - 1; i++) {
ObjectAnimator animator = ObjectAnimator.ofFloat(imageViewList.get(i), "translationY", 200 * i, 0);
animator.setDuration(500);
//设置插值器
animator.setInterpolator(new AnticipateInterpolator());
animator.start();
}
}
private void startAnimation() {
for (int i = 1; i < res.length - 1; i++) {
ObjectAnimator animator = ObjectAnimator.ofFloat(imageViewList.get(i), "translationY", 0f, 200 * i);
animator.setDuration(500);
//设置延迟,让动画更加丰富
animator.setStartDelay(i * 100);
//设置插值器
animator.setInterpolator(new OvershootInterpolator());
animator.start();
}
}
}
上述代码当中有非常详细的注释,这里就不多解释了。为了让动画效果更棒,有时我们还可以为动画添加差插器(interpolator)。android内置的插值器有如下
- Accelerate
- Decelerate
- Accelerate/Decelerate、
- Overshoot
- Bounce
通过插值器,我们让某一属性在数值上的变化时,可以拥有不同的加速曲线,进而让我们的动画更加丰富。