抽絲剝繭RecyclerView - LayoutManager

RecyclerView

前言

抽絲剝繭RecyclerView系列文章的目的在于幫助Android開發(fā)者提高對RecyclerView的認知,本文是整個系列的第二篇媳友。

LayoutManagerRecyclerView中的重要一環(huán)迂曲,使用LayoutManager就跟玩捏臉蛋的游戲一樣,即使好看的五官(好看的子View)都具備了邓深,也不一定能捏出漂亮的臉蛋粤攒,好在RecyclerView為我們提供了默認的模板:LinearLayoutManagerGridLayoutManagerStaggeredGridLayoutManager尖坤。

說來慚愧稳懒,如果不是看了GridLayoutManager的源碼,我還真不知道GridLayoutManager竟然可以這么使用慢味,圖片來自網(wǎng)絡(luò):

GridLayoutManager

不過呢场梆,今天我們講解的源碼不是來自GridLayoutManager佛致,而是線性布局LinearLayoutManagerGridLayoutManager也是繼承自LinearLayoutManager),分析完源碼辙谜,我還將給大家?guī)韺崙?zhàn),完成以下的效果:

TwoSideLayoutManager

時間軸的效果來自TimeLine感昼,自己稍微處理了一下装哆,現(xiàn)在開始進入正題。

代碼地址:https://github.com/mCyp/Orient-Ui

目錄

目錄

一定嗓、源碼分析

本著認真負責的精神蜕琴,我把RecyclerView中用到LayoutManager的地方大致看了一遍,發(fā)現(xiàn)其負責的主要業(yè)務(wù):

  • 回收和復(fù)用子View(當然宵溅,這會交給Recyler處理)凌简。
  • 測量和布局子View。
  • 關(guān)于滑動的處理恃逻。

回收和復(fù)用子View顯然不是LayoutManager實際完成的雏搂,不過,子View的新增和刪除都是LayoutManager通知的寇损,除此以外凸郑,滑動處理的本質(zhì)還是對子View進行管理,所以矛市,本文要討論的只有測量和布局子View的芙沥。

測量和布局子View發(fā)生在RecyclerView三大工作流程,又...又回到了最初的起點浊吏?這是我們在上篇討論過的而昨,如果不涉及到LayoutManager的知識,我們將一筆帶過即可找田。

1. 自動測量機制

RecyclerView#onMeasure方法中歌憨,LayoutManager是否支持自動測量會走不同的流程:

protected void onMeasure(int widthSpec, int heightSpec) {
    // ...
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);
        // 未復(fù)寫的情況下默認調(diào)用RecyclerView#defaultOnMeasure方法
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        final Boolean measureSpecModeIsExactly =
                            widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        // 長和寬的MeasureSpec都為EXACTLY的情況下會return
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // 1. 計算寬度和長度等
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        // 2. 布局子View
        dispatchLayoutStep2();
        // 3. 測量子View的寬和高,并再次測量父布局
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        if (mLayout.shouldMeasureTwice()) {
            // 再走一遍1,2午阵,3
        }
    } else {
        // ...
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        // ....
    }
}

從代碼上來看躺孝,使用自動測量機制需要具備:

  1. RecyclerView布局的長和寬的SpecMode不能是MeasureSpec.EXACTLY(大概率指的是布局中RecyclerView長或?qū)捴杏?code>WrapContent)。
  2. RecyclerView設(shè)置的LayoutMangerisAutoMeasureEnabled返回為true底桂。

當設(shè)置自動測量機制的時候植袍,我們的流程如下:

自動測量機制

從上圖可以看出,是否使用自動測量機制帶來的差距還是挺明顯的籽懦,使用自動測量機制需要經(jīng)歷那么多流程于个,反正都要使用LayoutManager#onMeasure方法,還不如不使用測量機制呢暮顺!

顯然厅篓,這種想法是不對的秀存,因為官方是這么說的,如果不使用自動測量機制羽氮,需要在自定義LayoutManager過程中復(fù)寫LayoutManager#onMeasure方法或链,所以呢,這個方法應(yīng)該是包括自動測量機制的全部過程档押,包括:測量父布局-布置子View-重新測量子View-重新測量父布局澳盐,而使用自動測量機制是不需要復(fù)寫這個方法的令宿,該方法默認測量父布局叼耙。

需要提及的是,我們平時使用的三大LayoutManager都開啟了自動測量機制粒没。

2. onLayoutChildren

即使RecyclerViewonMeasure方法中逃過了布局子View筛婉,那么在onLayout中也不可避免,在上一篇博客中癞松,我們了解到RecyclerView通過LayoutManager#onLayoutChildren方法實現(xiàn)給子View布局爽撒,我們以LinearLayoutManager為例,看看其中的奧秘拦惋。

在正式開始之前匆浙,我們先看看LinearLayoutManager中幾個重要的類:

重要的類 解釋
LinearLayoutManager 這個大家都懂,線性布局厕妖。
AnchorInfo 繪制子View的時候首尼,記錄其位置、偏移量言秸、方向等基礎(chǔ)信息软能。
LayoutChunkResult 加載子View結(jié)果情況的記錄,比如已經(jīng)填充的子View的數(shù)量举畸。
LayoutState 當前加載的狀態(tài)記錄查排,比如當前繪制的偏移量,屏幕還剩余多少空間等

直接看最重要的LinearLayoutManager#onLayoutChildren抄沮,代碼被我一刪再刪后如下:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //... 省略的代碼為:數(shù)據(jù)為0的情況下移除所有的子View跋核,將子View加入到緩存
    // 第一步:初始化LayoutState 配置LayoutState參數(shù)
    ensureLayoutState();
    mLayoutState.mRecycle = false;
    // ... 
    // 第二步:尋找焦點子View
    final View focused = getFocusedChild();
    // ...
    // 第三步:移除界面中已經(jīng)存在的子View,并放入緩存
    detachAndScrapAttachedViews(recycler);
    if (mAnchorInfo.mLayoutFromEnd) {
        // ...
    } else {
        // 第四步:更新LayoutSatete叛买,填充子View
        // 填充也分為兩步:1.從錨點處向結(jié)束方向填充 2.從錨點處向開始方向填充
      
        // fill towards end 往結(jié)束方向填充子View
        // 更新LayoutState
        updateLayoutStateToFillEnd(mAnchorInfo);
        fill(recycler, mLayoutState, state, false);
        //...
        // fill towards start 往開始方向填充子View
        // 更新LayoutState等信息
        updateLayoutStateToFillStart(mAnchorInfo);
        fill(recycler, mLayoutState, state, false);
        if (mLayoutState.mAvailable > 0) {
            // 如果還有剩余空間
            updateLayoutStateToFillEnd(lastElement, endOffset);
            fill(recycler, mLayoutState, state, false);
            // ...
        }
    }
    // ...
    // 第五步:整理一些參數(shù)砂代,以及做一下結(jié)束處理
    // 不是預(yù)布局的狀態(tài)下結(jié)束給子View布局,否則率挣,重置錨點信息
    if (!state.isPreLayout()) {
        mOrientationHelper.onLayoutComplete();
    } else {
        mAnchorInfo.reset();
    }
    //...
}

整個onLayoutChildren可以分為如下五個過程:

  • 第一步:創(chuàng)建LayoutState
  • 第二步:獲取焦點子View
  • 第三步:移除視圖中已經(jīng)存在的View刻伊,回收ViewHolder
  • 第四步:填充子View
  • 第五步:填充結(jié)束后的處理
2.1 第一步、第二步

第一步是創(chuàng)建LayoutState,第二步是獲取屏幕中的焦點子View捶箱,代碼比較簡單智什,感興趣的同學(xué)們可以自己查詢。

2.2 第三步

在填充子View前丁屎,如果當前已經(jīng)存在子View并將繼續(xù)存在的時候荠锭,會先從屏幕中暫時移除,將ViewHolder暫存在Recycler的一級緩存mAttachedScrap中:

/**
 * Temporarily detach and scrap all currently attached child views. Views will be scrapped
 * into the given Recycler. The Recycler may prefer to reuse scrap views before
 * other views that were previously recycled.
 *
 * @param recycler Recycler to scrap views into
 */
public void detachAndScrapAttachedViews(Recycler recycler) {
    final int childCount = getChildCount();
    for (int i = childCount - 1; i >= 0; i--) {
        final View v = getChildAt(i);
        scrapOrRecycleView(recycler, i, v);
    }
}

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()) {
        // 無效的ViewHolder會被添加進RecyclerPool
        removeViewAt(index);
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        // 添加進一級緩存
        detachViewAt(index);
        recycler.scrapView(view);
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

上面的英文注釋其實就是我開始所說的晨川,暫時保存被detachViewHolder节沦,至于Recycler如何保存,我們在上一篇博客中已經(jīng)討論過础爬,這里不再贅述。

2.3 第四步

最復(fù)雜的就是子View的填充過程吼鳞,回到LinearLayoutManager#onLayoutChildren方法看蚜,我們假設(shè)mAnchorInfo.mLayoutFromEndfalse,那么LinearLayoutManager會先從錨點處往下填充赔桌,直至填滿供炎,往下填充前,會先更新LayoutState

private void updateLayoutStateToFillEnd(AnchorInfo anchorInfo) {
    updateLayoutStateToFillEnd(anchorInfo.mPosition, anchorInfo.mCoordinate);
}

private void updateLayoutStateToFillEnd(int itemPosition, int offset) {
    // mAvailable:可以填充的距離
    mLayoutState.mAvailable = mOrientationHelper.getEndAfterPadding() - offset;
    // 填充方向
    mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD :
                    LayoutState.ITEM_DIRECTION_TAIL;
    // 當前位置
    mLayoutState.mCurrentPosition = itemPosition;
    mLayoutState.mLayoutDirection = LayoutState.LAYOUT_END;
    // 當前位置的偏移量
    mLayoutState.mOffset = offset;
    mLayoutState.mScrollingOffset = LayoutState.SCROLLING_OFFSET_NaN;
}

更新完LayoutState以后疾党,就是子View的真實填充過程LinearLayoutManager#fill

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, Boolean stopOnFocusable) {
    // 獲取可以使用的空間
    final int start = layoutState.mAvailable;
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // ...
        // 滑動發(fā)生時回收ViewHolder
        recycleByLayoutState(recycler, layoutState);
    }
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // 核心加載過程
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        //...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
        //... 省略的是:加載一個ViewHolder之后處理狀態(tài)信息
    }
    // 返回消費的空間
    return start - layoutState.mAvailable;
}

最核心的就是while循環(huán)里面的LinearLayoutManager#layoutChunk音诫,最后來看一下該方法如何實現(xiàn)的:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
    // 利用緩存策略獲取 與Recycler相關(guān)
    View view = layoutState.next(recycler);
    // 添加或者刪除 最后會通知父布局新增或者移除子View
    if (layoutState.mScrapList == null) {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                                    == LayoutState.LAYOUT_START)) {
            addView(view);
        } else {
            addView(view, 0);
        }
    } else {
        if (mShouldReverseLayout == (layoutState.mLayoutDirection
                                    == LayoutState.LAYOUT_START)) {
            addDisappearingView(view);
        } else {
            addDisappearingView(view, 0);
        }
    }
    // 測量子View
    measureChildWithMargins(view, 0, 0);
    // 布局子View
    layoutDecoratedWithMargins(view, left, top, right, bottom);
    // ... 設(shè)置LayoutChunkResult參數(shù)
}

首先,View view = layoutState.next(recycler);就是我們在上一節(jié)中討論利用緩存Recycler去獲取ViewHolder雪位,接著獲取ViewHolder中綁定的子View竭钝,給它添加進父布局RecyclerView,然后給子View測量一下寬高雹洗,最后香罐,有了寬高信息,給它放置到具體的位置就完事了时肿,過程清晰明了庇茫。

回到上個方法LinearLayoutManager#fill,在While循環(huán)并且有數(shù)據(jù)的情況下螃成,不斷的將子View填充至RecyclerView中旦签,直至該方向填滿。

再回到一開始的LinearLayoutManager#onLayoutChildren方法寸宏,除了調(diào)用了我們第四步一開始介紹的LinearLayoutManager#updateLayoutStateToFillEnd宁炫,還調(diào)用了LinearLayoutManager#updateLayoutStateToFillStart,所以從整體上來看击吱,它是先填充錨點至結(jié)束的方向淋淀,再填充錨點至開始的方向(不絕對),如果用一圖表示,我覺得可以是這樣:

填充邏輯

先從錨點向下填充朵纷,再從錨點向上填充炭臭,不過,也有可能是先向上袍辞,再向下鞋仍,由一些參數(shù)決定。

第五步

第五步就是對之前的子View的填充結(jié)果做一些處理搅吁,不做過多介紹威创。

二、實戰(zhàn)

看了VivianTimeLine谎懦,你可能會這么吐槽肚豺,人家的庫借助StaggeredGridLayoutManager就可以實現(xiàn)時間軸,為何還要多此一舉界拦,使用我的TwoSideLayoutManager(我給實現(xiàn)的布局方式起名叫TwoSideLayoutManager)呢吸申?因為使用瀑布流StaggeredGridLayoutManager想要在時間軸上實現(xiàn)子View平均分布的效果還是比較困難的,但是享甸,使用TwoSideLayoutManager實現(xiàn)起來就簡單多了截碴。

那么我們?nèi)绾螌崿F(xiàn)RecyclerView的兩側(cè)布局呢?一張圖來打開思路:

實現(xiàn)思路

顯然蛉威,TwoSideLayoutManager的布局實現(xiàn)可以利用LinearLayoutManager的實現(xiàn)方式日丹,僅需要修改添加子View以后的測量邏輯和布局邏輯即可。

上面我們提到過蚯嫌,添加子View哲虾,給子View測量,布局都在LinearLayoutManager#layoutChunk中實現(xiàn)择示,那我們完全可以照搬LinearLayoutManager的填充邏輯妒牙,稍微改幾處代碼,限于篇幅对妄,我們就看一下核心方法TwoSideLayoutManager#layoutChunk

private void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
                             LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    if (view == null) {
        // 沒有更多的數(shù)據(jù)用來生成子View
        result.mFinished = true;
        return;
    }
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // 添加進RecyclerView
    if (layoutState.mLayoutDirection != LayoutState.LAYOUT_START) {
        addView(view);
    } else {
        addView(view, 0);
    }
    // 第一遍測量子View
    measureChild(view);
    // 布局子View
    layoutChild(view, result, params, layoutState, state);
    // Consume the available space if the view is not removed OR changed
    if (params.isItemRemoved() || params.isItemChanged()) {
        result.mIgnoreConsumed = true;
    }
    result.mFocusable = view.hasFocusable();
}

整體邏輯在注釋中已經(jīng)寫得很清楚了湘今,挨個看一下主要方法。

1. measureChild

測量子View

private void measureChild(View view) {
    final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
    int verticalUsed = lp.bottomMargin + lp.topMargin;
    int horizontalUsed = lp.leftMargin + lp.rightMargin;
    // 設(shè)置測量的長度為可用空間的一半
    final int availableSpace = (getWidth() - (getPaddingLeft() + getPaddingRight())) / 2;
    int widthSpec = getChildMeasureSpec(availableSpace, View.MeasureSpec.EXACTLY
                    , horizontalUsed, lp.width, true);
    int heightSpec = getChildMeasureSpec(mOrientationHelper.getTotalSpace(), getHeightMode(),
                    verticalUsed, lp.height, true);
    measureChildWithDecorationsAndMargin(view, widthSpec, heightSpec, false);
}

高度的使用方式跟LinearLayoutManager一樣剪菱,寬度控制在屏幕可用空間的一半摩瞎。

2. layoutChild

布局子View

private void layoutChild(View view, LayoutChunkResult result
            , RecyclerView.LayoutParams params, LayoutState layoutState, RecyclerView.State state) {
    final int size = mOrientationHelper.getDecoratedMeasurement(view);
    final RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) view.getLayoutParams();
    result.mConsumed = size;
    int left, top, right, bottom;
    int num = params.getViewAdapterPosition() % 2;
    // 根據(jù)位置 奇偶位來進行布局
    // 如果起始位置為左側(cè),那么偶數(shù)位為左側(cè)孝常,奇數(shù)位為右側(cè)
    if (isLayoutRTL()) {
        if (num == mStartSide) {
            right = (getWidth() - getPaddingRight()) / 2;
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            right = getWidth() - getPaddingRight();
            left = right - mOrientationHelper.getDecoratedMeasurementInOther(view) - (getWidth() - getPaddingRight()) / 2;
        }
    } else {
        if (num == mStartSide) {
            left = getPaddingLeft();
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        } else {
            left = getPaddingLeft() + (getWidth() - getPaddingRight()) / 2;
            right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
        }
    }
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        bottom = layoutState.mOffset;
        top = layoutState.mOffset - result.mConsumed;
    } else {
        top = layoutState.mOffset;
        bottom = layoutState.mOffset + result.mConsumed;
        if (mLayoutState.mCurrentPosition == state.getItemCount() && lastViewOffset != 0) {
            lp.setMargins(lp.leftMargin, lp.topMargin, lp.rightMargin, lp.bottomMargin + lastViewOffset);
            view.setLayoutParams(lp);
            bottom += lastViewOffset;
        }
    }
    layoutDecoratedWithMargins(view, left, top, right, bottom);
}

public void layoutDecoratedWithMargins(@NonNull View child, int left, int top, ![總結(jié).png](https://upload-images.jianshu.io/upload_images/9271486-9440574ea525a11a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
int right, int bottom) {
    RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams)child.getLayoutParams();
    Rect insets = lp.mDecorInsets;
    child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin, right - insets.right - lp.rightMargin, bottom - insets.bottom - lp.bottomMargin);
}

給子View測量完寬高之后旗们,根據(jù)奇偶位初始設(shè)置的一側(cè)mStartSide布局子View。如果需要顯示時間軸的結(jié)束節(jié)點构灸,那么需要在創(chuàng)建TwoSideLayoutManager對象的時候設(shè)置lastViewOffset上渴,預(yù)留最后位置的空間,不過,需要注意的是稠氮,如果設(shè)置了時間軸的結(jié)束節(jié)點曹阔,那么,最后一個子View最好還是不要回收隔披,不然赃份,最后一個子View回收給其他數(shù)據(jù)使用的時候還得處理Margin。只要在回收的時候稍稍處理就行了奢米,具體的代碼不再貼出了抓韩。

三、總結(jié)

總結(jié)

寫這個布局花的時間還挺多的鬓长,說明自己需要提升的地方還很多谒拴,有的時候代碼雖然能看懂,自己卻不一定能寫出來涉波,下周需要提升效率彪薛,保證每周產(chǎn)出。本人水平有限怠蹂,難免有誤,歡迎指出喲少态。

如果你對本系列文章感興趣

第一篇:《抽絲剝繭RecyclerView - 化整為零》

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末城侧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子彼妻,更是在濱河造成了極大的恐慌嫌佑,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,406評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件侨歉,死亡現(xiàn)場離奇詭異屋摇,居然都是意外死亡,警方通過查閱死者的電腦和手機幽邓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評論 3 393
  • 文/潘曉璐 我一進店門炮温,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牵舵,你說我怎么就攤上這事柒啤。” “怎么了畸颅?”我有些...
    開封第一講書人閱讀 163,711評論 0 353
  • 文/不壞的土叔 我叫張陵担巩,是天一觀的道長。 經(jīng)常有香客問我没炒,道長涛癌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,380評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮拳话,結(jié)果婚禮上先匪,老公的妹妹穿的比我還像新娘。我一直安慰自己假颇,他們只是感情好胚鸯,可當我...
    茶點故事閱讀 67,432評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著笨鸡,像睡著了一般姜钳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上形耗,一...
    開封第一講書人閱讀 51,301評論 1 301
  • 那天哥桥,我揣著相機與錄音,去河邊找鬼激涤。 笑死拟糕,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的倦踢。 我是一名探鬼主播送滞,決...
    沈念sama閱讀 40,145評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辱挥!你這毒婦竟也來了犁嗅?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評論 0 276
  • 序言:老撾萬榮一對情侶失蹤晤碘,失蹤者是張志新(化名)和其女友劉穎褂微,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體园爷,經(jīng)...
    沈念sama閱讀 45,443評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡宠蚂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,649評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了童社。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片求厕。...
    茶點故事閱讀 39,795評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扰楼,靈堂內(nèi)的尸體忽然破棺而出甘改,到底是詐尸還是另有隱情,我是刑警寧澤灭抑,帶...
    沈念sama閱讀 35,501評論 5 345
  • 正文 年R本政府宣布十艾,位于F島的核電站,受9級特大地震影響腾节,放射性物質(zhì)發(fā)生泄漏忘嫉。R本人自食惡果不足惜荤牍,卻給世界環(huán)境...
    茶點故事閱讀 41,119評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望庆冕。 院中可真熱鬧康吵,春花似錦、人聲如沸访递。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拷姿。三九已至惭载,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間响巢,已是汗流浹背描滔。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留踪古,地道東北人含长。 一個月前我還...
    沈念sama閱讀 47,899評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像伏穆,于是被迫代替她去往敵國和親拘泞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,724評論 2 354

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

  • 前言 抽絲剝繭RecyclerView系列文章的目的在于幫助Android開發(fā)者提高對RecyclerView的認...
    九心_閱讀 7,813評論 5 113
  • 這篇文章分三個部分枕扫,簡單跟大家講一下 RecyclerView 的常用方法與奇葩用法陪腌;工作原理與ListView比...
    LucasAdam閱讀 4,388評論 0 27
  • RecyclerView 概要 RecyclerView是Android 5.0開始提供一個可回收容器,它比 Li...
    rexyren閱讀 5,623評論 10 27
  • 又到周末铡原。 這周,三叔回來商叹,把他得車子開走了燕刻。 家里得車,媳婦在開剖笙。 我沒車了卵洗。 計劃周五晚上回家,于是四處張羅便...
    勵志英語老師閱讀 264評論 0 3
  • 文|雨蝶 故事前言:這是發(fā)生在上世紀八十年代末九十年代初魯西南農(nóng)村的一個真實的案例弥咪。因故事太過血腥过蹂,我三番五次地寫...
    雨蝶_b154閱讀 14,581評論 31 49