RecyclerView嵌套WebView的兩種解決方案

前言

眾所周知,RecyclerView是可以上下滑動的(當(dāng)然根據(jù)對LayoutManager設(shè)置的不同也可以左右滑動),而WebView也是可以上下左右滑動的。如果RecyclerView和WebView相互嵌套(即RecyclerView的一個條目為WebView),就會產(chǎn)生滑動沖突。具體表現(xiàn)就是只有RecyclerView能滑動,WebView的滑動事件被攔截了。原因也很好理解,如果你是RecyclerView的設(shè)計(jì)者,你也不會默認(rèn)把滑動事件交給itemView去處理,因?yàn)檫@樣很容易亂套,會出現(xiàn)很多奇奇怪怪的bug。RecyclerView對事件具體的處理策略,可以自行查看其源碼。下面直接開門見山的說解決方案。

解決方案一

方案一其實(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有兩個條目(其中一個是WebView)。此時對WebView的高度設(shè)成wrap_content和match_parent時,對比如下:

match_parent和wrap_content.png

當(dāng)WebView高度設(shè)置成wrap_content時,WebView加載的網(wǎng)頁的內(nèi)容在WebView里全部展現(xiàn)了,只需要滑動RecyclerView,就可以查看到未顯示的內(nèi)容了。
如果設(shè)置成match_parent,網(wǎng)頁的內(nèi)容并沒有全部展示在WebView當(dāng)中,需要滑動WebView來展示沒有展現(xiàn)的剩下的內(nèi)容;而此時WebView并不會獲得滑動事件,所以剩下的內(nèi)容永遠(yuǎn)也沒有展現(xiàn)的機(jī)會了。

既然wrap_content能完美解決,又如此簡單,就用這種方案好了,為什么還會有方案二呢?wrap_content會有些問題,就我發(fā)現(xiàn)的:
1,如果網(wǎng)頁會有彈窗,彈窗會顯示在網(wǎng)頁的正中間,也就是WebView的正中間,對照上圖(1),正常情況下不會顯示在屏幕范圍內(nèi),需要向上滑動一段才能看見彈窗,這樣對用戶是不友好的;
2,會造成部分JS代碼執(zhí)行錯誤。
如果設(shè)置成match_parent就沒有這些問題;下面剩下的問題就是解決滑動沖突,在合適的時候?qū)ecyclerView的事件傳遞給WebView。即下面的解決方案二。

解決方案二

其實(shí)思路很簡單,重寫RecyclerView的onTouchEvent方法,在合適的時候?qū)⑹录鬟f給WebView。但是這樣做需要寫個自定義的RecyclerView然后覆蓋onTouchEvent方法,比較麻煩。
View對外提供有setOnTouchListener的接口,只需要傳一個OnTouchListener的對象,實(shí)現(xiàn)onTouch方法,對事件進(jìn)行處理即可。
OnTouchListener的優(yōu)先級比onTouchEvent的優(yōu)先級要高,可以參見View的源碼的dispatchTouchEvent方法:

public boolean dispatchTouchEvent(MotionEvent event) {
        //代碼省略
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        //優(yōu)先調(diào)用 li.mOnTouchListener.onTouch(this, event),如果返回true,就不會調(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)就是在合適的時候將事件傳遞給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對象,以便將事件傳遞給他
            WebView webView = (WebView) webViewHolder.itemView.findViewById(R.id.web_view);
            //獲取WebView所在item的頂部相對于其父控件(即RecyclerView的父控件)的距離
            int itemViewTop = webViewHolder.itemView.getTop();
            if(itemViewTop > 0) {
                return false;
            }
            if(itemViewTop < 0) {
                webViewHolder.itemView.scrollTo(0, 0);
                return false;
            }

            //計(jì)算dy,用來判斷滑動方向。dy<0-->向上滑動;dy>0-->向下滑動。
            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頂部滑動到RecyclerView父控件頂部重合時,
            // 此時需要攔截滑動事件交給WebView處理。
            if(itemViewTop == 0) {
                if(shouldIntercept(webView, dy)) {
                    webView.onTouchEvent(event);
                    return true;
                }
            }
            return false;
        }

        /**
         * 是否攔截滑動事件,判斷的邏輯是:<br/>
         * 1,如果是向上滑動,并且webview能夠向上滑動,則攔截事件;<br/>
         * 2,如果是向下滑動,并且webview能夠向下滑動,則攔截事件。
         * @param view 判斷能夠滑動的view
         * @param dy 滑動間距
         * @return true攔截,false不攔截。
         */
        private boolean shouldIntercept(View view, int dy) {
            //canScrollVertically方法的第二個參數(shù)direction,傳1時返回是否能夠向上滑動,傳-1時返回能否向下滑動。
            //dy<0-->向上滑動;dy>0-->向下滑動。
            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)甚至理解錯誤的地方,歡迎指正,共同進(jìn)步。

江湖規(guī)矩,源碼見 github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容