现在有这样一个需求,点击listview中的任意一个item,出现一个轨迹为曲线的动画。
我们知道Android动画分为帧动画(Frame)和补间动画(Tween)两种。帧动画和gif类似,将不同的帧以一定速度连续播放产生动画,需要我们事先准备好每一帧的静态画面。补间动画只用确定好动画的开始状态和结束状态,系统便自动生成中间所缺的帧。对于要求比较简单的曲线动画来说,可以巧妙的利用AnimationSet和Animation的Interpolator属性。AnimationSet是动画Animation的集合体,可以通过它的addAnimation方法,把多个不同的Animation添加到一个AnimationSet并同时播放,以实现丰富的动画功能。而Interpolator属性是Animation的播放速度属性,可以设置AccelerateInterpolator(播放速度逐渐加快)、LinearInterpolator(匀速播放)等值。接下来,重点来了。一段曲线轨迹可以拟合为水平方向匀速移动加上垂直方向的加速运动,这样运动的轨迹就是曲线。我是可以这样来写
animX = new TranslateAnimation(animeObj.startX, animeObj.endX, 0, 0);
animY = new TranslateAnimation(0, 0, animeObj.startY, animeObj.endY);
animX.setInterpolator(new AccelerateInterpolator());
animY.setInterpolator(new LinearInterpolator());
animeSet = new AnimationSet(true);
animeSet.addAnimation(animX);
animeSet.addAnimation(animY);
animeSet.setDuration(ANIME_DURATION);
animeObj.view.startAnimation(animeSet);</span>
很显然,这次需要使用补间动画中的位移动画来做。位移动画TranslateAnimation的参数仅提供设置起始位置、终止位置来实现直线位移,因此要实现曲线位移的做法就是将曲线分解为许多直线段,将无数小的位移动画合成一段曲线动画,因此,曲线轨迹方程就很重要了。虽然要求的是类抛物线,但由于抛物线轨迹曲线对点的要求比较高,基本上不满足实际需求。我选择二次贝塞尔曲线来拟合曲线轨迹(http://zh.wikipedia.org/wiki/%E8%B2%9D%E8%8C%B2%E6%9B%B2%E7%B7%9A)。
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
方程曲线的曲线如下:
因此,我们可以通过轨迹方程获得每一个点的坐标,再套用TranslateAnimation动画即可完成。这样我们得到的是一个匀速的曲线移动动画,如果要是加速动画,我们可以这样设置(其中t取值从0开始,具体可以参考上面贝塞尔曲线wiki):
cachePoint.x = (1-move)*(1-move) * startPoint.x + 2*move*(1-move) * middlePoint.x + move*move * endPoint.x;
cachePoint.y = (1-move)*(1-move) * startPoint.y + 2*move*(1-move) * middlePoint.y + move*move * endPoint.y;
<span style="white-space:pre"> </span> // 实现动画的加速效果
if (moveAdd < 0.4) {
moveAdd += 0.04f;
}
完整的DEMO代码如下:
package com.torchmu.animetest;
import java.util.Timer;
import java.util.TimerTask;
import android.app.Activity;
import android.content.Context;
import android.graphics.PointF;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AnimationSet;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
public class MainActivity extends Activity {
/** FLAG播放动画 */
private static final int MSG_ANIME = 1;
/** FLAG停止动画 */
private static final int MSG_END = 3;
/** 每一小段动画的持续时间 */
private static final int ANIME_DURATION = 100;
private String[] mArray = new String[20];
private ListView mListView;
private BaseAdapter mAdapter;
private ViewGroup mViewRoot;
private float mScreenWidth;
private float mScreenHeight;
/** ui线程响应动画的handler */
private Handler mHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mViewRoot = (ViewGroup) findViewById(R.id.layout_root);
DisplayMetrics metric = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metric);
mScreenWidth = (float) metric.widthPixels; // 屏幕宽度(像素)
mScreenHeight = (float) metric.heightPixels; // 屏幕高度(像素)
// 用于在ui线程播放动画
mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
AnimeObj animeObj = (AnimeObj) msg.obj;
switch(msg.what) {
case MSG_ANIME:
AnimationSet animSet = new AnimationSet(true);
TranslateAnimation animMove = new TranslateAnimation(animeObj.start.x, animeObj.end.x,
animeObj.start.y, animeObj.end.y);
ScaleAnimation animScale = new ScaleAnimation(animeObj.scaleFrom, animeObj.scaleTo,
animeObj.scaleFrom, animeObj.scaleTo);
// addAnimation顺序影响最终效果,animScale必须先行
animSet.addAnimation(animScale);
animSet.addAnimation(animMove);
animSet.setDuration(ANIME_DURATION);
animeObj.view.startAnimation(animSet);
Log.v("anime", "x:" + animeObj.start.x + " " + animeObj.end.x);
Log.v("anime", "y:" + animeObj.start.y + " " + animeObj.end.y);
break;
case MSG_END:
// 释放资源
animeObj.view.setVisibility(View.GONE);
animeObj.view = null;
break;
default:
break;
}
};
};
mListView = (ListView) findViewById(R.id.listview);
mAdapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, mArray);
for (int i=0; i<mArray.length;) {
mArray[i] = "test" + ++i;
}
mListView.setAdapter(mAdapter);
mListView.setOnItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final float startX = view.getX();
final float startY = view.getY();
Log.v("anime", "clickPos x" + view.getX() + " y:" + view.getY());
new Timer().schedule(new TimerTask() {
/*
* 采用二次贝塞尔方程,startPoint、middlePoint、endPoint为确定曲线轨迹的三个点
* 其中startPoint为listitem的位置,endPoint为屏幕右下角,middlePoint为自定义的点
* 并且middlePoint的位置确定了曲线的轨迹
*/
PointF startPoint = new PointF();
PointF middlePoint = new PointF();
PointF endPoint = new PointF();
PointF cachePoint = new PointF();
View imageView;
// 动画缩放缓存
float scale;
float scaleMinus;
// 位移缩放缓存
float move;
float moveAdd;
{
startPoint.x = startX;
startPoint.y = startY;
// 修改endPoint、middlePoint的坐标可以修正曲线轨迹
endPoint.x = mScreenWidth * 4 / 5;
endPoint.y = mScreenHeight - 100;
middlePoint.x = endPoint.x * 4 / 5;
middlePoint.y = startPoint.y - 400;;
cachePoint.x = startPoint.x;
cachePoint.y = startPoint.y;
Log.v("anime", "screen x:" + mScreenWidth + " y:" + mScreenHeight);
Log.v("anime", "start x:" + startPoint.x + " y:" + startPoint.y);
Log.v("anime", "middle x:" + middlePoint.x + " y:" + middlePoint.y);
Log.v("anime", "end x:" + endPoint.x + " y:" + endPoint.y);
imageView = createAnimeView((int)startPoint.x, (int)startPoint.y);
// 缩放动画初始值和递减值
scale = 1.0f;
scaleMinus = 0.01f;
// 位移动画初始值和递增值
move = 0.01f;
moveAdd = 0.04f;
}
@Override
public void run() {
Message msg = new Message();
AnimeObj animeObj = new AnimeObj();
msg.obj = animeObj;
animeObj.view = imageView;
if (scale - scaleMinus <= 0.4) {
// 缩放动画临界值,减少都一定程度不进行缩放动画
animeObj.scaleFrom = 0.4f;
animeObj.scaleTo = 0.4f;
} else {
animeObj.scaleFrom = scale;
scale -= scaleMinus;
animeObj.scaleTo = scale;
scaleMinus += 0.02f;
}
if (cachePoint.x < mScreenWidth && cachePoint.y < mScreenHeight) {
animeObj.start.x = cachePoint.x - startPoint.x;
animeObj.start.y = cachePoint.y - startPoint.y;
// 计算轨迹曲线的所需坐标值
cachePoint.x = (1-move)*(1-move) * startPoint.x + 2*move*(1-move) * middlePoint.x + move*move * endPoint.x;
cachePoint.y = (1-move)*(1-move) * startPoint.y + 2*move*(1-move) * middlePoint.y + move*move * endPoint.y;
animeObj.end.x = cachePoint.x - startPoint.x;
animeObj.end.y = cachePoint.y - startPoint.y;
msg.what = MSG_ANIME;
move += moveAdd;
// 实现动画的加速效果
if (moveAdd < 0.4) {
moveAdd += 0.04f;
}
} else {
// 位移动画临界值,超出后发送动画结束的message
msg.what = MSG_END;
this.cancel();
}
mHandler.sendMessage(msg);
}
}, 0, ANIME_DURATION);
}
});
}
/**
* 在指定坐标位置生成动画ImageView
* @param posX
* @param posY
* @return
*/
private View createAnimeView(int posX, int posY) {
ImageView iv = new ImageView(this);
iv.setImageResource(R.drawable.ic_launcher);
RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
lp.addRule(RelativeLayout.ALIGN_PARENT_LEFT); //与父容器的左侧对齐
lp.addRule(RelativeLayout.ALIGN_PARENT_TOP); //与父容器的上侧对齐
lp.leftMargin = posX;
lp.topMargin = posY;
iv.setLayoutParams(lp);
mViewRoot.addView(iv);
Log.v("anime", "createPosition x:" + posX + " y:" + posY);
return iv;
}
/**
* 用于Message传递的类
* 包含动画主体,动画位移坐标,动画缩放变量
*/
private class AnimeObj {
public PointF start = new PointF(); //位移动画起始坐标
public PointF end = new PointF(); //位移动画终点坐标
public float scaleFrom; //缩放动画起始值
public float scaleTo; //缩放动画终止值
public View view = null; //动画主题Object
public AnimeObj() {
}
}
}
布局文件:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout_root"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:id="@+id/layout_btnbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal" >
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:text="confirm" />
<Button
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
style="?android:attr/buttonBarButtonStyle"
android:text="cancle" />
</LinearLayout>
<ListView
android:id="@+id/listview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_above="@id/layout_btnbar"
android:descendantFocusability="afterDescendants" />
</RelativeLayout>