Android学习笔记之View

导图

一、View事件体系

1.什么是 View 和 View的位置坐标

  • View是什么:

View 是一种界面层的控件的一种抽象,一组 View 则称为 ViewGroup,同时 ViewGroup 继承了 View。意味着 View 可以是单个控件也可以是多个控件组成的组控件,通过这种关系形成了 View 树的结构。

  • View的位置坐标
  • Android坐标系:以屏幕的左上角为坐标原点,向右为x轴增大方向,向下为y轴增大方向。
  • View的宽高和坐标关系:width = right - left,height = bottom - top。
  • 从android3.0开始,View增加了额外几个参数:x,y,translationX、translationY。其中x和y是View左上角的坐标,translationX和translationY是新View左上角相对于父容器的偏移量,它们默认值是0。
  • 存在关系:x = left + translationX,y = top + translationY
  • 由此可见,x和left不同体现在:left是View的初始坐标,在绘制完毕后就不会再改变;而x是View偏移后的实时坐标,是实际坐标。y和top的区别同理。

2.MotionEvent和TouchTop

  • MotionEvent:在手指接触屏幕后所产生的一系列事件,任何事件列都是以DOWN事件开始,UP事件结束,中间有无数的MOVE事件。
    • ACTION_DOWM:手指刚接触屏幕。
    • ACTION_MOVE:手指在屏幕上移动。
    • ACTION_UP:手指在屏幕上松开的一瞬间。
MotionEvent点击view位置示意图

通过MotionEvent 对象可以得到触摸位置的x、y坐标。其中通过getX()、getY()可获取相对于当前view左上角的x、y坐标;通过getRawX()、getRawY()可获取相对于手机屏幕左上角的x,y坐标。


  • TouchTop: 系统所能识别的被认为是滑动的最小距离。即当手指在屏幕上滑动时,如果两次滑动之间的距离小于这个常量,那么系统就不认为你是在进行滑动操作。获取这个常量:

该常量和设备有关,可用它来判断用户的滑动是否达到阈值,获取方法:
ViewConfiguration.get(getContext()).getScaledTouchSlop()。

3.VelocityTracker 和 GestureDetector

  • VelocityTracker:速度追踪,用于追踪手指在滑动过程中的速度,包括水平和竖直方向的速度。

A、首先在view的onTouchEvent方法中追踪当前单击事件的速度:

//实例化一个VelocityTracker 对象
VelocityTracker velocityTracker = VelocityTracker.obtain();
//添加追踪事件
velocityTracker.addMovement(event);

B、接着在 ACTION_UP 事件中获取当前的速度。注意这里计算的是1000ms时间间隔移动的像素值,假设像素是100,即速度是每秒100像素。另外,手指逆着坐标系的正方向滑动,所产生的速度为负值 ,顺着正反向滑动,所产生的速度为正值

//获取速度前先计算速度,这里计算的是在1000ms内
velocityTracker .computeCurrentVelocity(1000);
//得到的是1000ms内手指在水平方向从左向右滑过的像素数,即水平速度
float xVelocity = velocityTracker .getXVelocity();
//得到的是1000ms内手指在水平方向从上向下滑过的像素数,垂直速度
float yVelocity = velocityTracker .getYVelocity();

C、最后,当不需要使用它的时候,需要调用clear方法来重置并回收内存:

velocityTracker.clear();
velocityTracker.recycle();

  • GestureDetector:手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。

A、使用过程:创建一个GestureDetecor对象并实现OnGestureListener接口,根据需要实现单击等方法:

GestureDetector mGestureDetector = new GestureDetector(this);//实例化一个GestureDetector对象
mGestureDetector.setIsLongpressEnabled(false);// 解决长按屏幕后无法拖动的现象

B、接着,接管目标view的onTouchEvent方法,在待监听view的onTouchEvent方法中添加如下实现:

boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

C、然后,就可以有选择的实现OnGestureListener和OnDoubleTapListener中的方法了。

建议:如果只是监听滑动操作,建议在onTouchEvent中实现;如果要监听双击这种行为,则使用GestureDetector 。


二、View的滑动

滑动方式1:通过View本身提供的scrollTo/scrollBy方法

  • scrollBy 是内部调用了 scrollTo 的,它是基于当前位置的相对滑动;而scrollTo是绝对滑动,因此如果利用相同输入参数多次调用scrollTo()方法,由于View初始位置是不变只会出现一次View滚动的效果而不是多次。
  • 注意:两者都只能对view内容进行滑动,而不能使view本身滑动。
image
  • mScrollX和mScrollY分别表示View在X、Y方向的滚动距离。
  • mScrollX:View的左边缘减去View的内容的左边缘;
  • mScrollY:View的上边缘减去View的内容的上边缘。从右向左滑动,mScrollX为正值,反之为负值;从下往上滑动,mScrollY为正值,反之为负值。

滑动方式2:通过动画给View施加平移效果

  • View动画:layout文件 < set > 标签下的 < translate fromXDelta和 toXDelta > 来平移。
  • 属性动画:ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

滑动方式3:通过改变View的LayoutParams使得View重新布局

比如将一个View向右移动100像素,向右,只需要把它的marginLeft参数增大即可,代码见下:

MarginLayoutParams params = (MarginLayoutParams) btn.getLayoutParams();
params.leftMargin += 100;
btn.requestLayout();// 请求重新对View进行measure、layout

三种方法比较

  • scrollTo/scrollBy:操作简单,适合对view内容滑动,非平滑。
  • 动画:操作简单,主要适用于没有交互的view和实现复杂的动画效果。
  • 改变LayoutParams:操作稍微复杂,适用于有交互的view,非平滑。

三、弹性滑动

弹性滑动方式1:使用 Scroller

  • 与scrollTo/scrollBy不同:scrollTo/scrollBy过程是瞬间完成的,非平滑;而Scroller则有过渡滑动的效果。
  • 注意:Scoller本身无法让View弹性滑动,它需要和 View 的 computerScroller 方法配合使用。
Scroller scroller = new Scroller(mContext); //实例化一个Scroller对象

private void smoothScrollTo(int dstX, int dstY) {
  int scrollX = getScrollX();//View的左边缘到其内容左边缘的距离
  int scrollY = getScrollY();//View的上边缘到其内容上边缘的距离
  int deltaX = dstX - scrollX;//x方向滑动的位移量
  int deltaY = dstY - scrollY;//y方向滑动的位移量
  scroller.startScroll(scrollX, scrollY, deltaX, deltaY, 1000); //开始滑动
  invalidate(); //刷新界面
}

@Override//计算一段时间间隔内偏移的距离,并返回是否滚动结束的标记
public void computeScroll() {
  if (scroller.computeScrollOffset()) { 
    scrollTo(scroller.getCurrX(), scroller.getCurY());
    postInvalidate();//通过不断的重绘不断的调用computeScroll方法
  }
}

具体实现:在 MotionEvent.ACTION_UP 事件触发时调用 startScroll 方法->马上调用invalidate/postInvalidate 方法->会请求 View 重绘,导致 View.draw 方法被执行->会调用 View.computeScroll 方法,此方法是空实现,需要自己处理逻辑。具体逻辑是:先判断 computeScrollOffset,若为 true(表示滚动未结束),则执行 scrollTo 方法,它会再次调用 postInvalidate,如此反复执行,直到返回值为 false

image

原理:Scroll 的 computeScrollOffset() 根据时间的流逝动态百分比计算一小段时间里View滑动的距离,并得到当前View位置,再通过scrollTo继续滑动。即把一次滑动拆分成无数次小距离滑动从而实现弹性滑动。

弹性滑动方式2:通过动画

动画本身就是一种渐近的过程,故可通过动画来实现弹性滑动。

//在100ms内使得View从原始位置向右平移100像素
ObjectAnimator.ofFloat(targetView,"translationX",0,100).setDuration(100).start();

弹性滑动方式3:使用延时策略

通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过HandlerView的postDelayed方法,也可使用线程的sleep方法。


四、View的事件分发机制

  • 事件分发的本质:其实就是对 MotionEvent 事件分发的过程。即当一个 MotionEvent 产生了以后,系统需要将这个点击事件传递到一个具体的View上。

  • 点击事件的传递顺序:Activity(Window) -> ViewGroup -> View

  • 其中三个主要方法:

    • dispatchTouchEvent:进行事件的分发(传递)。
    • onInterceptTouchEvent:对事件进行拦截。
    • onTouchEvent:进行事件处理。
事件分发流程
  1. 事件分发是逐级下发的,目的是将事件传递给一个View。当最后一个View 没有消费事件,这个事件会依次返转,最后回到最高位的Activity,如果这样都没消费的话才抛弃。(责任链)
  2. 在ViewGroup事件分发中,View本身是不存在分发,所以也没有拦截方法(onInterceptTouchEvent),它只能在onTouchEvent方法中进行处理消费或者不消费。
  3. 当一个 View 需要处理事件时,如果设置了 OnTouchListener,那么 OnTouchListener 的 onTouch 方法会回调,如果返回 true,那么 onTouchEvent 方法将不会调用(同时onClick事件是在onTouchEvent中调用)所以三者优先级是onTouch->onTouchEvent->onClick(onClickListener)。

View的事件分发机制

public boolean dispatchTouchEvent(MotionEvent event) {
    if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) 
          == ENABLED && mOnTouchListener.onTouch(this, event)) {
        return true;
    }
    return onTouchEvent(event);
}
  • ①、在View的dispatchTouchEvent方法中首先会执行onTouch这个回调函数,但是执行onTouch这个回调函数有两个前提条件、第一个提前提条件是该控件注册了触摸监听,第二个条件是该控件是的状态是Enable的。
  • ②、onTouch回调函数会有一个返回值,如果返回为true的话就代表本次触摸事件被消耗掉了,执行接触;返回值为false的话会继续执行onTouchEvent方法。
  • ③、onTouchEvent方法就是真正执行点击事件的地方,也就是我们重写的onClick方法。
  • ④、onTouchEvent方法中能够捕获一次点击事件当中ACTION_DOWN、ACTION_ MOVE、ACTION_UP这三个动作,当触摸动作为ACTION_UP会调用onClick方法。

ViewGroup的事件分发机制

public boolean dispatchTouchEvent(MotionEvent event){
    boolean consume = false;
    if(onInterceptTouchEvent(event)){
        consume = onTouchEvent(event);
    }else{
        consume = child.dispatchTouchEvent(event);
    }
    return consume;
}
  • ①、首先会去调用ViewGroup的dispatchTouchEvent方法,其中会有onInterceptTouchEvent方法对事件传递进行拦截,如果返回值为true的话就表示事件不往子View中传递,如果为false的话就表示不对事件传递进行拦截,事件会往子View中传递。

五、View滑动冲突

1.常见的滑动冲突场景

  1. 外部滑动方向与内部滑动方向不一致(左右滑动:Fragment,上下滑动: ListView)。
  2. 外部滑动方向与内部滑动方向一致,(ScrollView 中包含 ListView );
  3. 上面两种情况的嵌套;

2.滑动冲突的处理规则

  1. 滑动路径和水平方向所形成的夹角
  2. 水平方向和竖直方向上的距离差
  3. 水平和竖直方向的速度差
  4. 从业务的需求上得出相应的处理规则。

3.滑动冲突的解决方式

第一种:外部拦截法

  • 含义:指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。

  • 方法:需要重写父容器的 onInterceptTouchEvent 方法,在内部做出相应的拦截。

  • 过程:在 onInterceptTouchEvent 方法中,首先在 ACTION_DOWN 这个事件中,父容器必须返回 false,即不拦截 ACTION_DOWN 事件,因为一旦父容器拦截了 ACTION_DOWN,那么后续的 ACTION_MOVE / ACTION_UP 都会直接交给父容器处理;其次是 ACTION_MOVE,根据需求来决定是否要拦截;最后 ACTION_UP 事件,这里必须要返回 false,一旦拦截子View的onClick事件将不会触发。

public boolean onInterceptTouchEvent (MotionEvent event){
    boolean intercepted = false;
    int x = (int) event.getX();
    int y = (int) event.getY();
    switch (event.getAction()) {
      case MotionEvent.ACTION_DOWN:
         intercepted = false;
         break;
      case MotionEvent.ACTION_MOVE:
         if (父容器需要当前事件) {
             intercepted = true;
         } else {
             intercepted = flase;
         }
         break;
   }
      case MotionEvent.ACTION_UP:
         intercepted = false;
         break;
      default : break;
   }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;
   }

第二种:内部拦截法

  • 含义:指父容器不拦截任何事件,而将所有的事件都传递给子容器,如果子容器需要此事件就直接消耗,否则就交由父容器进行处理。

  • 方法:需要配合requestDisallowInterceptTouchEvent方法。

  • 过程:父元素需要默认拦截除 ACTION_DOWN 以外的事,这样子元素调用 parent.requestDisallowInterceptTouchEvent(false) 方法时,父元素才能继续拦截需要的事件。(ACTION_DOWN 事件不受 requestDisallowInterceptTouchEvent 方法影响,所以一旦父元素拦截 ACTION_DOWN 事件,那么所有元素都无法传递到子元素去)。

public boolean dispatchTouchEvent ( MotionEvent event ) {
  int x = (int) event.getX();
  int y = (int) event.getY();

  switch (event.getAction) {
      case MotionEvent.ACTION_DOWN:
         parent.requestDisallowInterceptTouchEvent(true);//为true表示禁止父容器拦截
         break;
      case MotionEvent.ACTION_MOVE:
         int deltaX = x - mLastX;
         int deltaY = y - mLastY;
         if (父容器需要此类点击事件) {
             parent.requestDisallowInterceptTouchEvent(false);
         }
         break;
      case MotionEvent.ACTION_UP:
         break;
      default :
         break;        
 }

  mLastX = x;
  mLastY = y;
  return super.dispatchTouchEvent(event);
}

六、View工作原理

1.View的绘制流程

  1. 整个 View 树的绘图流程是在 ViewRoot 类的 performTraversals() 方法展开的。
  2. performTraversals() 依次调用 performMeasure()performLayout()performDraw()三个方法,分别完成顶级View的绘制。
  3. 其中,performMeasure() 会调用 measure(),measure() 中又调用onMeasure(),实现对其所有子元素的 measure 过程,这样就完成了一次 measure 过程;接着子元素会重复父容器的 measure 过程,如此反复至完成整个 View 树的遍历。
  4. performLayout 和 performDraw 的传递流程也是类似,唯一不同的是 performDraw 都传递过程是用 draw 方法中的 dispatchDraw 来实现。
  • measure 过程决定了View的宽高,Measure完成以后,可以通过getMeasuredWidthgetMeasureHeight方法来获取到View的宽/高
  • Layout 过程决定了View的四个顶点的坐标和View的实际宽高,完成以后通过getTop、getBottom、getLeft、getRight来拿到四个顶点的位置,并通过getWidth和getHeight方法来拿到View的最终宽高。
  • Draw 过程决定了View的显示,只有draw方法完成后View的内容才能呈现在屏幕上。
image

2.measure方法

2.1 MeasureSpec

  • 作用:通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec 决定View的大小。

  • 组成:32 位的 int 值,高 2 位 代表 SpecMode测量模式,低 30 位代表 SpecSize 规格大小。

  • 三种测量模式:

    • UNSPECIFIED:不确定,父容器不对 View 进行任何限制,要多大给多大,一般用于系统内部。
    • EXACTLY:父容器检测到 View 所需要的精确大小,这时候 View 的最终大小就是 SpecSize 所指定的值,对应 LayoutParams 中的“match_parent”和“具体数值”这两种模式。
    • AT_MOST:父容器指定了最大的尺寸 SpecSize,View 的大小不能大于这个值,对应 LayoutParams 中的 wrap_content。
  • 决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。

  • LayoutParams布局参数:具体数值、match_parent和wrap_content。

2.2 measure测量过程图

  • ①View的measure:View的measure由measure方法来完成,而它是一个final类型的方法,意味这不能重写,在View的measure中去调用View的onMeasure方法,因此只需要在onMeasure中实现即可。
image

从getDefaultSize()中可以看出,直接继承View的自定义View需要重写onMeasure()并设置wrap_content时的自身大小,否则效果相当于macth_parent。

  • ②ViewGroup的measure
    • measure():基本测量逻辑的判断,调用onMeasure进行下一步测量;
    • onMeasure()(需要重写):计算View宽/高,并存储。
    • measureChildren():遍历子View并调用measureChild()进行子View下一步测量。
    • measureChild():计算单个子View的MeasureSpec;调用每个子View的measure()进行下一步测量。
    • getChildMeasureSpec():计算子View的MeasureSpec参数。
    • ......遍历子View测量(View的measure过程)........
    • setMeasureDimension:存储测量后的子View宽高。
image
image

3. layout方法

  • 作用:确定 View 的最终宽高和四个顶点的位置。
  • 大致流程:从顶级View开始依次调用 layout(),其中子 View 的 layout() 会调用 setFrame() 来设定自己的四个顶点(mLeft、mRight、mTop、mBottom),接着调用 onLayout() 来确定其坐标,注意该方法是空方法,因为不同的 ViewGroup 对其子 View 的布局是不相同的。
image
image

4. draw方法

  • 绘制顺序:
    1. 绘制背景:background.draw(canvas)
    2. 绘制自己:onDraw(canvas)
    3. 绘制children:dispatchDraw(canvas)
    4. 绘制装饰:onDrawScrollBars(canvas)

ViewGroup 通常情况下不需要绘制,因为本身没有需要绘制的东西,如果不是指定了ViewGroup的背景颜色,那么连 ViewGroup 都不会调用。ViewGroup 会使用dispatchDraw方法来绘制其子View。


七、自定义View

1. 自定义 View 的分类

  • 继承 View 重写 onDraw 方法:主要实现一些不规则的效果,重写onDraw方法,需要支持wrap_content并且需要自己处理padding。(重写view实现新的控件)
  • 继承 ViewGroup 派生特殊的 Layout:实现自定义布局,除了LinearLayout、RelativeLayout等这几种系统之外的布局。稍微复杂需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局的过程。
  • 继承特定的 View,比如 TextView:扩展某种已有View的功能,比如TextView。容易实现,不需要自己支持wrap_content和padding。(对现有的控件进行拓展)
  • 继承特定的 ViewGroup(比如 LinearLayout):与第二种的区别在于不用自己处理ViewGroup的测量和布局,第二种更接近底层。(通过组合来创建复合控件)

2. 自定义 View 须知

  • 让 View 支持 wrap_content;
  • 如果有必要,让你的 View 支持 padding;
  • 尽量不要在 View 中使用 Handler,没有必要,View 内部提供了 post 系列的方法;
  • View 中如果有线程或者动画,需要继续停止,参考View#onDetachedFromWindow;
  • View 带有滑动嵌套情形时,需要处理好滑动冲突;

3. 自定义 View 的思想

  • 首先要掌握基本功,比如 View 的弹性滑动、滑动冲突、绘制原理等,这些都是自定义 View 所必须的。
  • 熟练掌握基本功后,在面对新的自定义 View 时,要能够对其分类并选择合适的实现思路。
  • 另外平时还要多积累一些自定义 View 相关的经验,并逐渐做到融会贯通。

4. 为什么自定义控件

  • 特定的显示风格。
  • 处理特有的用户风格
  • 优化布局
  • 封装

5.如何自定义控件

  1. 自定义属性的声明与获取。
  2. 测量onMeasure。
  3. 布局onLayout(viewgroup)
  4. 绘制onDraw
  5. onTouchEvent。
  6. onInterceptTouchEvent(ViewGroup)。

6. 自定义属性声明与获取

  1. 分析需要的自定义属性。
  2. 在res/values/attrs.xml定义声明
  3. 在layout.xml方法中进行使用。
  4. 在View的构造方法中进行获取。

7. 自定义View的三要素

  1. Canvas:画布,用于绘制View所要显示的内容,一般来自onDraw()函数的传入。
  2. Paint :画笔,用于绘制View所需要绘制的内容,相当于笔,在其内部可以设置颜色,粗细,是否实心等信息,一般通过new的方式获取该类对象;
  3. Point:点,用于确定View所需要绘制的内容大小及位置等相关信息,当然这里的Point不具有实际意义,也有可能是线段,矩形等,只不过大多数是通过点的组合关系确定而已;

问题1:onTouch()、onTouchEvent()和onClick()关系?

优先度onTouch()>onTouchEvent()>onClick()。因此onTouchListener的onTouch()方法会先触发;如果onTouch()返回false才会接着触发onTouchEvent(),同样的,内置诸如onClick()事件的实现等等都基于onTouchEvent();如果onTouch()返回true,这些事件将不会被触发。

问题2:SurfaceView和View的区别?

SurfaceView是从View基类中派生出来的显示类,它和View的区别有:

  • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新
  • View适用于主动更新的情况,而SurfaceView适用于被动更新,如频繁刷新,这是因为如果使用View频繁刷新会阻塞主线程,导致界面卡顿
  • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于需要频繁刷新、刷新时数据处理量很大的页面

问题3:invalidate()和postInvalidate()的区别?

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中需要配合handler使用;而postInvalidate()可在子线程中直接调用。

问题4: requestLayout()和invalidate()的区别?

  1. 调用invalidate()只会执行onDraw方法;调用requestLayout()只会执行onMeasure方法和onLayout方法,并不会执行onDraw方法。

  2. 所以当我们进行View更新时,若仅View的显示内容发生改变,则只需调用invalidate方法;若View宽高和位置发生改变,则调用requestLayout方法;若两者均发生改变,则需先调用requestLayout()再调用invalidate()。

View的生命周期

问题5:Android中真实宽高getWidth和getMeasuredWidth的区别:哪个计算的是真实的宽?

getWidth():得到的是View在父Layout中布局好后的宽度值,如果没有父布局,那么默认的父布局就是整个屏幕。
getMeasuredWidth():得到的是最近一次调用measure()方法测量后得到的是View的宽度,它仅仅用在测量和Layout的计算中。所以此方法得到的是View的内容占据的实际宽度。
总结:
getWidth(): View在设定好布局后整个View的宽度。
getMeasuredWidth(): 对View上的内容进行测量后得到的View内容占据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法里调用measure(0,0);否则你得到的结果和getWidth()得到的结果是一样的。

问题6:为什么Viewgroup的Measure过程和View的过程不一样,还要自己重写onMeasure()方法?

因为不同的ViewGroup子类(LinearLayout、RelativeLayout / 自定义ViewGroup子类等)具备不同的布局特性,这导致他们子View的测量方法各有不同。

总结一句话:View的measure过程的onMeasure()具有统一实现,而ViewGroup则没有。

本文参考资料:

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

推荐阅读更多精彩内容