淺談Android實踐之ScrollView中滑動沖突處理解決方案
1. 前言
在Android開發(fā)中,如果是一些簡單的布局,都很容易搞定,但是一旦涉及到復(fù)雜的頁面,特別是為了兼容小屏手機而使用了ScrollView以后,就會出現(xiàn)很多點擊事件的沖突,最經(jīng)典的就是ScrollView中嵌套了ListView。我想大部分剛開始接觸Android的同學(xué)們都踩到過這個坑,這一篇文章就從最近做的一個項目講起,然后在過程中提供一些解決沖突的思路。
2. 項目起始
項目有一個頁面,涉及到了ViewPager,MapView,ListView,也就是說在一個頁面中,會有這三個View,很明顯,屏幕無法完全顯示,需要ScrollView來做一下支援,就引入了ScrollView作為外層的容器。但是由于這個頁面的數(shù)據(jù)展示需要做到用戶手動下拉刷新,于是又引入了官方的SwipeRefreshLayout。
于是這個頁面的布局就成了這樣子。如下圖(細節(jié)布局忽略)。
圖-1 布局圖
加入了ScrollView和SwipeRefreshLayout之后引入了新的問題,就是各個控件之間的事件沖突,嵌套在ScrollView中的ViewPager、MapView、ListView都需要能夠正確的處理點擊事件,特別是ListView,需求要求它在ScrollView中可以滑動,兩種滑動混淆在一起,不是特別好處理。
問題提出來了,下面直接看解決思路。
3. 解決滑動沖突的思路
在ViewGroup中有個方法叫requestDisallowInterceptTouchEvent(boolean disallowIntercept),這個方法可以用來控制該ViewGroup是否截斷點擊事件。我們解決滑動沖突的時候,其實就是在某個時機去調(diào)用這個方法,讓父布局不截斷點擊事件,將點擊事件傳遞到子View,讓相關(guān)的子View去處理。
下面就是關(guān)于在項目中處理各種點擊事件沖突的一些例子和思考。處理的方法只是提供一種思路,可能并不是最優(yōu)的方法,肯定存在其他思路的解決方案。
以下處理滑動沖突的方案都是在子View的OnTouchListener里面進行處理,并沒有去復(fù)寫控件的點擊事件處理過程,在使用中還是比較方便的。
3.1 MapView地圖頁面滑動沖突
MapView與ScrollView的沖突主要在于,當(dāng)用戶點擊到MapView地圖并且滑動的時候,希望由地圖Map去處理點擊事件,并包括后續(xù)的滑動事件、雙手指縮放地圖等等。
在ScrollView中,是會默認截斷點擊事件的,導(dǎo)致用戶點擊到地圖后,地圖基本是沒有反應(yīng),更別談雙手指縮放地圖了。
用戶手指點擊到地圖,并且滑動的時候,很難確定用戶是要ScrollView上下滑動還是操控地圖內(nèi)容滑動,所以我簡單的認為,只要用戶手指點擊到地圖,就是要對地圖進行操作;當(dāng)用戶手指抬起,就認為用戶不需要操作地圖了。
解決思路也很簡單,就是在用戶點擊到地圖或者滑動地圖時候,讓ScrollView不截斷點擊事件,并傳遞給子View處理,也就是地圖去處理點擊事件;當(dāng)用戶手指抬起的時候,將ScrollView的狀態(tài)恢復(fù)至之前的狀態(tài),也就是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; } });
3.2 ViewPager滑動沖突解決
在這個項目中,ViewPager在頁面最頂層,如果只是ScrollView里面嵌套了ViewPager,因為這兩個控件是不同方向的滑動事件,所以基本不會出現(xiàn)沖突。
但是由于引入了SwipeRefreshLayout,我發(fā)現(xiàn)在滑動ViewPager的時候,很容易就觸發(fā)了SwipeRefreshLayout的下來刷新,進而有可能阻斷了ViewPager的左右滑動效果,體驗很不好。而且在滑動ViewPager的過程中,用戶滑動肯定不是一直水平的,會有一定程度向上向下的滑動。
ViewPager處理沖突和地圖處理沖突有些不同,因為當(dāng)用戶點擊到ViewPager,在滑動過程中,基本就可以猜測到用戶是想左右滑動ViewPager還是上下滑動ScrollView(或者下拉刷新),這就不能像地圖一樣,在點擊到ViewPager就禁止ScrollView截斷點擊事件(或者SwipeRefreshLayout下拉刷新功能),需要在滑動過程中做出判斷。
解決思路就是,設(shè)定一個閾值,一旦用戶在X軸也就是橫向滑動距離超過這個閾值,我就認為用戶是要左右滑動ViewPager,就禁止ScrollView截斷點擊事件同時設(shè)置SwipeRefreshLayout不能下拉刷新。當(dāng)用戶抬起手指,就認為用戶對ViewPager的操作已經(jīng)完畢,將ScrollView和SwipeRefreshLayout狀態(tài)恢復(fù)。
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) { // 用戶抬起手指,恢復(fù)父布局狀態(tài) mScrollView.requestDisallowInterceptTouchEvent(false); mRefreshLayout.setEnabled(true); } return false; } });
用戶點擊到ViewPager時候,記錄下點擊位置的X坐標,當(dāng)用戶滑動過程中,如果在X軸上面的滑動超過閾值(我寫的是60f,這個可以在實際使用中自行設(shè)置最佳的閾值),就禁止ScrollView截斷點擊事件,同時設(shè)置不可下拉刷新。當(dāng)用戶手指離開屏幕,將ScrollView和SwipeRefreshLayout的狀態(tài)恢復(fù)。
3.3 ListView滑動沖突解決
在ScrollView中嵌套ListView,會出現(xiàn)各種各樣奇怪的問題。比如說ListView顯示有問題,可能才一兩個Item那么高,并沒有完全的展開。網(wǎng)上流傳解決這種問題的方法會有兩種。
- 根據(jù)展示數(shù)據(jù)的個數(shù)乘以每一個Item的高度,計算出ListView的總體高度,然后動態(tài)的設(shè)置ListView的高度
- 復(fù)寫ListView的onMeasure(int widthMeasureSpec, int heightMeasureSpec)方法,讓ListView完全展開
這兩種方法都可以解決ListView展示不完全的問題,而且也可以滑動(其實是使用ScrollView的滑動效果),但是有一個最大的遺憾,就是ListView里面的View不能復(fù)用了。因為這兩種方法都是算出了ListView的全部高度,然后將ListView控件的高度設(shè)置成這個高度,這樣的話,ListView就相當(dāng)于一個LinearLayout的布局了,失去了復(fù)用View的優(yōu)勢,而且在某些場景可能還沒有LinearLayout好用,更甚的是,如果有大量圖片的話,很容易就OOM了,這是在研發(fā)過程中最不希望看見的。
可以參考一下美團,美團的首頁,就是一個ScrollView,下滑的時候會發(fā)現(xiàn),并不能無限向下滑動,到了底部會提醒跳轉(zhuǎn)到一個二級頁面去查看全部的團購信息。這是處理ScrollView里面嵌套類似ListView列表布局的時候的一種解決方案。
但是在我遇見的這個項目里面,并不能這樣處理。
上面的提到的兩種解決思路很明確,如果想要ListView正常展示就需要確定ListView的高度,這個很重要。
所以首先,我需要在布局文件中設(shè)置ListView的高度,是一個明確的數(shù)值。設(shè)置高度之后,如果ListView中的數(shù)據(jù)的Item總高度超過ListView所設(shè)置的高度,就可以復(fù)用View了。但是這只是解決了ListView的顯示問題,ListView與ScrollView的滑動沖突,并沒有解決。
要解決滑動的沖突,最主要的是確定禁止ScrollView截斷點擊事件的時機,然后來分析有哪些時機。
- ScrollView在未滑動到底部時候,如果點擊并滑動ListView時候,ListView是不能滑動的,不禁止。
- 如果ScrollView滑動到底部,且ListView已經(jīng)到頂部,繼續(xù)下拉ListView,其實會拉動ScrollView,不禁止。
- 如果ScrollView滑動到底部,用戶向上滑,ListView滑動,禁止ScrollView截斷點擊事件能力
很明顯,在判斷禁止ScrollView截斷點擊事件時機的時候,需要知道ScrollView是否滑動到了底部。于是,重寫了ScrollView的ScrollChanged()方法,來判斷ScrollView是否滑動到底部(SDK API 23版本中ScrollView可以設(shè)置setOnScrollChangeListener()來監(jiān)聽滑動的變化,但是之前版本不支持,為了兼容,自己需要重寫)。
@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(); }
有了思路,而且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; } });
相對于其他的控件來說,ListView和ScrollView之間的滑動沖突更難解決,但其實在實際使用中并不推薦ScrollView里面嵌套ListView,一旦業(yè)務(wù)復(fù)雜,很容易出現(xiàn)各種UI和業(yè)務(wù)邏輯沖突的錯誤。
4. 運行效果
由于地圖加入比較麻煩,所以在Demo中并沒有引入地圖??匆幌逻\行效果。
圖-2 運行效果
5. 總結(jié)
本篇文章只是提供一種解決方法的思路,在具體的場景下,交互往往是貼合具體業(yè)務(wù)需求的。但是不管怎么樣,找出點擊事件截斷和處理的時機是最重要的,圍繞這個關(guān)鍵點,總能找出相應(yīng)的解決方法。
附上Demo工程地址:Demo
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Android使用 Retrofit 2.X 上傳多文件和多表單示例
本篇文章主要介紹了Android使用 Retrofit 2.X 上傳多文件和多表單示例,具有一定的參考價值,有興趣的小伙伴一起來了解一下2017-08-08Eclipse開發(fā)環(huán)境導(dǎo)入android sdk的sample中的源碼
初學(xué)Android編程,Android SDK中提供的Sample代碼自然是最好的學(xué)習(xí)材料,需要的朋友可以參考下2012-12-12淺談Android PathMeasure詳解和應(yīng)用
本篇文章主要介紹了淺談Android PathMeasure詳解和應(yīng)用,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-01-01