侧滑菜单之NavigationView原理分析

大家好,上次我们分析了侧滑菜单DrawerLayout的实现原理,明白了它是如何管理主体内容和侧滑菜单之间的关系,包括布局,触摸事件等的分析。我们同时也知道,侧滑菜单的内容大致上是顶部一块头像内容区域,下面是一系列的菜单项,那么它的菜单内容是如何实现的呢,我们接着分析。

本次的分析内容主要为以下几项:

  1. 结构分析
  2. 流程分析
  3. 菜单内容布局实现
  4. 菜单解析实现

1.结构分析

本次分析涉及的类有如下:

  • NavigationView

即是菜单内容的总体View,是所有菜单内容显示管理的一个封装,使用它有多简单,内容的提供只需要一个xml布局定义就够了。

<android.support.design.widget.NavigationView
      android:id="@+id/nav_view"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"
      android:layout_gravity="start"
      android:fitsSystemWindows="true"
      app:headerLayout="@layout/nav_header_main2"
      app:menu="@menu/activity_main2_drawer"
      />

可以看到,通过指定headerLayout属性即可设置菜单的头部布局,通过指menu属性即可设置菜单的菜单项布局,当然layout_gravity同时也是需要指定的,这样DrawerLayout才能识别它为侧滑菜单View。

  • NavigationMenuPresenter

实现MenuPresenter接口,是实际管理菜单内容布局的负责人,是NavigationView的管家,NavigationView中大部分方法都是交由它代理实现的。例如解析菜单的头部布局

/**
 * Inflates a View and add it as a header of the navigation menu.
 *
 * @param res The layout resource ID.
 * @return a newly inflated View.
 */
public View inflateHeaderView(@LayoutRes int res) {
    return mPresenter.inflateHeaderView(res);
}
  • NavigationMenu

菜单内容解析类,继承自MenuBuilder,它的工作就是负责解析上面NavigationView布局的menu属性指定的menu菜单的内容。

/**
 * Inflate a menu resource into this navigation view.
 *
 * <p>Existing items in the menu will not be modified or removed.</p>
 *
 * @param resId ID of a menu resource to inflate
 */
public void inflateMenu(int resId) {
    mPresenter.setUpdateSuspended(true);
    //这里mMenu就是NavigationMenu对象,配合MenuInflater完成解析
    getMenuInflater().inflate(resId, mMenu);
    mPresenter.setUpdateSuspended(false);
    mPresenter.updateMenuView(false);
}
  • NavigationMenuView

它才是真正的菜单内容显示View,NavigationView只是容器而已,NavigationMenuView继承RecyclerView,实现MenuView接口,看到这,是不是有点明白菜单内容布局的实现了?是的,菜单内容布局上的所有内容就是用一个RecyclerView列表实现的。包括头部的headView,还是后面才一系列菜单项View,为什么要这样实现,RecyclerView的优点想必是人尽皆知吧。那么我们先猜想一下,包括头布局,菜单项,子菜单项,分割线等实现都是通过itemViewType分别实现的。

public class NavigationMenuView extends RecyclerView implements MenuView {

    public NavigationMenuView(Context context) {
        this(context, null);
    }

    public NavigationMenuView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public NavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
    }

    @Override
    public void initialize(MenuBuilder menu) {

    }

    @Override
    public int getWindowAnimations() {
        return 0;
    }

}
  • NavigationMenuAdapter

和NavigationMenuView这个列表配对的RecyclerView适配器,用于管理和填充菜单列表数据到列表中。

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
    ...
}
  • 多种ViewHolder实现

既然有不同类型的布局,就会对应有不同的ViewHolder实现

//普通列表项
private static class NormalViewHolder extends ViewHolder {

    public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,
            View.OnClickListener listener) {
        super(inflater.inflate(R.layout.design_navigation_item, parent, false));
        itemView.setOnClickListener(listener);
    }

}
//子菜单项
private static class SubheaderViewHolder extends ViewHolder {

    public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {
        super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false));
    }

}
//分隔线项
private static class SeparatorViewHolder extends ViewHolder {

    public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {
        super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false));
    }

}
//头部项
private static class HeaderViewHolder extends ViewHolder {

    public HeaderViewHolder(View itemView) {
        super(itemView);
    }

}
  • 多种NavigationMenuItem实现

不同类型的布局,同时对应有不同的NavigationMenuItem接口实现,它是作为数据项接口,通过它获取菜单的内容数据。

/**
 * 普通菜单项,或者子菜单数据项
 */
private static class NavigationMenuTextItem implements NavigationMenuItem {
    ...
}

/**
 * 分隔线数据项
 */
private static class NavigationMenuSeparatorItem implements NavigationMenuItem{
    ...
}

/**
 * 头部数据项
 */
private static class NavigationMenuHeaderItem implements NavigationMenuItem {
    ...
}

2. 流程分析

接下来我们从入口分析主要的执行流程,以让我们对它的实现原理有个整体的认识。这里我会剔除一些细节和分支,专注于主要流程的执行。

从NavigationView构造方法开始

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

    ThemeUtils.checkAppCompatTheme(context);

    // 创建NavigationMenu
    mMenu = new NavigationMenu(context);

    // 读取NavigationView布局中定义的属性值,将这些属性值交给NavigationMenuPresenter做后续的使用
    TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
            R.styleable.NavigationView, defStyleAttr,
            R.style.Widget_Design_NavigationView);
    ...
    
    //将NavigationMenu和NavigationMenuPresenter进行绑定
    mMenu.addMenuPresenter(mPresenter);
    
    //将mPresenter管理的RecyclerView布局添加到NavigationView上,所以说NavigationView只是一个容器而已
    addView((View) mPresenter.getMenuView(this));

    //解析菜单数据,并刷新列表显示这些菜单
    if (a.hasValue(R.styleable.NavigationView_menu)) {
        inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
    }

    //解析头部布局
    if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
        inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
    }

    ...
}

接着我们分析inflateMenu方法,解析并显示菜单数据的操作这里开始。

/**
 * Inflate a menu resource into this navigation view.
 *
 * <p>Existing items in the menu will not be modified or removed.</p>
 *
 * @param resId ID of a menu resource to inflate
 */
public void inflateMenu(int resId) {
    mPresenter.setUpdateSuspended(true);
    
    //这里通过MenuInflater将菜单数据解析保存到NavigationMenu中
    getMenuInflater().inflate(resId, mMenu);
    mPresenter.setUpdateSuspended(false);
    
    //刷新列表,更新并显示菜单
    mPresenter.updateMenuView(false);
}

可以看到,这里做了菜单内容的解析,然后刷新列表,显示菜单内容了。

既然我们知道菜单是由列表实现的,那我们就具体看看它是如何实现的。

2. 菜单内容布局实现

我们直接看NavigationMenuAdapter这个列表适配器

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {

    NavigationMenuAdapter() {
        //这里去获取所有的菜单信息
        prepareMenuItems();
    }
    
    //这里去获取所有的菜单信息
    private void prepareMenuItems() {
        if (mUpdateSuspended) {
            return;
        }
        mUpdateSuspended = true;
        //清除之前的数据
        mItems.clear();
        
        //这里添加用于显示头部的菜单项信息,最先显示头部
        mItems.add(new NavigationMenuHeaderItem());
    
        int currentGroupId = -1;
        int currentGroupStart = 0;
        boolean currentGroupHasIcon = false;
        //遍历所有可见的菜单项,分别处理添加到列表中
        for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
            MenuItemImpl item = mMenu.getVisibleItems().get(i);
            if (item.isChecked()) {
                setCheckedItem(item);
            }
            if (item.isCheckable()) {
                item.setExclusiveCheckable(false);
            }
            if (item.hasSubMenu()) {
                //这里处理子菜单
                SubMenu subMenu = item.getSubMenu();
                if (subMenu.hasVisibleItems()) {
                    if (i != 0) {
                        mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
                    }
                    mItems.add(new NavigationMenuTextItem(item));
                    boolean subMenuHasIcon = false;
                    int subMenuStart = mItems.size();
                    for (int j = 0, size = subMenu.size(); j < size; j++) {
                        MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
                        if (subMenuItem.isVisible()) {
                            if (!subMenuHasIcon && subMenuItem.getIcon() != null) {
                                subMenuHasIcon = true;
                            }
                            if (subMenuItem.isCheckable()) {
                                subMenuItem.setExclusiveCheckable(false);
                            }
                            if (item.isChecked()) {
                                setCheckedItem(item);
                            }
                            mItems.add(new NavigationMenuTextItem(subMenuItem));
                        }
                    }
                    if (subMenuHasIcon) {
                        appendTransparentIconIfMissing(subMenuStart, mItems.size());
                    }
                }
            } else {
                //处理添加菜单项
                int groupId = item.getGroupId();
                if (groupId != currentGroupId) { // first item in group
                    currentGroupStart = mItems.size();
                    currentGroupHasIcon = item.getIcon() != null;
                    if (i != 0) {
                        currentGroupStart++;
                        mItems.add(new NavigationMenuSeparatorItem(
                                mPaddingSeparator, mPaddingSeparator));
                    }
                } else if (!currentGroupHasIcon && item.getIcon() != null) {
                    currentGroupHasIcon = true;
                    appendTransparentIconIfMissing(currentGroupStart, mItems.size());
                }
                NavigationMenuTextItem textItem = new NavigationMenuTextItem(item);
                textItem.needsEmptyIcon = currentGroupHasIcon;
                mItems.add(textItem);
                currentGroupId = groupId;
            }
        }
        mUpdateSuspended = false;
    }
    
}

在列表适配器初始化时,调用prepareMenuItems准备了最终需要显示菜单项数据。有了数据之后,我们再看看其他

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {

    @Override
    public long getItemId(int position) {
        return position;
    }

    @Override
    public int getItemCount() {
        return mItems.size();
    }

    @Override
    public int getItemViewType(int position) {
        //根据数据类型判断返回相应的布局类型
        NavigationMenuItem item = mItems.get(position);
        if (item instanceof NavigationMenuSeparatorItem) {
            //分隔区域类型
            return VIEW_TYPE_SEPARATOR;
        } else if (item instanceof NavigationMenuHeaderItem) {
            //头部区域类型
            return VIEW_TYPE_HEADER;
        } else if (item instanceof NavigationMenuTextItem) {
            //菜单项类型
            NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
            if (textItem.getMenuItem().hasSubMenu()) {
                //子菜单项头部类型
                return VIEW_TYPE_SUBHEADER;
            } else {
                //普通菜单项类型
                return VIEW_TYPE_NORMAL;
            }
        }
        throw new RuntimeException("Unknown item type.");
    }

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //根据不同的布局类型,返回不同的ViewHolder
        switch (viewType) {
            case VIEW_TYPE_NORMAL:
                return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
            case VIEW_TYPE_SUBHEADER:
                return new SubheaderViewHolder(mLayoutInflater, parent);
            case VIEW_TYPE_SEPARATOR:
                return new SeparatorViewHolder(mLayoutInflater, parent);
            case VIEW_TYPE_HEADER:
                return new HeaderViewHolder(mHeaderLayout);
        }
        return null;
    }

    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        //具体菜单项类型的内容填充了
        switch (getItemViewType(position)) {
            case VIEW_TYPE_NORMAL: {
                //普通菜单项
                NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;
                itemView.setIconTintList(mIconTintList);
                if (mTextAppearanceSet) {
                    itemView.setTextAppearance(mTextAppearance);
                }
                if (mTextColor != null) {
                    itemView.setTextColor(mTextColor);
                }
                ViewCompat.setBackground(itemView, mItemBackground != null ?
                        mItemBackground.getConstantState().newDrawable() : null);
                NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
                itemView.setNeedsEmptyIcon(item.needsEmptyIcon);
                itemView.initialize(item.getMenuItem(), 0);
                break;
            }
            case VIEW_TYPE_SUBHEADER: {
                //子菜单项
                TextView subHeader = (TextView) holder.itemView;
                NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
                subHeader.setText(item.getMenuItem().getTitle());
                break;
            }
            case VIEW_TYPE_SEPARATOR: {
                //分隔区域项
                NavigationMenuSeparatorItem item =
                        (NavigationMenuSeparatorItem) mItems.get(position);
                holder.itemView.setPadding(0, item.getPaddingTop(), 0,
                        item.getPaddingBottom());
                break;
            }
            case VIEW_TYPE_HEADER: {
                //头部区域,它和定义的菜单数据是独立分开的,这里不实现
                break;
            }
        }

    }
    
    
}

看到这里,我们就了解菜单项的布局了,那么头部区域是如何处理的呢?我们继续来看NavigationMenuPresenter

private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
    public View inflateHeaderView(@LayoutRes int res) {
        //这里解析头部布局
        View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
        //这里添加头部布局
        addHeaderView(view);
        return view;
    }
    
    public void addHeaderView(@NonNull View view) {
        //这里添加头部布局
        mHeaderLayout.addView(view);
        // The padding on top should be cleared.
        mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
    }
    
    public void removeHeaderView(@NonNull View view) {
        //这里移除头部布局
        mHeaderLayout.removeView(view);
        if (mHeaderLayout.getChildCount() == 0) {
            mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
        }
    }
    
    public int getHeaderCount() {
        return mHeaderLayout.getChildCount();
    }
    
    public View getHeaderView(int index) {
        return mHeaderLayout.getChildAt(index);
    }
}

我们看到上面有添加头部布局,而mHeaderLayout是包装在HeaderViewHolder中的,这样头部布局也就能显示在列表中了,而且是在第一位。接下来我们分析一个菜单xml文件定义的数据是如何解析成菜单数据的。

3. 菜单解析实现

菜单xml文件定义的数据解析成菜单数据,我们很自然的能想到,使用xml解析方式,例如android提供的PullParser,可以实现数据的解析,然后根据数据类型转换为我们需要的数据就可以了。包括布局xml文件的解析成View也是一样的道理。那么我们看看具体的实现吧。

我们这里主要分析MenuInflater这个菜单解析类。先从inflate方法开始

public class MenuInflater {

    //解析菜单的入口
    public void inflate(@MenuRes int menuRes, Menu menu) {
        XmlResourceParser parser = null;
        try {
            //获取菜单资源解析器
            parser = mContext.getResources().getLayout(menuRes);
            AttributeSet attrs = Xml.asAttributeSet(parser);
            
            //开始解析
            parseMenu(parser, attrs, menu);
        } catch (XmlPullParserException e) {
            throw new InflateException("Error inflating menu XML", e);
        } catch (IOException e) {
            throw new InflateException("Error inflating menu XML", e);
        } finally {
            if (parser != null) parser.close();
        }
    }

}

接着进入到parseMenu开始解析工作。

public class MenuInflater {

    private void parseMenu(XmlPullParser parser, AttributeSet attrs, Menu menu)
            throws XmlPullParserException, IOException {
        
        //菜单状态类,通过它读取,并临时保存数据
        MenuState menuState = new MenuState(menu);

        int eventType = parser.getEventType();
        String tagName;
        boolean lookingForEndOfUnknownTag = false;
        String unknownTagName = null;

        // 这里确保包含menu标签,并且menu标签在最开始,不然抛异常
        do {
            if (eventType == XmlPullParser.START_TAG) {
                tagName = parser.getName();
                if (tagName.equals(XML_MENU)) {
                    // Go to next tag
                    eventType = parser.next();
                    break;
                }
                
                throw new RuntimeException("Expecting menu, got " + tagName);
            }
            eventType = parser.next();
        } while (eventType != XmlPullParser.END_DOCUMENT);
        
        //然后开始遍历处理menu的子标签
        boolean reachedEndOfMenu = false;
        while (!reachedEndOfMenu) {
            switch (eventType) {
                case XmlPullParser.START_TAG:
                    if (lookingForEndOfUnknownTag) {
                        break;
                    }
                    
                    tagName = parser.getName();
                    if (tagName.equals(XML_GROUP)) {
                        //这里读取group标签
                        menuState.readGroup(attrs);
                    } else if (tagName.equals(XML_ITEM)) {
                        //这里读取item标签
                        menuState.readItem(attrs);
                    } else if (tagName.equals(XML_MENU)) {
                        // 这里表面遇到了子菜单标签,递归parseMenu读取子菜单数据
                        SubMenu subMenu = menuState.addSubMenuItem();
                        registerMenu(subMenu, attrs);

                        // Parse the submenu into returned SubMenu
                        parseMenu(parser, attrs, subMenu);
                    } else {
                        lookingForEndOfUnknownTag = true;
                        unknownTagName = tagName;
                    }
                    break;
                    
                case XmlPullParser.END_TAG:
                    //表示当前标签读取结束
                    tagName = parser.getName();
                    if (lookingForEndOfUnknownTag && tagName.equals(unknownTagName)) {
                        lookingForEndOfUnknownTag = false;
                        unknownTagName = null;
                    } else if (tagName.equals(XML_GROUP)) {
                        //读取到group的结束标签
                        //重置group相关的数据,便于下次循环使用
                        menuState.resetGroup();
                    } else if (tagName.equals(XML_ITEM)) {
                        //读取到item的结束标签
                        // Add the item if it hasn't been added (if the item was
                        // a submenu, it would have been added already)
                        if (!menuState.hasAddedItem()) {
                            if (menuState.itemActionProvider != null &&
                                    menuState.itemActionProvider.hasSubMenu()) {
                               
                               //这里根据解析的数据,添加新建的子菜单item到menu中 registerMenu(menuState.addSubMenuItem(), attrs);
                            } else {
                                //这里根据解析的数据,添加新建的ca菜单项Item到menu中
                                registerMenu(menuState.addItem(), attrs);
                            }
                        }
                    } else if (tagName.equals(XML_MENU)) {
                        //读到menu结束标签了,结束读取
                        reachedEndOfMenu = true;
                    }
                    break;
                    
                case XmlPullParser.END_DOCUMENT:
                    //表面最后没有读取到menu结束标签,menu资源错误
                    throw new RuntimeException("Unexpected end of document");
            }
            
            eventType = parser.next();
        }
    }

}

可见这里面就完成了菜单资源数据的解析,并将数据添加到menu中了。接着继续看MenuState是如何读取单个标签数据的。

private class MenuState {
    //读取group标签中的设置的属性值
    public void readGroup(AttributeSet attrs) {
        TypedArray a = mContext.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.MenuGroup);
        
        groupId = a.getResourceId(com.android.internal.R.styleable.MenuGroup_id, defaultGroupId);
        groupCategory = a.getInt(com.android.internal.R.styleable.MenuGroup_menuCategory, defaultItemCategory);
        groupOrder = a.getInt(com.android.internal.R.styleable.MenuGroup_orderInCategory, defaultItemOrder);
        groupCheckable = a.getInt(com.android.internal.R.styleable.MenuGroup_checkableBehavior, defaultItemCheckable);
        groupVisible = a.getBoolean(com.android.internal.R.styleable.MenuGroup_visible, defaultItemVisible);
        groupEnabled = a.getBoolean(com.android.internal.R.styleable.MenuGroup_enabled, defaultItemEnabled);

        a.recycle();
    }
    
    //读取item标签中的设置的属性值
    public void readItem(AttributeSet attrs) {
        TypedArray a = mContext.obtainStyledAttributes(attrs,
                com.android.internal.R.styleable.MenuItem);

        // Inherit attributes from the group as default value
        itemId = a.getResourceId(com.android.internal.R.styleable.MenuItem_id, defaultItemId);
        final int category = a.getInt(com.android.internal.R.styleable.MenuItem_menuCategory, groupCategory);
        
        ...

        a.recycle();

        itemAdded = false;
    }
    
}

接下来看MenuState是如何添加单个标签数据解析后的item的。

private class MenuState {

    //添加普通菜单项item
    public MenuItem addItem() {
        itemAdded = true;
        MenuItem item = menu.add(groupId, itemId, itemCategoryOrder, itemTitle);
        setItem(item);
        return item;
    }
    
    //添加子菜单item
    public SubMenu addSubMenuItem() {
        itemAdded = true;
        SubMenu subMenu = menu.addSubMenu(groupId, itemId, itemCategoryOrder, itemTitle);
        setItem(subMenu.getItem());
        return subMenu;
    }
    
    //设置item项的数据,将MenuState当前读取到的属性值填充到该item中
    private void setItem(MenuItem item) {
        item.setChecked(itemChecked)
            .setVisible(itemVisible)
            .setEnabled(itemEnabled)
            .setCheckable(itemCheckable >= 1)
            .setTitleCondensed(itemTitleCondensed)
            .setIcon(itemIconResId)
            .setAlphabeticShortcut(itemAlphabeticShortcut)
            .setNumericShortcut(itemNumericShortcut);
        
        ...
    }
    
}

到这里的话,我们就清楚菜单资源的解析过程了。

这一篇解析中,我们清楚了侧滑菜单内部菜单的布局实现原理,通过在布局文件中给NavigationView设置headerLayout和menu就能快速实现头部布局,和菜单布局,很大的降低了耦合度,且简单清晰。结合上一篇侧滑菜单DrawerLayout的实现原理,我相信大家会对侧滑菜单有一个清楚的认识,包括自定义View的实现思路,整体的架构设计等。实现一个功能并不算高明,更重要是如何设计,使它结构更加清晰,各个模块层次分明,职责清晰,可扩展性更高,我觉得这应该算是编程的一个乐趣吧。

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

推荐阅读更多精彩内容