前言
眾所周知仪召,RecyclerView是可以上下滑動(dòng)的(當(dāng)然根據(jù)對(duì)LayoutManager設(shè)置的不同也可以左右滑動(dòng))乍钻,而WebView也是可以上下左右滑動(dòng)的即纲。如果RecyclerView和WebView相互嵌套(即RecyclerView的一個(gè)條目為WebView),就會(huì)產(chǎn)生滑動(dòng)沖突浆劲。具體表現(xiàn)就是只有RecyclerView能滑動(dòng)砂沛,WebView的滑動(dòng)事件被攔截了烫扼。原因也很好理解曙求,如果你是RecyclerView的設(shè)計(jì)者碍庵,你也不會(huì)默認(rèn)把滑動(dòng)事件交給itemView去處理,因?yàn)檫@樣很容易亂套悟狱,會(huì)出現(xiàn)很多奇奇怪怪的bug静浴。RecyclerView對(duì)事件具體的處理策略,可以自行查看其源碼挤渐。下面直接開門見山的說解決方案苹享。
解決方案一
方案一其實(shí)很簡單,在布局里面設(shè)置WebView的高度為“wrap_content”浴麻。至于為什么這樣就可以得问,我們先來復(fù)習(xí)一下wrap_content和match_parent
-
wrap_content
wrap content翻譯成漢語就是“包裹內(nèi)容”,WebView的內(nèi)容就是網(wǎng)頁的內(nèi)容软免,如果WebView的高度設(shè)置為“wrap_content”宫纬,那WebView的高度就是網(wǎng)頁內(nèi)容的高度。 -
match_parent
match parent即匹配父窗體膏萧,父控件有多高漓骚,高度設(shè)置成match_parent的View就有多高。
我們假設(shè)RecyclerView有兩個(gè)條目(其中一個(gè)是WebView)榛泛。此時(shí)對(duì)WebView的高度設(shè)成wrap_content和match_parent時(shí)蝌蹂,對(duì)比如下:
當(dāng)WebView高度設(shè)置成wrap_content時(shí),WebView加載的網(wǎng)頁的內(nèi)容在WebView里全部展現(xiàn)了曹锨,只需要滑動(dòng)RecyclerView孤个,就可以查看到未顯示的內(nèi)容了。
如果設(shè)置成match_parent沛简,網(wǎng)頁的內(nèi)容并沒有全部展示在WebView當(dāng)中硼身,需要滑動(dòng)WebView來展示沒有展現(xiàn)的剩下的內(nèi)容;而此時(shí)WebView并不會(huì)獲得滑動(dòng)事件覆享,所以剩下的內(nèi)容永遠(yuǎn)也沒有展現(xiàn)的機(jī)會(huì)了佳遂。
既然wrap_content能完美解決,又如此簡單撒顿,就用這種方案好了丑罪,為什么還會(huì)有方案二呢?wrap_content會(huì)有些問題,就我發(fā)現(xiàn)的:
1吩屹,如果網(wǎng)頁會(huì)有彈窗跪另,彈窗會(huì)顯示在網(wǎng)頁的正中間,也就是WebView的正中間煤搜,對(duì)照上圖(1)免绿,正常情況下不會(huì)顯示在屏幕范圍內(nèi),需要向上滑動(dòng)一段才能看見彈窗擦盾,這樣對(duì)用戶是不友好的嘲驾;
2,會(huì)造成部分JS代碼執(zhí)行錯(cuò)誤迹卢。
如果設(shè)置成match_parent就沒有這些問題辽故;下面剩下的問題就是解決滑動(dòng)沖突,在合適的時(shí)候?qū)ecyclerView的事件傳遞給WebView腐碱。即下面的解決方案二誊垢。
解決方案二
其實(shí)思路很簡單,重寫RecyclerView的onTouchEvent方法症见,在合適的時(shí)候?qū)⑹录鬟f給WebView喂走。但是這樣做需要寫個(gè)自定義的RecyclerView然后覆蓋onTouchEvent方法,比較麻煩谋作。
View對(duì)外提供有setOnTouchListener的接口芋肠,只需要傳一個(gè)OnTouchListener的對(duì)象,實(shí)現(xiàn)onTouch方法瓷们,對(duì)事件進(jìn)行處理即可业栅。
OnTouchListener的優(yōu)先級(jí)比onTouchEvent的優(yōu)先級(jí)要高,可以參見View的源碼的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
//代碼省略
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
//優(yōu)先調(diào)用 li.mOnTouchListener.onTouch(this, event)谬晕,如果返回true碘裕,就不會(huì)調(diào)用onTouchEvent了
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//
if (!result && onTouchEvent(event)) {
result = true;
}
//省略代碼
return result;
}
接下來的難點(diǎn)就是在合適的時(shí)候將事件傳遞給WebView了。直接看代碼注釋吧:
private class RecyclerViewOnTouchListener implements View.OnTouchListener {
private int mLastY;
private int mCurrentY;
@Override
public boolean onTouch(View v, MotionEvent event) {
WebViewHolder webViewHolder = mAdapter.getWebViewHolder();
if(webViewHolder == null) {
return false;
}
//獲取WebView對(duì)象攒钳,以便將事件傳遞給他
WebView webView = (WebView) webViewHolder.itemView.findViewById(R.id.web_view);
//獲取WebView所在item的頂部相對(duì)于其父控件(即RecyclerView的父控件)的距離
int itemViewTop = webViewHolder.itemView.getTop();
if(itemViewTop > 0) {
return false;
}
if(itemViewTop < 0) {
webViewHolder.itemView.scrollTo(0, 0);
return false;
}
//計(jì)算dy帮孔,用來判斷滑動(dòng)方向。dy<0-->向上滑動(dòng)不撑;dy>0-->向下滑動(dòng)文兢。
int dy = 0;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
mLastY = (int) event.getY();
break;
case MotionEvent.ACTION_MOVE:
mCurrentY = (int) event.getY();
dy = mCurrentY - mLastY;
mLastY = mCurrentY;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
dy = (int) (event.getY() - mLastY);
mLastY = 0;
mCurrentY = 0;
break;
}
Log.d(TAG, "dy = " + dy);
Log.d(TAG, "itemViewTop = " + itemViewTop);
//如果WebView頂部距離其父控件距離未0,即WebView頂部滑動(dòng)到RecyclerView父控件頂部重合時(shí)焕檬,
// 此時(shí)需要攔截滑動(dòng)事件交給WebView處理姆坚。
if(itemViewTop == 0) {
if(shouldIntercept(webView, dy)) {
webView.onTouchEvent(event);
return true;
}
}
return false;
}
/**
* 是否攔截滑動(dòng)事件,判斷的邏輯是:<br/>
* 1,如果是向上滑動(dòng)实愚,并且webview能夠向上滑動(dòng)兼呵,則攔截事件兔辅;<br/>
* 2,如果是向下滑動(dòng),并且webview能夠向下滑動(dòng)击喂,則攔截事件维苔。
* @param view 判斷能夠滑動(dòng)的view
* @param dy 滑動(dòng)間距
* @return true攔截,false不攔截懂昂。
*/
private boolean shouldIntercept(View view, int dy) {
//canScrollVertically方法的第二個(gè)參數(shù)direction介时,傳1時(shí)返回是否能夠向上滑動(dòng),傳-1時(shí)返回能否向下滑動(dòng)凌彬。
//dy<0-->向上滑動(dòng)沸柔;dy>0-->向下滑動(dòng)。
boolean scrollUp = dy < 0 && ViewCompat.canScrollVertically(view, 1);
boolean scrollDown = dy > 0 && ViewCompat.canScrollVertically(view, -1);
return scrollUp || scrollDown || dy == 0;
}
}
接下來把該OnTouchListener設(shè)置給RecyclerView就可以了饿序。
recyclerView.setOnTouchListener(new RecyclerViewOnTouchListener());
具體邏輯代碼注釋已經(jīng)寫的很清楚了勉失,這里就不再啰嗦了羹蚣。
結(jié)語
方案二邏輯比較復(fù)雜原探,沒有完整測試,不知道有沒有bug顽素。方案一比較簡單直接咽弦,如果你要加載的網(wǎng)頁環(huán)境比較簡單,沒有彈窗胁出,就直接用方案一吧型型,開發(fā)工作量也要小很多。
另外本人才疏學(xué)淺全蝶,可能有表述不當(dāng)甚至理解錯(cuò)誤的地方闹蒜,歡迎指正,共同進(jìn)步抑淫。
江湖規(guī)矩绷落,源碼見 github