仿饿了么添加购物车效果

本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

最近有一个需求,要实现一个像饿了么添加购物车的效果,下面是效果图
主要有以下几点
  • 1 沉浸式状态栏

  • 2 上下滑动的动画

  • 3 添加减少的动画

  • 4 贝塞尔曲线动画

  • 5 底部购物车弹窗动画

  • 6 购物车缓存

沉浸式状态栏
沉浸式状态栏网上有很多,通常都是放v19和v21的包,然后在最外层ViewGroup设置fitsSystemWindows = "true" ,这种方式我不太推荐,因为你每次都要写fitsSystemWindows 这个属性,可能你会说我抽取出来不就好了,ok,那有时候要的效果不是我想要的,就比如我图上的效果,那怎么办呢,其实,有时候,你只需要一点小小的bang助!!!,几行代码的事
仿饿了么添加购物车效果_第1张图片

values


values-v21

 
重头戏来了,以ToolBar为例,动态设置ToolBar的高度,并且设置一个padding,top为状态栏的高度,搞定收工!
  • activity
  @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            setHeight(mToolbar);
        }
    }

    public void setHeight(View view) {
        // 获取actionbar的高度
        TypedArray actionbarSizeTypedArray = obtainStyledAttributes(new int[]{
                android.R.attr.actionBarSize
        });
        float height = actionbarSizeTypedArray.getDimension(0, 0);
        // ToolBar的top值
        ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) view.getLayoutParams();
        double statusBarHeight = getStatusBarHeight(this);
        lp.height = (int) (statusBarHeight + height);
        view.setPadding(0,(int) statusBarHeight,0, 0);
        mToolbar.setLayoutParams(lp);
    }

    private double getStatusBarHeight(Context context) {
        int result = 0;
        int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen",
                "android");
        if (resourceId > 0) {
            result = context.getResources().getDimensionPixelSize(resourceId);
        }
        return result;
    }
  • xml



    

        

        
    

下面是效果图
仿饿了么添加购物车效果_第2张图片
上下滑动的动画
没什么好说的,design包下的协调式布局(CoordinatorLayout),可以很好地实现这种效果,监听AppBarLayout的状态,打开的时候,隐藏标题,关闭的时候.显示标题,同时监听高度变化做透明度动画效果
  • XML布局


    

        

            

                

                

                

                

                

                

                    

                    

                    

                    
                

                
            

            
        
    

    

    

  • AppBarStateChangeListener
public abstract class AppBarStateChangeListener implements AppBarLayout.OnOffsetChangedListener {

    private State mCurrentState = State.IDLE;

    @Override
    public final void onOffsetChanged(AppBarLayout appBarLayout, int i) {
        if (i == 0) {
            if (mCurrentState != State.EXPANDED) {
                onStateChanged(appBarLayout, State.EXPANDED);
            }
            mCurrentState = State.EXPANDED;
        } else if (Math.abs(i) >= appBarLayout.getTotalScrollRange()) {
            if (mCurrentState != State.COLLAPSED) {
                onStateChanged(appBarLayout, State.COLLAPSED);
            }
            mCurrentState = State.COLLAPSED;
        } else {
            if (mCurrentState != State.IDLE) {
                onStateChanged(appBarLayout, State.IDLE);
            }
            mCurrentState = State.IDLE;
        }
        onStateChanged(i);
        Logger.d("滑动的的高度" + i);
    }

    public abstract void onStateChanged(AppBarLayout appBarLayout, State state);

    public void onStateChanged(int i) {
    }

    public enum State {
        EXPANDED,       // 展开状态
        COLLAPSED,      // 折叠状态
        IDLE            // 准备状态
    }
}
  • activity
private void initToolbar() {
        mAppBarLayout.addOnOffsetChangedListener(new AppBarStateChangeListener() {
            @Override
            public void onStateChanged(AppBarLayout appBarLayout, State state) {
                if (state == State.EXPANDED) {
                    // 展开状态
                    mTvTitle.setText("");
                    mRlHeader.setVisibility(View.VISIBLE);
                } else if (state == State.COLLAPSED) {
                    // 折叠状态
                    mTvTitle.setText("芭比馒头");
                    mRlHeader.setVisibility(View.GONE);
                } else {
                    mTvTitle.setText("");
                    mRlHeader.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onStateChanged(int i) {
                float height = mRlHeader.getHeight();
                float alpha = i / height;
                Logger.d("透明度" + (1 - Math.abs(alpha)));
                mRlHeader.setAlpha(1 - Math.abs(alpha));
            }
        });
    }
上面要注意的是,动态设置mRlHeader的显示隐藏,因为透明度有时候不为0的时候,还是会显示出来的
添加减少的动画
使用的是属性动画,单这里有一个坑,后面会讲到,尝试过用补间动画,但是由于补间动画的特性,本身的位置不变,达不到预期效果,有兴趣的可以尝试一下
  • 动画的代码
 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();
}
  • 动画打开还是关闭
        // 减少
        final ImageView iv_goods_reduce = holder.getView(R.id.iv_goods_reduce);
        iv_goods_reduce.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                // 获取减少图标的位置
                reduceLeft = iv_goods_reduce.getLeft();
                iv_goods_reduce.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        iv_goods_reduce.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = holder.getLayoutPosition();
                ShopGoodsBean shopGoodsBean = mGoodsList.get(position);
                int count = shopGoodsBean.getCount();
                count--;
                // 防止过快点击出现多个关闭动画
                if (count == 0) {
                    animClose(iv_goods_reduce);
                    tv_goods_count.setText("");
                    // 考虑到用户点击过快
                    allCount--;
                } else if (count < 0) {
                    // 防止过快点击出现商品数为负数
                    count = 0;
                } else {
                    allCount--;
                    tv_goods_count.setText(String.valueOf(count));
                }
                // 商品的数量是否显示
                if (allCount <= 0) {
                    allCount = 0;
                    mTvShoppingCartCount.setVisibility(View.GONE);
                } else {
                    mTvShoppingCartCount.setText(String.valueOf(allCount));
                    mTvShoppingCartCount.setVisibility(View.VISIBLE);
                }
                shopGoodsBean.setCount(count);
            }
        });
// 增加
        final ImageView iv_goods_add = holder.getView(R.id.iv_goods_add);
        iv_goods_add.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                // 获取增加图标的位置
                addLeft = iv_goods_add.getLeft();
                iv_goods_add.getViewTreeObserver().removeOnGlobalLayoutListener(this);
            }
        });
        iv_goods_add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                int position = holder.getLayoutPosition();
                ShopGoodsBean shopGoodsBean = mGoodsList.get(position);
                int count = shopGoodsBean.getCount();
                count++;
                allCount++;
                if (allCount > 0) {
                    mTvShoppingCartCount.setVisibility(View.VISIBLE);
                }
                mTvShoppingCartCount.setText(String.valueOf(allCount));![1.gif](https://upload-images.jianshu.io/upload_images/5286943-8616ee25e4dd2edc.gif?imageMogr2/auto-orient/strip)

                if (count == 1) {
                    iv_goods_reduce.setVisibility(View.VISIBLE);
                    animOpen(iv_goods_reduce);
                }
                addGoods2CartAnim(iv_goods_add);
                tv_goods_count.setText(String.valueOf(count));
                shopGoodsBean.setCount(count);
            }
        });
通过getViewTreeObserver获取图标的位置,计算距离,做属性动画,同时通过数据层记录,要注意的是,如果手指点击点击过快的时候,会使减少的变为负数,所以我们要做判断,如果为0不减少
仿饿了么添加购物车效果_第3张图片
你可以看到,减少的图标不见了,而且点击也没效果,很简单,主要是RecycleView的item复用机制,属性动画改变控件位置,所以做完动画位置还原就好了
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);
            }
        });
    }

4 贝塞尔曲线动画

一个二阶贝塞尔动画,没有涉及到动态点,比较简单,在你点击的时候,在最外层布局添加一个ImageView,做值动画,动画结束的时候,移除该控件
/**
     * 贝塞尔曲线动画
     *
     * @param goodsImageView
     */
    public void addGoods2CartAnim(ImageView goodsImageView) {
        final ImageView goods = new ImageView(ShoppingGoodsActivity.this);
        goods.setImageResource(R.mipmap.icon_goods_add);
        int size = Util.dp2px(ShoppingGoodsActivity.this, 24);
        ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(size, size);
        goods.setLayoutParams(lp);
        mCoordinatorLayout.addView(goods);
        // 控制点的位置
        int[] recyclerLocation = new int[2];
        mCoordinatorLayout.getLocationInWindow(recyclerLocation);
        // 加入点的位置起始点
        int[] startLocation = new int[2];
        goodsImageView.getLocationInWindow(startLocation);
        // 购物车的位置终点
        int[] endLocation = new int[2];
        mIvShoppingCart.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) {
                // 移除图片
                mCoordinatorLayout.removeView(goods);
                // 购物车数量增加
                mTvShoppingCartCount.setText(String.valueOf(allCount));
            }
        });
    }

5 底部购物车弹窗动画

底部的一个动画效果,大致原理就是,用一个FrameLayout,底部保持不动,上面的ViewGroup做位移动画,同时判断商品个数,动态控制RecycleView的高度
  • 动态设置RecycleView的高度
private void initAdapter() {
        // 如果商品个数大于指定数时,高度写死,其他wrap_content
        if (list.size() >= 4) {
            ViewGroup.LayoutParams lp = mRvCartGoods.getLayoutParams();
            lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
            lp.height = DensityUtil.dp2px(200);
            mRvCartGoods.setLayoutParams(lp);
        }
        mAdapter = new BaseAdapter(list, R.layout.item_cart_goods, this);
        mRvCartGoods.setLayoutManager(new LinearLayoutManager(mActivity));
        mRvCartGoods.setAdapter(mAdapter);
    }
网上找过很多设置最大值的,都没有效果,后面直接改用代码动态设置
  • 动画效果
private void initScreen() {
        WindowManager wm = (WindowManager) mActivity.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics dm = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(dm);
        // 获取屏幕的高度
        mHeightPixels = dm.heightPixels;
    }

    public void openAnim() {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mLlShoppingCart, "translationY", mHeightPixels, 0);
        objectAnimator.setDuration(TIME);
        objectAnimator.start();
    }

    public void closeAnim() {
        ObjectAnimator objectAnimator = ObjectAnimator.ofFloat(mLlShoppingCart, "translationY", 0, mHeightPixels);
        objectAnimator.setDuration(TIME);
        objectAnimator.start();
        objectAnimator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                dismiss();
            }
        });
    }
可以看到,在关闭的时候做了一个动画监听,直接调用dismiss()方法,会直接销毁掉,我们要等到动画执行完毕之后在销毁掉
购物车缓存
storeId为文件名,同时内部用一个map集合储存数据,key为goodsID,value为count,allCount为商铺总数
    /**
   * 添加商品缓存
   *
   * @param storeId 商品的id
   */
  public ShoppingCartHistoryManager add(String storeId, @NonNull StoreGoodsBean storeGoodsBean) {
    File file = new File(PATH);
    if (!file.exists()) {
      file.mkdirs();
    }
    FileOutputStream fileOutputStream = null;
    ObjectOutputStream objectOutputStream = null;
    try {
      fileOutputStream = new FileOutputStream(file.getAbsolutePath() + File.separator + storeId + FILE_FORMAT);
      objectOutputStream = new ObjectOutputStream(fileOutputStream);
      objectOutputStream.writeObject(storeGoodsBean);
      objectOutputStream.flush();
      objectOutputStream.close();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return this;
  }

  /**
   * 得到商品缓存
   *
   * @param storeId 商铺的id
   */
  public HashMap get(String storeId) {
    File file = new File(PATH);
    if (!file.exists()) {
      return null;
    }
    FileInputStream FileInputStream = null;
    ObjectInputStream objectInputStream = null;
    StoreGoodsBean storeGoodsBean = null;
    try {
      FileInputStream = new FileInputStream(file.getAbsolutePath() + File.separator + storeId + FILE_FORMAT);
      objectInputStream = new ObjectInputStream(FileInputStream);
      storeGoodsBean = (StoreGoodsBean) objectInputStream.readObject();
      objectInputStream.close();
    } catch (Exception e) {
      e.printStackTrace();
      return null;
    }
    return storeGoodsBean.getHashMap();
  }

  /**
   * 获取商铺选择的总个数
   *
   * @param storeId 商铺id
   * @return
   */
  public int getAllGoodsCount(String storeId) {
    HashMap hashMap = get(storeId);
    int allCount = 0;
    if (hashMap != null) {
      for (Map.Entry entry : hashMap.entrySet()) {
        Integer value = entry.getValue();
        if (value != 0) {
          allCount += value;
        }
      }
    }
    return allCount;
  }

  /**
   * 删除商铺缓存,如果数量为0
   *
   * @param storeId 商铺的id
   */
  public ShoppingCartHistoryManager delete(@NonNull String storeId) {
    File file = new File(PATH, storeId + FILE_FORMAT);
    if (file.exists()) {
      file.delete();
    }
    return this;
  }

    /**
     * 删除商铺缓存,如果数量为0
     *
     * @param ShopId 商铺的id
     */
    public ShoppingCartHistoryManager delete(@NonNull int ShopId) {
        File file = new File(PATH, +ShopId + FILE_FORMAT);
        if (file.exists()) {
            file.delete();
        }
        return this;
    }
退出页面时,根据商铺的总数是否为0,选择删除数据(如果文件之前存在)还是保存数据
 @Override
    protected void onDestroy() {
        super.onDestroy();
        if (allCount != 0) {
            HashMap hashMap = new HashMap<>();
            StoreGoodsBean storeGoodsBean = new StoreGoodsBean(hashMap);
            for (ShopGoodsBean bean : mGoodsList) {
                int count = bean.getCount();
                String goodsId = bean.getGoodsId();
                if (count != 0) {
                    hashMap.put(goodsId, count);
                }
            }
            ShoppingCartHistoryManager.getInstance().add(SHOP_ID, storeGoodsBean);
        } else {
            ShoppingCartHistoryManager.getInstance().delete(SHOP_ID);
        }
    }
进入页面的时候,获取缓存数据(如果存在)
private void initData() {
        int id = 0x100;
        HashMap hashMap = ShoppingCartHistoryManager.getInstance().get(SHOP_ID);
        this.allCount = ShoppingCartHistoryManager.getInstance().getAllGoodsCount(SHOP_ID);
        showToast("商品总数" + allCount);
        // 根据缓存是否显示
        mTvShoppingCartCount.setVisibility(allCount == 0 ? View.GONE : View.VISIBLE);
        mTvShoppingCartCount.setText(String.valueOf(allCount));
        // TODO: 2018/6/5 0005 模拟请求到的数据
        for (int i = 0; i < 10; i++) {
            mGoodsList.add(new ShopGoodsBean(0, "小猪包套餐" + i, id++ + ""));
        }
        if (hashMap != null) {
            for (ShopGoodsBean bean : mGoodsList) {
                String goodsId = bean.getGoodsId();
                if (hashMap.containsKey(goodsId)) {
                    Integer count = hashMap.get(goodsId);
                    bean.setCount(count);
                }
            }
        }
    }
在最外面的商铺列表通过storeId获取商铺总数
this.allCount =ShoppingCartHistoryManager.getInstance().getAllGoodsCount(SHOP_ID);
仿饿了么添加购物车效果_第4张图片
github:https://github.com/GongWnbo/SuperRecycleView
有疑问,不足的地方,或更好的方式,请以留言的方式告知,如果觉得不错就顺手给个Star![]

你可能感兴趣的:(仿饿了么添加购物车效果)