仿餓了麼懸浮購物車按鈕

一、需求分析及思路分析

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

懸浮購物車按鈕顯示

懸浮購物車按鈕半隱藏

用戶沒有觸摸界面時,購物車就正常懸浮在右下角,當界面滑動時,它就自覺地將自身的一半縮到屏幕以外,並且會變得半透明,再也不遮擋底下的內容。到了這一步,相信你們都會想到是用觸摸事件來實現了。android

@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);
    }

觸摸事件有三種,每一步的做用和實現的效果都不同:git

  • 手指按下(ACTION_DOWN):用戶手指在屏幕上按下時,記下此時的y座標做爲起始y座標(startY);
  • 手指擡起(ACTION_UP):獲取此時的座標做爲結束座標與
  • 手指滑動(ACTION_MOVE):根據手指滑動的距離

二、項目建立及佈局編寫

建立一個新項目,MainActivity的佈局以下:github

<?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,並填充了一些數據。app

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));
    }

}

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

三、懸浮按鈕的動畫效果

懸浮按鈕的動畫效果很簡單,就倆:佈局

  • 位移動畫:懸浮按鈕向右平移,直至一半在屏幕以外;
  • 漸變更畫:懸浮按鈕在位移的同時逐漸變得半透明,此處將透明度設爲0.5.

3.1 位移動畫

首先咱們要明確懸浮按鈕的位移距離,以下圖所示:優化

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

咱們沒法直接獲取控件右側到右邊屏幕的距離,可是咱們能夠換個思路,先獲取整個屏幕的寬度,再減去按鈕右側到左邊的距離就好了,然後者可使用getRight輕鬆獲得。獲取手機屏幕寬高可使用下面的方法:this

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。

四、監聽手指觸摸事件

分析完動畫效果以後,咱們如今就要來調用了,前面已經說過了是在觸摸事件中監聽,那麼好的,咱們如今就將觸摸事件和動畫的代碼集合起來吧:

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再讓懸浮圖案還原。固然,不要忘了動畫是要主線程中進行的。

運行一下,就能夠看到以下的效果了:

初始效果

五、優化動畫效果

到如今咱們差很少實現了咱們想要的效果了,可是若是你試着快速滑動一下就會發現一個可怕的問題:
快速滑動出現的問題

頻繁滑動時動畫就會頻繁地觸發,甚至你快速滑動幾回後,懸浮按鈕看起來就像抽了風同樣。這顯然是不行的,咱們接下來就作以下的優化:

  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);
                }
            });
        }
    }

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

GitHub源碼

相關文章
相關標籤/搜索