ViewPager源码分析(2):滑动及冲突处理

我的CSDN博客同步发布:ViewPager源码分析(2):滑动及冲突处理

转载请注明出处:【huachao1001的简书://www.greatytc.com/users/0a7e42698e4b/latest_articles】

上一篇介绍了ViewPageronMeasureonLayout两个方法,这是自定义View最基本的两个函数。但是我们的ViewPager有个需求就是滑动,接下来我们一起去学习ViewPager在滑动方面做了哪些工作,以及ViewPager如何处理与子View之间的滑动冲突。由于ViewPager的子View有Decor View还有普通的子View,而本篇文章讲的主要是普通子View,因此,不再去刻意区分,以下所说的子View不包括DecorView。

1 Scroller典型用法

我们知道,Android内置了Scroller对象,用于实现渐近式的滑动。假设我们自定义一个函数smoothScrollTo(int destX,int destY),用于让ViewPager渐近式的滑动到(destX,destY)这个坐标位置,那么使用Scroller实现步骤一般如下:

  1. 创建Scroller对象:Scroller scroller=new Scroller(context);
  2. 重写computeScroll()方法
  3. 最后,在我们的smoothScrollTo方法中调用startScroll方法

参考如下代码:

@Override
public void computeScroll(){
    if(scroller.computeScrollOffset()){
        scrollTo(scroller.getCurrX(),scroller.getCurrY());
        postInvalidate();
    }
}  
public void smoothScrollTo(int destX,int destY){
    int scrollX=getScrollX();
    int deltaX=destX-scrollX;
    scroller.startScroll(scrollX,0,deltaX,0,1000);
}

以上的smoothScrollTo实现的是x方向的平滑,其中startScroll函数的形参分别表示:起始位置的x坐标、起始位置的y坐标、x方向要移动的距离、y方向上要移动的距离以及整个滑动过程完成所需的时间。

2 ViewPager滑动

2.1 ViewPager定义Scroller

参照我们上一节提到的Scroller典型用法,我们进入到ViewPager源码。我们在ViewPager的initViewPager方法中找到:

void initViewPager() { 
    //····
    final Context context = getContext();
    mScroller = new Scroller(context, sInterpolator);
    //····
}

它跟我们上一节使用到的Scroller构造器不同,他选择使用2个形参的构造器。其实,第二个形参就是插值器(interpolator),对插值器不熟悉的童鞋可以去搜索一下动画插值器相关内容。其实这个插值器就是根据不同的时间控制滑动的速度,就像高中物理中的物体变速运动。我们继续看看ViewPager中自定义的插值器sInterpolator,从变量名称中以s开头,就知道sInterpolator是个static属性:

private static final Interpolator sInterpolator = new Interpolator() {
   public float getInterpolation(float t) {
       t -= 1.0f;
       return t * t * t * t * t + 1.0f;
   }
};

Interpolator是一个接口,它继承自TimeInterpolator这个接口,而Interpolator没有添加新的抽象方法,TimeInterpolator只有一个抽象方法:float getInterpolation(float input);其中,input形参是取值范围为0到1,表示当前的动画时间点,0表示动画开始,1表示动画结束。返回值表示移动到目标位置的比值,如果大于1,则表示超出了最大位置,小于0表示比最小位置还要小。怎么理解呢?举个例子,假设我们要实现变速动画,我们要持续的时间是[0,1000],要滑动的距离是[0,100],那么假设当前时间是200,则传入到getInterpolation的形参就是200/1000=0.2,表示时间过了0.2,具体的返回值可以根据你的变速需求计算,假设你的返回值是0.8,那么表示当前位置要处于100 * 0.8=80这个位置。如果你的返回值是1.8 ,那么肯定就是超出100了:100*1.8=180。

2.2 ViewPager重写computeScroll()方法

ViewPager实现的功能已经兼容性都是比较健全的,所有computeScroll()不会像我们所写的那么简单,我们一起"膜拜"一下官方代码吧:

@Override
public void computeScroll() {
//1.mIsScrollStarted标记当前在滑动
mIsScrollStarted = true;
//2.确保mScroller还没有结束计算滑动位置
if (!mScroller.isFinished() && mScroller.computeScrollOffset()) {
    //3.保存当前所处的位置oldX,oldY
    int oldX = getScrollX();
    int oldY = getScrollY();
    //4.取出由mScroller计算出来的位置
    int x = mScroller.getCurrX();
    int y = mScroller.getCurrY();
    //5.只要x和y方向有一个发生了变化,就去滚动
    if (oldX != x || oldY != y) {
        //6.滑到mScroller计算出来的新位置
        scrollTo(x, y);
        //7.调用pageScrolled,只有当ViewPager里面没有子View才会返回false
        if (!pageScrolled(x)) {
            //8.结束动画,并使得当前位置处于最终的位置
            mScroller.abortAnimation();
            //9.没有子View,说明x方向无需滑动,再次确保y方向滑动
            scrollTo(0, y);
        }
    }

    // 10.不断的postInvalidate,使得不断重绘,达到动画效果
    ViewCompat.postInvalidateOnAnimation(this);
    return;
}
//11.做一些滑动结束后的相关操作
// 注意到,上面的if里面有个return,也就是说,
// 只要是在滑动,就不会执行到下面的代码,
// 反之,执行到下面代码就说明已经滑动结束 
completeScroll(true);
}

computeScroll函数里面大部分代码比较清晰,只有两个函数,需要我们进去深究:pageScrolled以及completeScroll

2.2.1 pageScrolled

先看看pageScrolled函数,这个函数主要的作用是回调onPageScrolled,虽然做了很多计算,但这些计算的结果最终是为了作为形参传给onPageScrolled,看看他的源码:

private boolean pageScrolled(int xpos) {
//1.mItems是ArrayList类型,它保存的是每个子View的抽象描述类ItemInfo
//如果没有子View
if (mItems.size() == 0) {
    //2.先认为没有调用父类
    //mCalledSuper作用是:如果子类重写了onPageScrolled,
    // 那么子类的实现必须要先调用父类ViewPager的onPageScrolled
    //为了确保子类的实现中先调用了父类ViewPager的onPageScrolled,定义了mCalledSuper
    //并且在ViewPager类中的onPageScrolled将mCalledSuper设置为了true,用于判断子类有没有调用。
    mCalledSuper = false;
    //3.调用onPageScrolled,如果子类重写了该方法,调用的则是子类的onPageScrolled
    onPageScrolled(0, 0, 0);
    //4.如果没有执行ViewPager的onPageScrolled,抛出异常
    if (!mCalledSuper) {
        throw new IllegalStateException(
                "onPageScrolled did not call superclass implementation");
    }
    //5.如果没有子View,返回false
    return false;
}
//6.根据当前滑动的位置,得到当前显示的子View的抽象描述类ItemInfo
//只要存在子View,得到的ItemInfo对象肯定不为null
final ItemInfo ii = infoForCurrentScrollPosition();
//7.获取显示区域的宽度
final int width = getClientWidth();
//8.加上外边距后的宽度
final int widthWithMargin = width + mPageMargin;
final float marginOffset = (float) mPageMargin / width;
//保存当前是第几个页面(即第几个子View)
final int currentPage = ii.position;
//计算当前页面的偏移量,取值为[0,1),如果pageOffset不等于0,则下一个页面可见
final float pageOffset = (((float) xpos / width) - ii.offset) /
        (ii.widthFactor + marginOffset);
//当前页面移动的像素点个数
final int offsetPixels = (int) (pageOffset * widthWithMargin);

//以下作用与2、3、4类似
mCalledSuper = false;
onPageScrolled(currentPage, pageOffset, offsetPixels);
if (!mCalledSuper) {
    throw new IllegalStateException(
            "onPageScrolled did not call superclass implementation");
}
return true;
}

我们定位到第6个注释,我提到infoForCurrentScrollPosition函数是据当前滑动的位置,得到当前显示的子View的抽象描述类ItemInfo,如果当前滑动位置显示的恰好是一个完整的页面,这个页面的前一个页面和后一个页面都没有显示,那么很容易理解,返回的就是这个页面。可是如果当前显示区域是同时显示2个页面(两个页面都显示一部分出现在显示区域),那这个函数应该返回哪一个页面呢?从infoForCurrentScrollPosition源码看出每次是返回左边的页面,如下图所示:

根据滑动位置返回当前的Page

换句话说,只会是存在当前页面与下一个页面同时出现在显示区域,不可能是当前页面与上一个页面同时出现。关于infoForCurrentScrollPosition的具体实现,我们不要去关心,我们只要知道它帮我们实现了什么功能,如果对其感兴趣可以去看源码。

2.2.2 onPageScrolled

上面我们知道,pageScrolled函数是为了调用onPageScrolled做前期计算,并将计算结果作为onPageScrolled的形参,最终是为了回调onPageScrolled函数,那么我们看看onPageScrolled函数到底是干了啥~,从函数名看的出来,它是一个回调函数,那么是什么情况下回调呢?其实,在我们手指滑动或者是通过代码直接滑动到指定位置过程中,会使得一些页面滑动,如果我们想要在每个页面在显示区域滑动过程中实现某些效果,可以重写这个函数,当然了,我们前面分析pageScrolled函数时就提到,重写onPageScrolled时,必须先调用super.onPageScrolled(position, offset, offsetPixels),我们的ViewPager在滑动过程中,会不断回调onPageScrolled函数,这个“不断”是从这里体现:computeScroll—>onPageScrolled->onPageScrolled。滑动过程不断调用computeScroll,而computeScroll调用onPageScrolledonPageScrolled又调用onPageScrolled。好了,我们去看看onPageScrolled吧~首先看看三个参数:

  1. int position,表示当前是第几个页面
  2. float offset表示当前页面移动的距离,其实就是个相对实际宽度比例值,取值为[0,1)。0表示整个页面在显示区域,1表示整个页面已经完全左移出显示区域。
  3. int offsetPixels , 表示当前页面左移的像素个数。

我们已经了解形参的含义,接下来看看源码:

@CallSuper
protected void onPageScrolled(int position, float offset, int offsetPixels) {
    // Offset any decor views if needed - keep them on-screen at all times.
    //1.如果有Decor View,则需要使得它们时刻显示在屏幕中,不移出屏幕
    if (mDecorChildCount > 0) {
        //根据Gravity将Decor View摆放到指定位置,注释略,可以参考上一篇文章
        //代码略···
    }
    //2.分发页面滚动事件
    dispatchOnPageScrolled(position, offset, offsetPixels);
    //3.如果mPageTransformer不为null,则不断去调用mPageTransformer的transformPage函数
    if (mPageTransformer != null) {
        final int scrollX = getScrollX();
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = getChildAt(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            //只针对页面进行处理
            if (lp.isDecor) continue;
            //计算child位置
            final float transformPos = (float) (child.getLeft() - scrollX) / getClientWidth();
            //调用transformPage
            mPageTransformer.transformPage(child, transformPos);
        }
    }
    //标记ViewPager的onPageScrolled函数执行过
    mCalledSuper = true;
}

从源码上我们知道,onPageScrolled做了3件事,首先把Decor View固定在显示区域,其次,将滚动事件进行分发,即dispatchOnPageScrolled函数,dispatchOnPageScrolled函数内部就是调用OnPageChangeListeneronPageScrolled函数,我们添加的监听器就是此时被回调onPageScrolled函数,dispatchOnPageScrolled函数代码比较简单,不去追究。最后,就是判断是否设置了mPageTransformer,如果设置了,就去回调mPageTransformertransformPage函数,我们知道,我们可以通过自定义PageTransformer来实现每个页面的“出场动画”和“离场动画”,就是这里回调transformPage来实现的。

2.2.3 completeScroll

把目光回到computeScroll函数,我们前面说道,在computeScroll函数最后调用了completeScroll函数,这个函数是做滑动结束后的清理复位等工作。比如:确保滚动已经到最终位置,如果没有到最终位置,则滚动到最终位置。还有就是将每个页面对应的ItemInfo对象的scrolling设为false等等。

2.3 ViewPager 定义smoothScrollTo函数

根据第1节,我们知道,重写了computeScroll函数后,需要自定义一种平滑到指定位置的函数,一般命名为smoothScrollTo,当然咯,你也可以取其他名字,你开心就好~。但是在这个函数里面需要调用startScroll函数。我们来看看ViewPagersmoothScrollTo函数源码,其中x,y表示要移动到的位置,velocity表示手指移动速度,如果不是用户的手指触发的平滑操作,则velocity设为0即可:

void smoothScrollTo(int x, int y, int velocity) {
    if (getChildCount() == 0) {
        // 如果没有页面,啥也不干
        setScrollingCacheEnabled(false);
        return;
    }
    //定义x轴起始位置
    int sx;
    //判断在此之前mScroller是否还在计算滚动
    boolean wasScrolling = (mScroller != null) && !mScroller.isFinished();
    //如果当前在滚动
    if (wasScrolling) {
        //根据在此之前是否还在滚动来决定如何获取当前的x位置
        sx = mIsScrollStarted ? mScroller.getCurrX() : mScroller.getStartX();
        // 如果mScroller在此之前还在计算滚动,则将其停止计算,并直接滑动到最终位置,
        // 这个最终位置即为此刻smoothScrollTo的起始位置
        mScroller.abortAnimation();
        //不启用缓存
        setScrollingCacheEnabled(false);
    } else {//如果当前滚动结束
        sx = getScrollX();
    }
    //获取y轴起始位置
    int sy = getScrollY();
    //计算要移动的x和y方向的距离
    int dx = x - sx;
    int dy = y - sy;
    //如果x和y方向的移动距离都是0,说明无需移动,结束并返回
    if (dx == 0 && dy == 0) {
        //做一些清理和还原工作
        completeScroll(false);
        //已经确定好新的页面,将mCurItem设置为新的页面以及其他的相关处理
        populate();
        //设置当前的滚动状态
        setScrollState(SCROLL_STATE_IDLE);
        return;
    }
    //启用缓存,即对每个子View调用setDrawingCacheEnabled(true)
    setScrollingCacheEnabled(true);
    //设置当前的滚动状态
    setScrollState(SCROLL_STATE_SETTLING);
    //获取宽度及一半宽度
    final int width = getClientWidth();
    final int halfWidth = width / 2;
    //要移动的距离占宽度的比例,这个比例必须得小于等于1
    final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dx) / width);
    //smoothScrollTo并没有使用匀速滑动,而是通过distanceInfluenceForSnapDuration函数
    //来实现变速,这里与Scroller里面的插值器之间并无影响
    final float distance = halfWidth + halfWidth *
            distanceInfluenceForSnapDuration(distanceRatio);

    int duration;
    velocity = Math.abs(velocity);
    //如果手指滑动速度不为0
    if (velocity > 0) {
        //如果是手指滑动,则需要根据手指滑动速度计算滑动持续时间
        duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
    } else {
        //如果手指滑动速度为0,即,是通过代码的方式滑动到指定位置,则使用另一种方式计算滑动持续时间
        final float pageWidth = width * mAdapter.getPageWidth(mCurItem);
        final float pageDelta = (float) Math.abs(dx) / (pageWidth + mPageMargin);
        duration = (int) ((pageDelta + 1) * 100);
    }
    //确保整个滑动时间不超出最大的时间
    duration = Math.min(duration, MAX_SETTLE_DURATION);

    //将mIsScrollStarted标记重置为false,表示没有开始滚动,
    //这个标记会在computeScrollOffset函数中重置为true,
    //所以不用担心会影响到其他地方的判断
    mIsScrollStarted = false;
    //开始平滑
    mScroller.startScroll(sx, sy, dx, dy, duration);
    ViewCompat.postInvalidateOnAnimation(this);
}

从上面可以看到,ViewPagersmoothScrollTo的实现还是挺复杂的,代码实现出来的效果体验非常好以及所考虑的功能很全面。感觉非常值得去学习!另外,ViewPager提供了只有x,y两个参数的smoothScrollTo,其内部也是调用上面这个smoothScrollTo,只是将velocity参数设置为0。

3 滑动冲突

现在为止,ViewPager的滑动部分已经分析完毕,但是用过ViewPager都知道,ViewPager帮我们处理了滑动冲突。我们知道,ViewPager只关注水平方向的手指滑动,根据水平方向的手指滑动来切换页面。在垂直方向上,ViewPager并不关心,因此,ViewPager很有必要解决一下滑动冲突,把竖直方向的滑动传递给子View来处理。

我们知道,ViewGroup是在onInterceptTouchEvent函数中决定是否拦截触摸事件,那么我们就去学习一下ViewPageronInterceptTouchEvent函数。

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {

    //1. 触摸动作
    final int action = ev.getAction() & MotionEventCompat.ACTION_MASK;

    //2. 时刻要注意触摸是否已经结束
    if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
        //3. Release the drag.
        if (DEBUG) Log.v(TAG, "Intercept done!");
        //4. 重置一些跟判断是否拦截触摸相关变量
        resetTouch();
        //5. 触摸结束,无需拦截
        return false;
    }

    //6. 如果当前不是按下事件,我们就判断一下,是否是在拖拽切换页面
    if (action != MotionEvent.ACTION_DOWN) {
        //7. 如果当前是正在拽切换页面,直接拦截掉事件,后面无需再做拦截判断
        if (mIsBeingDragged) {
            if (DEBUG) Log.v(TAG, "Intercept returning true!");
            return true;
        }
        //8. 如果标记为不允许拖拽切换页面,我们就"放过"一切触摸事件
        if (mIsUnableToDrag) {
            if (DEBUG) Log.v(TAG, "Intercept returning false!");
            return false;
        }
    }
    //9. 根据不同的动作进行处理
    switch (action) {
        //10. 如果是手指移动操作
        case MotionEvent.ACTION_MOVE: {

            //11. 代码能执行到这里,就说明mIsBeingDragged==false,否则的话,在第7个注释处就已经执行结束了

            //12.使用触摸点Id,主要是为了处理多点触摸
            final int activePointerId = mActivePointerId;
            if (activePointerId == INVALID_POINTER) {
                //13.如果当前的触摸点id不是一个有效的Id,无需再做处理
                break;
            }
            //14.根据触摸点的id来区分不同的手指,我们只需关注一个手指就好
            final int pointerIndex = MotionEventCompat.findPointerIndex(ev, activePointerId);
            //15.根据这个手指的序号,来获取这个手指对应的x坐标
            final float x = MotionEventCompat.getX(ev, pointerIndex);
            //16.在x轴方向上移动的距离
            final float dx = x - mLastMotionX;
            //17.x轴方向的移动距离绝对值
            final float xDiff = Math.abs(dx);
            //18.同理,参照16、17条注释
            final float y = MotionEventCompat.getY(ev, pointerIndex);
            final float yDiff = Math.abs(y - mInitialMotionY);
            if (DEBUG) Log.v(TAG, "Moved x to " + x + "," + y + " diff=" + xDiff + "," + yDiff);

            //19.判断当前显示的页面是否可以滑动,如果可以滑动,则将该事件丢给当前显示的页面处理
            //isGutterDrag是判断是否在两个页面之间的缝隙内移动
            //canScroll是判断页面是否可以滑动
            if (dx != 0 && !isGutterDrag(mLastMotionX, dx) &&
                    canScroll(this, false, (int) dx, (int) x, (int) y)) {
                mLastMotionX = x;
                mLastMotionY = y;
                //20.标记ViewPager不去拦截事件
                mIsUnableToDrag = true;
                return false;
            }
            //21.如果x移动距离大于最小距离,并且斜率小于0.5,表示在水平方向上的拖动
            if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
                if (DEBUG) Log.v(TAG, "Starting drag!");
                //22.水平方向的移动,需要ViewPager去拦截
                mIsBeingDragged = true;
                //23.如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //24.设置滚动状态
                setScrollState(SCROLL_STATE_DRAGGING);
                //25.保存当前位置
                mLastMotionX = dx > 0 ? mInitialMotionX + mTouchSlop :
                        mInitialMotionX - mTouchSlop;
                mLastMotionY = y;
                //26.启用缓存
                setScrollingCacheEnabled(true);
            } else if (yDiff > mTouchSlop) {//27.否则的话,表示是竖直方向上的移动
                if (DEBUG) Log.v(TAG, "Starting unable to drag!");
                //28.竖直方向上的移动则不去拦截触摸事件
                mIsUnableToDrag = true;
            }
            if (mIsBeingDragged) {
                // 29.跟随手指一起滑动
                if (performDrag(x)) {
                    ViewCompat.postInvalidateOnAnimation(this);
                }
            }
            break;
        }
        //30.如果手指是按下操作
        case MotionEvent.ACTION_DOWN: {
             
            //31.记录按下的点位置
            mLastMotionX = mInitialMotionX = ev.getX();
            mLastMotionY = mInitialMotionY = ev.getY();
            //32.第一个ACTION_DOWN事件对应的手指序号为0
            mActivePointerId = MotionEventCompat.getPointerId(ev, 0);
            //33.重置允许拖拽切换页面
            mIsUnableToDrag = false;
            //34.标记开始滚动
            mIsScrollStarted = true;
            //35.手动调用计算滑动的偏移量
            mScroller.computeScrollOffset();
            //36.如果当前滚动状态为正在将页面放置到最终位置,
            //且当前位置距离最终位置足够远
            if (mScrollState == SCROLL_STATE_SETTLING &&
                    Math.abs(mScroller.getFinalX() - mScroller.getCurrX()) > mCloseEnough) {
                //37. 如果此时用户手指按下,则立马暂停滑动
                mScroller.abortAnimation();
                mPopulatePending = false;
                populate();
                mIsBeingDragged = true;
                //38.如果ViewPager还有父View,则还要向父View申请将触摸事件传递给ViewPager
                requestParentDisallowInterceptTouchEvent(true);
                //39.设置当前状态为正在拖拽
                setScrollState(SCROLL_STATE_DRAGGING);
            } else {
                //40.结束滚动
                completeScroll(false);
                mIsBeingDragged = false;
            }

            if (DEBUG) Log.v(TAG, "Down at " + mLastMotionX + "," + mLastMotionY
                    + " mIsBeingDragged=" + mIsBeingDragged
                    + "mIsUnableToDrag=" + mIsUnableToDrag);
            break;
        }

        case MotionEventCompat.ACTION_POINTER_UP:
            onSecondaryPointerUp(ev);
            break;
    }

    //41.添加速度追踪
    if (mVelocityTracker == null) {
        mVelocityTracker = VelocityTracker.obtain();
    }
    mVelocityTracker.addMovement(ev);

     
    //42.只有在当前是拖拽切换页面时我们才会去拦截事件
    return mIsBeingDragged;
}

我们看看ViewPager是如何决定是拦截还是不拦截,从源码上面看出,但斜率小于0.5时,则要拦截,否则不拦截,斜率是什么情况呢?高中数学可知,在第一象限中,越靠近y轴的直线,斜率越大,越靠近x轴直线斜率越小,先看简单图示:

第一象限斜率

也就是说,手指滑动的倾斜度比0.5小,就去拦截事件,由ViewPager来响应切换页面。

好啦,今天的学习就先到处为止啦,明天继续研究其他部分

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

推荐阅读更多精彩内容