自定义SwipeRefreshLayout,一个能同时支持RecyclerView和ListView的下拉刷新和上拉加载的布局

  之前我们介绍过RecyclerView和SwipeRefreshLayout的交互工作模式,现在,让我们扩展一下SwipeRefreshLayout的功能让它能支持上拉加载,并且能和原生的SwipeRefreshLayout效果切换。

使用RecyclerView的延展样式
使用RecyclerView并使用SwipeRefreshLayout的原生样式
使用ListView的延展样式
使用ListView并使用SwipeRefreshLayout的原生样式

1.功能

  • 支持RecyclerView和ListView下拉刷新和上拉加载

  • 支持主动刷新

  • 下拉刷新能使用SwipeRefreshLayout的原生样式或自定义的延展样式

  • 可自定义ViewGroup定制刷新样式和加载样式

2.说明

  总体来说,因为SwipeRefreshLayout本身就支持RecyclerView和ListView的下拉刷新功能,并且一定程度上支持嵌套滑动(对ListView无法支持,当RecyclerView开启嵌套滑动时可以支持),所以我们这个自定义控件继承SwipeRefreshLayout实现是相当方便的,但我们还要考虑到以下这些情况,这也是我们这个自定义控件的主要思路。

  • 要实现延展样式,需要在RecyclerView和ListView中加入headView和footView动态改变它们的高度,当控件处于最顶端继续下拉时,让headView的height随手势的滑动而变化,当控件处于最底端继续上拉时让footView的height随滑动而变化。

  • ListView本身支持添加headView和footView,但RecyclerView不支持,所以需要重写RecyclerView.Adapter

  • RecyclerView是否开启嵌套滑动决定了我们实现RecyclerView下拉刷新和上拉加载功能的难易程度,开启嵌套滑动时我们可以直接利用SwipeRefreshLayout支持的嵌套滑动方法来实现。其他情况都需要拦截事件进行滑动处理,我们看一下下面这张图对于事件进行拦截处理的分类情况

  从图中我们可以具体看到什么情况下需要拦截事件,我们这个自定义SwipeRefreshLayout实现的关键地方就是利用嵌套滑动或者事件拦截来处理,下面进入正题看下到底该怎么做。

  • 类的说明
描述
SwipeRefreshLoadLayout 继承SwipeRefreshLayout,增加上拉加载功能以及各种事件的处理,整个项目的和心类
RecycleViewAdapter 如果要用RecyclerView实现功能,使用此类代替原RecyclerView.Adapter
SwipeLinearLayoutManager RecyclerView布局管理器,继承LinearLayoutManager,一个辅助类,使用RecyclerView时必须使用此类
Swipe 内部有公开和非公开的监听接口
ListViewHeadAndFootManager ListView的headView和footView管理器

3.通过嵌套滑动方式实现

  我们从最简单的地方入手,就是当RecyclerView开启嵌套滑动功能的情况下实现延展样式,这个时候SwipeRefreshLayout能监听到RecyclerVIew是否滑动到了最顶端或最底部,我们需要覆写它下面几个方法:

  • void onNestedScrollAccepted(View child, View target, int axes):开启嵌套滑动功能后才会调用,仅在滑动开始时调用一次,可进行滑动相关数据的初始化操作。
  • void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed):当RecyclerView滑动到顶端并继续下拉或者滑动到底部并继续上拉时SwipeRefreshLayout会调用此方法,覆写此方法后可通过dyUnconsumed参数获取到当前滑动的距离,从而计算出headView或footView的height增大的值。
  • void onNestedPreScroll(View target, int dx, int dy, int[] consumed):当从下拉刷新或上拉加载状态返回时,SwipeRefreshLayout会调用此方法,覆写此方法可计算headView或footView的height减小的值。
  • void onStopNestedScroll(View target):手指离开屏幕时SwipeRefreshLayout会调用此方法,覆写此方法我们可以从这个状态开始计算headView或footView回收时其height的值。

  同理,按照这个思路,上拉加载也完全可以通过覆写这四个方法实现,当然,这是在RecyclerView开启嵌套滑动的前提下,我们结合代码看一下这部分的具体实现。

覆写SwipeRefreshLayout类中四个关于嵌套滑动的方法

//下拉刷新时手指的滑动距离,意味着headView的height需要改变的大小
private int mTotalUnconsumed;

//上拉加载时手指的滑动距离,意味着footView的height需要改变的大小
private int mTotalUnconsumed2;

@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
    super.onNestedScrollAccepted(child, target, axes);
    mTotalUnconsumed = 0;
    mTotalUnconsumed2 = 0;
}

/**
 * 下拉刷新返回或者上拉加载返回时需要覆写的方法,
 * 通过dy计算headView或者footView的height值,减小其高度,
 * 通过consumed[]改变RecyclerView的滑动距离,使之不会滑动的过快造成滑动效果降低,
 * 这里我们通过下拉刷新样式决定是调用父类的方法还是自己的方法,如果时需要原生样式,直接调用父类方法,
 * 否则调用自己的方法
 */
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
    if (refreshStyle == CIRCLE) {
        super.onNestedPreScroll(target, dx, dy, consumed);
    } else if (refreshStyle == SPREAD) {
        if (dy > 0 && mTotalUnconsumed > 0) {
            //下拉刷新返回操作
            if (dy > mTotalUnconsumed) {
                consumed[1] = dy - mTotalUnconsumed;
                mTotalUnconsumed = 0;
                finishParentDrag();
            } else {
                mTotalUnconsumed -= dy;
                consumed[1] = dy;
            }
            onPullDownBack(dy);
        }
    }
    //上拉加载返回操作
    if (dy < 0 && mTotalUnconsumed2 < 0) {
        if (dy < mTotalUnconsumed2) {
            consumed[1] = mTotalUnconsumed2 - dy;
            mTotalUnconsumed2 = 0;
            finishParentDrag();
        } else {
            mTotalUnconsumed2 -= dy;
            consumed[1] = dy;
        }
        onPullUpBack(dy);
    }
}

/**
 * 下拉刷新或者上拉加载时需要覆写的方法,
 * 通过dyUnconsumed计算headView或者footView的height值,增大其高度,
 * 通过下拉刷新样式决定是调用父类的方法还是自己的方法,如果时需要原生样式,直接调用父类方法,
 * 否则调用自己的方法
 */
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
    if (refreshStyle == CIRCLE) {
        super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
    } else if (refreshStyle == SPREAD) {
        //下拉刷新操作
        if (dyUnconsumed < 0 && !isRefreshing) {
            mTotalUnconsumed += -dyUnconsumed;
            onPullDown(dyUnconsumed);
        }
    }
    //上拉加载操作
    if (dyUnconsumed > 0 && !isLoading && footViewVisibility == VISIBLE) {
        mTotalUnconsumed2 += -dyUnconsumed;
        onPullUp(dyUnconsumed);
    }
}

/**
 * 手指释放时调用,开启一个动画让headView或者footView的height恢复
 */
@Override
public void onStopNestedScroll(View target) {
    if (refreshStyle == CIRCLE) {
        super.onStopNestedScroll(target);
    } else if (refreshStyle == SPREAD) {
        //下拉刷新释放操作
        if (mTotalUnconsumed > 0 && !isRefreshing) {
            release();
            mTotalUnconsumed = 0;
        }
    }
    //上拉加载释放操作
    if (mTotalUnconsumed2 < 0) {
        release();
        mTotalUnconsumed2 = 0;
    }
}

  对于以上代码来说,我们加入了下拉刷新的样式判断,如果样式是CIRCLE即SwipeRefreshLayout原生样式,那就直接调用父方法,否则如果是SPREAD即延展样式,我们才需要调用我们自己的实现代码。

  下面是下拉刷新或上拉加载时执行的代码,会分别调用onPullDown(int dy)、onPullDownBack(int dy)、onPullUp(int dy)、onPullUpBack(int dy)方法,然后通过Swipe.OnChangeViewHeight接口让headView或footView的height发生变化改变它的高度,这里只贴一小段代码,具体的可查看项目源码。

SwipeRefreshLoadLayout中:

/**
 * 下拉刷新
 */
private void onPullDown(int dy) {
    dragAction = DRAG_ACTION_PULL_DOWN;
    pullDownDistance += Math.abs(dy) / 2;
    changeTipsRefresh();
    if (onChangeViewHeight != null) {
        onChangeViewHeight.changeHeadViewHeight(pullDownDistance);
    }
}

RecycleViewAdapter实现了Swipe.OnChangeViewHeight接口,其中代码:

@Override
public void changeHeadViewHeight(int headViewHeight) {
    if (headViewHolder.headView != null && headViewHeight >= 0) {
        headViewLayoutParams.height = headViewHeight;
        headViewHolder.headView.setLayoutParams(headViewLayoutParams);
    }
}

  现在,通过SwipeRefreshLayout的帮助我们RecyclerView的下拉刷新和上拉加载已经实现了,但当RecyclerView禁止嵌套滑动时,上面的四个方法就会无法使用,并且如果我们要实现ListView的这一功能,我们必须要通过事件拦截来实现。

4.通过事件拦截方式实现

  事件分发我们不多做介绍了,这里就两点要说的,什么时候拦截事件,拦截以后要做什么,这是我们这篇自定义ViewGroup要面临的问题。在RecyclerView中,我们通过View类的boolean canScrollVertically(int direction)方法确定它是否滑到了最顶端或最底部。在ListView中,我们通过ListViewCompat类的boolean canScrollList(ListView listView, int direction)判断它是否滑到了最顶端或最底部,有了这两个方法,我们在onInterceptTouchEvent(MotionEvent ev)
中进行判断即可,符合情况的我们返回true然后交给onTouchEvent(MotionEvent ev)处理,不符合的直接调用父方法即可。

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (action == null) {
        return super.onTouchEvent(ev);
    }
    switch (ev.getAction()) {
        case MotionEvent.ACTION_MOVE:
            float currentY = ev.getY();
            int dy = (int) (currentY - initialDownY);
            if (dy >= 0) {//dy>0说明此时执行的是下拉刷新或者上拉加载返回操作
                if (action == ACTION_PULL_DOWN && !isRefreshing) {
                    onPullDown(dy);
                } else if (action == ACTION_PULL_UP && !isLoading) {
                    onPullUpBack(dy);
                    if (recyclerView != null) {
                        recyclerView.scrollBy(0, dy);
                    }
                }
            } else {//dy<0说明此时执行的是上拉加载或者下拉刷新返回操作
                if (action == ACTION_PULL_DOWN && !isRefreshing) {
                    onPullDownBack(dy);
                } else if (action == ACTION_PULL_UP && !isLoading && footViewVisibility == VISIBLE) {
                    onPullUp(dy);
                    if (recyclerView != null) {
                        recyclerView.scrollBy(0, -dy);
                    }
                    if (listView != null) {
                        ListViewCompat.scrollListBy(listView, -dy);
                    }
                }
            }
            initialDownY = currentY;
            break;
        case MotionEvent.ACTION_UP:
            action = null;
            release();
            break;
    }
    return action != null || super.onTouchEvent(ev);
}

private float initialDownY;
private final int ACTION_PULL_DOWN = 0X00C1;
private final int ACTION_PULL_UP = 0X00D1;
private Integer action = null;

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (recyclerView != null) {
        //当刷新类型为CIRCLE且处于刷新状态时,recyclerView的嵌套滑动不再响应,所以无法进行上拉加载,因此通过拦截事件利用onTouchEvent()实现上拉加载
        if (refreshStyle == CIRCLE && isRefreshing && !recyclerView.canScrollVertically(1)) {
            Boolean x = isIntercept(ev, true);
            if (x != null) return x;
        }
        //当recyclerView禁止嵌套滑动时,上拉加载需要使用onTouchEvent()实现,所以这里要进行拦截
        if (!recyclerView.isNestedScrollingEnabled() && !recyclerView.canScrollVertically(1)) {
            Boolean x = isIntercept(ev, true);
            if (x != null) return x;
        }
        //当刷新类型为SPREAD时,如果recyclerView禁止了嵌套滑动,那这里需要拦截事件让onTouchEvent()实现下拉刷新
        if (refreshStyle == SPREAD && !recyclerView.isNestedScrollingEnabled() && !recyclerView.canScrollVertically(-1)) {
            Boolean x = isIntercept(ev, false);
            if (x != null) return x;
        }
    }
    if (listView != null) {
        if (refreshStyle == CIRCLE && isRefreshing && !ListViewCompat.canScrollList(listView, 1)) {
            Boolean x = isIntercept(ev, true);
            if (x != null) return x;
        }
        if (!ListViewCompat.canScrollList(listView, 1)) {
            Boolean x = isIntercept(ev, true);
            if (x != null) return x;
        }
        if (refreshStyle == SPREAD && !ListViewCompat.canScrollList(listView, -1)) {
            Boolean x = isIntercept(ev, false);
            if (x != null) return x;
        }
    }
    return super.onInterceptTouchEvent(ev);
}

@Nullable
private Boolean isIntercept(MotionEvent ev, boolean type) {
    switch (ev.getAction()) {
        case MotionEvent.ACTION_DOWN:
            initialDownY = ev.getY();
            break;
        case MotionEvent.ACTION_MOVE:
            float y = ev.getY();
            if (type) {
                if (y - initialDownY < 0) {//判断上滑或者下滑操作
                    initialDownY = y;
                    action = ACTION_PULL_UP;
                    return true;
                }
            } else {
                if (y - initialDownY > 0) {
                    initialDownY = y;
                    action = ACTION_PULL_DOWN;
                    return true;
                }
            }
            break;
    }
    return null;
}

  上面这段代码实现了何时拦截事件的逻辑,以及拦截后的相应处理。当然,这只是我个人实现的一种方式或许并不适用于所有人,对于每个人而言都各有一种实现方式,所以我也不准备强行解释上面的代码了,以上这段代码只作为参考,提供一种实现思路。

  我们可以看到,在事件拦截后,将相应的操作交给了onTouchEvent()方法处理,在onTouchEvent()方法中,通过判断上下滑动,确定手指滑动的距离,调用onPullDown(int dy)、onPullDownBack(int dy)、onPullUp(int dy)、onPullUpBack(int dy)方法来改变headView和footView的高度,从而达到之前利用嵌套滑动方式同样的效果。

  现在,我们实现了图中所有情况的下拉刷新和上拉加载操作,我们再看看其他几个类。

5.RecycleViewAdapter类

  我们知道在RecyclerView中需要手动添加headView和footView,所以我们需要继承RecyclerView.Adapter实现一个自己的Adapter,在这个Adapter中,需要实现原有的Adapter抽象方法,并再次抽象出相应的方法供外部使用,看一下该类中和原有Adapter的对应方法, 我们使用时只需要实现这几个新的抽象方法即可。

RecyclerView.Adapter的原有方法 RecycleViewAdapter中对应的方法
int getItemCount() int getCounts()
RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) RecyclerView.ViewHolder onNewViewHolder(@NonNull ViewGroup parent, int viewType)
void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) void onSetViewHolder(@NonNull RecyclerView.ViewHolder holder, int position)
int getItemViewType(int position) int getItemType(int position)
  • 有一点要注意,在使用RecyclerView.Adapter类中其他没有覆写过的方法(也就是除了上述表中的其他方法)时要注意position,因为我们的RecyclerView默认已经有两个View了,headView和footView,所以在碰到有方法中有position时要注意当前position具体是哪一个,一般真正的值是position-1,因为要减去一个headView。

自定义headView或footView

  此外,在此类中额外实现了一个功能,那就是可以自定义ViewGrou实现延展样式的headView和footView,如果不满意我实现的样式,可以自己定义。实现此功能的代码如下:

@NonNull
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    if (viewType == VIEW_TYPE_FOOT) {
        if (footViewHolder == null) {
        //在这里判断一下是否有自定义的ViewGroup作为headView或footView
            if (mySwipe.getFootView() == null) {
                footViewHolder = new FootViewHolder(LayoutInflater.from(mContext).inflate(R.layout.srll_foot, null, false), true);
            } else {
                footViewHolder = new FootViewHolder(mySwipe.getFootView(), false);
            }
        }
        return footViewHolder;
    } else if (viewType == VIEW_TYPE_HEAD) {
        if (headViewHolder == null) {
            if (mySwipe.getHeadView() == null) {
                headViewHolder = new HeadViewHolder(LayoutInflater.from(mContext).inflate(R.layout.srll_head, null, true), true);
            } else {
                headViewHolder = new HeadViewHolder(mySwipe.getHeadView(), false);
            }
        }
        return headViewHolder;
    }
    return onNewViewHolder(parent, viewType);
}

private class FootViewHolder extends RecyclerView.ViewHolder {
    private TextView tvFootTip;
    private ViewGroup footView;
    private ImageView ivFootRefresh;

    private FootViewHolder(View itemView, boolean isFromSelf) {
        super(itemView);
        this.footView = (ViewGroup) itemView;
        this.footView.setVisibility(mySwipe.footViewVisibility);
        if (isFromSelf) {
            ViewGroup childAt = (ViewGroup) footView.getChildAt(0);
            ViewGroup.LayoutParams childLayoutParams = childAt.getLayoutParams();
            childLayoutParams.height = refreshViewHeight;
            //设置底部加载子视图高度
            childAt.setLayoutParams(childLayoutParams);
            this.tvFootTip = footView.findViewById(R.id.tv_foot_tip);
            this.ivFootRefresh = footView.findViewById(R.id.iv_foot_refresh);
            this.ivFootRefresh.animate().setInterpolator(new LinearInterpolator());
            LinearLayout llLoadMore = footView.findViewById(R.id.ll_load_more);
            llLoadMore.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    mySwipe.doLoadMore();
                }
            });
        }
        //设置底部加载父视图高度
        footViewLayoutParams = new RecyclerView.LayoutParams(-1, refreshViewHeight);
        footView.setLayoutParams(footViewLayoutParams);
    }
}

private class HeadViewHolder extends RecyclerView.ViewHolder {
    private ViewGroup headView;
    private TextView tvHeadTip;
    private ImageView ivHeadArrow;
    private TextView tvRefreshTime;
    private ImageView ivHeadRefresh;

    private HeadViewHolder(View itemView, boolean isFromSelf) {
        super(itemView);
        this.headView = (ViewGroup) itemView;
        if (isFromSelf) {
            ViewGroup childAt = (ViewGroup) headView.getChildAt(0);
            ViewGroup.LayoutParams childLayoutParams = childAt.getLayoutParams();
            childLayoutParams.height = refreshViewHeight;
            childAt.setLayoutParams(childLayoutParams);
            this.tvHeadTip = headView.findViewById(R.id.tv_head_tip);
            this.ivHeadArrow = headView.findViewById(R.id.iv_head_arrow);
            this.ivHeadArrow.animate().setInterpolator(new LinearInterpolator());
            this.ivHeadRefresh = headView.findViewById(R.id.iv_head_refresh);
            this.ivHeadRefresh.animate().setInterpolator(new LinearInterpolator());
            this.tvRefreshTime = headView.findViewById(R.id.tv_refresh_time);
            if (!TextUtils.isEmpty(Swipe.getLastRefreshTime(mContext))) {
                this.tvRefreshTime.setText("最后更新:" + Swipe.getLastRefreshTime(mContext));
            } else {
                this.tvRefreshTime.setVisibility(View.GONE);
            }
        }
        headViewLayoutParams = new RecyclerView.LayoutParams(-1, 0);
        headView.setLayoutParams(headViewLayoutParams);
    }
}

  原理很简单,在onCreateViewHolder()中,先判断是否有自定义的ViewGroup作为headView或者footView,如果有,传入ViewHolder中,作为当前的headView或footView,当我们做下拉刷新或者上拉加载时,就动态的改变此ViewGroup的height,当然,此时只能做到把当前headView或footView拉伸或压缩,无法改变其他的状态(比如自己定义的箭头图片或者刷新图片什么时候做出相应的变化),所以需要一个接口来监听我们做下拉刷新或上拉加载的状态,继续往下看。

6.Swipe类

  这是一个为了满足各种监听和通知实现的一个类,里面是各种接口,其中有两个public接口和两个protect接口。

/**
 * 上拉和下拉时滑动监听
 */
public interface OnSlideActionListener {
    /**
     * 释放刷新行为
     */
    void releaseRefreshAction();

    /**
     * 下拉刷新行为
     */
    void downRefreshAction();

    /**
     * 释放加载行为
     */
    void releaseLoadAction();

    /**
     * 上拉加载行为
     */
    void upLoadAction();
}

/**
 * 改变头尾提示信息,仅限本包类
 */
interface OnChangeViewTip {
    /**
     * 改变底部view提示
     *
     * @param tips
     */
    void changeFootTips(String tips);

    /**
     * 改变头view提示
     *
     * @param tips
     */
    void changeHeadTips(String tips);
}

/**
 * 监听刷新和加载更多
 */
public interface OnRefreshAndLoadListener {
    /**
     * 下拉刷新
     */
    void refresh();

    /**
     * 上拉加载
     */
    void loadMore();

}

/**
 * 监听头尾view的高度变化
 */
interface OnChangeViewHeight {
    /**
     * 改变头view高度
     *
     * @param headViewHeight
     */
    void changeHeadViewHeight(int headViewHeight);

    /**
     * 改变底部view高度
     *
     * @param footViewHeight
     */
    void changeFootViewHeight(int footViewHeight);
}
  • OnSlideActionListener:上拉和下拉时滑动监听,一般不用实现,它监听了所有的滑动事件,只有在自定义headView和footView时需要实现这个接口,可以通过这个接口中的方法改变状态。
  • OnChangeViewTip:用protect修饰,不对外开放,改变提示信息,如是自定义headView或footView,直接实现OnSlideActionListener接口即可,所以开放此接口无意义,仅由RecycleViewAdapter类和ListViewHeadAndFootManager类实现。
  • OnRefreshAndLoadListener:刷新或加载监听。
  • OnChangeViewHeight:改变headView或footView高度,实现延展样式,用protect修饰,由RecycleViewAdapter类和ListViewHeadAndFootManager类实现。

  以上这些接口在SwipeRefreshLoadLayout类中都有调用的地方,都非常容易理解所以不贴代码了,感兴趣的可以看下整个项目。

7.ListViewHeadAndFootManager类

  此类和RecycleViewAdapter类功能相似,由于ListView本身有setHeadView()和setFootView()方法,所以不需要继承BaseAdapter来实现headView和footView,里面的代码和RecycleViewAdapter的代码大致相同,所以就不多做介绍了。

8.SwipeLinearLayoutManager类

  继承于LinearLayoutManager类,里面只覆写了一个方法:

@Override
public int getDecoratedBottom(View child) {
    if (mySwipe != null && child.getId() == mySwipe.headViewId) {
        return 1 + getBottomDecorationHeight(child);
    }
    return super.getDecoratedBottom(child);
}
  • 我们在使用RecyclerView时,禁用嵌套滑动的情况下需要使用boolean canScrollVertically(int direction)方法判断是否滑到了最顶端或最底部,当滑到最顶端时,如果headView的height为0,那么此方法就会判断失误,无法触发下拉刷新,具体原因可以追踪一下源码,此方法会调用一次int getDecoratedBottom(View child)方法,我们只要判断当child为headView时返回值大于0即可。

  以上,就是本问自定义ViewGroup的全部内容了,具体使用方法和源码可以在GitHub上查看:

https://github.com/chengzhicao/RefreshAndLoad

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

推荐阅读更多精彩内容