Android 自定義RecyclerView.LayoutManager

代碼


import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;

import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.Keep;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;

import com.example.R;

import java.util.ArrayList;
import java.util.List;


/**
 * A {@link RecyclerView.LayoutManager} which displays a regular grid (i.e. all cells are the same
 * size) and allows simultaneous row & column spanning.
 */
public class SpannedGridLayoutManager extends RecyclerView.LayoutManager {

    private GridSpanLookup spanLookup;
    private int columns = 1;
    private float cellAspectRatio = 1f;

    private int cellHeight;
    private int[] cellBorders;
    private int firstVisiblePosition;
    private int lastVisiblePosition;
    private int firstVisibleRow;
    private int lastVisibleRow;
    private boolean forceClearOffsets;
    private SparseArray<GridCell> cells;
    private List<Integer> firstChildPositionForRow; // key == row, val == first child position
    private int totalRows;
    private final Rect itemDecorationInsets = new Rect();

    public SpannedGridLayoutManager(GridSpanLookup spanLookup, int columns, float cellAspectRatio) {
        this.spanLookup = spanLookup;
        this.columns = columns;
        this.cellAspectRatio = cellAspectRatio;
        setAutoMeasureEnabled(true);
    }

    @Keep /* XML constructor, see RecyclerView#createLayoutManager */
    public SpannedGridLayoutManager(
            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        TypedArray a = context.obtainStyledAttributes(
                attrs, R.styleable.UI_SpannedGridLayoutManager, defStyleAttr, defStyleRes);
        columns = a.getInt(R.styleable.UI_SpannedGridLayoutManager_spanCount, 1);
        parseAspectRatio(a.getString(R.styleable.UI_SpannedGridLayoutManager_aspectRatio));
        // TODO use this!
        int orientation = a.getInt(
                R.styleable.UI_SpannedGridLayoutManager_android_orientation, RecyclerView.VERTICAL);
        a.recycle();
        setAutoMeasureEnabled(true);
    }

    public interface GridSpanLookup {
        SpanInfo getSpanInfo(int position);
    }

    public void setSpanLookup(@NonNull GridSpanLookup spanLookup) {
        this.spanLookup = spanLookup;
    }

    public static class SpanInfo {
        public int columnSpan;
        public int rowSpan;

        public SpanInfo(int columnSpan, int rowSpan) {
            this.columnSpan = columnSpan;
            this.rowSpan = rowSpan;
        }

        public static final SpanInfo SINGLE_CELL = new SpanInfo(1, 1);
    }

    public static class LayoutParams extends RecyclerView.LayoutParams {

        int columnSpan;
        int rowSpan;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.MarginLayoutParams source) {
            super(source);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }

        public LayoutParams(RecyclerView.LayoutParams source) {
            super(source);
        }
    }

    @Override
    public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
        calculateWindowSize();
        calculateCellPositions(recycler, state);

        if (state.getItemCount() == 0) {
            detachAndScrapAttachedViews(recycler);
            firstVisibleRow = 0;
            resetVisibleItemTracking();
            return;
        }

        // TODO use orientationHelper
        int startTop = getPaddingTop();
        int scrollOffset = 0;
        if (forceClearOffsets) { // see #scrollToPosition
            startTop = -(firstVisibleRow * cellHeight);
            forceClearOffsets = false;
        } else if (getChildCount() != 0) {
            scrollOffset = getDecoratedTop(getChildAt(0));
            startTop = scrollOffset - (firstVisibleRow * cellHeight);
            resetVisibleItemTracking();
        }

        detachAndScrapAttachedViews(recycler);
        int row = firstVisibleRow;
        int availableSpace = getHeight() - scrollOffset;
        int lastItemPosition = state.getItemCount() - 1;
        while (availableSpace > 0 && lastVisiblePosition < lastItemPosition) {
            availableSpace -= layoutRow(row, startTop, recycler, state);
            row = getNextSpannedRow(row);
        }

        layoutDisappearingViews(recycler, state, startTop);
    }

    @Override
    public RecyclerView.LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT);
    }

    @Override
    public RecyclerView.LayoutParams generateLayoutParams(Context c, AttributeSet attrs) {
        return new LayoutParams(c, attrs);
    }

    @Override
    public RecyclerView.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        if (lp instanceof ViewGroup.MarginLayoutParams) {
            return new LayoutParams((ViewGroup.MarginLayoutParams) lp);
        } else {
            return new LayoutParams(lp);
        }
    }

    @Override
    public boolean checkLayoutParams(RecyclerView.LayoutParams lp) {
        return lp instanceof LayoutParams;
    }

    @Override
    public void onAdapterChanged(RecyclerView.Adapter oldAdapter, RecyclerView.Adapter newAdapter) {
        removeAllViews();
        reset();
    }

    @Override
    public boolean supportsPredictiveItemAnimations() {
        return true;
    }

    @Override
    public boolean canScrollVertically() {
        return true;
    }

    @Override
    public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state){
        if (getChildCount() == 0 || dy == 0) return 0;

        int scrolled;
        int top = getDecoratedTop(getChildAt(0));

        if (dy < 0) { // scrolling content down
            if (firstVisibleRow == 0) { // at top of content
                int scrollRange = -(getPaddingTop() - top);
                scrolled = Math.max(dy, scrollRange);
            } else {
                scrolled = dy;
            }
            if (top - scrolled >= 0) { // new top row came on screen
                int newRow = firstVisibleRow - 1;
                if (newRow >= 0) {
                    int startOffset = top - (firstVisibleRow * cellHeight);
                    layoutRow(newRow, startOffset, recycler, state);
                }
            }
            int firstPositionOfLastRow = getFirstPositionInSpannedRow(lastVisibleRow);
            int lastRowTop = getDecoratedTop(
                    getChildAt(firstPositionOfLastRow - firstVisiblePosition));
            if (lastRowTop - scrolled > getHeight()) { // last spanned row scrolled out
                recycleRow(lastVisibleRow, recycler, state);
            }
        } else { // scrolling content up
            int bottom = getDecoratedBottom(getChildAt(getChildCount() - 1));
            if (lastVisiblePosition == getItemCount() - 1) { // is at end of content
                int scrollRange = Math.max(bottom - getHeight() + getPaddingBottom(), 0);
                scrolled = Math.min(dy, scrollRange);
            } else {
                scrolled = dy;
            }
            if ((bottom - scrolled) < getHeight()) { // new row scrolled in
                int nextRow = lastVisibleRow + 1;
                if (nextRow < getSpannedRowCount()) {
                    int startOffset = top - (firstVisibleRow * cellHeight);
                    layoutRow(nextRow, startOffset, recycler, state);
                }
            }
            int lastPositionInRow = getLastPositionInSpannedRow(firstVisibleRow, state);
            int bottomOfFirstRow =
                    getDecoratedBottom(getChildAt(lastPositionInRow - firstVisiblePosition));
            if (bottomOfFirstRow - scrolled < 0) { // first spanned row scrolled out
                recycleRow(firstVisibleRow, recycler, state);
            }
        }
        offsetChildrenVertical(-scrolled);
        return scrolled;
    }

    @Override
    public void scrollToPosition(int position) {
        if (position >= getItemCount()) position = getItemCount() - 1;

        firstVisibleRow = getRowIndex(position);
        resetVisibleItemTracking();
        forceClearOffsets = true;
        removeAllViews();
        requestLayout();
    }

    @Override
    public void smoothScrollToPosition(
            RecyclerView recyclerView, RecyclerView.State state, int position) {
        if (position >= getItemCount()) position = getItemCount() - 1;

        LinearSmoothScroller scroller = new LinearSmoothScroller(recyclerView.getContext()) {
            @Override
            public PointF computeScrollVectorForPosition(int targetPosition) {
                final int rowOffset = getRowIndex(targetPosition) - firstVisibleRow;
                return new PointF(0, rowOffset * cellHeight);
            }
        };
        scroller.setTargetPosition(position);
        startSmoothScroll(scroller);
    }

    @Override
    public int computeVerticalScrollRange(RecyclerView.State state) {
        // TODO update this to incrementally calculate
        if (firstChildPositionForRow == null) return 0;
        return getSpannedRowCount() * cellHeight + getPaddingTop() + getPaddingBottom();
    }

    @Override
    public int computeVerticalScrollExtent(RecyclerView.State state) {
        return getHeight();
    }

    @Override
    public int computeVerticalScrollOffset(RecyclerView.State state) {
        if (getChildCount() == 0) return 0;
        return getPaddingTop() + (firstVisibleRow * cellHeight) - getDecoratedTop(getChildAt(0));
    }

    @Override
    public View findViewByPosition(int position) {
        if (position < firstVisiblePosition || position > lastVisiblePosition) return null;
        return getChildAt(position - firstVisiblePosition);
    }

    public int getFirstVisibleItemPosition() {
        return firstVisiblePosition;
    }

    private static class GridCell {
        final int row;
        final int rowSpan;
        final int column;
        final int columnSpan;

        GridCell(int row, int rowSpan, int column, int columnSpan) {
            this.row = row;
            this.rowSpan = rowSpan;
            this.column = column;
            this.columnSpan = columnSpan;
        }
    }

    /**
     * This is the main layout algorithm, iterates over all items and places them into [column, row]
     * cell positions. Stores this layout info for use later on. Also records the adapter position
     * that each row starts at.
     * <p>
     * Note that if a row is spanned, then the row start position is recorded as the first cell of
     * the row that the spanned cell starts in. This is to ensure that we have sufficient contiguous
     * views to layout/draw a spanned row.
     */
    private void calculateCellPositions(RecyclerView.Recycler recycler, RecyclerView.State state) {
        final int itemCount = state.getItemCount();
        cells = new SparseArray<>(itemCount);
        firstChildPositionForRow = new ArrayList<>();
        int row = 0;
        int column = 0;
        recordSpannedRowStartPosition(row, column);
        int[] rowHWM = new int[columns]; // row high water mark (per column)
        for (int position = 0; position < itemCount; position++) {

            SpanInfo spanInfo;
            int adapterPosition = recycler.convertPreLayoutPositionToPostLayout(position);
            if (adapterPosition !=  RecyclerView.NO_POSITION) {
                spanInfo = spanLookup.getSpanInfo(adapterPosition);
            } else {
                // item removed from adapter, retrieve its previous span info
                // as we can't get from the lookup (adapter)
                spanInfo = getSpanInfoFromAttachedView(position);
            }

            if (spanInfo.columnSpan > columns) {
                spanInfo.columnSpan = columns; // or should we throw?
            }

            // check horizontal space at current position else start a new row
            // note that this may leave gaps in the grid; we don't backtrack to try and fit
            // subsequent cells into gaps. We place the responsibility on the adapter to provide
            // continuous data i.e. that would not span column boundaries to avoid gaps.
            if (column + spanInfo.columnSpan > columns) {
                row++;
                recordSpannedRowStartPosition(row, position);
                column = 0;
            }

            // check if this cell is already filled (by previous spanning cell)
            while (rowHWM[column] > row) {
                column++;
                if (column + spanInfo.columnSpan > columns) {
                    row++;
                    recordSpannedRowStartPosition(row, position);
                    column = 0;
                }
            }

            // by this point, cell should fit at [column, row]
            cells.put(position, new GridCell(row, spanInfo.rowSpan, column, spanInfo.columnSpan));

            // update the high water mark book-keeping
            for (int columnsSpanned = 0; columnsSpanned < spanInfo.columnSpan; columnsSpanned++) {
                rowHWM[column + columnsSpanned] = row + spanInfo.rowSpan;
            }

            // if we're spanning rows then record the 'first child position' as the first item
            // *in the row the spanned item starts*. i.e. the position might not actually sit
            // within the row but it is the earliest position we need to render in order to fill
            // the requested row.
            if (spanInfo.rowSpan > 1) {
                int rowStartPosition = getFirstPositionInSpannedRow(row);
                for (int rowsSpanned = 1; rowsSpanned < spanInfo.rowSpan; rowsSpanned++) {
                    int spannedRow = row + rowsSpanned;
                    recordSpannedRowStartPosition(spannedRow, rowStartPosition);
                }
            }

            // increment the current position
            column += spanInfo.columnSpan;
        }
        totalRows = rowHWM[0];
        for (int i = 1; i < rowHWM.length; i++) {
            if (rowHWM[i] > totalRows) {
                totalRows = rowHWM[i];
            }
        }
    }

    private SpanInfo getSpanInfoFromAttachedView(int position) {
        for (int i = 0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            if (position == getPosition(child)) {
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                return new SpanInfo(lp.columnSpan, lp.rowSpan);
            }
        }
        // errrrr?
        return SpanInfo.SINGLE_CELL;
    }

    private void recordSpannedRowStartPosition(final int rowIndex, final int position) {
        if (getSpannedRowCount() < (rowIndex + 1)) {
            firstChildPositionForRow.add(position);
        }
    }

    private int getRowIndex(final int position) {
        return position < cells.size() ? cells.get(position).row : -1;
    }

    private int getSpannedRowCount() {
        return firstChildPositionForRow.size();
    }

    private int getNextSpannedRow(int rowIndex) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int nextRow = rowIndex + 1;
        while (nextRow < getSpannedRowCount()
                && getFirstPositionInSpannedRow(nextRow) == firstPositionInRow) {
            nextRow++;
        }
        return nextRow;
    }

    private int getFirstPositionInSpannedRow(int rowIndex) {
        return firstChildPositionForRow.get(rowIndex);
    }

    private int getLastPositionInSpannedRow(final int rowIndex, RecyclerView.State state) {
        int nextRow = getNextSpannedRow(rowIndex);
        return (nextRow != getSpannedRowCount()) ? // check if reached boundary
                getFirstPositionInSpannedRow(nextRow) - 1
                : state.getItemCount() - 1;
    }

    /**
     * Lay out a given 'row'. We might actually add more that one row if the requested row contains
     * a row-spanning cell. Returns the pixel height of the rows laid out.
     * <p>
     * To simplify logic & book-keeping, views are attached in adapter order, that is child 0 will
     * always be the earliest position displayed etc.
     */
    private int layoutRow(
            int rowIndex, int startTop, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
        boolean containsRemovedItems = false;

        int insertPosition = (rowIndex < firstVisibleRow) ? 0 : getChildCount();
        for (int position = firstPositionInRow;
             position <= lastPositionInRow;
             position++, insertPosition++) {

            View view = recycler.getViewForPosition(position);
            LayoutParams lp = (LayoutParams) view.getLayoutParams();
            containsRemovedItems |= lp.isItemRemoved();
            GridCell cell = cells.get(position);
            addView(view, insertPosition);

            // TODO use orientation helper
            int wSpec = getChildMeasureSpec(
                    cellBorders[cell.column + cell.columnSpan] - cellBorders[cell.column],
                    View.MeasureSpec.EXACTLY, 0, lp.width, false);
            int hSpec = getChildMeasureSpec(cell.rowSpan * cellHeight,
                    View.MeasureSpec.EXACTLY, 0, lp.height, true);
            measureChildWithDecorationsAndMargin(view, wSpec, hSpec);

            int left = cellBorders[cell.column] + lp.leftMargin;
            int top = startTop + (cell.row * cellHeight) + lp.topMargin;
            int right = left + getDecoratedMeasuredWidth(view);
            int bottom = top + getDecoratedMeasuredHeight(view);
            layoutDecorated(view, left, top, right, bottom);
            lp.columnSpan = cell.columnSpan;
            lp.rowSpan = cell.rowSpan;
        }

        if (firstPositionInRow < firstVisiblePosition) {
            firstVisiblePosition = firstPositionInRow;
            firstVisibleRow = getRowIndex(firstVisiblePosition);
        }
        if (lastPositionInRow > lastVisiblePosition) {
            lastVisiblePosition = lastPositionInRow;
            lastVisibleRow = getRowIndex(lastVisiblePosition);
        }
        if (containsRemovedItems) return 0; // don't consume space for rows with disappearing items

        GridCell first = cells.get(firstPositionInRow);
        GridCell last = cells.get(lastPositionInRow);
        return (last.row + last.rowSpan - first.row) * cellHeight;
    }

    /**
     * Remove and recycle all items in this 'row'. If the row includes a row-spanning cell then all
     * cells in the spanned rows will be removed.
     */
    private void recycleRow(
            int rowIndex, RecyclerView.Recycler recycler, RecyclerView.State state) {
        int firstPositionInRow = getFirstPositionInSpannedRow(rowIndex);
        int lastPositionInRow = getLastPositionInSpannedRow(rowIndex, state);
        int toRemove = lastPositionInRow;
        while (toRemove >= firstPositionInRow) {
            int index = toRemove - firstVisiblePosition;
            removeAndRecycleViewAt(index, recycler);
            toRemove--;
        }
        if (rowIndex == firstVisibleRow) {
            firstVisiblePosition = lastPositionInRow + 1;
            firstVisibleRow = getRowIndex(firstVisiblePosition);
        }
        if (rowIndex == lastVisibleRow) {
            lastVisiblePosition = firstPositionInRow - 1;
            lastVisibleRow = getRowIndex(lastVisiblePosition);
        }
    }

    private void layoutDisappearingViews(
            RecyclerView.Recycler recycler, RecyclerView.State state, int startTop) {
        // TODO
    }

    private void calculateWindowSize() {
        // TODO use OrientationHelper#getTotalSpace
        int cellWidth =
                (int) Math.floor((getWidth() - getPaddingLeft() - getPaddingRight()) / columns);
        cellHeight = (int) Math.floor(cellWidth * (1f / cellAspectRatio));
        calculateCellBorders();
    }

    private void reset() {
        cells = null;
        firstChildPositionForRow = null;
        firstVisiblePosition = 0;
        firstVisibleRow = 0;
        lastVisiblePosition = 0;
        lastVisibleRow = 0;
        cellHeight = 0;
        forceClearOffsets = false;
    }

    private void resetVisibleItemTracking() {
        // maintain the firstVisibleRow but reset other state vars
        // TODO make orientation agnostic
        int minimumVisibleRow = getMinimumFirstVisibleRow();
        if (firstVisibleRow > minimumVisibleRow) firstVisibleRow = minimumVisibleRow;
        firstVisiblePosition = getFirstPositionInSpannedRow(firstVisibleRow);
        lastVisibleRow = firstVisibleRow;
        lastVisiblePosition = firstVisiblePosition;
    }

    private int getMinimumFirstVisibleRow() {
        int maxDisplayedRows = (int) Math.ceil((float) getHeight() / cellHeight) + 1;
        if (totalRows < maxDisplayedRows) return 0;
        int minFirstRow = totalRows - maxDisplayedRows;
        // adjust to spanned rows
        return getRowIndex(getFirstPositionInSpannedRow(minFirstRow));
    }

    /* Adapted from GridLayoutManager */

    private void calculateCellBorders() {
        cellBorders = new int[columns + 1];
        int totalSpace = getWidth() - getPaddingLeft() - getPaddingRight();
        int consumedPixels = getPaddingLeft();
        cellBorders[0] = consumedPixels;
        int sizePerSpan = totalSpace / columns;
        int sizePerSpanRemainder = totalSpace % columns;
        int additionalSize = 0;
        for (int i = 1; i <= columns; i++) {
            int itemSize = sizePerSpan;
            additionalSize += sizePerSpanRemainder;
            if (additionalSize > 0 && (columns - additionalSize) < sizePerSpanRemainder) {
                itemSize += 1;
                additionalSize -= columns;
            }
            consumedPixels += itemSize;
            cellBorders[i] = consumedPixels;
        }
    }

    private void measureChildWithDecorationsAndMargin(View child, int widthSpec, int heightSpec) {
        calculateItemDecorationsForChild(child, itemDecorationInsets);
        RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) child.getLayoutParams();
        widthSpec = updateSpecWithExtra(widthSpec, lp.leftMargin + itemDecorationInsets.left,
                lp.rightMargin + itemDecorationInsets.right);
        heightSpec = updateSpecWithExtra(heightSpec, lp.topMargin + itemDecorationInsets.top,
                lp.bottomMargin + itemDecorationInsets.bottom);
        child.measure(widthSpec, heightSpec);
    }

    private int updateSpecWithExtra(int spec, int startInset, int endInset) {
        if (startInset == 0 && endInset == 0) {
            return spec;
        }
        int mode = View.MeasureSpec.getMode(spec);
        if (mode == View.MeasureSpec.AT_MOST || mode == View.MeasureSpec.EXACTLY) {
            return View.MeasureSpec.makeMeasureSpec(
                    View.MeasureSpec.getSize(spec) - startInset - endInset, mode);
        }
        return spec;
    }

    /* Adapted from ConstraintLayout */

    private void parseAspectRatio(String aspect) {
        if (aspect != null) {
            int colonIndex = aspect.indexOf(':');
            if (colonIndex >= 0 && colonIndex < aspect.length() - 1) {
                String nominator = aspect.substring(0, colonIndex);
                String denominator = aspect.substring(colonIndex + 1);
                if (nominator.length() > 0 && denominator.length() > 0) {
                    try {
                        float nominatorValue = Float.parseFloat(nominator);
                        float denominatorValue = Float.parseFloat(denominator);
                        if (nominatorValue > 0 && denominatorValue > 0) {
                            cellAspectRatio = Math.abs(nominatorValue / denominatorValue);
                            return;
                        }
                    } catch (NumberFormatException e) {
                        // Ignore
                    }
                }
            }
        }
        throw new IllegalArgumentException("Could not parse aspect ratio: '" + aspect + "'");
    }

}

使用

SpannedGridLayoutManager layoutManager = new SpannedGridLayoutManager(position -> {
                    if (position == 0) {//表示第一個(gè)占兩行兩列属划,其他都是一行一列
                        return new SpannedGridLayoutManager.SpanInfo(2, 2);
                    } else {
                        return new SpannedGridLayoutManager.SpanInfo(1, 1);
                    }
                }, 3, 1f);
                if (recyclerView.getItemDecorationCount() == 0) {
                    recyclerView.addItemDecoration(new ItemDecorations(3, getSpacing(), getIncludeEdge()));
                }
                recyclerView.setLayoutManager(layoutManager);

效果


Dingtalk_20221102190911.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末喘沿,一起剝皮案震驚了整個(gè)濱河市胸墙,隨后出現(xiàn)的幾起案子被丧,更是在濱河造成了極大的恐慌咧虎,老刑警劉巖闺属,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異牡昆,居然都是意外死亡姚炕,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門丢烘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柱宦,“玉大人,你說(shuō)我怎么就攤上這事播瞳〉Э” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵赢乓,是天一觀的道長(zhǎng)忧侧。 經(jīng)常有香客問(wèn)我,道長(zhǎng)牌芋,這世上最難降的妖魔是什么蚓炬? 我笑而不...
    開(kāi)封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮躺屁,結(jié)果婚禮上肯夏,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好驯击,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布烁兰。 她就那樣靜靜地躺著,像睡著了一般徊都。 火紅的嫁衣襯著肌膚如雪沪斟。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天暇矫,我揣著相機(jī)與錄音币喧,去河邊找鬼。 笑死袱耽,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的干发。 我是一名探鬼主播朱巨,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼枉长!你這毒婦竟也來(lái)了冀续?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤必峰,失蹤者是張志新(化名)和其女友劉穎洪唐,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吼蚁,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凭需,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了肝匆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粒蜈。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖旗国,靈堂內(nèi)的尸體忽然破棺而出枯怖,到底是詐尸還是另有隱情,我是刑警寧澤能曾,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布度硝,位于F島的核電站,受9級(jí)特大地震影響寿冕,放射性物質(zhì)發(fā)生泄漏蕊程。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一蚂斤、第九天 我趴在偏房一處隱蔽的房頂上張望存捺。 院中可真熱鬧,春花似錦、人聲如沸捌治。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)肖油。三九已至兼吓,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間森枪,已是汗流浹背视搏。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留县袱,地道東北人浑娜。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像式散,于是被迫代替她去往敵國(guó)和親筋遭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

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