轉(zhuǎn)載注明出處:http://www.reibang.com/p/87a41b8c0dd0
前言
在Android開(kāi)發(fā)中,如果是一些簡(jiǎn)單的布局蹦哼,都很容易搞定,但是一旦涉及到復(fù)雜的頁(yè)面攘乒,特別是為了兼容小屏手機(jī)而使用了ScrollView以后设预,就會(huì)出現(xiàn)很多點(diǎn)擊事件的沖突,最經(jīng)典的就是ScrollView中嵌套了ListView碎乃。我想大部分剛開(kāi)始接觸Android的同學(xué)們都踩到過(guò)這個(gè)坑姊扔,這一篇文章就從最近做的一個(gè)項(xiàng)目講起,然后在過(guò)程中提供一些解決沖突的思路梅誓。
項(xiàng)目起始
項(xiàng)目有一個(gè)頁(yè)面恰梢,涉及到了ViewPager,MapView梗掰,ListView嵌言,也就是說(shuō)在一個(gè)頁(yè)面中,會(huì)有這三個(gè)View及穗,很明顯摧茴,屏幕無(wú)法完全顯示,需要ScrollView來(lái)做一下支援埂陆,就引入了ScrollView作為外層的容器苛白。但是由于這個(gè)頁(yè)面的數(shù)據(jù)展示需要做到用戶手動(dòng)下拉刷新,于是又引入了官方的SwipeRefreshLayout焚虱。
于是這個(gè)頁(yè)面的布局就成了這樣子购裙。如下圖(細(xì)節(jié)布局忽略)。
加入了ScrollView和SwipeRefreshLayout之后引入了新的問(wèn)題鹃栽,就是各個(gè)控件之間的事件沖突躏率,嵌套在ScrollView中的ViewPager、MapView、ListView都需要能夠正確的處理點(diǎn)擊事件禾锤,特別是ListView私股,需求要求它在ScrollView中可以滑動(dòng),兩種滑動(dòng)混淆在一起恩掷,不是特別好處理倡鲸。
問(wèn)題提出來(lái)了,下面直接看解決思路黄娘。
解決滑動(dòng)沖突的思路
在ViewGroup中有個(gè)方法叫requestDisallowInterceptTouchEvent(boolean disallowIntercept)
峭状,這個(gè)方法可以用來(lái)控制該ViewGroup是否截?cái)帱c(diǎn)擊事件。我們解決滑動(dòng)沖突的時(shí)候逼争,其實(shí)就是在某個(gè)時(shí)機(jī)去調(diào)用這個(gè)方法优床,讓父布局不截?cái)帱c(diǎn)擊事件,將點(diǎn)擊事件傳遞到子View誓焦,讓相關(guān)的子View去處理胆敞。
下面就是關(guān)于在項(xiàng)目中處理各種點(diǎn)擊事件沖突的一些例子和思考。處理的方法只是提供一種思路杂伟,可能并不是最優(yōu)的方法移层,肯定存在其他思路的解決方案。
以下處理滑動(dòng)沖突的方案都是在子View的OnTouchListener里面進(jìn)行處理赫粥,并沒(méi)有去復(fù)寫(xiě)控件的點(diǎn)擊事件處理過(guò)程观话,在使用中還是比較方便的。
MapView地圖頁(yè)面滑動(dòng)沖突
MapView與ScrollView的沖突主要在于越平,當(dāng)用戶點(diǎn)擊到MapView地圖并且滑動(dòng)的時(shí)候频蛔,希望由地圖Map去處理點(diǎn)擊事件,并包括后續(xù)的滑動(dòng)事件秦叛、雙手指縮放地圖等等晦溪。
在ScrollView中,是會(huì)默認(rèn)截?cái)帱c(diǎn)擊事件的挣跋,導(dǎo)致用戶點(diǎn)擊到地圖后尼变,地圖基本是沒(méi)有反應(yīng),更別談雙手指縮放地圖了浆劲。
用戶手指點(diǎn)擊到地圖,并且滑動(dòng)的時(shí)候哀澈,很難確定用戶是要ScrollView上下滑動(dòng)還是操控地圖內(nèi)容滑動(dòng)牌借,所以我簡(jiǎn)單的認(rèn)為,只要用戶手指點(diǎn)擊到地圖割按,就是要對(duì)地圖進(jìn)行操作膨报;當(dāng)用戶手指抬起,就認(rèn)為用戶不需要操作地圖了。
解決思路也很簡(jiǎn)單现柠,就是在用戶點(diǎn)擊到地圖或者滑動(dòng)地圖時(shí)候院领,讓ScrollView不截?cái)帱c(diǎn)擊事件,并傳遞給子View處理够吩,也就是地圖去處理點(diǎn)擊事件比然;當(dāng)用戶手指抬起的時(shí)候,將ScrollView的狀態(tài)恢復(fù)至之前的狀態(tài)周循,也就是ScrollView可以截?cái)帱c(diǎn)擊事件强法。
我使用的是百度地圖,直接上代碼湾笛,更容易理解饮怯。
mMapView.getChildAt(0).setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_UP){
//允許ScrollView截?cái)帱c(diǎn)擊事件,ScrollView可滑動(dòng)
mScrollView.requestDisallowInterceptTouchEvent(false);
}else{
//不允許ScrollView截?cái)帱c(diǎn)擊事件嚎研,點(diǎn)擊事件由子View處理
mScrollView.requestDisallowInterceptTouchEvent(true);
}
return false;
}
});
ViewPager滑動(dòng)沖突解決
在這個(gè)項(xiàng)目中蓖墅,ViewPager在頁(yè)面最頂層,如果只是ScrollView里面嵌套了ViewPager临扮,因?yàn)檫@兩個(gè)控件是不同方向的滑動(dòng)事件论矾,所以基本不會(huì)出現(xiàn)沖突。
但是由于引入了SwipeRefreshLayout公条,我發(fā)現(xiàn)在滑動(dòng)ViewPager的時(shí)候拇囊,很容易就觸發(fā)了SwipeRefreshLayout的下來(lái)刷新,進(jìn)而有可能阻斷了ViewPager的左右滑動(dòng)效果靶橱,體驗(yàn)很不好寥袭。而且在滑動(dòng)ViewPager的過(guò)程中,用戶滑動(dòng)肯定不是一直水平的关霸,會(huì)有一定程度向上向下的滑動(dòng)传黄。
ViewPager處理沖突和地圖處理沖突有些不同,因?yàn)楫?dāng)用戶點(diǎn)擊到ViewPager队寇,在滑動(dòng)過(guò)程中膘掰,基本就可以猜測(cè)到用戶是想左右滑動(dòng)ViewPager還是上下滑動(dòng)ScrollView(或者下拉刷新),這就不能像地圖一樣佳遣,在點(diǎn)擊到ViewPager就禁止ScrollView截?cái)帱c(diǎn)擊事件(或者SwipeRefreshLayout下拉刷新功能)之宿,需要在滑動(dòng)過(guò)程中做出判斷。
解決思路就是钾军,設(shè)定一個(gè)閾值嘲驾,一旦用戶在X軸也就是橫向滑動(dòng)距離超過(guò)這個(gè)閾值,我就認(rèn)為用戶是要左右滑動(dòng)ViewPager诵盼,就禁止ScrollView截?cái)帱c(diǎn)擊事件同時(shí)設(shè)置SwipeRefreshLayout不能下拉刷新惠豺。當(dāng)用戶抬起手指银还,就認(rèn)為用戶對(duì)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) {
// 記錄點(diǎn)擊到ViewPager時(shí)候蛹疯,手指的X坐標(biāo)
mLastX = event.getX();
}
if(action == MotionEvent.ACTION_MOVE) {
// 超過(guò)閾值
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;
}
});
用戶點(diǎn)擊到ViewPager時(shí)候热监,記錄下點(diǎn)擊位置的X坐標(biāo)捺弦,當(dāng)用戶滑動(dòng)過(guò)程中,如果在X軸上面的滑動(dòng)超過(guò)閾值(我寫(xiě)的是60f狼纬,這個(gè)可以在實(shí)際使用中自行設(shè)置最佳的閾值)羹呵,就禁止ScrollView截?cái)帱c(diǎn)擊事件,同時(shí)設(shè)置不可下拉刷新疗琉。當(dāng)用戶手指離開(kāi)屏幕冈欢,將ScrollView和SwipeRefreshLayout的狀態(tài)恢復(fù)。
ListView滑動(dòng)沖突解決
在ScrollView中嵌套ListView盈简,會(huì)出現(xiàn)各種各樣奇怪的問(wèn)題凑耻。比如說(shuō)ListView顯示有問(wèn)題,可能才一兩個(gè)Item那么高柠贤,并沒(méi)有完全的展開(kāi)香浩。網(wǎng)上流傳解決這種問(wèn)題的方法會(huì)有兩種。
- 根據(jù)展示數(shù)據(jù)的個(gè)數(shù)乘以每一個(gè)Item的高度臼勉,計(jì)算出ListView的總體高度邻吭,然后動(dòng)態(tài)的設(shè)置ListView的高度
- 復(fù)寫(xiě)ListView的
onMeasure(int widthMeasureSpec, int heightMeasureSpec)
方法,讓ListView完全展開(kāi)
這兩種方法都可以解決ListView展示不完全的問(wèn)題宴霸,而且也可以滑動(dòng)(其實(shí)是使用ScrollView的滑動(dòng)效果)囱晴,但是有一個(gè)最大的遺憾,就是ListView里面的View不能復(fù)用了瓢谢。因?yàn)檫@兩種方法都是算出了ListView的全部高度畸写,然后將ListView控件的高度設(shè)置成這個(gè)高度,這樣的話氓扛,ListView就相當(dāng)于一個(gè)LinearLayout的布局了枯芬,失去了復(fù)用View的優(yōu)勢(shì),而且在某些場(chǎng)景可能還沒(méi)有LinearLayout好用采郎,更甚的是千所,如果有大量圖片的話,很容易就OOM了蒜埋,這是在研發(fā)過(guò)程中最不希望看見(jiàn)的淫痰。
可以參考一下美團(tuán),美團(tuán)的首頁(yè)理茎,就是一個(gè)ScrollView黑界,下滑的時(shí)候會(huì)發(fā)現(xiàn),并不能無(wú)限向下滑動(dòng)皂林,到了底部會(huì)提醒跳轉(zhuǎn)到一個(gè)二級(jí)頁(yè)面去查看全部的團(tuán)購(gòu)信息朗鸠。這是處理ScrollView里面嵌套類似ListView列表布局的時(shí)候的一種解決方案。
但是在我遇見(jiàn)的這個(gè)項(xiàng)目里面础倍,并不能這樣處理烛占。
上面的提到的兩種解決思路很明確,如果想要ListView正常展示就需要確定ListView的高度沟启,這個(gè)很重要忆家。
所以首先,我需要在布局文件中設(shè)置ListView的高度德迹,是一個(gè)明確的數(shù)值芽卿。設(shè)置高度之后,如果ListView中的數(shù)據(jù)的Item總高度超過(guò)ListView所設(shè)置的高度胳搞,就可以復(fù)用View了卸例。但是這只是解決了ListView的顯示問(wèn)題,ListView與ScrollView的滑動(dòng)沖突肌毅,并沒(méi)有解決筷转。
要解決滑動(dòng)的沖突,最主要的是確定禁止ScrollView截?cái)帱c(diǎn)擊事件的時(shí)機(jī)悬而,然后來(lái)分析有哪些時(shí)機(jī)呜舒。
- ScrollView在未滑動(dòng)到底部時(shí)候,如果點(diǎn)擊并滑動(dòng)ListView時(shí)候笨奠,ListView是不能滑動(dòng)的袭蝗,不禁止。
- 如果ScrollView滑動(dòng)到底部艰躺,且ListView已經(jīng)到頂部呻袭,繼續(xù)下拉ListView,其實(shí)會(huì)拉動(dòng)ScrollView腺兴,不禁止左电。
- 如果ScrollView滑動(dòng)到底部,用戶向上滑页响,ListView滑動(dòng)篓足,禁止ScrollView截?cái)帱c(diǎn)擊事件能力
很明顯,在判斷禁止ScrollView截?cái)帱c(diǎn)擊事件時(shí)機(jī)的時(shí)候闰蚕,需要知道ScrollView是否滑動(dòng)到了底部栈拖。于是,重寫(xiě)了ScrollView的ScrollChanged()
方法没陡,來(lái)判斷ScrollView是否滑動(dòng)到底部(SDK API 23版本中ScrollView可以設(shè)置setOnScrollChangeListener()來(lái)監(jiān)聽(tīng)滑動(dòng)的變化涩哟,但是之前版本不支持索赏,為了兼容,自己需要重寫(xiě))贴彼。
@Override
protected void onScrollChanged(int l, int t, int oldl, int oldt){
super.onScrollChanged(l,t,oldl,oldt);
// 滑動(dòng)的距離加上本身的高度與子View的高度對(duì)比
if(t + getHeight() >= getChildAt(0).getMeasuredHeight()){
// ScrollView滑動(dòng)到底部
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滑動(dòng)到底部的標(biāo)識(shí)也可以拿到,下面就可以直接來(lái)解決滑動(dòng)沖突了器仗,直接看代碼融涣。
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攔截點(diǎn)擊事件, scrollView滑動(dòng)
mScrollView.requestDisallowInterceptTouchEvent(false);
} else if(top == 0 && nowY - mLastY > THRESHOLD_Y_LIST_VIEW) {
// 允許scrollview攔截點(diǎn)擊事件, scrollView滑動(dòng)
mScrollView.requestDisallowInterceptTouchEvent(false);
} else {
// 不允許scrollview攔截點(diǎn)擊事件, listView滑動(dòng)
mScrollView.requestDisallowInterceptTouchEvent(true);
}
}
return false;
}
});
相對(duì)于其他的控件來(lái)說(shuō)精钮,ListView和ScrollView之間的滑動(dòng)沖突更難解決威鹿,但其實(shí)在實(shí)際使用中并不推薦ScrollView里面嵌套ListView,一旦業(yè)務(wù)復(fù)雜轨香,很容易出現(xiàn)各種UI和業(yè)務(wù)邏輯沖突的錯(cuò)誤忽你。
運(yùn)行效果
由于地圖加入比較麻煩,所以在Demo中并沒(méi)有引入地圖弹沽√醇校看一下運(yùn)行效果。
總結(jié)
本篇文章只是提供一種解決方法的思路策橘,在具體的場(chǎng)景下炸渡,交互往往是貼合具體業(yè)務(wù)需求的。但是不管怎么樣丽已,找出點(diǎn)擊事件截?cái)嗪吞幚淼臅r(shí)機(jī)是最重要的蚌堵,圍繞這個(gè)關(guān)鍵點(diǎn),總能找出相應(yīng)的解決方法沛婴。
附上Demo工程地址:Demo工程地址鏈接