公司项目需要向购物车添加商品,需要添加商品的动画效果。参照当前主流APP的一些效果,最终达到以下效果:
点击Item,显示点击第几项;点击购买,将商品添加到购物车中,同时增加购物车商品总数。
实现过程: 首先,商品被添加到购物车的轨迹中,类似于抛物线。幸运的是,安卓为我们提供了相关的方法–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值时,函数都会发生变化.getLength() ,当参数为measure时.getLength() 时间表示动画结束。通过参数Transformation 获取变换矩阵(matrix),各种复杂的效果可以通过改变矩阵来实现。 通过getmatrix().setttranslate函数实现移动,该函数的两个参数代表商品的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。最后,打开动画以达到最终效果。
~~~~~~~~~~~~~~~~~~~~~~~~~~
之前说想用属性动画来实现,周末回去好好看看这方面的知识,最后达到同样的效果,实现不同的动画。废话少说,看代码(记得设置为局部变量,否则会重叠):
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(); // 当前的点坐标包装在mcurentPosion中 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和mpathmeasuree传输到这里.getLength()表示将值从0平滑过渡到mpathmeasure.getLength()。 2.通过adddupdatelistener()添加动画监听器,在动画执行过程中不断回调。我们只需要在回调方法中使用mpathmeasure.getPosTan()将当前值取出并设置给商品的方法可以达到动画效果。 3.addlistener方法监控动画完成后的操作,删除动态添加的view,增加购物车数量。