轉載註明出處:http://blog.csdn.net/xiaohanluo/article/details/52130923css
在Android開發中,假設是一些簡單的佈局。都很是easy搞定。但是一旦涉及到複雜的頁面,特別是爲了兼容小屏手機而使用了ScrollView之後,就會出現很是多點擊事件的衝突。最經典的就是ScrollView中嵌套了ListView。java
我想大部分剛開始接觸Android的同窗們都踩到過這個坑,這一篇文章就從近期作的一個項目講起。而後在過程當中提供一些解決衝突的思路。git
項目有一個頁面,涉及到了ViewPager。MapView。ListView,也就是說在一個頁面中,會有這三個View,很是明顯,屏幕沒法全然顯示,需要ScrollView來作一下支援,就引入了ScrollView做爲外層的容器。github
但是因爲這個頁面的數據展現需要作到用戶手動下拉刷新,因而又引入了官方的SwipeRefreshLayout。markdown
因而這個頁面的佈局就成了這樣子。ide
例如如下圖(細節佈局忽略)。佈局
增長了ScrollView和SwipeRefreshLayout以後引入了新的問題。就是各個控件之間的事件衝突,嵌套在ScrollView中的ViewPager、MapView、ListView都需要可以正確的處理點擊事件,特別是ListView。需求要求它在ScrollView中可以滑動,兩種滑動混淆在一塊兒,不是特別優勢理。post
問題提出來了,如下直接看解決思路。this
在ViewGroup中有個方法叫requestDisallowInterceptTouchEvent(boolean disallowIntercept)
,這種方法可以用來控制該ViewGroup是否截斷點擊事件。咱們解決滑動衝突的時候,事實上就是在某個時機去調用這種方法。讓父佈局不截斷點擊事件,將點擊事件傳遞到子View。讓相關的子View去處理。
如下就是關於在項目中處理各類點擊事件衝突的一些樣例和思考。處理的方法僅僅是提供一種思路,可能並不是最優的方法,確定存在其它思路的解決方式。
如下處理滑動衝突的方案都是在子View的OnTouchListener裏面進行處理。並無去複寫控件的點擊事件處理過程。在使用中仍是比較方便的。spa
MapView與ScrollView的衝突主要在於,當用戶點擊到MapView地圖而且滑動的時候。但願由地圖Map去處理點擊事件。幷包含興許的滑動事件、雙手指縮放地圖等等。
在ScrollView中,是會默認截斷點擊事件的,致使用戶點擊到地圖後,地圖基本是沒有反應。更別談雙手指縮放地圖了。
用戶手指點擊到地圖,而且滑動的時候,很是難肯定用戶是要ScrollView上下滑動仍是操控地圖內容滑動,因此我簡單的以爲,僅僅要用戶手指點擊到地圖。就是要對地圖進行操做。當用戶手指擡起。就以爲用戶不需要操做地圖了。
解決思路也很是簡單。就是在用戶點擊到地圖或者滑動地圖時候,讓ScrollView不截斷點擊事件。並傳遞給子View處理。也就是地圖去處理點擊事件;當用戶手指擡起的時候,將ScrollView的狀態恢復至以前的狀態。也就是ScrollView可以截斷點擊事件。
我使用的是百度地圖,直接上代碼,更easy理解。
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;
}
});
在這個項目中,ViewPager在頁面最頂層。假設僅僅是ScrollView裏面嵌套了ViewPager。因爲這兩個控件是不一樣方向的滑動事件,因此基本不會出現衝突。
但是因爲引入了SwipeRefreshLayout,我發現在滑動ViewPager的時候。很是easy就觸發了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;
}
});
用戶點擊到ViewPager時候,記錄下點擊位置的X座標,當用戶滑動過程當中,假設在X軸上面的滑動超過閾值(我寫的是60f,這個可以在實際使用中自行設置最佳的閾值)。就禁止ScrollView截斷點擊事件,同一時候設置不可下拉刷新。當用戶手指離開屏幕。將ScrollView和SwipeRefreshLayout的狀態恢復。
在ScrollView中嵌套ListView,會出現各類各樣奇怪的問題。比方說ListView顯示有問題,可能才一兩個Item那麼高。並無全然的展開。
網上流傳解決這種問題的方法會有兩種。
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法,讓ListView全然展開這兩種方法都可以解決ListView展現不全然的問題。而且也可以滑動(事實上是使用ScrollView的滑動效果),但是有一個最大的遺憾。就是ListView裏面的View不能複用了。因爲這兩種方法都是算出了ListView的全部高度,而後將ListView控件的高度設置成這個高度。這種話,ListView就至關於一個LinearLayout的佈局了。失去了複用View的優點。而且在某些場景可能尚未LinearLayout好用。更甚的是,假設有大量圖片的話,很是easy就OOM了。這是在研發過程當中最不但願看見的。
可以參考一下美團,美團的首頁。就是一個ScrollView,下滑的時候會發現,並不能無限向下滑動,到了底部會提醒跳轉到一個二級頁面去查看全部的團購信息。這是處理ScrollView裏面嵌套類似ListView列表佈局的時候的一種解決方式。
但是在我碰見的這個項目裏面,並不能這樣處理。
上面的提到的兩種解決思路很是明白,假設想要ListView正常展現就需要肯定ListView的高度,這個很是重要。
因此首先。我需要在佈局文件裏設置ListView的高度。是一個明白的數值。設置高度以後。假設ListView中的數據的Item總高度超過ListView所設置的高度,就可以複用View了。但是這僅僅是攻克了ListView的顯示問題,ListView與ScrollView的滑動衝突,並無解決。
要解決滑動的衝突,最基本的是肯定禁止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();
}
有了思路,而且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,一旦業務複雜,很是easy出現各類UI和業務邏輯衝突的錯誤。
因爲地圖增長比較麻煩。因此在Demo中並無引入地圖。看一下執行效果。
本篇文章僅僅是提供一種解決方法的思路。在詳細的場景下,交互每每是貼合詳細業務需求的。但是不管怎麼樣,找出點擊事件截斷和處理的時機是最重要的,環繞這個關鍵點,總能找出對應的解決方法。
附上Demoproject地址:Demoproject地址連接