Android實踐之ScrollView中滑動衝突處理,mapView百度地圖滑動衝突

原文出處:http://blog.csdn.net/xiaohanluo/article/details/52130923

1. 前言

       在Android開發中,如果是一些簡單的佈局,都很容易搞定,但是一旦涉及到複雜的頁面,特別是爲了兼容小屏手機而使用了ScrollView以後,就會出現很多點擊事件的衝突,最經典的就是ScrollView中嵌套了ListView。我想大部分剛開始接觸Android的同學們都踩到過這個坑,這一篇文章就從最近做的一個項目講起,然後在過程中提供一些解決衝突的思路。

2. 項目起始

       項目有一個頁面,涉及到了ViewPager,MapView,ListView,也就是說在一個頁面中,會有這三個View,很明顯,屏幕無法完全顯示,需要ScrollView來做一下支援,就引入了ScrollView作爲外層的容器。但是由於這個頁面的數據展示需要做到用戶手動下拉刷新,於是又引入了官方的SwipeRefreshLayout。
於是這個頁面的佈局就成了這樣子。如下圖(細節佈局忽略)。


圖-1 佈局圖

       加入了ScrollView和SwipeRefreshLayout之後引入了新的問題,就是各個控件之間的事件衝突,嵌套在ScrollView中的ViewPager、MapView、ListView都需要能夠正確的處理點擊事件,特別是ListView,需求要求它在ScrollView中可以滑動,兩種滑動混淆在一起,不是特別好處理。

問題提出來了,下面直接看解決思路。

3. 解決滑動衝突的思路

       在ViewGroup中有個方法叫requestDisallowInterceptTouchEvent(boolean disallowIntercept),這個方法可以用來控制該ViewGroup是否截斷點擊事件。我們解決滑動衝突的時候,其實就是在某個時機去調用這個方法,讓父佈局不截斷點擊事件,將點擊事件傳遞到子View,讓相關的子View去處理。
       下面就是關於在項目中處理各種點擊事件衝突的一些例子和思考。處理的方法只是提供一種思路,可能並不是最優的方法,肯定存在其他思路的解決方案。
       以下處理滑動衝突的方案都是在子View的OnTouchListener裏面進行處理,並沒有去複寫控件的點擊事件處理過程,在使用中還是比較方便的。

3.1 MapView地圖頁面滑動衝突

       MapView與ScrollView的衝突主要在於,當用戶點擊到MapView地圖並且滑動的時候,希望由地圖Map去處理點擊事件,幷包括後續的滑動事件、雙手指縮放地圖等等。
       在ScrollView中,是會默認截斷點擊事件的,導致用戶點擊到地圖後,地圖基本是沒有反應,更別談雙手指縮放地圖了。
       用戶手指點擊到地圖,並且滑動的時候,很難確定用戶是要ScrollView上下滑動還是操控地圖內容滑動,所以我簡單的認爲,只要用戶手指點擊到地圖,就是要對地圖進行操作;當用戶手指擡起,就認爲用戶不需要操作地圖了。
       解決思路也很簡單,就是在用戶點擊到地圖或者滑動地圖時候,讓ScrollView不截斷點擊事件,並傳遞給子View處理,也就是地圖去處理點擊事件;當用戶手指擡起的時候,將ScrollView的狀態恢復至之前的狀態,也就是ScrollView可以截斷點擊事件。

我使用的是百度地圖,直接上代碼,更容易理解。

mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                if(event.getAction() == MotionEvent.ACTION_UP){
                    //允許ScrollView截斷點擊事件,ScrollView可滑動
                    mScrollView.requestDisallowInterceptTouchEvent(false);
                }else{
                    //不允許ScrollView截斷點擊事件,點擊事件由子View處理
                    mScrollView.requestDisallowInterceptTouchEvent(true);
                }
                return false;
            }
        });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

3.2 ViewPager滑動衝突解決

       在這個項目中,ViewPager在頁面最頂層,如果只是ScrollView裏面嵌套了ViewPager,因爲這兩個控件是不同方向的滑動事件,所以基本不會出現衝突。
       但是由於引入了SwipeRefreshLayout,我發現在滑動ViewPager的時候,很容易就觸發了SwipeRefreshLayout的下來刷新,進而有可能阻斷了ViewPager的左右滑動效果,體驗很不好。而且在滑動ViewPager的過程中,用戶滑動肯定不是一直水平的,會有一定程度向上向下的滑動。
       ViewPager處理衝突和地圖處理衝突有些不同,因爲當用戶點擊到ViewPager,在滑動過程中,基本就可以猜測到用戶是想左右滑動ViewPager還是上下滑動ScrollView(或者下拉刷新),這就不能像地圖一樣,在點擊到ViewPager就禁止ScrollView截斷點擊事件(或者SwipeRefreshLayout下拉刷新功能),需要在滑動過程中做出判斷。
       解決思路就是,設定一個閾值,一旦用戶在X軸也就是橫向滑動距離超過這個閾值,我就認爲用戶是要左右滑動ViewPager,就禁止ScrollView截斷點擊事件同時設置SwipeRefreshLayout不能下拉刷新。當用戶擡起手指,就認爲用戶對ViewPager的操作已經完畢,將ScrollView和SwipeRefreshLayout狀態恢復。

mViewPager.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();

        if(action == MotionEvent.ACTION_DOWN) {
            // 記錄點擊到ViewPager時候,手指的X座標
            mLastX = event.getX();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            // 超過閾值
            if(Math.abs(event.getX() - mLastX) > 60f) {
                mRefreshLayout.setEnabled(false);
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        if(action == MotionEvent.ACTION_UP) {
            // 用戶擡起手指,恢復父佈局狀態
            mScrollView.requestDisallowInterceptTouchEvent(false);
            mRefreshLayout.setEnabled(true);
        }
        return false;
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

       用戶點擊到ViewPager時候,記錄下點擊位置的X座標,當用戶滑動過程中,如果在X軸上面的滑動超過閾值(我寫的是60f,這個可以在實際使用中自行設置最佳的閾值),就禁止ScrollView截斷點擊事件,同時設置不可下拉刷新。當用戶手指離開屏幕,將ScrollView和SwipeRefreshLayout的狀態恢復。

3.3 ListView滑動衝突解決

       在ScrollView中嵌套ListView,會出現各種各樣奇怪的問題。比如說ListView顯示有問題,可能才一兩個Item那麼高,並沒有完全的展開。網上流傳解決這種問題的方法會有兩種。

  • 根據展示數據的個數乘以每一個Item的高度,計算出ListView的總體高度,然後動態的設置ListView的高度
  • 複寫ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,讓ListView完全展開

       這兩種方法都可以解決ListView展示不完全的問題,而且也可以滑動(其實是使用ScrollView的滑動效果),但是有一個最大的遺憾,就是ListView裏面的View不能複用了。因爲這兩種方法都是算出了ListView的全部高度,然後將ListView控件的高度設置成這個高度,這樣的話,ListView就相當於一個LinearLayout的佈局了,失去了複用View的優勢,而且在某些場景可能還沒有LinearLayout好用,更甚的是,如果有大量圖片的話,很容易就OOM了,這是在研發過程中最不希望看見的。
       可以參考一下美團,美團的首頁,就是一個ScrollView,下滑的時候會發現,並不能無限向下滑動,到了底部會提醒跳轉到一個二級頁面去查看全部的團購信息。這是處理ScrollView裏面嵌套類似ListView列表佈局的時候的一種解決方案。
但是在我遇見的這個項目裏面,並不能這樣處理。
       上面的提到的兩種解決思路很明確,如果想要ListView正常展示就需要確定ListView的高度,這個很重要。
       所以首先,我需要在佈局文件中設置ListView的高度,是一個明確的數值。設置高度之後,如果ListView中的數據的Item總高度超過ListView所設置的高度,就可以複用View了。但是這只是解決了ListView的顯示問題,ListView與ScrollView的滑動衝突,並沒有解決。
       要解決滑動的衝突,最主要的是確定禁止ScrollView截斷點擊事件的時機,然後來分析有哪些時機。

  • ScrollView在未滑動到底部時候,如果點擊並滑動ListView時候,ListView是不能滑動的,不禁止。
  • 如果ScrollView滑動到底部,且ListView已經到頂部,繼續下拉ListView,其實會拉動ScrollView,不禁止。
  • 如果ScrollView滑動到底部,用戶向上滑,ListView滑動,禁止ScrollView截斷點擊事件能力

       很明顯,在判斷禁止ScrollView截斷點擊事件時機的時候,需要知道ScrollView是否滑動到了底部。於是,重寫了ScrollView的ScrollChanged()方法,來判斷ScrollView是否滑動到底部(SDK API 23版本中ScrollView可以設置setOnScrollChangeListener()來監聽滑動的變化,但是之前版本不支持,爲了兼容,自己需要重寫)。

@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt){
    super.onScrollChanged(l,t,oldl,oldt);
    // 滑動的距離加上本身的高度與子View的高度對比
    if(t + getHeight() >=  getChildAt(0).getMeasuredHeight()){
        // ScrollView滑動到底部
        if(mOnScrollToBottomListener != null) {
            mOnScrollToBottomListener.onScrollToBottom();
        }
    } else {
        if(mOnScrollToBottomListener != null) {
            mOnScrollToBottomListener.onNotScrollToBottom();
        }
    }
}

public void setScrollToBottomListener(OnScrollToBottomListener listener) {
    this.mOnScrollToBottomListener = listener;
}

public interface OnScrollToBottomListener {
    void onScrollToBottom();
    void onNotScrollToBottom();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

有了思路,而且ScrollView滑動到底部的標識也可以拿到,下面就可以直接來解決滑動衝突了,直接看代碼。

mScrollView.setScrollToBottomListener(new BottomScrollView.OnScrollToBottomListener() {
    @Override
    public void onScrollToBottom() {
        isSvToBottom = true;
    }

    @Override
    public void onNotScrollToBottom() {
        isSvToBottom = false;
    }
});

mListView.setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        int action = event.getAction();

        if(action == MotionEvent.ACTION_DOWN) {
            mLastY = event.getY();
        }
        if(action == MotionEvent.ACTION_MOVE) {
            int top = mListView.getChildAt(0).getTop();
            float nowY = event.getY();
            if(!isSvToBottom) {
                // 允許scrollview攔截點擊事件, scrollView滑動
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
                // 允許scrollview攔截點擊事件, scrollView滑動
                mScrollView.requestDisallowInterceptTouchEvent(false);
            } else {
                // 不允許scrollview攔截點擊事件, listView滑動
                mScrollView.requestDisallowInterceptTouchEvent(true);
            }
        }
        return false;
    }
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37

       相對於其他的控件來說,ListView和ScrollView之間的滑動衝突更難解決,但其實在實際使用中並不推薦ScrollView裏面嵌套ListView,一旦業務複雜,很容易出現各種UI和業務邏輯衝突的錯誤。

4. 運行效果

由於地圖加入比較麻煩,所以在Demo中並沒有引入地圖。看一下運行效果。



圖-2 運行效果

5. 總結

       本篇文章只是提供一種解決方法的思路,在具體的場景下,交互往往是貼合具體業務需求的。但是不管怎麼樣,找出點擊事件截斷和處理的時機是最重要的,圍繞這個關鍵點,總能找出相應的解決方法。

附上Demo工程地址:Demo工程地址鏈接


發佈了45 篇原創文章 · 獲贊 46 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章