仿饿了么添加购物车动画

原文网址

//www.greatytc.com/p/b7bacc6805c2

根据以上方法结合自己的框架修改实现效果。

0 效果

效果图

1 Item布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="140dp"
    android:padding="5dp">

    <com.zykj.djs.widget.RoundImageView
        android:id="@+id/iv_good"
        android:layout_width="120dp"
        android:layout_height="120dp"
        android:src="@color/colorText"
        app:radius="5dp"/>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="10dp"
            android:text="地浆水入门套装,精华补水提亮肤色"
            android:textColor="@color/colorBlack"
            android:textSize="16sp" />

        <TextView
            android:id="@+id/tv_money"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:text="¥ 186.00"
            android:textColor="@color/colorText"
            android:textSize="20sp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_vertical"
            android:orientation="horizontal">

            <TextView
                android:layout_width="0dp"
                android:layout_height="match_parent"
                android:layout_weight="1" />

            <!--放在加的图标前面-->
            <ImageView
                android:id="@+id/iv_goods_reduce"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignTop="@id/iv_goods_add"
                android:layout_marginLeft="37dp"
                android:layout_toLeftOf="@id/tv_goods_count"
                android:src="@mipmap/one_jianshao"
                android:visibility="visible"/>

            <TextView
                android:id="@+id/tv_goods_count"
                android:layout_width="28dp"
                android:layout_height="24dp"
                android:layout_alignTop="@id/iv_goods_add"
                android:layout_toLeftOf="@id/iv_goods_add"
                android:gravity="center"
                android:textColor="#333333"
                android:textSize="14sp"/>

            <ImageView
                android:id="@+id/iv_goods_add"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_alignParentRight="true"
                android:src="@mipmap/one_tianjia"/>



            <TextView
                android:id="@+id/tv_unit"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginRight="10dp"
                android:text="彩妆/瓶" />

        </LinearLayout>

    </LinearLayout>


</LinearLayout>

Item布局效果如下图所示,主要是对加号和减号做动画。


Item布局

2 Adapter写法

public class ProductAdapter extends BaseAdapter<ProductAdapter.ProductHolder, ProductBean> {

    private int reduceLeft = 0;
    private int addLeft = 0;

    private static final long TIME = 300;   // 动画的执行时间

    public ProductAdapter(Context context) {
        super(context);
        setShowFooter(false);
    }

    @Override
    public int provideItemLayoutId() {
        return R.layout.item_product;
    }

    @Override
    public ProductHolder createVH(View view) {
        return new ProductHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull ProductHolder holder, int i) {
        holder.mTvTitle.setText(mData.get(i).name);
        holder.mTvMoney.setText(mData.get(i).price);
        holder.mTvGoodsCount.setText(mData.get(i).count == 0 ? "" : String.valueOf(mData.get(i).count));
        holder.mIvGoodsReduce.setVisibility(mData.get(i).count == 0 ? View.INVISIBLE : View.VISIBLE);
    }

    public class ProductHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        @Nullable
        @Bind(R.id.iv_good)
        RoundImageView mIvGood;
        @Nullable
        @Bind(R.id.tv_title)
        TextView mTvTitle;
        @Nullable
        @Bind(R.id.tv_money)
        TextView mTvMoney;
        @Nullable
        @Bind(R.id.iv_goods_reduce)
        ImageView mIvGoodsReduce;
        @Nullable
        @Bind(R.id.tv_goods_count)
        TextView mTvGoodsCount;
        @Nullable
        @Bind(R.id.iv_goods_add)
        ImageView mIvGoodsAdd;


        public ProductHolder(View itemView) {
            super(itemView);
            ButterKnife.bind(this, itemView);
            mIvGoodsReduce.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // 获取减少图标的位置
                    reduceLeft = mIvGoodsReduce.getLeft();
                    mIvGoodsReduce.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            });
            mIvGoodsReduce.setOnClickListener(this);
            mIvGoodsAdd.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    // 获取增加图标的位置
                    addLeft = mIvGoodsAdd.getLeft();
                    mIvGoodsAdd.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                }
            });
            mIvGoodsAdd.setOnClickListener(this);
        }

        @Override
        public void onClick(View view) {
            if (mOnItemClickListener != null) {
                mOnItemClickListener.onItemClick(view, this.getAdapterPosition());
            }

            switch (view.getId()){
                case R.id.iv_goods_reduce:
                    mData.get(this.getAdapterPosition()).count--;
                    // 防止过快点击出现多个关闭动画
                    if (mData.get(this.getAdapterPosition()).count == 0) {
                        animClose(mIvGoodsReduce);
                        mTvGoodsCount.setText("");
                        // 考虑到用户点击过快
                    } else if (mData.get(this.getAdapterPosition()).count < 0) {
                        // 防止过快点击出现商品数为负数
                        mData.get(this.getAdapterPosition()).count = 0;
                    } else {
                        mTvGoodsCount.setText(String.valueOf(mData.get(this.getAdapterPosition()).count));
                    }
                    break;
                case R.id.iv_goods_add:
                    mData.get(this.getAdapterPosition()).count++;

//                    if (allCount > 0) {
//                        mTvShoppingCartCount.setVisibility(View.VISIBLE);
//                    }
//                    mTvShoppingCartCount.setText(String.valueOf(allCount));
                    if (mData.get(this.getAdapterPosition()).count == 1) {
                        mIvGoodsReduce.setVisibility(View.VISIBLE);
                        animOpen(mIvGoodsReduce);
                    }
                    //addGoods2CartAnim(iv_goods_add);
                    mTvGoodsCount.setText(String.valueOf(mData.get(this.getAdapterPosition()).count));
                    break;
            }
        }
    }

    public void animOpen(final ImageView imageView) {
        AnimatorSet animatorSet = new AnimatorSet();
        ObjectAnimator translationAnim = ObjectAnimator.ofFloat(imageView, "translationX", addLeft - reduceLeft, 0);
        ObjectAnimator rotationAnim = ObjectAnimator.ofFloat(imageView, "rotation", 0, 180);
        animatorSet.play(translationAnim).with(rotationAnim);
        animatorSet.setDuration(TIME).start();
    }

    public void animClose(final ImageView imageView) {
        AnimatorSet animatorSet = new AnimatorSet();
        ObjectAnimator translationAnim = ObjectAnimator.ofFloat(imageView, "translationX", 0, addLeft - reduceLeft);
        ObjectAnimator rotationAnim = ObjectAnimator.ofFloat(imageView, "rotation", 0, 180);
        animatorSet.play(translationAnim).with(rotationAnim);
        animatorSet.setDuration(TIME).start();
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // TODO: 2018/5/19 因为属性动画会改变位置,所以当结束的时候,要回退的到原来的位置,同时用补间动画的位移不好控制
                ObjectAnimator oa = ObjectAnimator.ofFloat(imageView, "translationX", addLeft - reduceLeft, 0);
                oa.setDuration(0);
                oa.start();
                imageView.setVisibility(View.GONE);
            }
        });
    }
}

3 购物车页面布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/snack_layout"
    android:layout_height="match_parent">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <include layout="@layout/ui_view_toolbar" />


        <android.support.v7.widget.RecyclerView
            android:id="@+id/recycle_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scrollbars="none"></android.support.v7.widget.RecyclerView>

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true">

        <!--这个是下面去结算的页面--!>
        <include layout="@layout/shop_car" />

    </LinearLayout>
</RelativeLayout>

购物车页面布局效果


购物车页面

4 功能实现

public class ProductActivity extends RecycleViewActivity<ListsPresenter, ProductAdapter, ProductBean> {
    @Bind(R.id.snack_layout)
    RelativeLayout mSnackLayout;
    @Bind(R.id.tv_shopping_cart_count)
    TextView mTvShoppingCartCount;
    @Bind(R.id.iv_shopping_cart)
    ImageView iv_shopping_cart;

    private int allCount;


    // 贝塞尔曲线中间过程点坐标
    private float[] mCurrentPosition = new float[2];

    @Override
    protected int provideContentViewId() {
        return R.layout.activity_product;
    }

    @Override
    protected String provideTitle() {
        return "购物车";
    }

    @Override
    protected String provideButton() {
        return null;
    }

    @Override
    protected void initAllMembersView() {
        super.initAllMembersView();

        /*===================自己造的数据 begin====================*/
        List<ProductBean> productBeans = new ArrayList<>();
        ProductBean bean = new ProductBean();
        bean.name = "化妆品1";
        bean.price = "199.00";
        productBeans.add(bean);
        ProductBean bean1 = new ProductBean();
        bean1.name = "化妆品2";
        bean1.price = "99.00";
        productBeans.add(bean1);
        addNews(productBeans, 2);
        /*===================自己造的数据 end====================*/
    }

    @Override
    protected RecyclerView.LayoutManager provideLayoutManager() {
        return new LinearLayoutManager(getContext());
    }

    @Override
    protected ProductAdapter provideAdapter() {
        return new ProductAdapter(getContext());
    }

    @Override
    public ListsPresenter createPresenter() {
        return new ListsPresenter();
    }

    @Override
    public void onItemClick(View view, int position) {
        switch (view.getId()) {
            case R.id.iv_goods_reduce:
                allCount--;
                // 商品的数量是否显示
                if (allCount <= 0) {
                    allCount = 0;
                    mTvShoppingCartCount.setVisibility(View.GONE);
                } else {
                    mTvShoppingCartCount.setText(String.valueOf(allCount));
                    mTvShoppingCartCount.setVisibility(View.VISIBLE);
                }
                break;
            case R.id.iv_goods_add:
                allCount++;
                if (allCount > 0) {
                    mTvShoppingCartCount.setVisibility(View.VISIBLE);
                }
                mTvShoppingCartCount.setText(String.valueOf(allCount));
                addGoods2CartAnim((ImageView) view);
                break;
        }
    }


    /**
     * 贝塞尔曲线动画
     *
     * @param goodsImageView
     */
    public void addGoods2CartAnim(ImageView goodsImageView) {
        final ImageView goods = new ImageView(ProductActivity.this);
        goods.setImageResource(R.mipmap.icon_goods_add);
        int size = TextUtil.dp2px(ProductActivity.this, 24);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(size, size);
        goods.setLayoutParams(lp);
        //mSnackLayout是整个页面布局id
        mSnackLayout.addView(goods);
        // 控制点的位置
        int[] recyclerLocation = new int[2];
        mSnackLayout.getLocationInWindow(recyclerLocation);
        // 加入点的位置起始点
        int[] startLocation = new int[2];
        goodsImageView.getLocationInWindow(startLocation);
        // 购物车的位置终点
        int[] endLocation = new int[2];
        //iv_shopping_cart是动画运行轨迹最终结束的地方  我这里用的是购物车的图标控件
        iv_shopping_cart.getLocationInWindow(endLocation);
        // TODO: 2018/5/21 0021 考虑到状态栏的问题,不然会往下偏移状态栏的高度
        int startX = startLocation[0] - recyclerLocation[0];
        int startY = startLocation[1] - recyclerLocation[1];
        // TODO: 2018/5/21 0021 和上面一样
        int endX = endLocation[0] - recyclerLocation[0];
        int endY = endLocation[1] - recyclerLocation[1];
        // 开始绘制贝塞尔曲线
        Path path = new Path();
        // 移动到起始点位置(即贝塞尔曲线的起点)
        path.moveTo(startX, startY);
        // 使用二阶贝塞尔曲线:注意第一个起始坐标越大,贝塞尔曲线的横向距离就会越大,一般按照下面的式子取即可
        path.quadTo((startX + endX) / 2, startY, endX, endY);
        // mPathMeasure用来计算贝塞尔曲线的曲线长度和贝塞尔曲线中间插值的坐标,如果是true,path会形成一个闭环
        final PathMeasure pathMeasure = new PathMeasure(path, false);
        // 属性动画实现(从0到贝塞尔曲线的长度之间进行插值计算,获取中间过程的距离值)
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, pathMeasure.getLength());
        // 计算距离
        int tempX = Math.abs(startX - endX);
        int tempY = Math.abs(startY - endY);
        // 根据距离计算时间
        int time = (int) (0.3 * Math.sqrt((tempX * tempX) + tempY * tempY));
        valueAnimator.setDuration(time);
        valueAnimator.start();
        valueAnimator.setInterpolator(new AccelerateInterpolator());
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                // 当插值计算进行时,获取中间的每个值,
                // 这里这个值是中间过程中的曲线长度(下面根据这个值来得出中间点的坐标值)
                float value = (Float) animation.getAnimatedValue();
                // 获取当前点坐标封装到mCurrentPosition
                // boolean getPosTan(float distance, float[] pos, float[] tan) :
                // 传入一个距离distance(0<=distance<=getLength()),然后会计算当前距离的坐标点和切线,pos会自动填充上坐标,这个方法很重要。
                // mCurrentPosition此时就是中间距离点的坐标值
                pathMeasure.getPosTan(value, mCurrentPosition, null);
                // 移动的商品图片(动画图片)的坐标设置为该中间点的坐标
                goods.setTranslationX(mCurrentPosition[0]);
                goods.setTranslationY(mCurrentPosition[1]);
            }
        });
        valueAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                // 移除图片
                mSnackLayout.removeView(goods);
                // 购物车数量增加
                mTvShoppingCartCount.setText(String.valueOf(allCount));
            }
        });
    }

}

特别注意:

(1)

     //mSnackLayout是整个页面布局id
     mSnackLayout.addView(goods);

(2)

   //iv_shopping_cart是动画运行轨迹最终结束的地方  我这里用的是购物车的图标控件
    iv_shopping_cart.getLocationInWindow(endLocation);

以上。

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