android 条件筛选吸顶,自定义吸顶LayoutManager
吸頂效果
RecyclerView已經(jīng)成為在Android Native開發(fā)過程中的明星組件,出鏡率超高,只要需要列表展示的內(nèi)容,我們第一想到的就是使用RecyclerView。RecyclerView確實(shí)是一個(gè)很容易上手功能又很強(qiáng)大的組件,通過設(shè)置不同的LayoutManager就可以實(shí)現(xiàn)不同的顯示樣式列表、網(wǎng)格等。在日常的開發(fā)過程中我經(jīng)常會(huì)遇到“吸頂”這種情況,就是列表中的某些Item在滾動(dòng)到列表的頂部的時(shí)候需要固定住,如上圖的效果。要實(shí)現(xiàn)這種效果的兩種最常見的方案是使用ItemDecoration和組合布局的方式,這兩種方案分別有個(gè)字的優(yōu)缺點(diǎn)這里我們簡(jiǎn)單的分析一下。
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" />
大體實(shí)現(xiàn)方案如上所示,將要吸頂?shù)腣iewHolder(為方便后面的描述我們這里把顯示在RecyclerView中的ViewHolder叫真ViewHolder,飄在RecyclerView上面的叫假ViewHolder)的布局放在RecyclerView布局上層,在業(yè)務(wù)層的代碼中通過監(jiān)聽RecyclerView的滾動(dòng)事件,控制假ViewHolder的顯示、隱藏以及移動(dòng)等,目前市面上大部分App使用的都是這種方案(我是怎么知道的?用AS的ViewTree工具分析一下就知道了😊),但是這種方案存在以下缺點(diǎn):
如果有多種不同的ViewHolder需要吸頂?shù)臅r(shí)候,業(yè)務(wù)處理的復(fù)雜度會(huì)呈幾何級(jí)數(shù)上升,這會(huì)導(dǎo)致bug層出不窮。
吸頂?shù)腣iewHolder如果是可交互的(例如響應(yīng)橫向滾動(dòng),選中等)就需要做真假ViewHolder的數(shù)據(jù)和狀態(tài)的雙向同步工作,如果吸頂?shù)腣iewHolder業(yè)務(wù)比較復(fù)雜,這一定是一個(gè)讓人心力憔悴的活。
擴(kuò)展能力弱,相似的功能復(fù)用成本很高,總是要修修補(bǔ)補(bǔ)才能復(fù)用。
也許你會(huì)問,如果真如你所說有這么多問題,那為什么還有這么多人使用這種方案?呵呵,因?yàn)楹?jiǎn)單啊,這個(gè)方案是最容易想到的不是嗎?說實(shí)話這個(gè)方案我也用過,否則我咋知道會(huì)有這么多問題😄。
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通常用來實(shí)現(xiàn)RecyclerView中item的分割線效果,利用其本身的一些特性也能做出吸頂效果來,大體思路如下:
通過ItemDecoration的getItemOffsets方法將吸頂區(qū)域空出來
通過View.getDrawingCache()拿到需要吸頂ViewHolder的bitmap
通過ItemDecoration的onDrawOver將吸頂ViewHolder的bitmap繪制在吸頂區(qū)域中
該方案跟上面的使用組合布局的方案比起來,通用性要好很多,復(fù)用起來也比較方便,但是該方案也有一個(gè)致命的缺點(diǎn),那就是吸頂?shù)腣iewHolder不能響應(yīng)事件,如果需要吸頂?shù)腣iewHolder中有動(dòng)態(tài)的內(nèi)容如Gif或視頻等,也不能做到很好的兼容。
3. 自定義LayoutManager
除了這兩種方案還有沒有別的方案?答案肯定是有的,使用LayoutManager!對(duì),沒錯(cuò)!我肯定我不是第一個(gè)想到這個(gè)方案的人,稍微對(duì)RecyclerView有點(diǎn)了解的人都會(huì)想到這個(gè)解決方案,目前我在網(wǎng)上還沒發(fā)現(xiàn)(可能有只是我沒找到)使用LayoutManager解決這個(gè)問題的成熟方案。RecyclerView加LayoutManager大約有1萬多行代碼,要想從頭讀到尾確實(shí)需要費(fèi)點(diǎn)時(shí)間,我覺得其實(shí)我們也沒必要從頭讀到尾把所有的技術(shù)細(xì)節(jié)都弄明白,只要能達(dá)到自己的目的就可以了,就拿創(chuàng)建一個(gè)自定義LayoutManager這件事來說我們只需要弄明白R(shí)ecyclerView的緩存策略和布局流程,我覺得就可以了,如果你時(shí)間和精力充足要把它扒個(gè)底朝天那也很棒,下面我們就簡(jiǎn)單分析閱讀下這兩部分的源碼。
真愛生命,遠(yuǎn)離源碼??
3.1 緩存策略
RecyclerView的緩存策略一直是RecyclerView的熱門知識(shí)點(diǎn),不管你是想斬offer還是吹牛*這個(gè)是必備。在RecyclerView中ViewHolder復(fù)用相關(guān)的邏輯都封裝在Recycler中,按照順訊分為四層:
mAttachedScrap 和 mChangedScrap
有人說這一級(jí)緩存是告訴緩存,我就有點(diǎn)納悶,“高速”是咋體現(xiàn)出來的?我是沒看出來!這四層緩存如果按照適用場(chǎng)景來劃分我覺得會(huì)更容易理解
mAttachedScrap -- 當(dāng)前RecyclerView中已經(jīng)有ViewHolder填充,RecyclerView又觸發(fā)onLayoutChildren的時(shí)候,當(dāng)前正在顯示的這部分ViewHolder會(huì)被回收到mAttachedScrap中,在layoutChunk方法中被重新取出。
mChangedScrap -- 只會(huì)被用在預(yù)布局中
mAttachedScrap 和 mChangedScrap 只有在onLayoutChildren()方法調(diào)用的時(shí)候才會(huì)用到,在滾動(dòng)的過程中沒用,只有觸發(fā)requestLayout()的時(shí)候才會(huì)調(diào)用。
mCachedViews
在滾動(dòng)過程中滾出屏幕區(qū)域而被回收的ViewHolder會(huì)被加入到該層緩存,緩存數(shù)量支持自定義默認(rèn)為2,按照先進(jìn)先出的規(guī)則溢出。
mViewCacheExtension
用戶自定義緩存
mRecyclerPool
該層緩存用于存儲(chǔ)從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);
}
緩存復(fù)用最終會(huì)調(diào)用到tryGetViewHolderForPositionByDeadline方法,這個(gè)方法源碼巨長(zhǎng)省略不相關(guān)源碼,核心源碼如下:
@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級(jí)緩存中取
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.
// 從三級(jí)自定義緩存中取
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");
}
//從四級(jí)緩存中取
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
...
//通過Adapter重新創(chuàng)建新的ViewHolder實(shí)例
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
}
...
//綁定數(shù)據(jù)相關(guān)邏輯省略
return holder;
}
3.2 布局流程
RecyclerView的布局分為兩部分非別為初始布局和滾動(dòng)過程中的布局,兩者的處理邏輯有所不同。初始布局相關(guān)業(yè)務(wù)邏輯主要由onLayoutChildren()方法承載,滾動(dòng)過程中的布局相關(guān)邏輯主要由scrollVerticallyBy()承載。其中有一個(gè)比較核心的方法是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;
//判斷是否產(chǎn)生有效滾動(dòng)
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
//檢查時(shí)候有需要回收的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在滾動(dòng)布局過程中如果沒有新的ViewHolder產(chǎn)生的時(shí)候是不會(huì)掉用fill()方法的。
3.3 實(shí)現(xiàn)方案
有了上面那西基礎(chǔ)做鋪墊我們就可以開始動(dòng)手寫一個(gè)LayoutManager了,整體思路如下:
在RecyclerView現(xiàn)有的四層緩存之上,再創(chuàng)建一層緩存,用于緩存吸頂?shù)腣iewHolder
篩選出需要吸頂?shù)腣iewHolder加入自定義緩存
向上滾動(dòng)(手指上滑)的過程中,在目標(biāo)ViewHolder到達(dá)上邊緣的位置的吸頂位置時(shí)候阻止其繼續(xù)滾動(dòng),將目標(biāo)ViewHolder強(qiáng)制繪制在屏幕的上部,并將其加入吸頂ViewHolder緩存(止其進(jìn)入RecyclerView的內(nèi)部回收機(jī)制)。
向下滾動(dòng)(手指下滑)的過程中,在目標(biāo)ViewHolder離開吸頂區(qū)域后,將其從吸頂緩存中移除,并將其重新放回到RecyclerView內(nèi)部的緩存中。
總結(jié)起來就兩句話:吸頂?shù)腣iewHolder加到新增的自定義緩存中,將LinearLayoutManager排完的ViewHolder重新排列一下。
3.3.1 吸頂協(xié)議
整體的開發(fā)思路我們已經(jīng)確定,首先我們要解決的問題就是如何將要吸頂?shù)腣iewHolder篩選出來呢?這里我的方案是定義一個(gè)協(xié)議接口Section,通過檢測(cè)該ViewHolder是否實(shí)現(xiàn)該接口判斷該ViewHolder是否需要被吸頂。
/**
* 協(xié)議接口所有實(shí)現(xiàn)該接口的`ViewHolder`在滾動(dòng)的過程中都會(huì)被吸頂
* @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 自定義緩存
因?yàn)橐淮沃挥幸粋€(gè)ViewHolder吸頂,當(dāng)列表中有多個(gè)可以吸頂?shù)腣iewHolder的時(shí)候,在向上滾動(dòng)的時(shí)候新出現(xiàn)的吸頂ViewHolder會(huì)將當(dāng)前正在吸頂?shù)腣iewHolder頂上去,我們需要將這些被頂上去的ViewHolder保存起來(阻止進(jìn)入系統(tǒng)緩存),這樣在向下滾動(dòng)的時(shí)候這些ViewHolder重新顯示的時(shí)候才會(huì)保持之前的狀態(tài),否則會(huì)進(jìn)入系統(tǒng)緩存被重新綁定數(shù)據(jù),導(dǎo)致之前的狀態(tài)丟失。所以我們需要?jiǎng)?chuàng)建一個(gè)緩存棧(后進(jìn)先出)用于保存吸頂?shù)腣iewHolder,在列表向上滾動(dòng)的過程中,有符合條件的ViewHolder出現(xiàn)的時(shí)候我們就將其入棧,在列表向下滾動(dòng)的過程中如果吸頂ViewHolder離開吸頂位置的時(shí)候我們就將其出棧。這個(gè)緩存棧就是我們新加的自定義緩存,棧頂?shù)腣iewHolder就是當(dāng)前吸頂?shù)腣iewHolder,代碼如下:
/**
* 吸頂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();
//避免存在重復(fù)的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();
}
/**
* 棧頂清理,在快速滾動(dòng)的情境下可能會(huì)出現(xiàn)一次多個(gè)吸頂?shù)腣iewHolder出棧的情況,這個(gè)時(shí)候需要
* 根據(jù)LayoutPosition清理?xiàng)m?#xff0c;保證棧內(nèi)ViewHolder和列表當(dāng)前的狀態(tài)一致。
*
* @param layoutPosition 大于position的內(nèi)容會(huì)被清理
*/
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
這里我們需要把當(dāng)前正在顯示的目標(biāo)ViewHolder過濾出來并根據(jù)當(dāng)前的dy判斷是否會(huì)滾動(dòng)到吸頂位置,不幸的是LayoutManager并沒有提供獲取ViewHolder的api,只提供了獲取childView()的方法。查閱源碼發(fā)現(xiàn)ViewHolder中有這樣一個(gè)api
getChildViewHolderInt
childView對(duì)應(yīng)的ViewHolder會(huì)保存在其LayoutParams.mViewHolder中,通過這個(gè)方案我們可以把當(dāng)前正在顯示的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;
}
}
注意并不是說所有顯示出來的需要吸頂?shù)腣iewHolder都需要立即加入到我們的自定義緩存中,只有向上滾動(dòng)到吸頂位置的吸頂ViewHolder加入緩存棧。
image.png
假設(shè)A和E是兩個(gè)可以吸頂?shù)腣iewHolder,當(dāng)前屏幕正在向上滾動(dòng),此時(shí)A需要加入緩存棧,但是E不需要加入緩存隊(duì)列。E只有持續(xù)向上滾動(dòng)到A所在的位置的時(shí)候才會(huì)被加入我們自定義的緩存棧。
3.3.4 攔截
在列表的滾動(dòng)過程中我們除了要將這些需要吸頂?shù)腣iewHolder加入到我們自定義的緩存棧中,我們還要阻止其進(jìn)入RecylverView的緩存中,否則列表繼續(xù)向上滾動(dòng)ViewHolder A就會(huì)滾出屏幕,如下圖所示,這個(gè)時(shí)候ViewHolder A就會(huì)被Recycler回收,放入第二層緩存(mCachedViews)中,再有吸頂ViewHolder滾動(dòng)出來的時(shí)候之前回收的RecyclerView就會(huì)被復(fù)用和重新綁定數(shù)據(jù),之前的ViewHolder A的狀態(tài)就會(huì)丟失。
圖二
在列表上滑過程中圖三是我們所期望的結(jié)果,ViewHolder A在上滑到頂部的時(shí)候我們需要將其固定在RecyclerView的頂部。
圖三
RecyclerView滾動(dòng)相關(guān)的業(yè)務(wù)邏輯主要是在scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state)方法中,該方法有三個(gè)參數(shù)作用如下:
dy -- 本次滾動(dòng)的距離,dy > 0是向上滾動(dòng)(手指上滑),反之下滑
recycler -- 緩存器,定義了四層緩存策略
state -- 用于傳遞數(shù)據(jù)信息,例如是否是預(yù)布局等
在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()之前插入我們的回收代碼,在之后加入我們的重新布局代碼。因?yàn)橐嫒軱ienarLayoutManager所以我們不對(duì)scrollBy內(nèi)部的內(nèi)容進(jìn)行修改,這樣我們就可以保證兼容性。
我們分析RecyclerView四層緩存的時(shí)候我們已經(jīng)了解了內(nèi)部實(shí)現(xiàn)的一些細(xì)節(jié)問題緩存復(fù)用和滾動(dòng)處理等等,scrollBy()是包訪問權(quán)限,我們無法對(duì)其進(jìn)行重載,所以我們只能從scrollVerticallyBy()方法下手了,其實(shí)我們也沒必要關(guān)心scrollBy()方法內(nèi)被
for (RecyclerView.ViewHolder viewHolder : sectionCache) {
removeView(viewHolder.itemView);
}
在scrollBy()方法調(diào)用之前我們把吸頂?shù)腣iewHolder remove掉就可以阻止其進(jìn)入Recycler的緩存中,因?yàn)閂iewHolder相關(guān)的信息保存在itemView.layoutParams中,移除View就可以阻止其回收。就這么簡(jiǎn)單?對(duì)就這么簡(jiǎn)單!
3.3.5 重新布局
如果現(xiàn)在使用我們自定義的LayoutManager應(yīng)該是 圖四 這種效果,當(dāng)吸頂ViewHodler進(jìn)入吸頂位置后就會(huì)變成空白。
image-20201123101155909.png
我們需要將Remove掉的ViewHolder重新加回到RecyclerView中并將其布局在合適的位置,這里有幾個(gè)關(guān)鍵點(diǎn)需要注意下:
dy可能大于一個(gè)ViewHolder的高度
如果當(dāng)前吸頂位置已經(jīng)有吸頂ViewHolder占據(jù)的時(shí)候,后來的吸頂ViewHolder需要將其頂上去
在向下滾動(dòng)(手指下滑)的時(shí)候,由于吸頂?shù)腣iewHolder都沒有進(jìn)入Recycler的緩存,所以在向下滾動(dòng)的時(shí)候RecyclerView會(huì)重新創(chuàng)建ViewHolder實(shí)例,我們需要將其替換為我們自定義緩存中保存的實(shí)例。
具體實(shí)現(xiàn)代碼如下:
//檢查棧頂
RecyclerView.ViewHolder vh = getViewHolderByView(getChildAt(0));
RecyclerView.ViewHolder attachedSection = sectionCache.peek();
if ((vh instanceof Section)
&& attachedSection != null
&& attachedSection.getLayoutPosition() == vh.getLayoutPosition()) {
removeViewAt(0);
}
// 處理向下滾動(dòng)
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());
}
}
每段代碼的作用已經(jīng)用注釋描述,這里不再贅述,效果如下:
未命名.gif
源碼地址
如有錯(cuò)誤或意見歡迎在評(píng)論區(qū)討論。
作為一個(gè)碼農(nóng),腦袋偷懶身體受苦 --- 但是領(lǐng)導(dǎo)總是喜歡那些不動(dòng)腦筋拼命加班的人。。。
總結(jié)
以上是生活随笔為你收集整理的android 条件筛选吸顶,自定义吸顶LayoutManager的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 目录创建 android,创建目录浏览器
- 下一篇: 鸿蒙系统今日发布 中国人自己的操作系统,