自定义组合控件的一些心得

日常开发中,我们常常碰到需要复用一组控件的时候,比如常见的标题栏,一些列表的itemview等。这种由一系列普通View组合起来复用的形式,一般叫做组合控件。

组合控件相比于自定义控件,即不依靠原生的控件,完全自己设计的新控件,要简单、易上手的多,我们在实际开发中碰到的几率也大的多。我在自己参与的某主流APP的开发过程中,也常常碰到需要使用组合控件的情况。中间有一些学习和思考,在这里记录一下,同时也是跟大家一起分享。

下面来看例子,假如因为业务需要,我们创建了下面这个XML文件(R.layout.follow_layout):

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/add_follow_hint"
        android:textColor="@color/base_txt_gray1"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@+id/textView4"
        app:layout_constraintStart_toStartOf="@+id/textView4"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:text="及时接受对方最新动态"
        android:textColor="#aaaaaa"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <Button
        android:id="@+id/chat_header_foolow_layout_btn"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_marginBottom="10dp"
        android:layout_marginEnd="15dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/follow_btn_player_selector"
        android:drawablePadding="2dp"
        android:text="@string/follow"
        android:textColor="#FFFFFFFF"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

我们在需要复用的时候有下面三种方式,我们一一道来。

1. 不太灵活的方式

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <include layout="@layout/follow_layout"/>
</LinearLayout>

效果如下

效果图

这是我们复用的第一种方式。也是郭琳大神在《第一行代码》里的“引入布局”一节里提到的方式。为什么说不太灵活呢,是因为我们完全无法给自定义View添加自定义属性。

2. 最常见的方式

《Android进阶之光》一书中,刘望舒大神在“自定义组合控件”一节里提到了类似下面的这种方式。
注意,XML文件内容不变,跟上面提到的一致,但是额外创建了一个新的控件类,FollowLayout.java :

public class FollowLayout extends ConstraintLayout {
    private Button mFollowBtn;
    public FollowLayout(Context context) {
        super(context);
        initView(context);
    }

    public FollowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView(context);
    }

    public FollowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView(context);
    }

    private void initView(Context context) {
        /**
         * XML文件就是上面提到的那个
         */
        LayoutInflater.from(context).inflate(R.layout.follow_layout, this);
        mFollowBtn = findViewById(R.id.chat_header_foolow_layout_btn);
        mFollowBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 点击事件
                 */
            }
        });
    }
}

写了这样一个控件类的好处,除了可以统一的设置很多东西之外,引用控件的时候也可以这么修改:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />

    <!--<include layout="@layout/follow_layout"/>-->
    <com.example.xuqi.customlayoutdemo.FollowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</LinearLayout>

这样的好处是灵活性更好,而且我们可以自定义控件属性,关于自定义空间属性的方法大家可以自行查询,这里不展开来说了。

2.1 相关原理

这个写法,是大家在开发中使用最多的方式。需要解释的就是代码

LayoutInflater.from(context).inflate(R.layout.follow_layout, this);

这句代码将follow_layout.xml文件与FollowLayout.java文件关联在一起。我们看inflate方法的源码,会发现它实际会调用

inflate(parser, root, root != null)

即下面的代码:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            // 省略
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                // 省略
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    // 省略
                    // Inflate all children under temp against its context.
                    rInflateChildren(parser, temp, attrs, true);
                    // 省略
                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    /** 
                     * 关注这里就好
                     * attachToRoot 为 true
                     * 会将inflate的xml里的内容addView到传入的this里
                     */
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
            // 省略
            return result;
        }
    }

上面的注释里讲的很清楚,就是说inflate传入的layout文件里的内容,最终会被addView到FollowLayout这个自定义View里。我们用Layout Inspector来看一下:


Layout Inspector

这张图引出了下面的话题。

2.2 弊端

其实上面这个方式就是大家使用的最多的方式了,而且很好用。唯一的一个缺点就是像上面Layout Inspector里看到的那样,FollowLayout里面包裹了一个ConstraintLayout。ConstraintLayout就是R.layout.follow_layout的root节点。

而这个ConstraintLayout明显是多余的,我们的FollowLayout本身就是继承FollowLayout的,没必要里面再额外裹一层。想要去掉这一层,我们可以使用<merge>。

2.3 使用<merge>

来看控件的xml代码

<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/add_follow_hint"
        android:textColor="@color/base_txt_gray1"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@+id/textView4"
        app:layout_constraintStart_toStartOf="@+id/textView4"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:text="及时接受对方最新动态"
        android:textColor="#aaaaaa"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <Button
        android:id="@+id/chat_header_foolow_layout_btn"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_marginBottom="10dp"
        android:layout_marginEnd="15dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/follow_btn_player_selector"
        android:drawablePadding="2dp"
        android:text="@string/follow"
        android:textColor="#FFFFFFFF"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</merge>

除了最外层从ConstraintLayout改为merge之外没有别的变化,FollowLayout.java 不需要变化,别的xml中引用这个控件的代码也不用变。
我们接着看preview中的样子

控件的xml对应的preview

显然,merge不可能像正常的ViewGroup一样支持那么多属性,所以这里我们是无法正常预览出效果的。之前我以为使用merge就只能忍受这种恶心的效果了。。最近我才知道,还有一个属性tools:parentTag="android.support.constraint.ConstraintLayout",这个加上之后,就能预览你想要的效果了。。简直完美~~

那么在引用FollowLayout控件的xml里看到的是什么样子呢?

引用FollowLayout控件处的preview

这也就告诉我们,基本所有的自定义控件都可以这么优化。这应该是最好的一种方式了。

2.4 merge的一些注意事项

  • 必须指定父ViewGroup,attachToRoot 必须为true
  • merge标签中,不需要设置属性,因为设置了不起作用,会被系统忽略掉

具体的原因,我们可以继续看2.1中的 inflate方法,中间有这么一段

if (TAG_MERGE.equals(name)) {
        // 这里说明了第一点
    if (root == null || !attachToRoot) {
        throw new InflateException("<merge /> can be used only with a valid "+ "ViewGroup root and attachToRoot=true");
        }
     rInflate(parser, root, inflaterContext, attrs, false);
  }

满足root == null || !attachToRoot之后,我们继续看rInflate方法

void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
                        
            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                    // 注意看这里,要通过name和xml的attrs创建对应的View
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                // 获取控件对应的LayoutParams
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                // 如果该控件还包裹了子控件,会递归这个流程
                rInflateChildren(parser, view, attrs, true);
                                // 将最终的View添加到parent里
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

在上面的整个过程中,merge的xml参数都始终没有使用过。所以merge设置参数也会被系统忽略掉,最后起作用的是parent里的xml参数。

所以除了merge, 还有没有什么办法呢,接着往下看。

3. onFinishInflate方式

3.1 代码实例

这个方式我还没有在别的博客里看到过,可能是我孤陋寡闻了哈。但是我个人觉得,没有上面merge那种方式使用的方便,我是不太推荐。

我们工程里有些自定义的View,没有像上面那样,在constructor里使用initView来初始化各种子View。而是在onFinishInflate里初始化。上面的FollowLayout可以用下面的方式写:

public class FollowLayout extends ConstraintLayout {
    private Button mFollowBtn;
    public FollowLayout(Context context) {
        super(context);
    }

    public FollowLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public FollowLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
        mFollowBtn = findViewById(R.id.chat_header_foolow_layout_btn);
        mFollowBtn.setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                /**
                 * 点击事件
                 */
            }
        });
    }

同时,chat_header_foolow_layout_btn.xml文件也修改了root节点,从ConstraintLayout改成了FollowLayout

<?xml version="1.0" encoding="utf-8"?>
<com.example.xuqi.customlayoutdemo.FollowLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/white">

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/add_follow_hint"
        android:textColor="@color/base_txt_gray1"
        android:textSize="14sp"
        app:layout_constraintBottom_toTopOf="@+id/textView4"
        app:layout_constraintStart_toStartOf="@+id/textView4"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintVertical_chainStyle="packed" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="15dp"
        android:text="及时接受对方最新动态"
        android:textColor="#aaaaaa"
        android:textSize="12sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView3" />

    <Button
        android:id="@+id/chat_header_foolow_layout_btn"
        android:layout_width="60dp"
        android:layout_height="30dp"
        android:layout_marginBottom="10dp"
        android:layout_marginEnd="15dp"
        android:layout_marginTop="10dp"
        android:background="@drawable/follow_btn_player_selector"
        android:drawablePadding="2dp"
        android:text="@string/follow"
        android:textColor="#FFFFFFFF"
        android:textSize="14sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</com.example.xuqi.customlayoutdemo.FollowLayout>

引用这个控件的时候无法像2.中的那样使用,需要用1.中的方式,include上面的chat_header_foolow_layout_btn.xml文件。

3.2 onFinishInflate()

可能很多人是第一次关注onFinishInflate方法。网上查了一下,大部分人都是草草的说了一下----当View中所有的子控件均被映射成xml后触发。这里我们详细解释一下。

onFinishInflate()方法是在LayoutInflater里的rInflate方法里调用的,按照如下的顺序

graph TB
A[LayoutInflater.from.inflate]-->B[inflate -> parser, root, attachToRoot]
B-->|填充子View|C[rInflateChildren -> finishInflate = true]
C-->D[rInflate]
D-->|调用|E[parent.onFinishInflate]

下面按顺序给出源码:

3.2.1 inflate的源码

其实上面给过了,为了大家看的方便,再放一份吧~~~

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            // 省略
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                // 省略
                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    /**
                     * 注意这里与下面调用rInflateChildren相比
                     * 最后一个参数传的是false
                     * 会导致rInflate方法的最后不调用onFinishInflate方法
                     */
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }
                    // 省略
                    // 调用rInflateChildren来填充子View
                    rInflateChildren(parser, temp, attrs, true);
                    // 省略
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }
            } 
            // 省略
            return result;
        }
    }
3.2.2 rInflateChildren的源码
    /**
     * Recursive method used to inflate internal (non-root) children. This
     * method calls through to {@link #rInflate} using the parent context as
     * the inflation context.
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * call it.
     */
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }
3.2.3 rInflate的源码
/**
     * 该方法是一个递归方法
     * 用于遍历xml层次结构并实例化View和他们的子View
     * 然后调用最外层的View的onFinishInflate
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     */
    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                
                // 1. 注意这个方法,会再次调用rInflate方法,且finishInflate为true
                rInflateChildren(parser, view, attrs, true);
                
                viewGroup.addView(view, params);
            }
        }
        // 省略。。。
        // 2. 注意这里 调用了parent的onFinishInflate()方法
        // finishInflate决定是否调用onFinishInflate
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

源码开头的注释中说的递归,说白了就是inflate方法中调用rInflateChildren, rInflateChildren里面调rInflate,rInflate中的 1. 处调用 rInflateChildren。

3.2.4 解释

结合上面的源码我们不难看出,只有inflate方法中 type == XmlPullParser.START_TAG 时,才会调用 rInflateChildren(parser, temp, attrs, true)。

只有在finishInflate == true,rInflate方法中才能调用parent.onFinishInflate()。

START_TAG一般都是像下面这种

<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

</android.support.constraint.ConstraintLayout>

因此当FollowLayout在XML中引用时,必须像上面那样去写,才能回调到我们重写的onFinishInflate方法。

而 2.中的写法,是无法触发onFinishInflate回调的,而且即使触发了。。
ChatHeadFollowLayout里面没有写子View,初始化的还是个null。。

<com.changba.message.view.ChatHeadFollowLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
3.2.5 优点

解决了2.2 中所说的弊端,见图


Layout Inspector

FollowLayout直接包裹了内容,少了ConstraintLayout。不要小看少了这一层,积少成多带来的优势是很大的。
但是相比于2.的方式,还是牺牲了一部分的灵活性,具体用哪种方式就仁者见仁,智者见智了。

3.3 总结

总结一下就是,自定义的View, 比如CustomView,如果在XML里面是以

<CustomView>
</CustomView>

形式调用的,系统就会调用他的onFinishInflate。因为他是START_TAG。

如果以

<CustomView/>

形式就不会,因为不满足START_TAG。

这也是为什么定义XML文件时,以CustonView为root tag来定义里面的子View内容,并在CustomView类的onFinishInflate方法里实例化子View。

这个方式下,我们没办法在别的XML文件里以<CustonView/>的形式使用。因为拿不到内容。如果用<CustomView> </CustomView>,里面填上内容,那就可以了。所以onFinishInflate方式自定义的View,我们往往在XML使用他的时候,选择include整个CustomView的xml布局。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容