公司项目有添加商品到购物车的需求,需要一个添加商品的动画效果。参照了一些当下主流APP的效果,最后实现了以下效果:
点击Item,显示点击第几项;点击购买,添加商品到购物车,同时购物车商品总数加一。
实现过程:
首先是商品添加到购物车的轨迹,类似于一条抛物线,好在Android已经为我们提供了相关的方法–Path类(封装了贝塞尔曲线)。具体关于贝塞尔曲线,大家可以自行百度。这里我们主要研究Path为我们提供的构造路径的方法。
1.moveTo(float,float)
用于设置移动路径的起始点Point(x,y),对于android系统来说,屏幕的左上角的坐标是 (0,0) , 我们在做一些操作的时候默认基准点也是 (0,0)。Path 的moveTo 方法可以与此进行一个类比,就是为了改变 Path 的起始点。
2.quadTo(float x1, float y1, float x2, float y2 )
android 只对低阶贝塞尔曲线进行了封装,这是用于设置二次贝塞尔曲线的方法,先上图说明:
x3、y3 代表控制点的 x、y,即动态图中的P1,x2、y2 代表目标点的 x、y,即动态图中的P2。绘制路径轨迹已经找到了对应的类与方法,接下来就是在自己项目里的具体应用了。如下图:
(x0,y0)代表父布局的坐标,(x1,y1)代表商品,(x2,y2)代表购物车,(x3,y3)代表控制点。需要的点已经确定好,接下来就是代码实现了:
public class SecondActivity extends AppCompatActivity implements addListener {
private int i;
private TextView txt;
private ImageView cartImg;
private RelativeLayout relativeLayout;
private ListView list;
private LayoutInflater inflater;
private ListAdapter adapter;
private int[] imgs = new int[]{R.drawable.cake, R.drawable.milk, R.drawable.coffee, R.drawable.kettle, R.drawable.mobile};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
initviews();
}
private void initviews() {
relativeLayout = (RelativeLayout) findViewById(R.id.rl);
txt = (TextView) findViewById(R.id.second_txt);
cartImg = (ImageView) findViewById(R.id.cart_img);
inflater = LayoutInflater.from(this);
list = (ListView) findViewById(R.id.list);
adapter = new ListAdapter();
adapter.setListener(this);
list.setAdapter(adapter);
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Toast.makeText(SecondActivity.this, "你点击了第" + String.valueOf(position + 1) + "项", Toast.LENGTH_SHORT).show();
}
});
}
public class ListAdapter extends BaseAdapter {
private addListener listener;
public void setListener(addListener listener) {
this.listener = listener;
}
@Override
public int getCount() {
if (imgs != null) {
return imgs.length;
} else {
return 0;
}
}
@Override
public Object getItem(int position) {
return (position);
}
@Override
public long getItemId(int id) {
// TODO Auto-generated method stub
return id;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
final ViewHolder viewHolder;
if (convertView == null) {
convertView = inflater.inflate(R.layout.list_item, null);
viewHolder = new ViewHolder();
viewHolder.itemimg = (ImageView) convertView.findViewById(R.id.item_img);
viewHolder.itemtxt = (TextView) convertView.findViewById(R.id.item_txt);
convertView.setTag(viewHolder);
} else {
viewHolder = (ViewHolder) convertView.getTag();
}
viewHolder.itemimg.setImageResource(imgs[position]);
viewHolder.itemtxt.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
listener.addCart(position, viewHolder.itemimg);
}
});
return convertView;
}
public class ViewHolder {
public ImageView itemimg;
public TextView itemtxt;
}
}
数据的准备,Item的单击事件,接口回调处理“购买”点击事件
这里需要拿到具体商品的图片进行动画处理,传递了一个ImageView过去,Position则是方便我们进行其他具体的业务处理。接下来就是最重要的动画实现了:
//得到起始点坐标
int parentLoc[] = new int[2];
relativeLayout.getLocationInWindow(parentLoc);
int startLoc[] = new int[2];
imgview.getLocationInWindow(startLoc);
int endLoc[] = new int[2];
cartImg.getLocationInWindow(endLoc);
getLocationInWindow :获取该视图在整个窗口内的绝对坐,parentLoc [0]代表x坐标,parentLoc [1]代表y坐标。
float startX = startLoc[0] - parentLoc[0] + imgview.getWidth() / 2;
float startY = startLoc[1] - parentLoc[1] + imgview.getHeight() / 2;
float toX = endLoc[0] - parentLoc[0] + cartImg.getWidth() / 3;
float toY = endLoc[1] - parentLoc[1];
通过起始点坐标计算出控制点与目标点的坐标。
final ImageView goods = new ImageView(getApplicationContext());
goods.setImageDrawable(imgview.getDrawable());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(60, 60);
relativeLayout.addView(goods, params);
动态在父布局中添加一个执行添加动画的视图,也就是效果图中的商品缩略图。之前自己用的是传递过来的ImageView,发现每次动画一执行,Item的图片相应也会消失。所以用这种方法来替代这个商品,记住最后在动画完成的时候将父布局中动态添加的这个view移除即可。
Path path = new Path();
path.moveTo(startX, startY);
path.quadTo((startX + toX) / 2, startY, toX, toY);
调用Path类对应的方法模拟出这一条抛物线
现在路径曲线有了,还需要一个非常重要的辅助类:路径测量PathMeasure,无论Path路径多么复杂,PathMeasure也会将所有path中的路径看成一个直线,取出某一点的位置,然后计算出对应的坐标。
构造方法:
PathMeasure(Path path, boolean forceClosed)
常用方法:
float getLength() :测量path的长度
boolean getPosTan(float distance, float[] pos, float[] tan) :传入一个距离distance(0<=distance<=getLength()),然后会计算当前距离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。
路径上每一点的坐标都能够获取到,接下来就是动画的实现了,其实就是商品每次根据不同的点坐标移动到不同的位置,这样就实现了想要的效果。我这里用的是自定义的一个动画:
/**
* Created by tangyangkai on 16/4/20.
*/
public class PathAnimation extends Animation {
private PathMeasure measure;
private float[] pos = new float[2];
public PathAnimation(Path path) {
measure = new PathMeasure(path, false);
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
measure.getPosTan(measure.getLength() * interpolatedTime, pos, null);
t.getMatrix().setTranslate(pos[0], pos[1]);
}
}
通过重写Animation的 applyTransformation (float interpolatedTime, Transformation t)函数来实现自定义动画效果。在绘制动画的过程中会反复的调用applyTransformation 函数,每次调用参数interpolatedTime值都会变化,该参数从0渐变为measure.getLength() ,当该参数为measure.getLength() 时表明动画结束。通过参数Transformation 来获取变换的矩阵(matrix),通过改变矩阵就可以实现各种复杂的效果。
通过getMatrix().setTranslate函数来实现移动,该函数的两个参数代表商品的x坐标与y坐标,由于interpolatedTime是从0到measure.getLength() 变化,所在这里实现的效果就是商品会沿着制定的路径进行移动。
PathAnimation animation = new PathAnimation(path);
animation.setDuration(1000);
animation.setInterpolator(new LinearInterpolator());
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
i++;
txt.setText(String.valueOf(i));
relativeLayout.removeView(goods);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
goods.startAnimation(animation);
自定义动画完成,然后就是调用。依次设置动画持续时间,匀速动画线性插值器,动画监听。记住在动画完成的时候,将商品数量加一,同时移除动态添加的view。最后开启动画,达到最后的效果。
参考资料:
http://blog.csdn.net/tianjian4592/article/details/47067161
http://blog.csdn.net/gucun4848/article/details/8459280
忙着出效果,所以没有研究用属性动画来实现,有时间会去完成的,然后更新博客。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~分割线
之前说到想使用属性动画来实现,周末回去好好看了看这方面的知识,最后一样的效果,不同的动画实现。废话不多说,看代码(记得设置成局部变量,不然会出现重叠现象):
final PathMeasure mPathMeasure= new PathMeasure(path, false);
final float[] mCurrentPosition = new float[2];
路径测量辅助类path measure,数组存放x,y坐标
//添加购物车动画实现
@Override
public void addCart(int position, ImageView imgview) {
//得到起始点坐标
int parentLoc[] = new int[2];
relativeLayout.getLocationInWindow(parentLoc);
int startLoc[] = new int[2];
imgview.getLocationInWindow(startLoc);
int endLoc[] = new int[2];
cartImg.getLocationInWindow(endLoc);
final ImageView goods = new ImageView(getApplicationContext());
goods.setImageDrawable(imgview.getDrawable());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(60, 60);
relativeLayout.addView(goods, params);
float startX = startLoc[0] - parentLoc[0] + imgview.getWidth() / 2;
float startY = startLoc[1] - parentLoc[1] + imgview.getHeight() / 2;
float toX = endLoc[0] - parentLoc[0] + cartImg.getWidth() / 3;
float toY = endLoc[1] - parentLoc[1];
Path path = new Path();
path.moveTo(startX, startY);
path.quadTo((startX + toX) / 2, startY, toX, toY);
mPathMeasure = new PathMeasure(path, false);
Path路径以及PathMeasure路径测量的构造
//属性动画实现
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.setDuration(1000);
// 匀速插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = (Float) animation.getAnimatedValue();
// 获取当前点坐标封装到mCurrentPosition
mPathMeasure.getPosTan(value, mCurrentPosition, null);
goods.setTranslationX(mCurrentPosition[0]);
goods.setTranslationY(mCurrentPosition[1]);
}
});
valueAnimator.start();
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
i++;
txt.setText(String.valueOf(i));
relativeLayout.removeView(goods);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
这里是属性动画实现的核心,着重学习一下。
科普时间(引用郭神博客):
ValueAnimator是整个属性动画机制当中最核心的一个类,前面我们已经提到了,属性动画的运行机制是通过不断地对值进行操作来实现的,而初始值和结束值之间的动画过渡就是由ValueAnimator这个类来负责计算的。它的内部使用一种时间循环的机制来计算值与值之间的动画过渡,我们只需要将初始值和结束值提供给ValueAnimator,并且告诉它动画所需运行的时长,那么ValueAnimator就会自动帮我们完成从初始值平滑地过渡到结束值这样的效果。除此之外,ValueAnimator还负责管理动画的播放次数、播放模式、以及对动画设置监听器等,确实是一个非常重要的类。
其实添加购物车的动画实现就是对商品的x,y坐标不断赋值,不断更新,达到抛物线的效果。
1.调用ValueAnimator的ofFloat()方法就可以构建出一个ValueAnimator的实例,ofFloat()方法当中允许传入多个float类型的参数,这里传入0和mPathMeasure.getLength()就表示将值从0平滑过渡到mPathMeasure.getLength()。
2.通过addUpdateListener()方法来添加一个动画的监听器,在动画执行的过程中会不断地进行回调,我们只需要在回调方法当中通过mPathMeasure.getPosTan()方法将当前的值取出并设置给商品,就可以达到动画效果了。
3.addListener方法来监听动画完成以后的操作,移除动态添加的view,购物车数量加一。
参考博客:
http://blog.csdn.net/guolin_blog/article/details/43536355
http://blog.csdn.net/lmj623565791/article/details/38067475
欧了,关于属性动画细节,实现原理这几篇博客讲的很清楚,感谢这些大神。
下载地址:
欢迎大家star,fork,提issues,一起进步!
继续努力!