仿餓了麼新增購物車動畫
原文網址
ofollow,noindex">https://www.jianshu.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);
以上。