Android实现加入购物车动画(贝塞尔曲线)

Android实现加入购物车动画(贝塞尔曲线)

效果图

效果图

需求购物商品添加到购物车功能

思路分析:
1.确定动画起点、终点、控制点坐标;
2.二次贝塞尔曲线填充起终点之间点的轨迹
3.设置属性动画ValueAnimator插值器,获取中间点的坐标
4.将执行动画控件的x、y坐标设为上面得到的中间点坐标
5.开启属性动画

知识点

1,如何获取控件在屏幕中的绝对坐标


image.png

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) {
           }
       });

   }

}

效果图上来


效果图

笔者文笔太糟,欢迎吐槽,如有不对之处,请留言指点~~

呼吁大家动手实践,一切将会变得很容易~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容