一個FlowLayout帶你學會自定義ViewGroup

時間過得真快,又到了寫博客的時候了(/▽╲)。這次按照計劃記錄一個簡單的自定義ViewGroup:流布局FlowLayout的實現(xiàn)過程,將自定義控件知識儲備-View的繪制流程自定義控件知識儲備-LayoutParams的那些事里的知識點結合起來,付諸實踐。

1. 前言

早在學習Java的Swing基礎知識的時候,就見到過里面的流布局FlowLayout,基本的效果就是讓加入此容器的控件自左往右依次排列,如果當前行的寬度不足以容納下一個控件,就會將此控件放置到下一行。其實這也跟css里向左浮動的效果很相似。

在Android的世界里,系統(tǒng)是沒有提供類似FlowLayout布局的容器的。當然了,現(xiàn)在官方給我們提供了更強大也更復雜的FlexLayout了。不過嘛,本篇博客是總結一個自定義ViewGroup的實現(xiàn)流程,所以需要找一個難易適中的實例來進行分析,也就是FlowLayout了。(是的,我就是挑軟柿子捏︿( ̄︶ ̄)︿)。

2. 效果

閑話少說,還是先來看看蘑菇君寫的FlowLayout的功能:

  • 支持最基本的從左至右的排序,空間不足則換行
  • 支持設置子控件間的水平和豎直的間隔(也可以通過給每個child設置margin來實現(xiàn),不過沒有統(tǒng)一設置來的方便)
  • 支持繪制行之間的分割線
  • 支持FlowLayout本身的Gravity和child views的Gravity
  • 處理好FlowLayout的padding和child views的margin

這些都是FlowLayout基本的功能,效果如下圖所示:

FlowLayout效果展示

是不是感覺還行?至少一般的情況下是能滿足大部分人的需求滴。o( ̄▽ ̄)d

3. 分析

列舉一下自定義ViewGroup的流程:

  1. 自定義屬性:如果ViewGroup需要用到自定義屬性,則需要聲明、設置、解析并獲取自定義屬性值。
  2. 測量:在onMeasure方法里處理AT_MOSTEXACTLY兩種測量模式下ViewGroup的寬高和children的寬高。(UNSPECIFIED模式可以暫不考慮)
  3. 布局:在onLayout方法里確定children的位置。
  4. 繪制:如果ViewGroup里需要繪制,則重寫onDraw方法,按邏輯繪制。比如FlowLayout可以在每一行之間繪制一條分隔線。
  5. 處理LayoutParams:如果要為children定義布局屬性,如layout_gravity,則需要自定義LayoutParams,并且重寫ViewGroup相關的方法。
  6. 處理滑動事件:在本FlowLayout里暫時用不上...( ╯▽╰)

上面的步驟可能有所遺漏,不過也差不多啦。下面蘑菇君要根據(jù)上述的流程來一步一步的分析FlowLayout的源碼,源碼可能有點長,有些細節(jié)上的邏輯看不懂也莫方,只要了解流程對應的實現(xiàn)方式和注意事項就好,有興趣的話可以稍后自己下載源碼分析具體的邏輯實現(xiàn)。

好滴,那就讓我們來一步一步的看,這個FlowLayout是如何在我手里...被玩殘的...

3.1 自定義屬性

3.1.1 聲明屬性

首先,自定義屬性的第一步當然是聲明屬性,而最常使用的方式當然是在xml資源文件里(一般來說就是attrs.xml文件)聲明需要使用的屬性:

   <declare-styleable name="FlowLayout">
        <attr name="android:gravity"/>
        <attr name="horizonSpacing" format="dimension|reference"/>
        <attr name="verticalSpacing" format="dimension|reference"/>
        <attr name="dividerColor" format="color|reference"/>
        <attr name="dividerWidth" format="dimension|reference"/>
    </declare-styleable>

    <declare-styleable name="FlowLayout_Layout">
        <attr name="android:layout_gravity"/>
    </declare-styleable>

這里需要注意兩個地方:

  1. 我們聲明了兩個declare-styleable,一個是為FlowLayout自身設置自定義屬性;另一個是為孩子們提供額外屬性,需要在自定義的LayoutParams里解析獲取屬性值。

  2. 大家都知道,我們在xml布局文件里使用自定義屬性時,需要引入命名空間

xmlns:app="http://schemas.android.com/apk/res-auto"

使用自定義屬性時,需要加上前綴app(或者是其它命名,只要一一對應)。但是有時候啊,我們自定義的屬性名已經(jīng)在系統(tǒng)中存在了,而且語義與我們想要的也很符合,比如如andrioid:textandroid:gravity等等。這個時候估計誰都會有一種“拿來主義”的沖動:直接使用系統(tǒng)里已經(jīng)存在的屬性名就好了嘛,多“原生”!既然有這種“邪惡”的需求,那Google工程師自然是要滿足滴(~ ̄▽ ̄)~。

gravity屬性為例,我們只要在declare-styleable里直接寫上<attr name="android:gravity"/>即可,不過這里要注意的是不需要也不能再加上format屬性,加上format屬性就代表著這是在聲明一個新的屬性,不加則代表這是在使用已存在的一個屬性。

3.1.2 使用屬性

使用屬性就比較簡單了:

<wang.mogujun.widget.FlowLayout
        android:id="@+id/flow2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:background="#6A6A6A"
        android:gravity="start"
        android:padding="8dp"
        app:horizonSpacing="8dp"
        app:verticalSpacing="12dp"
        app:dividerColor="#cccccc"
        app:dividerWidth="2dp"
        >

3.1.3 解析并獲取屬性

在xml設置了相應的屬性后,就需要在FlowLayout里解析并獲取屬性值了:


public static final int DEFAULT_SPACING = 8;
    public static final int DEFAULT_DIVIDER_COLOR = Color.parseColor("#ececec");
    public static final int DEFAULT_DIVIDER_WIDTH = 3;

    private int mGravity = (isIcs() ? Gravity.START : Gravity.LEFT) | Gravity.TOP;

    private int mVerticalSpacing; //vertical spacing
    private int mHorizontalSpacing; //horizontal spacing
    private int mDividerColor;
    private int mDividerWidth;
    
private void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {

        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout, defStyleAttr, defStyleRes);

        try {
            mHorizontalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_horizonSpacing, DEFAULT_SPACING);
            mVerticalSpacing = (int) ta.getDimension(R.styleable.FlowLayout_verticalSpacing, DEFAULT_SPACING);
            mDividerWidth = (int) ta.getDimension(R.styleable.FlowLayout_dividerWidth, DEFAULT_DIVIDER_WIDTH);
            mDividerColor = ta.getColor(R.styleable.FlowLayout_dividerColor, DEFAULT_DIVIDER_COLOR);
            int index = ta.getInt(R.styleable.FlowLayout_android_gravity, -1);
            if (index > 0) {
                setGravity(index);
            }
            initPaint();
        } finally {
            ta.recycle();
        }
        setWillNotDraw(false);

    }

一般來說,我們的自定義屬性都得給個默認值,大家都這么懶,不能強人所難對不對。這默認值可以通過常量直接寫在自定義類里,如上述代碼所示。也可以寫在xml資源文件里,提供給別人統(tǒng)一修改。

其次呢,英明神武的蘑菇君自然也得提供方法讓別人方便的通過代碼去動態(tài)修改這些屬性啦(真不要臉~~( ﹁ ﹁ ) ~~~):

 public void setHorizontalSpacing(int pixelSize) {
        mHorizontalSpacing = pixelSize;
        requestLayout();
    }

    public void setVerticalSpacing(int pixelSize) {
        mVerticalSpacing = pixelSize;
        requestLayout();
    }

    public void setDividerColor(@ColorInt int color) {
        mDividerColor = color;
        mDividerPaint.setColor(color);
        invalidate();
    }
    ...

關于自定義屬性的一些詳細知識可以參考文章: Android 深入理解Android中的自定義屬性

3.2 測量

在自定義ViewGroup時,測量流程一般是所有流程中最為復雜的一環(huán)。因為我們不僅要測量ViewGroup自身的尺寸,還得測量所有孩子的尺寸。而ViewGroup和孩子們之間的尺寸又是相互影響的。

如下圖所示,在我們的FlowLayout里,當寬的測量模式為AT_MOST(比如FlowLayout的布局屬性android:layout_widthwrap_content時),F(xiàn)lowLayout的測量寬度應該是所有行里最長的那一行的寬度,在下圖中就是第二行的寬度。而當高的測量模式為AT_MOST,F(xiàn)lowLayout的測量高度應該是所有行的高度總和。

而對于child view來說,也有個小小的限制:當FlowLayout的layout_heightwrap_content,而child的layout_heightmatch_parent時,我希望child的測量高為它所處那一行的高度,而不是整個FlowLayout的高度或者是wrap_content。這也挺合情合理的吧,比如下圖中第一行的child 再見這群坑比layout_heightmatch_parent,所以它就和第一行的高度一樣高。

寬高為wrap_content時的FlowLayout

可能說得大家都有點暈了X﹏X,還是來一起看看onMeasure方法的源碼吧:

 //保存所有child view
private final List<List<View>> mLines = new ArrayList<>();
//保存所有行高
private final List<Integer> mLineHeights = new ArrayList<>();
//保存所有行寬
private final List<Integer> mLineWidths = new ArrayList<>();

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        mLines.clear();
        mLineHeights.clear();
        mLineWidths.clear();

        int sizeWidth = MeasureSpec.getSize(widthMeasureSpec);
        int sizeHeight = MeasureSpec.getSize(heightMeasureSpec);
        int modeWidth = MeasureSpec.getMode(widthMeasureSpec);
        int modeHeight = MeasureSpec.getMode(heightMeasureSpec);

        int widthUsed = getPaddingLeft() + getPaddingRight() + mHorizontalSpacing;
        int lineWidth = widthUsed;
        int lineHeight = 0;

        int childCount = getChildCount();
        List<View> lineViews = new ArrayList<>();
        
        for (int i = 0; i < childCount; i++) {

            View child = getChildAt(i);

            if (child.getVisibility() == View.GONE) {
                continue;
            }

            LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //測量每個child的寬高,每個child可用的最大寬高為sizeWidth-spacing-padding-margin
            measureChildWithMargins(child, widthMeasureSpec, mHorizontalSpacing * 2, heightMeasureSpec, mVerticalSpacing * 2);

            int childWidth = child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
            int childHeight = child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
            //判斷這一行是否還能容下這個child
            if (lineWidth + childWidth + mHorizontalSpacing > sizeWidth) {
                //需要換行,則記錄這一行的寬度,高度,下一行的初始寬度,初始高度
            
                mLineWidths.add(lineWidth);
                lineWidth = widthUsed + childWidth + mHorizontalSpacing;

                mLineHeights.add(lineHeight);
                lineHeight = childHeight;

                mLines.add(lineViews);
                lineViews = new ArrayList<>();
            } else {//容得下,則累加這一行的寬度,記錄這一行的高度
                lineWidth += childWidth + mHorizontalSpacing;
                lineHeight = Math.max(lineHeight, childHeight);
            }

            lineViews.add(child);

        }
        //最后一行的處理
        mLineHeights.add(lineHeight);
        mLineWidths.add(lineWidth);
        mLines.add(lineViews);

        int maxWidth = Collections.max(mLineWidths);

        processChildHeights();//計算所有行的累積高度
        int totalHeight = getChildHeights();

        //TODO 處理getMinimumWidth/height的情況

        //設置自身的測量寬高
        setMeasuredDimension(
                (modeWidth == MeasureSpec.EXACTLY) ? sizeWidth : Math.min(maxWidth, sizeWidth),
                (modeHeight == MeasureSpec.EXACTLY) ? sizeHeight : Math.min(totalHeight, sizeHeight));
                
        //重新測量child的lp.height為MATCH_PARENT時的child的尺寸
        remeasureChild(widthMeasureSpec);
    }



上面的代碼邏輯都有注釋,相信大家都能理清大概的邏輯。暫時沒理解也沒關系,稍后自己去看代碼再加上自己的思考肯定能看懂滴。(蘑菇君自我感覺腦子轉的算慢的,看Github上的FlowLayout源碼花了蠻久時間才弄懂大概邏輯,自己畫圖呀,運行demo呀,弄懂了以后,才開始自己動手寫自己的FlowLayout...(??????)??)

這里要特別注意的是對children的測量過程。在上面的代碼中,我使用了ViewGroup類里提供的measureChildWithMargins方法去測量每個child,對這個方法的具體剖析,可以去看自定義控件知識儲備-View的繪制流程,這篇文章講的很詳細。但在上文中有提到過,我們對child有個限制:

當child的layout_heightmatch_parent時,child的測量高為它所處那一行的高度,而不是整個FlowLayout的高度或者是wrap_content

但是這個child所處那一行的高度是那一行所有child的高度的最大值,所以只有在完成這一行所有child的測量后,才知道這一行的高度是多少。所以上面的要求無法滿足呀!我在測量該child的高度的時候,還不知道這一行的高度是多少啊!

這就尷尬了

該怎么辦呢?其實也簡單,既然當時測量某child的時候還不知道那一行的高度,那就在第一次所有child都測量完成后,再對那些layout_heightmatch_parent的child測量一遍就好啦。所以在上面onMeasure方法里的最后調用了remeasureChild這個方法去重新測量一遍child:

private void remeasureChild(int parentWidthSpec) {
        int numLines = mLines.size();
        for (int i = 0; i < numLines; i++) {//遍歷每一行
            int lineHeight = mLineHeights.get(i);
            List<View> lineViews = mLines.get(i);
            int children = lineViews.size();
            for (int j = 0; j < children; j++) {
                View child = lineViews.get(j);
                LayoutParams lp = (LayoutParams) child.getLayoutParams();
                if (lp.height == LayoutParams.MATCH_PARENT) {//對高為match_parent的child進行處理
                    if (child.getVisibility() == View.GONE) {
                        continue;
                    }

                    int widthUsed = lp.leftMargin + lp.rightMargin +
                            getPaddingLeft() + getPaddingRight() + 2 * mHorizontalSpacing;
                    //再次調用child的measure方法進行測量        
                    child.measure(
                            getChildMeasureSpec(parentWidthSpec, widthUsed, lp.width),
                            MeasureSpec.makeMeasureSpec(lineHeight - lp.topMargin - lp.bottomMargin, MeasureSpec.EXACTLY)
                    );
                }
            }
        }
    }

從這里我們也看得出來,一個View的onMeasure方法是很有可能被調用多次來確定最終的測量寬高的,所以下次遇到打印日志里或者斷點調試下發(fā)現(xiàn) onMeasure方法多次運行,莫要方呀o( ̄??)。

3.3 布局

布局過程呢,就稍微簡單一些,因為我們在onMeasure方法里已經(jīng)將所有child的寬高和位于哪一行等信息都計算好了,只要遍歷children調用它們的layout方法放置好它們就行。不過這里有點麻煩的就是,我們需要支持FlowLayout自身的gravity屬性和children的 gravity屬性。那就得根據(jù)具體的gravity來計算相應的偏移量了,代碼如下:

//根據(jù)gravity計算FlowLayout的垂直方向上的偏移量
private void processVerticalGravityMargin() {
        int verticalGravityMargin;
        int childHeights = getChildHeights();
        switch ((mGravity & Gravity.VERTICAL_GRAVITY_MASK)) {
            case Gravity.TOP://頂部
            default:
                verticalGravityMargin = 0;
                break;
            case Gravity.CENTER_VERTICAL://垂直居中
                verticalGravityMargin = Math.max((getHeight() - childHeights) / 2, 0);
                break;
            case Gravity.BOTTOM://底部
                verticalGravityMargin = Math.max(getHeight() - childHeights, 0);
                break;
        }
        mVerticalGravityMargin = verticalGravityMargin;
    }

//根據(jù)gravity計算FlowLayout的水平方向上的偏移量
    private void processHorizontalGravityMargins() {
        mLineMargins.clear();
        float horizontalGravityFactor;
        switch ((mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)) {
            case Gravity.LEFT://水平靠左
            default:
                horizontalGravityFactor = 0;
                break;
            case Gravity.CENTER_HORIZONTAL://水平居中
                horizontalGravityFactor = .5f;
                break;
            case Gravity.RIGHT://水平靠右
                horizontalGravityFactor = 1;
                break;
        }

        int linesNum = mLineWidths.size();
        for (int i = 0; i < linesNum; i++) {
            int lineWidth = mLineWidths.get(i);
            mLineMargins.add((int) ((getWidth() - lineWidth) * horizontalGravityFactor) + getPaddingLeft() + mHorizontalSpacing);
        }
    }

給FlowLayout設置gravity的效果如下:

內容居中:

FlowLayout內容居中

內容在右下角:

FlowLayout內容在右下角

計算好了每行的偏移量后,layout方法的邏輯就很清晰了:

protected void onLayout(boolean changed, int l, int t, int r, int b) {

        processHorizontalGravityMargins();
        processVerticalGravityMargin();

        int numLines = mLines.size();
        int left;
        int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;

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

            int lineHeight = mLineHeights.get(i);
            List<View> lineViews = mLines.get(i);
            left = mLineMargins.get(i);
            int children = lineViews.size();

            for (int j = 0; j < children; j++) {

                View child = lineViews.get(j);

                if (child.getVisibility() == View.GONE) {
                    continue;
                }

                LayoutParams lp = (LayoutParams) child.getLayoutParams();

                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                int gravityMargin = 0;
                //根據(jù)child的gravity計算child的相應偏移量
                if (Gravity.isVertical(lp.gravity)) {
                    switch (lp.gravity) {
                        case Gravity.TOP:
                        default:
                            gravityMargin = 0;
                            break;
                        case Gravity.CENTER_VERTICAL:
                        case Gravity.CENTER:
                            gravityMargin = (lineHeight - childHeight - lp.topMargin - lp.bottomMargin) / 2;
                            break;
                        case Gravity.BOTTOM:
                            gravityMargin = lineHeight - childHeight - lp.topMargin - lp.bottomMargin;
                            break;
                        //TODO 水平方向上可以支持gravity么?
                    }
                }

                child.layout(left + lp.leftMargin,
                        top + lp.topMargin + gravityMargin,
                        left + lp.leftMargin + childWidth,
                        top + lp.topMargin + gravityMargin + childHeight);

                Log.i(TAG, String.format("child (%d,%d) position: (%d,%d,%d,%d)",
                        i, j, child.getLeft(), child.getTop(), child.getRight(), child.getBottom()));

                left += childWidth + lp.leftMargin + lp.rightMargin + mHorizontalSpacing;

            }

            top += lineHeight + mVerticalSpacing;
        }

    }

3.4 繪制

本FlowLayout支持繪制分割線,這也是很容易的繪制,只要找準每條分割線的位置就行。不過萬變不離其宗嘛,我現(xiàn)在能畫一條線,下次就能畫一個圓,再下次就能畫個雞蛋,再再下次我就能飛上天,畫出太陽肩并肩...。咳咳,扯遠了,我們還是來看看onDraw方法里的繪制邏輯:

@Override
    protected void onDraw(Canvas canvas) {

        int top = getPaddingTop() + mVerticalSpacing + mVerticalGravityMargin;
        int numLines = mLines.size();
        for (int i = 0; i < numLines; i++) {
            int lineHeight = mLineHeights.get(i);
            top += lineHeight + mVerticalSpacing;
            canvas.drawLine(getPaddingLeft(), top - mVerticalSpacing / 2, 
            getWidth() - getPaddingRight(), top - mVerticalSpacing / 2, mDividerPaint);
        }

    }

確實很簡單,遍歷每一行,在兩行的中間根據(jù)配置的顏色和寬度畫出一條線段即可。

不過這里要注意View的一個特殊方法:setWillNotDraw,來看一下這個方法的源碼:

/**
     * If this view doesn't do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override {@link #onDraw(android.graphics.Canvas)}
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) {
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    }

從這個方法的注釋中可以看出,如果一個View不需要繪制任何內容,那么設置這個標記位為true后,系統(tǒng)會進行相應的優(yōu)化。默認情況下,View沒有啟用這個優(yōu)化標記位,而ViewGroup會默認啟用這個標記位。

當我們的自定義ViewGroup需要通過重寫onDraw來繪制內容時,我們需要顯式地關閉WILL_NOT_DRAW這個標記位。

所以,在這個FlowLayout的構造方法里,我們可以調用setWillNotDraw(false)來進行優(yōu)化。

3.5 處理LayoutParams

幾乎每個自定義ViewGroup都得自定義自己的LayoutParams,來給children提供更好的服務。在本FlowLayout里,能給children帶來的就是gravity屬性的支持。來看看自定義的LayoutParams:

 public static class LayoutParams extends MarginLayoutParams {

        public int gravity = -1;

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

            TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FlowLayout_Layout);

            try {
                gravity = a.getInt(R.styleable.FlowLayout_Layout_android_layout_gravity, -1);
            } finally {
                a.recycle();
            }
        }

        public LayoutParams(int width, int height) {
            super(width, height);
            gravity = Gravity.TOP;
        }

        public LayoutParams(int width, int height, int gravity) {
            super(width, height);
            this.gravity = gravity;
        }

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

    }

同時,F(xiàn)lowLayout還需要對以下幾個方法進行重寫:

@Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return super.checkLayoutParams(p) && p instanceof LayoutParams;
    }

    @Override
    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new LayoutParams(p);
    }

    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

啥?不知道為啥要按上述代碼那樣做?那是時候去看看自定義控件知識儲備-LayoutParams的那些事了。看完了你就大徹大悟,遁入......咳咳。

3. 展示

哎呀呀,這篇文章已經(jīng)夠長了,我就不貼資源文件,截圖等東西啦,大家有需要的話,可以去Github上下載源碼進行學習。

Github地址: https://github.com/yisizhu520/FlowLayout

PS:蘑菇君寫的這個FlowLayout肯定還存在bug,而且我自己也知道幾個不影響使用的小bug,但是我沒有去改,等待有緣人去發(fā)現(xiàn)哈(≧?≦)?。

也歡迎大家去提交issue和pull request,一起交流,一起進步。

4. 總結

終于寫完這篇博客了,真是寫死我了?(T?T)。希望這篇文章除了能加深自己對自定義ViewGroup的理解外,還能幫助到大家。以前一直以為自己了解了自定義ViewGroup的一些知識,想要寫一個容器控件出來應該不難的。然而,紙上得來終覺淺,當自己真的開始寫的時候,發(fā)現(xiàn)滿滿的都是細節(jié),滿滿的都是套路。比如在FlowLayout里的測量、布局、繪制都得考慮到間距的問題,什么margin啊,padding啊,spacing啊,都需要小心對待。不過,最終還是在不斷的調試和修改中寫出來了這個FlowLayout,想想還有點小激動呢!以后要做的應該就是不斷的練習和總結,畢竟編程這件事,沒啥好說的,just code it!

just code it

我是蘑菇君,我為自己帶鹽

5. 參考資料

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,002評論 6 542
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,400評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,136評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,714評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,452評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,818評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,812評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,997評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,552評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,292評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,510評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,035評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,721評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,121評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,429評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,235評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,480評論 2 379

推薦閱讀更多精彩內容