加入購物車動畫效果實現

weixin_34247155發表於2018-08-15

不知道大家有沒有發現,主流的電商類APP新增商品到購物車時,都會伴隨一個小的“新增”動畫。你有沒有想過它是怎麼實現的呢?今天我們就一起來學習下。

其實實現這個效果很簡單,主要涉及到兩個知識點:貝塞爾曲線和自定義Evaluator估值器。下面我先簡單介紹下這兩個知識點,如果你之前有了解過這兩塊的話,可以直接翻到下面看下實現程式碼。

貝塞爾曲線是數值分析領域的重要引數曲線,在我們的生活中隨處都可以看到它的影子,比如:QQ聊天氣泡的拖拽效果、直播室送花點贊效果、電量水波紋效果等等。貝塞爾曲線可以分為一階貝塞爾曲線、二階貝塞爾曲線、三階貝塞爾曲線 .....。在這裡我們用到了二階貝塞爾曲線,下面我們先來看下它的計算公式:

12936399-e253048d476bd674.jpg
公式.jpg
公式中,B(t) 的值隨時間 t 變化,B(t)運動點的運動軌跡就形成了二階貝塞爾曲線,P0是起始點,P1是控制點,P2是終點。這裡我找了一個二階貝塞爾曲線的動畫,更直觀一些:https://img-blog.csdn.net/20160328202508739

相信大家在開發過程中都有用到過屬性動畫吧,屬性動畫中有兩個很重要的知識點,那就是差值器Interpolator和估值器Evaluator。簡單來講,差值器就類似於我們物理中所學的“加速度”,比如我們的執行動畫需要先加速再減速。Android系統為我們內建了幾種常見的差值器,在某些特定情況下不能滿足需要的話,我們就需要實現TimeInterpolator介面實現自定義差值器。估值器Evaluator其實就是一個轉換器,他能把小數進度轉換成對應的具體數值,我們可以實現TypeEvaluator介面來自定義估值器。

我們先思考一下,加入購物車動畫效果就相當於數學中常見的拋物線,可以藉助二階貝塞爾曲線來實現,那我們怎麼確定起始點P0、控制點P1、終點P2的位置呢?起始點P0的位置就是我們的商品新增按鈕所在位置,終點P2位置就是介面左下角購物車Icon圖示所在的位置,控制點的位置要怎麼選取呢?在這裡我們可以沿新增按鈕水平向左,沿購物車Icon圖示豎直向上,兩條線的交點處正可以作為我們的控制點座標,到此三個點正好構成一個倒立的直角三角形。控制點P1的橫座標等於終點P2的橫座標,控制點P1的縱座標等於起始點P0的縱座標。下面我們就可以進行編碼工作了。

首先自定義估值器CartEvaluator:

public class CartEvaluator implements TypeEvaluator<PointF>{

    private PointF pointCur;
    private PointF mControlPoint;

    public CartEvaluator(PointF mControlPoint) {

        this.mControlPoint = mControlPoint;
        pointCur = new PointF();
    }

    @Override
    public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
       // 將二階貝塞爾曲線的計算公式直接代入即可
        pointCur.x = (1 - fraction) * (1 - fraction) * startValue.x
                + 2 * fraction * (1 - fraction) * mControlPoint.x + fraction * fraction * endValue.x;
        pointCur.y = (1 - fraction) * (1 - fraction) * startValue.y
                + 2 * fraction * (1 - fraction) * mControlPoint.y + fraction * fraction * endValue.y;

        return pointCur;
    }
}

在這裡我們建立了一個pointCur物件,專門用來儲存當前移動點的座標。

下面看下Demo中的佈局檔案效果,程式碼就不貼出來了,就是佈局左下角放置了一個購物車圖示,右上角放置了三個新增按鈕,用來模擬新增商品操作:
12936399-0e4f7cbbf47b847a.jpg
佈局檔案

最後看下Activity中的程式碼實現:

public class MainActivity extends AppCompatActivity {

    private ImageView mAddOne;
    private ImageView mAddTwo;
    private ImageView mAddThree;
    private ImageView mCart;
    private ViewGroup mRootView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();

        initListener();
    }

    // 初始化控制元件
    private void initView(){
        mRootView = (ViewGroup) getWindow().getDecorView();
        mCart = findViewById(R.id.mCart);
        mAddOne = findViewById(R.id.mAddOne);
        mAddTwo = findViewById(R.id.mAddTwo);
        mAddThree = findViewById(R.id.mAddThree);
    }

    // 初始化監聽
    private void initListener(){
        mAddOne.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                playAnim(view);
            }
        });

        mAddTwo.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                playAnim(view);
            }
        });

        mAddThree.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                playAnim(view);
            }
        });
    }
    
    // 執行動畫
    private void playAnim(final View view){

        //建立int陣列,用來接收貝塞爾起始點座標和終點座標值
        int[] startPosition = new int[2];
        int[] endPosition = new int[2];

        view.getLocationInWindow(startPosition);
        mCart.getLocationInWindow(endPosition);

        PointF startF = new PointF();        //起始點 startF
        PointF endF = new PointF();          //終點 endF
        PointF controlF = new PointF();      //控制點 controlF

        startF.x = startPosition[0];
        startF.y = startPosition[1] ;
        endF.x = endPosition[0]+mCart.getWidth()/2-view.getWidth()/2;             //微調處理,確保動畫執行完畢“新增”圖示中心點與購物車中心點重合
        endF.y = endPosition[1]+mCart.getHeight()/2 - view.getHeight()/2;
        controlF.x = endF.x;
        controlF.y = startF.y;
 
        // 建立執行動畫的“新增”圖示
        final ImageView imageView = new ImageView(this);           
        mRootView.addView(imageView);
        imageView.setImageResource(R.mipmap.cartadd);
        imageView.getLayoutParams().width = view.getMeasuredWidth();
        imageView.getLayoutParams().height = view.getMeasuredHeight();

        ValueAnimator valueAnimator = ValueAnimator.ofObject(new CartEvaluator(controlF), startF, endF);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF) animation.getAnimatedValue();
                imageView.setX(pointF.x);
                imageView.setY(pointF.y);
            }
        });

        valueAnimator.addListener(new AnimatorListenerAdapter() {

            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                // 動畫執行完畢,將執行動畫的“新增”圖示移除掉
                mRootView.removeView(imageView);
                
                // 執行購物車縮放動畫
                AnimatorSet animatorSet = new AnimatorSet();
                ObjectAnimator animatorX = ObjectAnimator.ofFloat(mCart, "scaleX", 1f, 1.2f, 1f);
                ObjectAnimator animatorY = ObjectAnimator.ofFloat(mCart, "scaleY", 1f, 1.2f, 1f);
                animatorSet.play(animatorX).with(animatorY);
                animatorSet.setDuration(400);
                animatorSet.start();
            }
        });

        valueAnimator.setDuration(800);
        valueAnimator.start();
    }
}

程式碼中相關地方都標上註釋了,相信大家都能夠理解,整體程式碼量還是很少的。最後我們看下實現效果:
12936399-914ff923155d8ed7.gif
luping.gif

相關文章