上篇给大家讲了有关AnimatorSet的代码实现方法,这篇我们就分别来看看如何利用xml来实现ValueAnimator、ObjectAnimator和AnimatorSet;
在文章最后,将利用AnimatorSet来实现一个路径动画,效果图如下:
在xml中对应animator总共有三个标签,分别是
<animator />:对应ValueAnimator<objectAnimator />:对应ObjectAnimator<set />:对应AnimatorSet
下面我们逐个来看各个标签的用法
下面是完整的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"]/>
在定义了一个xml后,我们需要将其加载到程序中,使用的方法如下:
ValueAnimator valueAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(MyActivity.this,R.animator.animator);valueAnimator.start();
通过loadAnimator将animator动画的xml文件,加载进来,根据类型进行强转。
下面我们就举个例子来看看如何来使用xml生成对应的animator动画
先看看整体效果图:
<?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属性;
同样,我们先来看看它的所有标签的意义:
<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中,没有设置目标的参数,所以我们必须通过代码将目标控件与动画绑定。
我们先写一个动画的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)
最后是设置重复次数和重复模式。将动画激活延时设置为2000毫秒;
然后是加载动画:
ObjectAnimator animator = (ObjectAnimator) AnimatorInflater.loadAnimator(MyActivity.this, R.animator.object_animator);animator.setTarget(mTv1);animator.start();
效果图如下:
这里我们就演示一下如何使用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();
效果图如下:
这个是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;
在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();
动画效果如下:
最后总结一下,所有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>
各字段的取值意义在上面讲解时已经给出,大家可以翻回去看看。
在讲完了XML使用方法之后,AnimatorSet的部分就完全结束了,下面我们就利用学到的知识来看一下开篇时的那个效果是如何实现的吧。
在开始写代码之前,我们先讲讲,如何根据圆半径来定位每个图片的位置,先看下图:
/** * 求对应弧度的正弦值 */double sin(double d)/** * 求对应弧度的余弦值 */double cos(double d)/** * 求对应弧度的正切值 */double tan(double d)
这里要非常注意的是,这三个函数的输入参数不是度数,而是对应的度数的弧度值!
角度与其对应的弧度值对应关系如下:
/** * Math中根据度数得到弧度值的函数 */double toRadians(double angdeg)
这个函数就是Math中根据度数得到弧度值的函数,参数angdeg指度数,返回值是对应的弧度值。
所以比如我们要求22度对应的弧度值就是Math.toRadians(22);所以如果我们要求22度所对应的正弦值就是Math.sin(Math.toRadians(22))
在讲了如何根据半径求得每个菜单项的位置之后,我们来看看示例工程的代码。
布局代码很简单,就是利用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中的处理。
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()的代码;
先贴出完整代码:
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));
在理解了弹出的部分之后,收回的代码就好理解了
收回菜单就是把弹出菜单的动画反过来,让它从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,这样,这两个公式就是完全一样的了。
到这里,这个路径动画就讲解完了,但如果多使用几次就会发现,这里的动画存在问题:当菜单收回以后,再点击菜单展开时所在的位置,仍然会响应点击事件:
问题在于,在将菜单缩回时:
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动画相关的知识。
(BlogAnimatorSetDemo中存在的问题,在源码中是没有修改的,大家注意自行修复)
源码内容:
联系客服