1. 程式人生 > >RecyclerView 體驗優化及入坑總結

RecyclerView 體驗優化及入坑總結

前言

本文所講RecyclerView 是來自support 庫 26 版本,本文主要來源於自身開發及組內同事遇到問題的經驗總結,作為知識沉澱記錄一下,以備日後檢視。

本文主要講解以下幾部分:

1.RecyclerView 滑動體驗篇

  • 橫向ViewPager與內嵌橫向RecyclerView之間的滑動衝突;
  • 縱向RecycleView/ListView與橫向RecycleView之間的滑動衝突;
  • 橫向RecyclerView ItemView滑動不停留在中間態;
  • 記錄、恢復RecyclerView滾動偏移位置;

2.RecyclerView 入坑篇

  • RecyclerView導致的記憶體洩漏(support 26 + 7.0以下機型);
  • RecyclerView呼叫notifyDataSetChanged 會閃爍;
  • RecycleView/ListView設定itemView 為View.GONE 效果等同於View.Invisible;

RecycleView滑動體驗

1.ViewPager與橫向RecyclerView之間的滑動衝突

目前,企鵝FM專案中,很多頁面使用ViewPager+ TabLayout (如首頁、詳情頁、搜尋結果頁等),而對應頁面很多時候會巢狀一個橫向RecycleView,用來展現更多的資訊,如下,在RecycleView中滑動到最後一個元素時,會同時帶動ViewPager滑動,這種體驗極差。

原因分析:

作為子View 的RecyclerView在滑到最後一個或第一個ItemView到導致ViewPager滑動,這一定是ViewPager在此刻對滑動事件進行了攔截,解決的最簡單辦法就是不讓ViewPager攔截橫向RecyclerView的滑動事件(即ViewPager::onInterceptTouchEvent方法返回false),ViewPager::onInterceptTouchEvent中的Move 事件如下:

目前,有以下兩種方式使ViewPager 不去攔截橫向RecyclerView 滑動事件:

1).在RecyclerView 對應滑動事件分發中呼叫 

getParent().requestDisallowInterceptTouchEvent(true); 阻止ViewPager對其MOVE或者UP事件進行攔截,但是考慮的因素比較多,而且效果不是太好,故放棄這種方式。

2).修改某些方法,進入到上圖if判斷中

在滑動橫向RecyclerView 到兩端時,dx != 0 && !isGutterDrag(mLastMotionX, dx) 肯定滿足條件,那說明canScroll()(用來判斷一個View以及它的子View是否可以滑動)一定返回了false, 複寫canScroll()方法,打log,發現返回果然為false,驗證了自己的判斷。

解決辦法:複寫canScroll,當View 是橫向RecyclerView(LinearLayoutManager 包含GridLayoutManager)時,直接返回true即可解決問題,解決程式碼如下:

類似的衝突還有ViewPager 和HorizontalScrollView 等等,解決方式與上面類似。 

2.縱向RecyclerView/ListView 與 橫向RecyclerView 之間的滑動衝突

在有些時候因為產品需求,需要在縱向的RecyclerView/ListView內巢狀一個橫向的RecyclerView,當這個橫向RecyclerView的item 比高度較大的時候(企鵝FM書城排行榜模組),在橫向滑動時,容易導致整體向上滑,體驗效果較差,如下圖所示(網路圖) :

造成上述現象的原因是:外層縱向滑動的RecyclerView對 橫向滑動的RecyclerView 的滑動事件進行了攔截,如下圖2 所示,canScrollVertically 此刻為true,因此這裡僅僅只判斷了Math.abs(dy)>mTouchSlop(可以認為是一個滑動閥值,是一個定值8dp) ,並未判斷方向或角度,從而決定是否攔截。

解決辦法 :

既然RecyclerView::onInterceptTouchEvent 內部沒有判斷滑動角度或方向,那我們就人為去判斷,在上面判讀的基礎上繼續判斷 Math.abs(dy) 和Math.abs(dx) 的大小,從而決定是否攔截:具體分析細節可參照此地址:

使用上述方法,可以很快解決上述滑動體驗問題,那是不是隻有上述一種解決方式了,答案是否定的,作為一名Android 開發者我們知道,除了上述方式攔截滑動事件外,我們還可以通過getParent().requestDisallowInterceptTouchEvent(true); 讓父RecyclerView不去攔截橫向滑動,如下是RecyclerView::onTouchEvent() ,內部已經實現了requestDisallowInterceptTouchEvent(true) 。

我們需要考慮的是,當我們橫向上或橫向下滑動時,需要 進入上圖中1的判斷 ,2的判斷還未滿足,此時內部橫向RecyclerView 會攔截內部itemView的滑動事件,進而執行自己的onTouchEvent事件,從而呼叫requestDisallowInterceptTouchEvent(true) ,讓外層RecyclerView不去攔截內部RecyclerView的橫向滑動事件,至此需要解決如何保證先進入1判斷而不進入2判斷。

解決辦法:通過調整TouchSlop值的大小 

在開始我們已介紹RecyclerView 的預設TouchSlop 值是8dp,如果要先保證進入1判斷條件,必須調大TouchSlop值(反射獲取),經過調整TouchSlop (按倍數調整比較簡單,可以先知道一個大致範圍)驗證,當TouchSlop擴大1倍時就能滿足條件。

總結:上述兩種方式各有優缺點,方法1,對原生RecyclerView 侵入性較強(特別是對RecyclerView 進行多層封裝的情況下,影響比較大),優點是TouchSlop 值保持與系統一致,不會帶來其他未知問題;方法 2 ,修改方式簡單,入侵性小,缺點,需要調整TouchSlop 值,可能還會帶來其他問題。

3.橫向RecyclerView ItemView 滑動不停留在中間態

如下圖所示,正在滑動的模組是書城——排行榜模組,排行榜模組主要由橫向RecyclerView 構成,內部包含兩個榜單形式,列舉前top3的內容,在(2)的基礎上解決了縱向RecyclerView 巢狀橫向RecyclerView 滑動問題外,還有有個小問題那就是,RecyclerView ItemView 滑動多少就停在那裡,這種效果不是我們想要的,我們想要的是滑到左邊就顯示第一個榜單,滑到右邊就顯示第二個榜單。

那有沒有好的辦法做到這一點了,官方考慮到這一點,針對RecyclerView 滑動情況,詳細介紹可以自己去查一查,使用相當簡單,針對上述問題解決方式如下:

4.記錄、恢復RecyclerView 滾動偏移位置

熟悉RecyclerView 快取的同學應該知道(後面在也會介紹RecyclerView快取機制),當RecyclerView中的itemView 滑出屏幕後會快取在mCacheView 中(預設快取最大數是2),因此當滑出螢幕超過2後,再滑回來,原來的位置資訊都會被重置,對於一般的RecyclerView 沒有什麼影響,但是如果內嵌了一個橫向RecyclerView (如下圖中分類模組位置) ,起初”懸疑推理“ 在一排第一個位置,向左滑動到其他位置後,再縱向滑動外層RecyclerView ,發現分類模組第一個又變成了”懸疑推理“ ,這個是產品不能接受的。

那如何修正上述問題了,RecyclerView 佈局 及位置相關資訊都是由對應LayoutManager決定,因此檢視對應LayoutManager::onSaveInstanceState() 如下所示,內部確實記錄了position及offset 值。

解決辦法步驟:

(1).在Adapter::onViewRecycled 中儲存對應LayoutManager的onSaveInstanceState ,同時記錄儲存下來

(2).在setData()資料給Adapter 時,恢復對應LayoutManager 之前儲存在資料資訊

(3).儲存記錄RecyclerView 後的效果

RecycleView入坑

1.RecyclerView 導致的記憶體洩漏(support 26 + 7.0以下機型)

在進行4.0 版本迭代時,發現在之前的廣播聚合頁存在RecyclerView導致的記憶體洩漏,下圖為記憶體洩漏的引用鏈,引用物件可以追到GapWorker。這裡的RecyclerView是一個橫向的RecyclerView ,作為廣播聚合頁(ListView)的HeaderView。

由於廣播頁面是比較老的頁面,最近幾個版本也未發現此類洩漏,細細想一下,可能與RecyclerView 版本有關(4.0版本直接將support 庫由23.1升級到26.1版本),剛好這幾個版本,support 庫 修復了修復很多RecyclerView 的bug 及添加了許多新功能。通過AndroidXRef 查詢知(查詢結果如下),GapWorker 果然是在support 26 新增的。

檢視GapWorker ,裡面sGapWorker 是一個ThreadLocal 帶GapWorker 的物件,同時維持了一個RecyclerView 的List物件(通過add 和remove 方法進行)。

而GapWorker的add 和remove 方法分別在RecyclerView::onAttachedToWindow 和RecyclerView::onDetachedFromWindow 中呼叫,如下圖所示:

根據上面的引用鏈知,RecyclerView::onDetachedFromWindow 方法 沒有被主動呼叫,斷點驗證,在退出廣播頁面的時候也沒有呼叫(導致洩漏),按理說在滑動離屏的時候就應該呼叫的,難道和RecylerView 做為ListView 的HeaderView 有關,順著這條思路發現果然和上述使用方式有關。

之前遇到過:ListView 巢狀GridView時,GridView資料錯亂問題(7.0及其以上有問題),裡面剛好說明了7.0及其以上版本,官方修正了RecylerView 做為ListView 的HeaderView 情況,滑出螢幕,不呼叫onDetachedFromWindow()的原因,具體如下:

從分析中,可以獲取到兩個重要的資訊:

  • GapWorker 是在support 26 以上才有的,且SDK_INT>=21,才會進行對應add 和remove 操作 ;
  • 在SDK_INT< 24(7.0) 時,不會主動呼叫View::dispatchDetachedFromWindow()。

因此,上述問題的解決辦是:在對應Fragment 的onDetach() 或 其他場景主要去呼叫上圖中的ViewGroup::removeDetachedView() (這裡需要使用反射),具體如下:

2.RecyclerView呼叫notifyDataSetChanged 會閃爍

直接看此文章就可以了,地址為:

3.RecycleView /ListView 設定itemView 為View.GONE 效果等同於View.Invisible

解決辦法:

將itemView 的寬高設定成 0 ,重新設定一下LayoutParams