上篇给大家讲了有关AnimatorSet的代码实现方法,这篇我们就分别来看看如何利用xml来实现ValueAnimator、ObjectAnimator和AnimatorSet;
在文章最后,将利用AnimatorSet来实现一个路径动画,效果图如下:
(这里实现的是一个动画菜单,在点击菜单按钮时,弹出各个菜单)
一、联合动画的XML实现
在xml中对应animator总共有三个标签,分别是
<animator />:对应ValueAnimator
<objectAnimator />:对应ObjectAnimator
<set />:对应AnimatorSet
下面我们逐个来看各个标签的用法
1、animator
(1)、animator所有字段及意义
下面是完整的animator所有的字段及取值范围:
<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]
android:interpolator=["@android:interpolator/XXX"]/>
android:duration:每次动画播放的时长
android:valueFrom:初始动化值;取值范围为float,int和color,如果取值为float对应的值样式应该为89.0,取值为Int时,对应的值样式为:89;当取值为clolor时,对应的值样式为 #333333;
android:valueTo:动画结束值;取值范围同样是float,int和color这三种类型的值;
android:startOffset:动画激活延时;对应代码中的startDelay(long delay)函数;
android:repeatCount:动画重复次数
android:repeatMode:动画重复模式,取值为repeat和reverse;repeat表示正序重播,reverse表示倒序重播
android:valueType:表示参数值类型,取值为intType和floatType;与android:valueFrom、android:valueTo相对应。如果这里的取值为intType,那么android:valueFrom、android:valueTo的值也就要对应的是int类型的数值。如果这里的数值是floatType,那么android:valueFrom、android:valueTo的值也要对应的设置为float类型的值。非常注意的是,如果android:valueFrom、android:valueTo的值设置为color类型的值,那么不需要设置这个参数;
android:interpolator:设置加速器;有关系统加速器所对应的xml值对照表如下:
(2)、将xml加载到程序中
在定义了一个xml后,我们需要将其加载到程序中,使用的方法如下:
ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(MyActivity.this,R.animator.animator);
valueAnimator.start();
通过loadAnimator将animator动画的xml文件,加载进来,根据类型进行强转。
(3)、简单示例
下面我们就举个例子来看看如何来使用xml生成对应的animator动画
先看看整体效果图:
在效果图中可以看到,我们生成了一个动画,动态了改变了当前控件的坐标位置。
我们先在res/animator文件夹下生成一个动画的xml文件:
<?xml version="1.0" encoding="utf-8"?>
<animator xmlns:android="http://schemas.android.com/apk/res/android"
android:valueFrom="0"
android:valueTo="300"
android:duration="1000"
android:valueType="intType"
android:interpolator="@android:anim/bounce_interpolator"/>
在这里,我们将valueType设置为intType,所以对应的android:valueFrom、android:valueTo都必须是int类型的值;插值器使用bounce回弹插值器
然后看看加载到程序中过程:
ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.animator);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int offset = (int)animation.getAnimatedValue();
mTv1.layout( offset,offset,mTv1.getWidth()+offset,mTv1.getHeight() + offset);
}
});
valueAnimator.start();
由于我们xml中根属性是<animator/>所以它对应的是ValueAnimator,所以在加载后,将其强转为valueAnimator;然后对其添加控件监听。在监听时,动态改变当前textview的位置。有关这些代码就不再细讲了,如果看到前面的文章,这段代码应该是无比熟悉的。
最后的效果就是开头时所演示的效果。
源码在文章底部给出
有关android:valueFrom、android:valueTo取值为color属性时的用法,这里就不讲了,因为在xml中使用color属性,我也不会用;尝试了下,不成功,也不想尝试了,没什么太大意义,下面我们会讲如何在objectanimator中使用color属性;
2、objectAnimator
(1)字段意义及使用方法
同样,我们先来看看它的所有标签的意义:
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]
android:interpolator=["@android:interpolator/XXX"]/>
意义:
- android:propertyName:对应属性名,即ObjectAnimator所需要操作的属性名。
其它字段的意义与animator的意义与取值是一样的,下面再重新列举一下。
- android:duration:每次动画播放的时长
- android:valueFrom:初始动化值;取值范围为float,int和color;
- android:valueTo:动画结束值;取值范围同样是float,int和color这三种类型的值;
- android:startOffset:动画激活延时;对应代码中的startDelay(long delay)函数;
- android:repeatCount:动画重复次数
- android:repeatMode:动画重复模式,取值为repeat和reverse;repeat表示正序重播,reverse表示倒序重播
- android:valueType:表示参数值类型,取值为intType和floatType;与android:valueFrom、android:valueTo相对应。如果这里的取值为intType,那么android:valueFrom、android:valueTo的值也就要对应的是int类型的数值。如果这里的数值是floatType,那么android:valueFrom、android:valueTo的值也要对应的设置为float类型的值。非常注意的是,如果android:valueFrom、android:valueTo的值设置为color类型的值,那么不需要设置这个参数;
- android:interpolator:设置加速器;
下面我们就看看如何使用:
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.object_animator);
animator.setTarget(mTv1);
animator.start();
同样是使用loadAnimator加载对应的xml动画。然后使用animator.setTarget(mTv1);绑定上动画目标。因为在xml中,没有设置目标的参数,所以我们必须通过代码将目标控件与动画绑定。
(2)、使用示例
我们先写一个动画的xml:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="TranslationY"
android:duration="2000"
android:valueFrom="0.0"
android:valueTo="400.0"
android:interpolator="@android:anim/accelerate_interpolator"
android:valueType="floatType"
android:repeatCount="1"
android:repeatMode="reverse"
android:startOffset="2000"/>
在这个xml中,我们定义了更改属性为TranslationY,即改变纵坐标;时长为2000毫秒。从0变到400;使用的插值器是加速插值器,对应的值类型为float类型。
有些同学可能会问,为什么是float类型,因为setTranslationY函数的参数是float类型的,声明如下:
public void setTranslationY(float translationY)
1
最后是设置重复次数和重复模式。将动画激活延时设置为2000毫秒;
然后是加载动画:
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.object_animator);
animator.setTarget(mTv1);
animator.start();
效果图如下:
在点击后,延时2000毫秒后,开始运行。逆序重复运行一次。
(3)、使用color属性示例
这里我们就演示一下如何使用android:valueFrom、android:valueTo的color属性用法,
我们建立一个objectAnimator的动画文件:
<?xml version="1.0" encoding="utf-8"?>
<objectAnimator xmlns:android="http://schemas.android.com/apk/res/android"
android:propertyName="BackgroundColor"
android:duration="5000"
android:valueFrom="#ffff00ff"
android:valueTo="#ffffff00"/>
设置属性名为BackgroundColor,即对应的set函数为setBackgroundColor(int color);
android:valueFrom和android:valueTo的取值都为颜色值,即#开头的八位数值;即ARGB值;
使用方法不变:
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.color_animator);
animator.setTarget(mTv1);
animator.start();
效果图如下:
从效果图中可以看到,虽然实现了颜色变化,但会一直闪;所以直接利用xml实现的动画效果并不怎么好,所以如果想要实现颜色变化,还是利用代码来实现吧。前面的文章中,我们已经讲过如何利用ValueAnimator和ObjectAnimator来实现颜色过渡和原理了。大家可以翻看下。
3、set
(1)字段意义及使用方法
这个是AnimatorSet所对应的标签。它只有一个属性:
<set
android:ordering=["together" | "sequentially"]>
android:ordering:表示动画开始顺序。together表示同时开始动画,sequentially表示逐个开始动画;
加载方式为:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.set_animator);
set.setTarget(mTv1);
set.start();
同样是通过loadAnimator加载动画,然后将其强转为AnimatorSet;
(2)、示例
在res/animator文件夹下新建一个文件(set_animator.xml):
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:ordering="together">
<objectAnimator
android:propertyName="x"
android:duration="500"
android:valueFrom="0"
android:valueTo="400"
android:valueType="floatType"/>
<objectAnimator
android:propertyName="y"
android:duration="500"
android:valueFrom="0"
android:valueTo="300"
android:valueType="floatType"/>
</set>
这里有两个objectAnimator动画,一个改变值x坐标,一个改变值y坐标;取值分别为0-400和0-300;
然后在代码中加载:
AnimatorSet set = (AnimatorSet) AnimatorInflater.loadAnimator(MyActivity.this,
R.animator.set_animator);
set.setTarget(mTv1);
set.start();
动画效果如下:
4、总结
最后总结一下,所有animator标签及取值范围如下:
<set
android:ordering=["together" | "sequentially"]>
<objectAnimator
android:propertyName="string"
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<animator
android:duration="int"
android:valueFrom="float | int | color"
android:valueTo="float | int | color"
android:startOffset="int"
android:repeatCount="int"
android:repeatMode=["repeat" | "reverse"]
android:valueType=["intType" | "floatType"]/>
<set>
...
</set>
</set>
各字段的取值意义在上面讲解时已经给出,大家可以翻回去看看。
二、开篇示例——AnimatorSet应用
在讲完了XML使用方法之后,AnimatorSet的部分就完全结束了,下面我们就利用学到的知识来看一下开篇时的那个效果是如何实现的吧。
我们先来分析下这个效果,在用户点击按钮时,把菜单弹出来;弹出来的时候,动画一点从小变到大,一边透明度从0变到1.关键问题是,怎么样实现各个菜单以当前点击按钮为圆心排列在圆形上;
1、原理
在开始写代码之前,我们先讲讲,如何根据圆半径来定位每个图片的位置,先看下图:
在上面的图中,我们可以清晰的看到,假如当前菜单与Y轴的夹角是a度,那么这个菜单所移动的X轴距离为radius * sin(a);Y轴的移动距离为radius * cos(a);
这是非常简单的三角函数的计算。想必这块大家理解起来是没有问题的。
那么第一个问题来了,这个夹角a是多少度呢?
很显然,这里所有的菜单的夹角之和是90度。我们总共有五个菜单项,把90度夹角做了4等分。所以夹角a的度数为90/4 = 22;所以这五个菜单,第一个菜单的夹角是0度,第二个菜单的夹角是22度,第三个菜单的夹角是22*2度,第四个夹角是22*3度,第五个的夹角是22*4度.
我们假设index表示当前菜单的位置索引,从0开始,即第一个菜单的索引是0,第二个菜单的索引是1,第三个菜单的索引是2……,而当前的菜单与y轴的夹角恰好占了22度的index份;所以当前菜单与Y轴的夹角为22 * index;这个公式非常重要,大家在这里一定要理解,下面代码中会用到。
第二个问题来了,如何求对应角度的sin,cos值
想必很多同学都知道,JAVA中有一个Math类,它其中有四个函数:
/**
* 求对应弧度的正弦值
*/
double sin(double d)
/**
* 求对应弧度的余弦值
*/
double cos(double d)
/**
* 求对应弧度的正切值
*/
double tan(double d)
这里要非常注意的是,这三个函数的输入参数不是度数,而是对应的度数的弧度值!
角度与其对应的弧度值对应关系如下:
在Math中有两种方法可以得到弧度值:
第一种方法:在Math中,Math.PI不仅代表圆周率π,也代表180度角所对应的弧度值。所以Math.sin(Math.PI)就表示180度的正弦值,Math.sin(Math.PI/2)就表示90度的正弦值。
第二种方法:根据度数获得弧度值
在Math中也提供了一个方法
/**
* Math中根据度数得到弧度值的函数
*/
double toRadians(double angdeg)
这个函数就是Math中根据度数得到弧度值的函数,参数angdeg指度数,返回值是对应的弧度值。
所以比如我们要求22度对应的弧度值就是Math.toRadians(22);所以如果我们要求22度所对应的正弦值就是Math.sin(Math.toRadians(22))
在讲了如何根据半径求得每个菜单项的位置之后,我们来看看示例工程的代码。
2、布局代码(main.xml)
布局代码很简单,就是利用FrameLayout将所有的菜单都盖在按钮的下面,效果图如下:
对应代码为:
<?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"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp">
<Button
android:id="@+id/menu"
style="@style/MenuStyle"
android:background="@drawable/menu"/>
<Button
android:id="@+id/item1"
style="@style/MenuItemStyle"
android:background="@drawable/circle1"
android:visibility="gone"/>
<Button
android:id="@+id/item2"
style="@style/MenuItemStyle"
android:background="@drawable/circle2"
android:visibility="gone"/>
<Button
android:id="@+id/item3"
style="@style/MenuItemStyle"
android:background="@drawable/circle3"
android:visibility="gone"/>
<Button
android:id="@+id/item4"
style="@style/MenuItemStyle"
android:background="@drawable/circle4"
android:visibility="gone"/>
<Button
android:id="@+id/item5"
style="@style/MenuItemStyle"
android:background="@drawable/circle5"
android:visibility="gone"/>
</FrameLayout>
其中的style代码为:
<resources>
<style name="MenuStyle">
<item name="android:layout_width">50dp</item>
<item name="android:layout_height">50dp</item>
<item name="android:layout_gravity">right|bottom</item>
</style>
<style name="MenuItemStyle">
<item name="android:layout_width">45dp</item>
<item name="android:layout_height">45dp</item>
<item name="android:layout_gravity">right|bottom</item>
</style>
</resources>
布局是没什么难度的,下面我们就来看看MyActivity中的处理。
3、MyActivity.java
(1)、先看看框架部分:
public class MyActivity extends Activity implements View.OnClickListener{
private static final String TAG = "MainActivity";
private Button mMenuButton;
private Button mItemButton1;
private Button mItemButton2;
private Button mItemButton3;
private Button mItemButton4;
private Button mItemButton5;
private boolean mIsMenuOpen = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
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);
}
@Override
public void onClick(View v) {
if (v == mMenuButton) {
if (!mIsMenuOpen) {
mIsMenuOpen = true;
doAnimateOpen(mItemButton1, 0, 5, 300);
doAnimateOpen(mItemButton2, 1, 5, 300);
doAnimateOpen(mItemButton3, 2, 5, 300);
doAnimateOpen(mItemButton4, 3, 5, 300);
doAnimateOpen(mItemButton5, 4, 5, 300);
} else {
mIsMenuOpen = false;
doAnimateClose(mItemButton1, 0, 5, 300);
doAnimateClose(mItemButton2, 1, 5, 300);
doAnimateClose(mItemButton3, 2, 5, 300);
doAnimateClose(mItemButton4, 3, 5, 300);
doAnimateClose(mItemButton5, 4, 5, 300);
}
} else {
Toast.makeText(this, "你点击了" + v, Toast.LENGTH_SHORT).show();
}
}
………………
}
这部分代码很简单,就是利用findviewById来找到每个菜单的实例,然后对他们添加点击响应:
public void onClick(View v) {
if (v == mMenuButton) {
if (!mIsMenuOpen) {
mIsMenuOpen = true;
doAnimateOpen(mItemButton1, 0, 5, 300);
…………
} else {
mIsMenuOpen = false;
doAnimateClose(mItemButton1, 0, 5, 300);
…………
}
} else {
Toast.makeText(this, "你点击了" + v, Toast.LENGTH_SHORT).show();
}
}
其中弹出主菜单的按钮是mMenuButton,当点击mMenuButton时,利用mIsMenuOpen来标识当前是否已经弹出菜单;如果没有弹出,则利用doAnimateOpen(mItemButton1, 0, 5, 300)将mItemButton1弹出来;其它按钮类似。如果已经弹出来,则利用doAnimateClose(mItemButton1, 0, 5, 300);将mItemButton1收回。
下面我们就分别来看看doAnimateOpen()和doAnimateClose()的代码;
(2)、doAnimateOpen()——弹出菜单
先贴出完整代码:
private void doAnimateOpen(View view, int index, int total, int radius) {
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
}
double degree = Math.toRadians(90)/(total - 1) * index;
int translationX = -(int) (radius * Math.sin(degree));
int translationY = -(int) (radius * Math.cos(degree));
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));
//动画周期为500ms
set.setDuration(1 * 500).start();
}
我们倒过来看,先看动画部分:
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));
这里构造的动画是利用translationX和translationY将控件移动到指定位置。同时,scaleX、scaleY、alpha都从0变到1;最关键的部分是如何得到translationX和translationY的值。
在这部分的开篇,我们首先讲了,如何讲了
translationX = radius * sin(a)
translationY = radius * cos(a)
我们来看看在代码中如何去做的:
double degree = Math.toRadians(90)/(total - 1) * index;
int translationX = -(int) (radius * Math.sin(degree));
int translationY = -(int) (radius * Math.cos(degree));
首先,是求得两个菜单的夹角,即公式里的a值。Math.toRadians(90)/(total - 1)表示90度被分成了total-1份,其中每一份的弧度值;
我们前面讲过,假设每一份的弧度值是22度,那么当前菜单与Y轴的夹角就是22 * index度。这里类似,当前菜单与y轴的弧度值就是Math.toRadians(90)/(total - 1) * index
在求得夹角以后,直接利用translationX = radius * sin(a)就可以得到x轴的移动距离,但又因为菜单是向左移动了translationX距离。所以根据坐标系向下为正,向右为正的原则。这里的移动距离translationX应该是负值。我们需要的translationY,因为是向上移动,所以也是负值:
int translationX = -(int) (radius * Math.sin(degree));
int translationY = -(int) (radius * Math.cos(degree));
在理解了弹出的部分之后,收回的代码就好理解了
(3)、doAnimateClose()——收回菜单
收回菜单就是把弹出菜单的动画反过来,让它从translateX,translateY的位置上回到0点,scaleX、scaleY、alpha的值从1变到0即可:
private void doAnimateClose(final View view, int index, int total,
int radius) {
if (view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
}
double degree = Math.PI * index / ((total - 1) * 2);
int translationX = -(int) (radius * Math.sin(degree));
int translationY = -(int) (radius * Math.cos(degree));
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));
set.setDuration(1 * 500).start();
}
这段代码是很容易理解的,但我在这里求degree的时候,换了一种方法:
double degree = Math.PI * index / ((total - 1) * 2);
其实这句代码与上面的
double degree = Math.toRadians(90)/(total - 1) * index;
是同一个意思。
还记得,我们在讲原理的时候,提到过Math.PI不仅表示圆周率,也表示180度所对应的弧度。
所以Math.toRadians(90)就等于Math.PI/2,这样,这两个公式就是完全一样的了。
(4)、存在问题
到这里,这个路径动画就讲解完了,但如果多使用几次就会发现,这里的动画存在问题:当菜单收回以后,再点击菜单展开时所在的位置,仍然会响应点击事件:
问题在于,在将菜单缩回时:
set.playTogether(
…………
ObjectAnimator.ofFloat(view, "scaleX", 1f, 0f),
ObjectAnimator.ofFloat(view, "scaleY", 1f, 0f));
在setScaleX\setScaleY动画时,由于我们将控件缩小到0,但是动画在控件被缩小到0以后,对它所做的属性动画,并不会实际改变控件的位置,这就像视图动画一样,虽然动画把控件移走了,但是响应点击事件的位置仍是在原来位置。这是Android的一个bug;所以,为了解决这个问题,可以有两种解决方案;
方案一:
第一个方案比较简单,在缩小时,既然在缩小到0以后,存在bug,那我们只需要不把控件缩小到0就可以了:
set.playTogether(
ObjectAnimator.ofFloat(view, "translationX", translationX, 0),
ObjectAnimator.ofFloat(view, "translationY", translationY, 0),
ObjectAnimator.ofFloat(view, "scaleX", 1f, 0.1f),
ObjectAnimator.ofFloat(view, "scaleY", 1f, 0.1f),
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f));
这里缩小到0.1大小,视觉上看不出来,也正好规避了缩小到0以后不会实际改变控件位置的问题。
方案二:
方案一是在缩小时做补救,而另一个方案而是监听动画状态,在动画结束时,将控件放大即可。
set.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationEnd(Animator animation) {
view.setScaleX(1.0f);
view.setScaleY(1.0f);
}
//其它监听函数只重写,不实现,代码省略
…………
});
好了,到这里有关AnimatorSet的部分就讲完了,下篇给大家讲讲有关viewGroup动画相关的知识。