日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

android 条件筛选吸顶,自定义吸顶LayoutManager

發布時間:2025/3/21 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 android 条件筛选吸顶,自定义吸顶LayoutManager 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

吸頂效果

RecyclerView已經成為在Android Native開發過程中的明星組件,出鏡率超高,只要需要列表展示的內容,我們第一想到的就是使用RecyclerView。RecyclerView確實是一個很容易上手功能又很強大的組件,通過設置不同的LayoutManager就可以實現不同的顯示樣式列表、網格等。在日常的開發過程中我經常會遇到“吸頂”這種情況,就是列表中的某些Item在滾動到列表的頂部的時候需要固定住,如上圖的效果。要實現這種效果的兩種最常見的方案是使用ItemDecoration和組合布局的方式,這兩種方案分別有個字的優缺點這里我們簡單的分析一下。

1. 使用組合布局

xmlns:tools="http://schemas.android.com/tools"

android:layout_width="match_parent"

android:layout_height="match_parent"

tools:context=".MainActivity">

android:id="@+id/rlv"

android:layout_width="match_parent"

android:layout_height="match_parent" />

大體實現方案如上所示,將要吸頂的ViewHolder(為方便后面的描述我們這里把顯示在RecyclerView中的ViewHolder叫真ViewHolder,飄在RecyclerView上面的叫假ViewHolder)的布局放在RecyclerView布局上層,在業務層的代碼中通過監聽RecyclerView的滾動事件,控制假ViewHolder的顯示、隱藏以及移動等,目前市面上大部分App使用的都是這種方案(我是怎么知道的?用AS的ViewTree工具分析一下就知道了😊),但是這種方案存在以下缺點:

如果有多種不同的ViewHolder需要吸頂的時候,業務處理的復雜度會呈幾何級數上升,這會導致bug層出不窮。

吸頂的ViewHolder如果是可交互的(例如響應橫向滾動,選中等)就需要做真假ViewHolder的數據和狀態的雙向同步工作,如果吸頂的ViewHolder業務比較復雜,這一定是一個讓人心力憔悴的活。

擴展能力弱,相似的功能復用成本很高,總是要修修補補才能復用。

也許你會問,如果真如你所說有這么多問題,那為什么還有這么多人使用這種方案?呵呵,因為簡單啊,這個方案是最容易想到的不是嗎?說實話這個方案我也用過,否則我咋知道會有這么多問題😄。

2. 使用ItemDecoration

class II extends RecyclerView.ItemDecoration{

@Override

public void getItemOffsets(@NonNull Rect outRect,

@NonNull View view,

@NonNull RecyclerView parent,

@NonNull RecyclerView.State state) {

super.getItemOffsets(outRect, view, parent, state);

}

@Override

public void onDrawOver(@NonNull Canvas c,

@NonNull RecyclerView parent,

@NonNull RecyclerView.State state) {

super.onDrawOver(c, parent, state);

}

}

ItemDecoration通常用來實現RecyclerView中item的分割線效果,利用其本身的一些特性也能做出吸頂效果來,大體思路如下:

通過ItemDecoration的getItemOffsets方法將吸頂區域空出來

通過View.getDrawingCache()拿到需要吸頂ViewHolder的bitmap

通過ItemDecoration的onDrawOver將吸頂ViewHolder的bitmap繪制在吸頂區域中

該方案跟上面的使用組合布局的方案比起來,通用性要好很多,復用起來也比較方便,但是該方案也有一個致命的缺點,那就是吸頂的ViewHolder不能響應事件,如果需要吸頂的ViewHolder中有動態的內容如Gif或視頻等,也不能做到很好的兼容。

3. 自定義LayoutManager

除了這兩種方案還有沒有別的方案?答案肯定是有的,使用LayoutManager!對,沒錯!我肯定我不是第一個想到這個方案的人,稍微對RecyclerView有點了解的人都會想到這個解決方案,目前我在網上還沒發現(可能有只是我沒找到)使用LayoutManager解決這個問題的成熟方案。RecyclerView加LayoutManager大約有1萬多行代碼,要想從頭讀到尾確實需要費點時間,我覺得其實我們也沒必要從頭讀到尾把所有的技術細節都弄明白,只要能達到自己的目的就可以了,就拿創建一個自定義LayoutManager這件事來說我們只需要弄明白RecyclerView的緩存策略和布局流程,我覺得就可以了,如果你時間和精力充足要把它扒個底朝天那也很棒,下面我們就簡單分析閱讀下這兩部分的源碼。

真愛生命,遠離源碼??

3.1 緩存策略

RecyclerView的緩存策略一直是RecyclerView的熱門知識點,不管你是想斬offer還是吹牛*這個是必備。在RecyclerView中ViewHolder復用相關的邏輯都封裝在Recycler中,按照順訊分為四層:

mAttachedScrap 和 mChangedScrap

有人說這一級緩存是告訴緩存,我就有點納悶,“高速”是咋體現出來的?我是沒看出來!這四層緩存如果按照適用場景來劃分我覺得會更容易理解

mAttachedScrap -- 當前RecyclerView中已經有ViewHolder填充,RecyclerView又觸發onLayoutChildren的時候,當前正在顯示的這部分ViewHolder會被回收到mAttachedScrap中,在layoutChunk方法中被重新取出。

mChangedScrap -- 只會被用在預布局中

mAttachedScrap 和 mChangedScrap 只有在onLayoutChildren()方法調用的時候才會用到,在滾動的過程中沒用,只有觸發requestLayout()的時候才會調用。

mCachedViews

在滾動過程中滾出屏幕區域而被回收的ViewHolder會被加入到該層緩存,緩存數量支持自定義默認為2,按照先進先出的規則溢出。

mViewCacheExtension

用戶自定義緩存

mRecyclerPool

該層緩存用于存儲從mCachedView緩存中溢出的ViewHolder。

RecyclerView緩存的訪問順序存取是保持一致的,回收部分的源碼:

private void scrapOrRecycleView(Recycler recycler, int index, View view) {

final ViewHolder viewHolder = getChildViewHolderInt(view);

if (viewHolder.shouldIgnore()) {

return;

}

if (viewHolder.isInvalid() && !viewHolder.isRemoved()

&& !mRecyclerView.mAdapter.hasStableIds()) {

removeViewAt(index);

//回收到mCachedViews或mRecyclerPool中

recycler.recycleViewHolderInternal(viewHolder);

} else {

detachViewAt(index);

//回收到mAttachedScrap 或 mChangedScrap中

recycler.scrapView(view);

mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);

}

緩存復用最終會調用到tryGetViewHolderForPositionByDeadline方法,這個方法源碼巨長省略不相關源碼,核心源碼如下:

@NonNull

public View getViewForPosition(int position) {

return getViewForPosition(position, false);

}

View getViewForPosition(int position, boolean dryRun) {

return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;

}

@Nullable

ViewHolder tryGetViewHolderForPositionByDeadline(int position,

boolean dryRun, long deadlineNs) {

...

boolean fromScrapOrHiddenOrCache = false;

ViewHolder holder = null;

// 0) If there is a changed scrap, try to find from there

if (mState.isPreLayout()) {

holder = getChangedScrapViewForPosition(position);

fromScrapOrHiddenOrCache = holder != null;

}

// 1) Find by position from scrap/hidden list/cache

if (holder == null) {

//從 1和2級緩存中取

holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

if (holder != null) {

...

}

}

if (holder == null) {

...

if (holder == null && mViewCacheExtension != null) {

// We are NOT sending the offsetPosition because LayoutManager does not

// know it.

// 從三級自定義緩存中取

final View view = mViewCacheExtension

.getViewForPositionAndType(this, position, type);

....

}

if (holder == null) { // fallback to pool

if (DEBUG) {

Log.d(TAG, "tryGetViewHolderForPositionByDeadline("

+ position + ") fetching from shared pool");

}

//從四級緩存中取

holder = getRecycledViewPool().getRecycledView(type);

if (holder != null) {

holder.resetInternal();

if (FORCE_INVALIDATE_DISPLAY_LIST) {

invalidateDisplayListInt(holder);

}

}

}

if (holder == null) {

...

//通過Adapter重新創建新的ViewHolder實例

holder = mAdapter.createViewHolder(RecyclerView.this, type);

...

}

}

...

//綁定數據相關邏輯省略

return holder;

}

3.2 布局流程

RecyclerView的布局分為兩部分非別為初始布局和滾動過程中的布局,兩者的處理邏輯有所不同。初始布局相關業務邏輯主要由onLayoutChildren()方法承載,滾動過程中的布局相關邏輯主要由scrollVerticallyBy()承載。其中有一個比較核心的方法是fill()方法,該方法是ViewHolder布局的核心方法。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,

RecyclerView.State state, boolean stopOnFocusable) {

// max offset we should set is mFastScroll + available

final int start = layoutState.mAvailable;

//判斷是否產生有效滾動

if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {

// TODO ugly bug fix. should not happen

if (layoutState.mAvailable < 0) {

layoutState.mScrollingOffset += layoutState.mAvailable;

}

//檢查時候有需要回收的ViewHolder

recycleByLayoutState(recycler, layoutState);

}

int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;

LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {

layoutChunkResult.resetInternal();

if (RecyclerView.VERBOSE_TRACING) {

TraceCompat.beginSection("LLM LayoutChunk");

}

...

//布局ViewHolder

layoutChunk(recycler, state, layoutState, layoutChunkResult);

...

if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {

layoutState.mScrollingOffset += layoutChunkResult.mConsumed;

if (layoutState.mAvailable < 0) {

layoutState.mScrollingOffset += layoutState.mAvailable;

}

recycleByLayoutState(recycler, layoutState);

}

if (stopOnFocusable && layoutChunkResult.mFocusable) {

break;

}

}

if (DEBUG) {

validateChildOrder();

}

return start - layoutState.mAvailable;

注意:RecyclerView在滾動布局過程中如果沒有新的ViewHolder產生的時候是不會掉用fill()方法的。

3.3 實現方案

有了上面那西基礎做鋪墊我們就可以開始動手寫一個LayoutManager了,整體思路如下:

在RecyclerView現有的四層緩存之上,再創建一層緩存,用于緩存吸頂的ViewHolder

篩選出需要吸頂的ViewHolder加入自定義緩存

向上滾動(手指上滑)的過程中,在目標ViewHolder到達上邊緣的位置的吸頂位置時候阻止其繼續滾動,將目標ViewHolder強制繪制在屏幕的上部,并將其加入吸頂ViewHolder緩存(止其進入RecyclerView的內部回收機制)。

向下滾動(手指下滑)的過程中,在目標ViewHolder離開吸頂區域后,將其從吸頂緩存中移除,并將其重新放回到RecyclerView內部的緩存中。

總結起來就兩句話:吸頂的ViewHolder加到新增的自定義緩存中,將LinearLayoutManager排完的ViewHolder重新排列一下。

3.3.1 吸頂協議

整體的開發思路我們已經確定,首先我們要解決的問題就是如何將要吸頂的ViewHolder篩選出來呢?這里我的方案是定義一個協議接口Section,通過檢測該ViewHolder是否實現該接口判斷該ViewHolder是否需要被吸頂。

/**

* 協議接口所有實現該接口的`ViewHolder`在滾動的過程中都會被吸頂

* @author Rango on 2020/11/6

*/

public interface Section {

}

public class SectionViewHolder extends RecyclerView.ViewHolder implements Section {

public TextView tv;

public SectionViewHolder(@NonNull View v) {

super(v);

}

}

3.3.2 自定義緩存

因為一次只有一個ViewHolder吸頂,當列表中有多個可以吸頂的ViewHolder的時候,在向上滾動的時候新出現的吸頂ViewHolder會將當前正在吸頂的ViewHolder頂上去,我們需要將這些被頂上去的ViewHolder保存起來(阻止進入系統緩存),這樣在向下滾動的時候這些ViewHolder重新顯示的時候才會保持之前的狀態,否則會進入系統緩存被重新綁定數據,導致之前的狀態丟失。所以我們需要創建一個緩存棧(后進先出)用于保存吸頂的ViewHolder,在列表向上滾動的過程中,有符合條件的ViewHolder出現的時候我們就將其入棧,在列表向下滾動的過程中如果吸頂ViewHolder離開吸頂位置的時候我們就將其出棧。這個緩存棧就是我們新加的自定義緩存,棧頂的ViewHolder就是當前吸頂的ViewHolder,代碼如下:

/**

* 吸頂ViewHolder的緩存

*

* @author Rango on 2020/11/17

*/

public class SectionCache extends Stack {

private Map

filterMap = new HashMap<>(16, 64);

@Override

public RecyclerView.ViewHolder push(RecyclerView.ViewHolder item) {

if (item == null) {

return null;

}

int position = item.getLayoutPosition();

//避免存在重復的Value

if (filterMap.containsKey(position)) {

//返回null說明沒有添加成功

return null;

}

filterMap.put(position, item);

return super.push(item);

}

@Override

public synchronized RecyclerView.ViewHolder peek() {

if (size() == 0) {

return null;

}

return super.peek();

}

/**

* 棧頂清理,在快速滾動的情境下可能會出現一次多個吸頂的ViewHolder出棧的情況,這個時候需要

* 根據LayoutPosition清理棧頂,保證棧內ViewHolder和列表當前的狀態一致。

*

* @param layoutPosition 大于position的內容會被清理

*/

public List clearTop(int layoutPosition) {

List removedViewHolders = new LinkedList<>();

Iterator it = iterator();

while (it.hasNext()) {

RecyclerView.ViewHolder top = it.next();

if (top.getLayoutPosition() > layoutPosition) {

it.remove();

filterMap.remove(top.getLayoutPosition());

removedViewHolders.add(top);

}

}

return removedViewHolders;

}

}

3.3.3 過濾ViewHolder

這里我們需要把當前正在顯示的目標ViewHolder過濾出來并根據當前的dy判斷是否會滾動到吸頂位置,不幸的是LayoutManager并沒有提供獲取ViewHolder的api,只提供了獲取childView()的方法。查閱源碼發現ViewHolder中有這樣一個api

getChildViewHolderInt

childView對應的ViewHolder會保存在其LayoutParams.mViewHolder中,通過這個方案我們可以把當前正在顯示的ViewHolder過濾出來。

for (int i = 0; i < getChildCount(); i++) {

View itemView = getChildAt(i);

RecyclerView.ViewHolder vh = getViewHolderByView(itemView);

if (!(vh instanceof Section) || sectionCache.peek() == vh) {

continue;

}

if (dy > 0 && vh.itemView.getTop() < dy) {

sectionCache.push(vh);

} else {

break;

}

}

注意并不是說所有顯示出來的需要吸頂的ViewHolder都需要立即加入到我們的自定義緩存中,只有向上滾動到吸頂位置的吸頂ViewHolder加入緩存棧。

image.png

假設A和E是兩個可以吸頂的ViewHolder,當前屏幕正在向上滾動,此時A需要加入緩存棧,但是E不需要加入緩存隊列。E只有持續向上滾動到A所在的位置的時候才會被加入我們自定義的緩存棧。

3.3.4 攔截

在列表的滾動過程中我們除了要將這些需要吸頂的ViewHolder加入到我們自定義的緩存棧中,我們還要阻止其進入RecylverView的緩存中,否則列表繼續向上滾動ViewHolder A就會滾出屏幕,如下圖所示,這個時候ViewHolder A就會被Recycler回收,放入第二層緩存(mCachedViews)中,再有吸頂ViewHolder滾動出來的時候之前回收的RecyclerView就會被復用和重新綁定數據,之前的ViewHolder A的狀態就會丟失。

圖二

在列表上滑過程中圖三是我們所期望的結果,ViewHolder A在上滑到頂部的時候我們需要將其固定在RecyclerView的頂部。

圖三

RecyclerView滾動相關的業務邏輯主要是在scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)方法中,該方法有三個參數作用如下:

dy -- 本次滾動的距離,dy > 0是向上滾動(手指上滑),反之下滑

recycler -- 緩存器,定義了四層緩存策略

state -- 用于傳遞數據信息,例如是否是預布局等

在LinearLayoutManager中該方法的源碼如下

/**

* {@inheritDoc}

*/

@Override

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,

RecyclerView.State state) {

if (mOrientation == HORIZONTAL) {

return 0;

}

return scrollBy(dy, recycler, state);

}

這里我們要做的就是在scollBy()之前插入我們的回收代碼,在之后加入我們的重新布局代碼。因為要兼容LienarLayoutManager所以我們不對scrollBy內部的內容進行修改,這樣我們就可以保證兼容性。

我們分析RecyclerView四層緩存的時候我們已經了解了內部實現的一些細節問題緩存復用和滾動處理等等,scrollBy()是包訪問權限,我們無法對其進行重載,所以我們只能從scrollVerticallyBy()方法下手了,其實我們也沒必要關心scrollBy()方法內被

for (RecyclerView.ViewHolder viewHolder : sectionCache) {

removeView(viewHolder.itemView);

}

在scrollBy()方法調用之前我們把吸頂的ViewHolder remove掉就可以阻止其進入Recycler的緩存中,因為ViewHolder相關的信息保存在itemView.layoutParams中,移除View就可以阻止其回收。就這么簡單?對就這么簡單!

3.3.5 重新布局

如果現在使用我們自定義的LayoutManager應該是 圖四 這種效果,當吸頂ViewHodler進入吸頂位置后就會變成空白。

image-20201123101155909.png

我們需要將Remove掉的ViewHolder重新加回到RecyclerView中并將其布局在合適的位置,這里有幾個關鍵點需要注意下:

dy可能大于一個ViewHolder的高度

如果當前吸頂位置已經有吸頂ViewHolder占據的時候,后來的吸頂ViewHolder需要將其頂上去

在向下滾動(手指下滑)的時候,由于吸頂的ViewHolder都沒有進入Recycler的緩存,所以在向下滾動的時候RecyclerView會重新創建ViewHolder實例,我們需要將其替換為我們自定義緩存中保存的實例。

具體實現代碼如下:

//檢查棧頂

RecyclerView.ViewHolder vh = getViewHolderByView(getChildAt(0));

RecyclerView.ViewHolder attachedSection = sectionCache.peek();

if ((vh instanceof Section)

&& attachedSection != null

&& attachedSection.getLayoutPosition() == vh.getLayoutPosition()) {

removeViewAt(0);

}

// 處理向下滾動

for (RecyclerView.ViewHolder removedViewHolder : sectionCache.clearTop(findFirstVisibleItemPosition())) {

Log.i(tag, "移除ViewHolder:" + removedViewHolder.toString());

for (int i = 0; i < getChildCount(); i++) {

RecyclerView.ViewHolder attachedViewHolder = getViewHolderByView(getChildAt(i));

if (removedViewHolder.getLayoutPosition() == attachedViewHolder.getLayoutPosition()) {

View attachedItemView = attachedViewHolder.itemView;

int left = attachedItemView.getLeft();

int top = attachedItemView.getTop();

int bottom = attachedItemView.getBottom();

int right = attachedItemView.getRight();

//這里的remvoe 和 add 是為了重新布局

removeView(attachedItemView);

addView(removedViewHolder.itemView, i);

removedViewHolder.itemView.layout(left, top, right, bottom);

break;

}

}

}

//重新布局

RecyclerView.ViewHolder section = sectionCache.peek();

if (section != null) {

View itemView = section.itemView;

if (!itemView.isAttachedToWindow()) {

addView(itemView);

}

View subItem = getChildAt(1);

if (getViewHolderByView(subItem) instanceof Section) {

int h = itemView.getMeasuredHeight();

int top = Math.min(0, -(h - subItem.getTop()));

int bottom = Math.min(h, subItem.getTop());

itemView.layout(0, top, itemView.getMeasuredWidth(), bottom);

} else {

itemView.layout(0, 0, itemView.getMeasuredWidth(), itemView.getMeasuredHeight());

}

}

每段代碼的作用已經用注釋描述,這里不再贅述,效果如下:

未命名.gif

源碼地址

如有錯誤或意見歡迎在評論區討論。

作為一個碼農,腦袋偷懶身體受苦 --- 但是領導總是喜歡那些不動腦筋拼命加班的人。。。

總結

以上是生活随笔為你收集整理的android 条件筛选吸顶,自定义吸顶LayoutManager的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。