Android实现加入购物车动画(贝塞尔曲线)
效果图
需求购物商品添加到购物车功能
思路分析:
1.确定动画起点、终点、控制点坐标;
2.二次贝塞尔曲线填充起终点之间点的轨迹
3.设置属性动画ValueAnimator插值器,获取中间点的坐标
4.将执行动画控件的x、y坐标设为上面得到的中间点坐标
5.开启属性动画
知识点
1,如何获取控件在屏幕中的绝对坐标
1、如图所示主要有三个点,起点、终点、以及贝塞尔曲线的控制点
2、起点即点击的View的位置,一般来说用如下方式即可取得。startA[0]为x轴开始坐标,startA[1]为Y轴终点坐标,两点可以看作对角线上面的两个端点(左上角x坐标,右下角y坐标)
//起点坐标
int startA[] = new int[2];
view.getLocationInWindow(startA);
3、终点即购物车篮子的位置,与起点类似
//获取终点的坐标
int endB[] = new int[2];
//tvGoodNum终点的view
tvGoodNum.getLocationInWindow(endB);
4、控制点,我选的控制点为上图的C点,即A点的y坐标,B点的X坐标
int parentC[] = new int[2];
rl.getLocationInWindow(parentC);
5、通过Path的quadTo方法绘制贝塞尔曲线,使用PathMeasure获取点的坐标
//商品起点坐标:购物车起始点-父布局起始点
float startX = startA[0] - parentC[0] ;
float startY = startA[1] - parentC[1] ;
//商品终点坐标:购物车起始点-父布局起始点
float toX = endB[0] - parentC[0] ;
float toY = endB[1] - parentC[1] ;
//开始绘制贝塞尔曲线
Path path = new Path();
//移动到起始点(贝塞尔曲线的起点)
path.moveTo(startX, startY);
//使用二次萨贝尔曲线:注意第一个起始坐标越大,贝塞尔曲线的横向距离就会越大,一般按照下面的式子取即可
path.quadTo((startX + toX) / 2, startY, toX, toY);
6、属性动画插值器ValueAnimator
final PathMeasure mPathMeasure = new PathMeasure(path, false);
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.setDuration(5000);
// 匀速线性插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {,
// 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
float value = (Float) animation.getAnimatedValue();
// ★★★★★获取当前点坐标封装到mCurrentPositio
mPathMeasure.getPosTan(value, mCurrentPosition, null);//mCurrentPosition此时就是中间距离点的坐标值
// 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
imageView.setTranslationX(mCurrentPosition[0]);
imageView.setTranslationY(mCurrentPosition[1]);
}
});
// 开始执行动画
valueAnimator.start();
完整代码
布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rl_path"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ListView
android:id="@+id/lv_main"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:background="@android:color/white">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#EFEFEF" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:id="@+id/rl_goods_fits_car"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginLeft="15dp">
<ImageView
android:id="@+id/iv_goods_car"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_centerVertical="true"
android:background="@drawable/ic_goods_fits_car" />
<TextView
android:id="@+id/tv_good_fitting_num"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:background="@drawable/sp_common_circle2yellow"
android:gravity="center"
android:text="0"
android:textColor="@android:color/white"
android:textSize="12sp" />
</RelativeLayout>
<TextView
android:id="@+id/tv_goods_fitt_all_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginLeft="10dp"
android:layout_toRightOf="@+id/rl_goods_fits_car"
android:text="¥0.0元"
android:textColor="#FF4001"
android:textSize="16sp"></TextView>
<TextView
android:id="@+id/tv_goods_fitt_submit"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginRight="20dp"
android:background="@drawable/sp_common_fill_bg"
android:gravity="center"
android:padding="10dp"
android:text="确定"
android:textColor="@android:color/white"></TextView>
</RelativeLayout>
</LinearLayout>
</RelativeLayout>
</RelativeLayout>
然后写购物车适配器代码如下:
public class FoodAdapter extends BaseAdapter implements View.OnClickListener {
private List<FoodModel> models;
private Context context;
private FoodActionCallback callback;
public FoodAdapter(Context context, List<FoodModel> models, FoodActionCallback callback) {
this.context = context;
this.models = models;
this.callback = callback;
}
@Override
public int getCount() {
return models.size();
}
@Override
public Object getItem(int position) {
return models.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder;
if (convertView == null) {
convertView = LayoutInflater.from(context).inflate(R.layout.item_food, null);
viewHolder = new ViewHolder(convertView);
convertView.setTag(viewHolder);
}
viewHolder = (ViewHolder) convertView.getTag();
viewHolder.tv_goods_fits_name.setText(models.get(position).getName());
viewHolder.tv_goods_fits_price.setText(models.get(position).getDescription());
Picasso.with(context)
.load(models.get(position).getPath())
.fit().centerCrop()
.into(viewHolder.iv_goods_fits_picture);
viewHolder.iv_goods_fits_add.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
callback.addAction(view,models.get(position).getPath());
}
});
return convertView;
}
@Override
public void onClick(View v) {
if(callback==null) return;
// callback.addAction(v, models.get(position).getPath());
}
public static class ViewHolder {
TextView tv_goods_fits_name;
TextView tv_goods_fits_price;
ImageView iv_goods_fits_picture;
ImageView iv_goods_fits_add;
public ViewHolder(View convertView) {
tv_goods_fits_name = (TextView) convertView.findViewById(R.id.tv_goods_fits_name);
tv_goods_fits_price = (TextView) convertView.findViewById(R.id.tv_goods_fits_price);
iv_goods_fits_picture = (ImageView) convertView.findViewById(R.id.iv_goods_fits_picture);
iv_goods_fits_add = (ImageView) convertView.findViewById(R.id.iv_goods_fits_add);
}
}
public interface FoodActionCallback {
void addAction(View view, int path);
}
}
item布局
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="70dp"
android:layout_marginTop="10dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingRight="10dp">
<ImageView
android:id="@+id/iv_goods_fits_picture"
android:layout_width="50dp"
android:layout_height="50dp"
android:scaleType="centerCrop"
android:src="@drawable/imag1" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginLeft="10dp"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/tv_goods_fits_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="#4A4A4A"
android:textSize="14sp"
tools:text="仙人掌蜡烛" />
<TextView
android:id="@+id/tv_goods_fits_price"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:textColor="#FF4001"
android:textSize="14sp"
tools:text="¥14.00" />
</LinearLayout>
<ImageView
android:id="@+id/iv_goods_fits_reduce"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_toLeftOf="@+id/ll_goods_fitting_count"
android:background="@drawable/ic_goods_fitting_reduce" />
<LinearLayout
android:id="@+id/ll_goods_fitting_count"
android:layout_width="wrap_content"
android:layout_height="25dp"
android:layout_alignParentBottom="true"
android:layout_marginBottom="10dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:layout_toLeftOf="@+id/iv_goods_fits_add"
android:gravity="center">
<TextView
android:id="@+id/tv_goods_fits_num"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="99"
android:textSize="16sp" />
</LinearLayout>
<ImageView
android:id="@+id/iv_goods_fits_add"
android:layout_width="25dp"
android:layout_height="25dp"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginBottom="10dp"
android:layout_marginRight="10dp"
android:background="@drawable/ic_goods_fitting_add" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
实体类
public class FoodModel implements Parcelable {
private String name;
private int path;
private String description;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPath() {
return path;
}
public void setPath(int path) {
this.path = path;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
@Override
public int describeContents() {
return 0;
}
public FoodModel(String name, int path, String description) {
this.name = name;
this.path = path;
this.description = description;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.path);
dest.writeString(this.description);
}
public FoodModel() {
}
protected FoodModel(Parcel in) {
this.name = in.readString();
this.path = in.readInt();
this.description = in.readString();
}
public static final Parcelable.Creator<FoodModel> CREATOR = new Parcelable.Creator<FoodModel>() {
@Override
public FoodModel createFromParcel(Parcel source) {
return new FoodModel(source);
}
@Override
public FoodModel[] newArray(int size) {
return new FoodModel[size];
}
};
}
界面代码如下:
/**
* 购物车界面
*/
package com.nianxin.github.animation;
import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.Intent;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.TextView;
import adapter.FoodAdapter;
import datas.AppConfig;
/**
* 购物车动画
*/
public class FoodActivity2 extends AppCompatActivity implements FoodAdapter.FoodActionCallback {
private ListView lvMain;
private TextView tvGoodNum;
private int count;
private RelativeLayout rl;
private ImageView ivGoodsCar;
/**
* 贝塞尔曲线中间过程的点的坐标
*/
private float[] mCurrentPosition = new float[2];
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_foods2);
rl = (RelativeLayout) findViewById(R.id.rl_path);
ivGoodsCar = (ImageView) findViewById(R.id.iv_goods_car);
lvMain = (ListView) findViewById(R.id.lv_main);
lvMain.setAdapter(new FoodAdapter(this, AppConfig.factoryFoods(), this));
tvGoodNum = (TextView) findViewById(R.id.tv_good_fitting_num);
}
public static void startActiivty(Context context) {
context.startActivity(new Intent(context, FoodActivity2.class));
}
@Override
public void addAction(View view, int pathImage) {
// 一 、创建购物的ImageView 添加到父布局中
final ImageView imageView=new ImageView(this);
imageView.setImageResource(R.drawable.ic_goods_fitting_add);
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(50, 50);
rl.addView(imageView, params);
// 二 、起点位置\终点的坐标\父布局控制点坐标
int startA[] = new int[2];
view.getLocationInWindow(startA);
// 获取终点的坐标
int endB[] = new int[2];
tvGoodNum.getLocationInWindow(endB);
// 父布局控制点坐标
int parentC[] = new int[2];
rl.getLocationInWindow(parentC);
// 三、正式开始计算动画开始/结束的坐标
//开始掉落的商品的起始点:商品起始点-父布局起始点+该商品图片的一半
float startX = startA[0] - parentC[0] ;
float startY = startA[1] - parentC[1] ;
//商品掉落后的终点坐标:购物车起始点-父布局起始点+购物车图片的1/5
float toX = endB[0] - parentC[0] ;
float toY = endB[1] - parentC[1] ;
// 四、计算中间动画的插值坐标(贝塞尔曲线)(其实就是用贝塞尔曲线来完成起终点的过程)
//开始绘制贝塞尔曲线
Path path = new Path();
//移动到起始点(贝塞尔曲线的起点)
path.moveTo(startX, startY);
//使用二次萨贝尔曲线:注意第一个起始坐标越大,贝塞尔曲线的横向距离就会越大,一般按照下面的式子取即可
path.quadTo((startX + toX) / 2, startY, toX, toY);
//mPathMeasure用来计算贝塞尔曲线的曲线长度和贝塞尔曲线中间插值的坐标,
// 如果是true,path会形成一个闭环
final PathMeasure mPathMeasure = new PathMeasure(path, false);
//★★★属性动画实现(从0到贝塞尔曲线的长度之间进行插值计算,获取中间过程的距离值)
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, mPathMeasure.getLength());
valueAnimator.setDuration(3000);
// 匀速线性插值器
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
// 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
float value = (Float) animation.getAnimatedValue();
// ★★★★★获取当前点坐标封装到mCurrentPosition
// 离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。
mPathMeasure.getPosTan(value, mCurrentPosition, null);//mCurrentPosition此时就是中间距离点的坐标值
// 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
imageView.setTranslationX(mCurrentPosition[0]);
imageView.setTranslationY(mCurrentPosition[1]);
}
});
//五、 开始执行动画
valueAnimator.start();
//六、动画结束后的处理
valueAnimator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
//当动画结束后:
@Override
public void onAnimationEnd(Animator animation) {
// 购物车的数量加1
count++;
tvGoodNum.setText(count+"");
// 把移动的图片imageview从父布局里移除
rl.removeView(imageView);
// 开始一个放大动画
Animation scaleAnim = AnimationUtils.loadAnimation(FoodActivity2.this, R.anim.shop_scale);
ivGoodsCar.startAnimation(scaleAnim);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}
}
效果图上来
笔者文笔太糟,欢迎吐槽,如有不对之处,请留言指点~~
呼吁大家动手实践,一切将会变得很容易~~~