1. 程式人生 > >仿餓了麼懸浮購物車按鈕

仿餓了麼懸浮購物車按鈕

1、需求分析及思路分析

今天新鮮出爐的需求來了:產品要在首頁上放置一個懸浮圖示,這個圖示既起著宣傳的作用(圖示上面有活動標題),也是一個按鈕,點選之後能跳轉到某個詳情頁面。而且為了使用者體驗更好,在滑動介面時,這個圖示要乖乖地藏起來,不能影響使用者操作。我仔細分析了一下,喲,這不就是中午點外賣時用的餓了麼上面的購物車按鈕麼?

懸浮購物車按鈕顯示

懸浮購物車按鈕半隱藏

使用者沒有觸控介面時,購物車就正常懸浮在右下角,當介面滑動時,它就自覺地將自身的一半縮到螢幕之外,而且會變得半透明,不再遮擋底下的內容。到了這一步,相信大家都會想到是用觸控事件來實現了。

    @Override
    public boolean dispatchTouchEvent
(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_MOVE: break; case MotionEvent.ACTION_UP: break; } return super.dispatchTouchEvent(event); }

觸控事件有三種,每一步的作用和實現的效果都不一樣:
- 手指按下(ACTION_DOWN):使用者手指在螢幕上按下時,記下此時的y座標作為起始y座標(startY);
- 手指擡起(ACTION_UP):獲取此時的座標作為結束座標與
- 手指滑動(ACTION_MOVE):根據手指滑動的距離

2、專案建立及佈局編寫

建立一個新專案,MainActivity的佈局如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.lindroid.floatshoppingcart.MainActivity">
<ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"> </ListView> <ImageView android:id="@+id/iv_cart" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:layout_marginBottom="55dp" android:layout_marginRight="20dp" android:src="@mipmap/ic_shopping_cart" android:layout_width="50dp" android:layout_height="50dp" /> </RelativeLayout>

右下角的ImageView就是我們的主角,圖示是我自己找的購物車圖示。為了模擬介面滑動,我在底下簡單放了一個ListView,並填充了一些資料。

public class MainActivity extends AppCompatActivity {
    private ListView listView;
    private ImageView ivCart;
    private List<String> titles = new ArrayList<>();

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

    private void initData() {
        for (int i = 0; i < 60; i++) {
            titles.add(new StringBuffer("這是一條資料").append(i).toString());
        }
    }

    private void initView() {
        listView = (ListView) findViewById(R.id.listView);
        ivCart = (ImageView) findViewById(R.id.iv_cart);
        listView.setAdapter(new ArrayAdapter<>(this,android.R.layout.simple_list_item_1,titles));
    }

}

此時的效果如下:
佈局效果圖
佈局寫完,下面就來實現我們想要的效果了。

3、懸浮按鈕的動畫效果

懸浮按鈕的動畫效果很簡單,就倆:
- 位移動畫:懸浮按鈕向右平移,直至一半在螢幕之外;
- 漸變動畫:懸浮按鈕在位移的同時逐漸變得半透明,此處將透明度設為0.5.

3.1 位移動畫

首先我們要明確懸浮按鈕的位移距離,如下圖所示:

平移示意圖
懸浮按鈕的總位移等於它的右側到右邊螢幕的距離(藍線),再加上它的半徑(紫線)。半徑我們可以用
getMeasuredWidth獲取它的寬度再除於2,那麼藍線的長度呢?

我們無法直接獲取控制元件右側到右邊螢幕的距離,但是我們可以換個思路,先獲取整個螢幕的寬度,再減去按鈕右側到左邊的距離就行了,而後者可以使用getRight輕鬆得到。獲取手機螢幕寬高可以使用下面的方法:

    private int[] getDisplayMetrics(Context context) {
        DisplayMetrics mDisplayMetrics = new DisplayMetrics();
        ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
        int W = mDisplayMetrics.widthPixels;
        int H = mDisplayMetrics.heightPixels;
        int array[] = {W, H};
        return array;
    }

3.2 漸變動畫

漸變動畫就比較簡單了,只需要設定起始透明度和結束透明度即可。

3.3 動畫集合

兩種動畫是同時開始和結束的,我們可以設定一個動畫集合:

    private void hideFloatImage(int distance) {
        isShowFloatImage = false;
        //位移動畫
        TranslateAnimation ta = new TranslateAnimation(
                0,//起始x座標,10表示與初始位置相距10
                distance,//結束x座標
                0,//起始y座標
                0);//結束y座標(正數向下移動)
        ta.setDuration(300);

        //漸變動畫
        AlphaAnimation al = new AlphaAnimation(1f, 0.5f);
        al.setDuration(300);

        AnimationSet set = new AnimationSet(true);
        //動畫完成後不回到原位
        set.setFillAfter(true);
        set.addAnimation(ta);
        set.addAnimation(al);
        ivCart.startAnimation(set);
    }

動畫發生之後不需要歸位,所以記得setFillAfter要設為true。

3.4 懸浮按鈕迴歸原位的動畫

前面我們討論的都是介面滑動,按鈕向右隱藏的動畫,而使用者的手指離開螢幕時,懸浮按鈕是要回歸原位,這時候的動畫效果就跟之前的相反了,所以只需小小修改一下引數即可:

    private void showFloatImage(int distance) {
        isShowFloatImage = false;
        //位移動畫
        TranslateAnimation ta = new TranslateAnimation(
                distance,//起始x座標
                0,//結束x座標
                0,//起始y座標
                0);//結束y座標(正數向下移動)
        ta.setDuration(300);

        //漸變動畫
        AlphaAnimation al = new AlphaAnimation(1f, 0.5f);
        al.setDuration(300);

        AnimationSet set = new AnimationSet(true);
        //動畫完成後不回到原位
        set.setFillAfter(true);
        set.addAnimation(ta);
        set.addAnimation(al);
        ivCart.startAnimation(set);
    }

注意一下這裡的位移動畫的起始座標。由於補間動畫的特性,動畫發生位移之後,移動的只是控制元件的內容,而不是控制元件本身,所以我們要以控制元件所在位置為座標原點,而不是發生位移後的內容!故這裡的起始座標是水平移動的距離,結束座標是回到座標原點,也就是0。

4、監聽手指觸控事件

分析完動畫效果之後,我們現在就要來呼叫了,前面已經說過了是在觸控事件中監聽,那麼好的,我們現在就將觸控事件和動畫的程式碼集合起來吧:

    private float startY;
    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                startY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(startY - event.getY()) > 10) {
                    hideFloatImage(moveDistance);
                }
                startY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                new Handler(new Handler.Callback() {
                    @Override
                    public boolean handleMessage(Message msg) {
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                showFloatImage(moveDistance);
                            }
                        });
                        return false;
                    }
                }).sendEmptyMessageDelayed(0, 1500);
                break;
        }
        return super.dispatchTouchEvent(event);
    }

手指按下時,我們只需要獲取到起始座標。這裡要注意的是我們的手指在手機螢幕上的觸控是一個面(手指的與螢幕的接觸面積)而不僅僅是一個點,所以MotionEvent.ACTION_MOVE是很容易就觸發的。為了避免使用者手指一按下懸浮按鈕就移動,我們可以設定一個值,當手指滑動的距離超過它時才視為有效的滑動。手指擡起時則延遲1.5s再讓懸浮圖案還原。當然,不要忘了動畫是要主執行緒中進行的。

執行一下,就可以看到如下的效果了:

初始效果

5、優化動畫效果

到現在我們差不多實現了我們想要的效果了,但是如果你試著快速滑動一下就會發現一個可怕的問題:
快速滑動出現的問題

頻繁滑動時動畫就會頻繁地觸發,甚至你快速滑動幾次後,懸浮按鈕看起來就像抽了風一樣。這顯然是不行的,我們接下來就做如下的優化:
1. 當懸浮按鈕處於顯示狀態時,不會觸發顯示動畫,處於隱藏狀態時,不會觸發隱藏動畫;
2. 當手指按下擡起,延時執行顯示動畫的1.5s內,如果使用者再次按下擡起手指,則中止之前的動畫,並重新計算延遲時間。

優化1

這個我們只需要加一個布林值isShowFloatImage來控制即可。每次呼叫動畫判斷一下。

優化2

之前是通過Handler傳送延遲訊息來執行動畫的,這樣無法控制動畫的中止。那麼現在,我們就需要用另外一種方法來控制顯示動畫了。這裡我選擇了Java的計時器Timer。當用戶的手指擡起時,我們記下當前時間upTime,下次使用者再次按下手指時將當前時間與upTime比較,差值小於1.5s的話則中止動畫。

完成上面兩步優化之後的程式碼如下:

    private Timer timer;
    /**使用者手指按下後擡起的實際*/
    private long upTime;

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                if (System.currentTimeMillis() - upTime < 1500) {
                    //本次按下距離上次的擡起小於1.5s時,取消Timer
                    timer.cancel();
                }
                startY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                if (Math.abs(startY - event.getY()) > 10) {
                    if (isShowFloatImage){
                        hideFloatImage(moveDistance);
                    }
                }
                startY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                if (!isShowFloatImage){
                    //開始1.5s倒計時
                    upTime = System.currentTimeMillis();
                    timer = new Timer();
                    timer.schedule(new FloatTask(), 1500);
                }
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(event);
    }

    class FloatTask extends TimerTask {
        @Override
        public void run() {
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    showFloatImage(moveDistance);
                }
            });
        }
    }

再次執行一下,就會發現動畫不會頻繁地觸發,比之前的體驗更好了。