使用二階貝塞爾曲線實現新增購物車動畫

張欽發表於2018-08-07

一、引入

  1. 其實之前一直以為像餓了麼或者是美團外賣那種把商品新增到購物車的動畫會很難做,但是實際做起來好像並沒有想象中的那麼難哈哈。
  2. 佈局主要使用CoordinatorLayout+AppBarLayout+CollapsingToolbarLayout+TabLayout+ViewPager
  3. 動畫主要使用二階貝塞爾曲線與屬性動畫
  4. 訊息傳遞使用EventBus普通事件

使用二階貝塞爾曲線實現新增購物車動畫

二、大致思路

使用二階貝塞爾曲線實現新增購物車動畫

  1. 如圖所示主要有三個點,起點、終點、以及貝塞爾曲線的控制點

  2. 起點即點選的View的位置,一般來說用如下方式即可取得。startPosition[0]為x軸開始座標,startPosition[1]為Y軸終點座標,兩點可以看作對角線上面的兩個端點(左上角x座標,右下角y座標)

    //貝塞爾起始資料點
    int[] startPosition = new int[2];
    view.getLocationOnScreen(startPosition);
    複製程式碼
  3. 終點即購物車籃子的位置,與起點類似

    mShoppingCart.getLocationInWindow(endPosition);
    複製程式碼
  4. 控制點,我選的控制點為上圖的C點,即A點的y座標,B點的X座標

    controlPosition[0] = endPosition[0];
    controlPosition[1] = startPosition[1];
    複製程式碼
  5. 需要注意的地方,我不清楚是不是因為我的佈局的問題,獲取到的點選的A點總是會有一個偏移,後來經同事提醒,減去了TabLayout的座標的y軸座標即位置才可以。

    // 起點
    int[] startPosition;
    // 終點
    int[] endPosition = new int[2];
    // 貝塞爾控制點
    int[] controlPosition = new int[2];
    // tablayout位置
    int[] tablayoutPosition = new int[2];
    
    startPosition = data.getStartPosition();
    mShoppingCart.getLocationInWindow(endPosition);
    mTabLayout.getLocationInWindow(tablayoutPosition);
    // 處理起點y座標偏移的問題
    startPosition[1] = startPosition[1] - tablayoutPosition[1] - mTabLayout.getHeight();
    // 終點進行一下居中處理
    endPosition[0] = endPosition[0] + (mShoppingCart.getWidth() / 2);
    controlPosition[0] = endPosition[0];
    controlPosition[1] = startPosition[1];
    複製程式碼
  6. 通過PathquadTo方法繪製貝塞爾曲線,使用PathMeasure獲取點的座標(藉助ValueAnimator.ofFloat()配合getPosTan()來獲取座標)

    Path path = new Path();
    path.moveTo(startPosition[0], startPosition[1]);
    path.quadTo(controlPosition[0], controlPosition[1], endPosition[0], endPosition[1]);
    PathMeasure pathMeasure = new PathMeasure();
    // false表示path路徑不閉合
    pathMeasure.setPath(path, false);
    
    // ofFloat是一個生成器
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, pathMeasure.getLength());
    // 勻速線性插值器
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.setDuration(800);
    valueAnimator.addUpdateListener(animation -> {
        float value = (Float) animation.getAnimatedValue();
        pathMeasure.getPosTan(value, currentPosition, null);
        imageView.setX(currentPosition[0]);
        imageView.setY(currentPosition[1]);
    });
    valueAnimator.start();
    複製程式碼
  7. 下面是用屬性動畫給購物車籃子做了一個放大縮小的動畫效果

    // mShoppingCart是View
    ObjectAnimator shoppingCartX = ObjectAnimator.ofFloat(mShoppingCart, "scaleX", 1.0f, 1.3f, 1.0f);
    ObjectAnimator shoppingCartY = ObjectAnimator.ofFloat(mShoppingCart, "scaleY", 1.0f, 1.3f, 1.0f);
    shoppingCartX.setInterpolator(new AccelerateInterpolator());
    shoppingCartY.setInterpolator(new AccelerateInterpolator());
    AnimatorSet shoppingCart = new AnimatorSet();
    shoppingCart
            .play(shoppingCartX)
            .with(shoppingCartY);
    shoppingCart.setDuration(800);
    shoppingCart.start();
    複製程式碼

三、稍完整的大部分程式碼

private void AddAnimation(AddEventBean data) {
    // 起點
    int[] startPosition;
    // 終點
    int[] endPosition = new int[2];
    // 貝塞爾控制點
    int[] controlPosition = new int[2];
    // 當前位置
    float[] currentPosition = new float[2];
    // tablayout位置
    int[] tablayoutPosition = new int[2];

    startPosition = data.getStartPosition();
    mShoppingCart.getLocationInWindow(endPosition);
    mTabLayout.getLocationInWindow(tablayoutPosition);
    // 處理起點y座標偏移的問題
    startPosition[1] = startPosition[1] - tablayoutPosition[1] - mTabLayout.getHeight();
    // 終點進行一下居中處理
    endPosition[0] = endPosition[0] + (mShoppingCart.getWidth() / 2);
    controlPosition[0] = endPosition[0];
    controlPosition[1] = startPosition[1];


    final ImageView imageView = new ImageView(this);
    mConView.addView(imageView);
    imageView.setImageResource(R.drawable.specialadd);
    imageView.getLayoutParams().width = getResources().getDimensionPixelSize(R.dimen.dp_px_30);
    imageView.getLayoutParams().height = getResources().getDimensionPixelSize(R.dimen.dp_px_30);
    imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
    imageView.setVisibility(View.VISIBLE);
    imageView.setX(startPosition[0]);
    imageView.setY(startPosition[1]);

    Path path = new Path();
    path.moveTo(startPosition[0], startPosition[1]);
    path.quadTo(controlPosition[0], controlPosition[1], endPosition[0], endPosition[1]);
    PathMeasure pathMeasure = new PathMeasure();
    // false表示path路徑不閉合
    pathMeasure.setPath(path, false);

    // ofFloat是一個生成器
    ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, pathMeasure.getLength());
    // 勻速線性插值器
    valueAnimator.setInterpolator(new LinearInterpolator());
    valueAnimator.setDuration(800);
    valueAnimator.addUpdateListener(animation -> {
        float value = (Float) animation.getAnimatedValue();
        pathMeasure.getPosTan(value, currentPosition, null);
        imageView.setX(currentPosition[0]);
        imageView.setY(currentPosition[1]);
    });
    valueAnimator.start();

    ObjectAnimator shoppingCartX = ObjectAnimator.ofFloat(mShoppingCart, "scaleX", 1.0f, 1.3f, 1.0f);
    ObjectAnimator shoppingCartY = ObjectAnimator.ofFloat(mShoppingCart, "scaleY", 1.0f, 1.3f, 1.0f);
    shoppingCartX.setInterpolator(new AccelerateInterpolator());
    shoppingCartY.setInterpolator(new AccelerateInterpolator());
    AnimatorSet shoppingCart = new AnimatorSet();
    shoppingCart
            .play(shoppingCartX)
            .with(shoppingCartY);
    shoppingCart.setDuration(800);
    shoppingCart.start();

    valueAnimator.addListener(new Animator.AnimatorListener() {
        @Override
        public void onAnimationStart(Animator animation) {

        }

        //當動畫結束後:
        @Override
        public void onAnimationEnd(Animator animation) {
            goodsChange(data);
        }

        @Override
        public void onAnimationCancel(Animator animation) {

        }

        @Override
        public void onAnimationRepeat(Animator animation) {

        }
    });
}
複製程式碼

四、大致寫下佈局(同時也算留做備份)

使用二階貝塞爾曲線實現新增購物車動畫

<?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"
    ... ...>

    <RelativeLayout
        ... ...>

        頂部常駐的toolbar

    </RelativeLayout>

    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <android.support.design.widget.AppBarLayout
            ... ...>

            <android.support.design.widget.CollapsingToolbarLayout
                ... ...
                app:layout_scrollFlags="scroll|exitUntilCollapsed">

                <LinearLayout
                    ... ...>

                    TabLayout上面的View
                    
                </LinearLayout>

            </android.support.design.widget.CollapsingToolbarLayout>

            <android.support.design.widget.TabLayout
                ... ... />

        </android.support.design.widget.AppBarLayout>

        <RelativeLayout
            ... ...
            android:fillViewport="true"
            app:layout_behavior="@string/appbar_scrolling_view_behavior">

            <android.support.v4.view.ViewPager
                android:id="@+id/view_pager"
                android:layout_width="match_parent"
                android:layout_height="match_parent" />
        </RelativeLayout>

    </android.support.design.widget.CoordinatorLayout>

    <LinearLayout
        ... ...>

        最下面的購物車一欄
        
    </LinearLayout>
</LinearLayout>
複製程式碼

五、推薦資源

  1. View的位置引數

  2. Path之貝塞爾曲線

相關文章