自定義吸頂LayoutManager

吸頂效果

RecyclerView已經(jīng)成為在Android Native開發(fā)過程中的明星組件旨袒,出鏡率超高练对,只要需要列表展示的內(nèi)容延旧,我們第一想到的就是使用RecyclerViewRecyclerView確實是一個很容易上手功能又很強大的組件噪舀,通過設(shè)置不同的LayoutManager就可以實現(xiàn)不同的顯示樣式列表夕春、網(wǎng)格等丈攒。在日常的開發(fā)過程中我經(jīng)常會遇到“吸頂”這種情況褥实,就是列表中的某些Item在滾動到列表的頂部的時候需要固定住,如上圖的效果等曼。要實現(xiàn)這種效果的兩種最常見的方案是使用ItemDecoration和組合布局的方式里烦,這兩種方案分別有個字的優(yōu)缺點這里我們簡單的分析一下。

1. 使用組合布局

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">


    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rlv"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
          
        <!--這是要吸頂ViewHolder的布局-->
    <include layout="@layout/section_item_layout" />
</FrameLayout>

大體實現(xiàn)方案如上所示禁谦,將要吸頂?shù)?code>ViewHolder(為方便后面的描述我們這里把顯示在RecyclerView中的ViewHolder叫真ViewHolder胁黑,飄在RecyclerView上面的叫假ViewHolder)的布局放在RecyclerView布局上層,在業(yè)務(wù)層的代碼中通過監(jiān)聽RecyclerView的滾動事件州泊,控制假ViewHolder的顯示丧蘸、隱藏以及移動等,目前市面上大部分App使用的都是這種方案(我是怎么知道的遥皂?用AS的ViewTree工具分析一下就知道了??)力喷,但是這種方案存在以下缺點:

  1. 如果有多種不同的ViewHolder需要吸頂?shù)臅r候,業(yè)務(wù)處理的復(fù)雜度會呈幾何級數(shù)上升演训,這會導(dǎo)致bug層出不窮弟孟。
  2. 吸頂?shù)?code>ViewHolder如果是可交互的(例如響應(yīng)橫向滾動,選中等)就需要做真假ViewHolder的數(shù)據(jù)和狀態(tài)的雙向同步工作样悟,如果吸頂?shù)?code>ViewHolder業(yè)務(wù)比較復(fù)雜拂募,這一定是一個讓人心力憔悴的活庭猩。
  3. 擴展能力弱,相似的功能復(fù)用成本很高陈症,總是要修修補補才能復(fù)用蔼水。

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

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通常用來實現(xiàn)RecyclerView中item的分割線效果,利用其本身的一些特性也能做出吸頂效果來蚤蔓,大體思路如下:

  1. 通過ItemDecorationgetItemOffsets方法將吸頂區(qū)域空出來
  2. 通過View.getDrawingCache()拿到需要吸頂ViewHolder的bitmap
  3. 通過ItemDecorationonDrawOver將吸頂ViewHolder的bitmap繪制在吸頂區(qū)域中

該方案跟上面的使用組合布局的方案比起來卦溢,通用性要好很多,復(fù)用起來也比較方便秀又,但是該方案也有一個致命的缺點单寂,那就是吸頂?shù)?code>ViewHolder不能響應(yīng)事件,如果需要吸頂?shù)?code>ViewHolder中有動態(tài)的內(nèi)容如Gif或視頻等吐辙,也不能做到很好的兼容宣决。

3. 自定義LayoutManager

除了這兩種方案還有沒有別的方案?答案肯定是有的昏苏,使用LayoutManager尊沸!對,沒錯贤惯!我肯定我不是第一個想到這個方案的人洼专,稍微對RecyclerView有點了解的人都會想到這個解決方案,目前我在網(wǎng)上還沒發(fā)現(xiàn)(可能有只是我沒找到)使用LayoutManager解決這個問題的成熟方案孵构。RecyclerViewLayoutManager大約有1萬多行代碼屁商,要想從頭讀到尾確實需要費點時間,我覺得其實我們也沒必要從頭讀到尾把所有的技術(shù)細(xì)節(jié)都弄明白颈墅,只要能達(dá)到自己的目的就可以了蜡镶,就拿創(chuàng)建一個自定義LayoutManager這件事來說我們只需要弄明白RecyclerView的緩存策略和布局流程,我覺得就可以了精盅,如果你時間和精力充足要把它扒個底朝天那也很棒帽哑,下面我們就簡單分析閱讀下這兩部分的源碼。

真愛生命叹俏,遠(yuǎn)離源碼??

3.1 緩存策略

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

  1. mAttachedScrapmChangedScrap

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

    • mAttachedScrap -- 當(dāng)前RecyclerView中已經(jīng)有ViewHolder填充度秘,RecyclerView又觸發(fā)onLayoutChildren的時候,當(dāng)前正在顯示的這部分ViewHolder會被回收到mAttachedScrap中饵撑,在layoutChunk方法中被重新取出剑梳。
    • mChangedScrap -- 只會被用在預(yù)布局中

    mAttachedScrapmChangedScrap 只有在onLayoutChildren()方法調(diào)用的時候才會用到,在滾動的過程中沒用滑潘,只有觸發(fā)requestLayout()的時候才會調(diào)用垢乙。

  2. mCachedViews

    在滾動過程中滾出屏幕區(qū)域而被回收的ViewHolder會被加入到該層緩存,緩存數(shù)量支持自定義默認(rèn)為2语卤,按照先進(jìn)先出的規(guī)則溢出追逮。

  3. mViewCacheExtension

    用戶自定義緩存

  4. 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);
}

緩存復(fù)用最終會調(diào)用到tryGetViewHolderForPositionByDeadline方法钮孵,這個方法源碼巨長省略不相關(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級緩存中取
        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重新創(chuàng)建新的ViewHolder實例
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            ...
        }
    }
    ...
    //綁定數(shù)據(jù)相關(guān)邏輯省略
    return holder;
}

3.2 布局流程

RecyclerView的布局分為兩部分非別為初始布局和滾動過程中的布局眼滤,兩者的處理邏輯有所不同巴席。初始布局相關(guān)業(yè)務(wù)邏輯主要由onLayoutChildren()方法承載,滾動過程中的布局相關(guān)邏輯主要由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;
//判斷是否產(chǎn)生有效滾動
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產(chǎn)生的時候是不會掉用fill()方法的诱担。

3.3 實現(xiàn)方案

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

  1. RecyclerView現(xiàn)有的四層緩存之上,再創(chuàng)建一層緩存蔫仙,用于緩存吸頂?shù)?code>ViewHolder
  2. 篩選出需要吸頂?shù)腣iewHolder加入自定義緩存
  3. 向上滾動(手指上滑)的過程中料睛,在目標(biāo)ViewHolder到達(dá)上邊緣的位置的吸頂位置時候阻止其繼續(xù)滾動,將目標(biāo)ViewHolder強制繪制在屏幕的上部摇邦,并將其加入吸頂ViewHolder緩存(止其進(jìn)入RecyclerView的內(nèi)部回收機制)恤煞。
  4. 向下滾動(手指下滑)的過程中,在目標(biāo)ViewHolder離開吸頂區(qū)域后施籍,將其從吸頂緩存中移除居扒,并將其重新放回到RecyclerView內(nèi)部的緩存中。

總結(jié)起來就兩句話:吸頂?shù)?code>ViewHolder加到新增的自定義緩存中丑慎,將LinearLayoutManager排完的ViewHolder重新排列一下喜喂。

3.3.1 吸頂協(xié)議

整體的開發(fā)思路我們已經(jīng)確定瓤摧,首先我們要解決的問題就是如何將要吸頂?shù)?code>ViewHolder篩選出來呢?這里我的方案是定義一個協(xié)議接口Section玉吁,通過檢測該ViewHolder是否實現(xiàn)該接口判斷該ViewHolder是否需要被吸頂照弥。

/**
 * 協(xié)議接口所有實現(xiàn)該接口的`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吸頂,當(dāng)列表中有多個可以吸頂?shù)?code>ViewHolder的時候进副,在向上滾動的時候新出現(xiàn)的吸頂ViewHolder會將當(dāng)前正在吸頂?shù)?code>ViewHolder頂上去这揣,我們需要將這些被頂上去的ViewHolder保存起來(阻止進(jìn)入系統(tǒng)緩存),這樣在向下滾動的時候這些ViewHolder重新顯示的時候才會保持之前的狀態(tài)影斑,否則會進(jìn)入系統(tǒng)緩存被重新綁定數(shù)據(jù)给赞,導(dǎo)致之前的狀態(tài)丟失。所以我們需要創(chuàng)建一個緩存棧(后進(jìn)先出)用于保存吸頂?shù)?code>ViewHolder鸥昏,在列表向上滾動的過程中塞俱,有符合條件的ViewHolder出現(xiàn)的時候我們就將其入棧姐帚,在列表向下滾動的過程中如果吸頂ViewHolder離開吸頂位置的時候我們就將其出棧吏垮。這個緩存棧就是我們新加的自定義緩存,棧頂?shù)?code>ViewHolder就是當(dāng)前吸頂?shù)?code>ViewHolder罐旗,代碼如下:

/**
 * 吸頂ViewHolder的緩存
 *
 * @author Rango on 2020/11/17
 */
public class SectionCache extends Stack<RecyclerView.ViewHolder> {

    private Map<Integer, RecyclerView.ViewHolder> 
            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();
    }

    /**
     * 棧頂清理膳汪,在快速滾動的情境下可能會出現(xiàn)一次多個吸頂?shù)腣iewHolder出棧的情況,這個時候需要
     * 根據(jù)LayoutPosition清理棧頂九秀,保證棧內(nèi)ViewHolder和列表當(dāng)前的狀態(tài)一致遗嗽。
     *
     * @param layoutPosition 大于position的內(nèi)容會被清理
     */
    public List<RecyclerView.ViewHolder> clearTop(int layoutPosition) {
        List<RecyclerView.ViewHolder> removedViewHolders = new LinkedList<>();
        Iterator<RecyclerView.ViewHolder> 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判斷是否會滾動到吸頂位置,不幸的是LayoutManager并沒有提供獲取ViewHolder的api鼓蜒,只提供了獲取childView()的方法痹换。查閱源碼發(fā)現(xiàn)ViewHolder中有這樣一個api

getChildViewHolderInt

childView對應(yīng)的ViewHolder會保存在其LayoutParams.mViewHolder中,通過這個方案我們可以把當(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都需要立即加入到我們的自定義緩存中娇豫,只有向上滾動到吸頂位置的吸頂ViewHolder加入緩存棧。

image.png

假設(shè)A和E是兩個可以吸頂?shù)腣iewHolder畅厢,當(dāng)前屏幕正在向上滾動冯痢,此時A需要加入緩存棧,但是E不需要加入緩存隊列框杜。E只有持續(xù)向上滾動到A所在的位置的時候才會被加入我們自定義的緩存棧浦楣。

3.3.4 攔截

在列表的滾動過程中我們除了要將這些需要吸頂?shù)腣iewHolder加入到我們自定義的緩存棧中,我們還要阻止其進(jìn)入RecylverView的緩存中咪辱,否則列表繼續(xù)向上滾動ViewHolder A就會滾出屏幕振劳,如下圖所示,這個時候ViewHolder A就會被Recycler回收油狂,放入第二層緩存(mCachedViews)中历恐,再有吸頂ViewHolder滾動出來的時候之前回收的RecyclerView就會被復(fù)用和重新綁定數(shù)據(jù)庐杨,之前的ViewHolder A的狀態(tài)就會丟失。

圖二

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

圖三

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

  • dy -- 本次滾動的距離哮洽,dy > 0是向上滾動(手指上滑)填渠,反之下滑
  • 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()之前插入我們的回收代碼氛什,在之后加入我們的重新布局代碼。因為要兼容LienarLayoutManager所以我們不對scrollBy內(nèi)部的內(nèi)容進(jìn)行修改匪凉,這樣我們就可以保證兼容性枪眉。

我們分析RecyclerView四層緩存的時候我們已經(jīng)了解了內(nèi)部實現(xiàn)的一些細(xì)節(jié)問題緩存復(fù)用和滾動處理等等,scrollBy()是包訪問權(quán)限再层,我們無法對其進(jìn)行重載贸铜,所以我們只能從scrollVerticallyBy()方法下手了,其實我們也沒必要關(guān)心scrollBy()方法內(nèi)被

for (RecyclerView.ViewHolder viewHolder : sectionCache) {
    removeView(viewHolder.itemView);
}

scrollBy()方法調(diào)用之前我們把吸頂?shù)?code>ViewHolder remove掉就可以阻止其進(jìn)入Recycler的緩存中聂受,因為ViewHolder相關(guān)的信息保存在itemView.layoutParams中蒿秦,移除View就可以阻止其回收。就這么簡單蛋济?對就這么簡單棍鳖!

3.3.5 重新布局

如果現(xiàn)在使用我們自定義的LayoutManager應(yīng)該是 圖四 這種效果,當(dāng)吸頂ViewHodler進(jìn)入吸頂位置后就會變成空白碗旅。

image-20201123101155909.png

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

  1. dy可能大于一個ViewHolder的高度

  2. 如果當(dāng)前吸頂位置已經(jīng)有吸頂ViewHolder占據(jù)的時候,后來的吸頂ViewHolder需要將其頂上去

  3. 在向下滾動(手指下滑)的時候祟辟,由于吸頂?shù)?code>ViewHolder都沒有進(jìn)入Recycler的緩存医瘫,所以在向下滾動的時候RecyclerView會重新創(chuàng)建ViewHolder實例,我們需要將其替換為我們自定義緩存中保存的實例川尖。

具體實現(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);
}

// 處理向下滾動
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

源碼地址
如有錯誤或意見歡迎在評論區(qū)討論叮喳。

作為一個碼農(nóng)被芳,腦袋偷懶身體受苦 --- 但是領(lǐng)導(dǎo)總是喜歡那些不動腦筋拼命加班的人。馍悟。畔濒。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锣咒,隨后出現(xiàn)的幾起案子侵状,更是在濱河造成了極大的恐慌赞弥,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件趣兄,死亡現(xiàn)場離奇詭異绽左,居然都是意外死亡,警方通過查閱死者的電腦和手機艇潭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門拼窥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹋凝,你說我怎么就攤上這事鲁纠。” “怎么了鳍寂?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵改含,是天一觀的道長。 經(jīng)常有香客問我迄汛,道長捍壤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任隔心,我火速辦了婚禮白群,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘硬霍。我一直安慰自己,他們只是感情好笼裳,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布唯卖。 她就那樣靜靜地躺著,像睡著了一般躬柬。 火紅的嫁衣襯著肌膚如雪拜轨。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天允青,我揣著相機與錄音橄碾,去河邊找鬼。 笑死颠锉,一個胖子當(dāng)著我的面吹牛法牲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琼掠,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼拒垃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了瓷蛙?” 一聲冷哼從身側(cè)響起悼瓮,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤戈毒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后横堡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體埋市,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年命贴,在試婚紗的時候發(fā)現(xiàn)自己被綠了恐疲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡套么,死狀恐怖培己,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胚泌,我是刑警寧澤省咨,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站玷室,受9級特大地震影響零蓉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜穷缤,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一敌蜂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧津肛,春花似錦章喉、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至部蛇,卻和暖如春摊唇,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涯鲁。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工巷查, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人抹腿。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓岛请,卻偏偏與公主長得像,于是被迫代替她去往敵國和親幢踏。 傳聞我的和親對象是個殘疾皇子髓需,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容