Android车辆运动轨迹平滑移动(高仿滴滴打车)最佳实践

版权声明:本文来自门心叼龙的博客,属于原创内容,转载请注明出处:https://menxindiaolong.blog.csdn.net/article/details/95789128

github源码下载地址:https://github.com/geduo83/android-amap-movecar

关于车辆运动的相关文章一共写过两篇,一篇为Android车辆运动轨迹大数据采集最佳实践,另外一篇是Android车辆运动轨迹数据采集服务保活的探索与发现
一直打算写一篇车辆运动轨迹平滑移动的文章,年后由于工作项目太忙也就没时间写,工作的事情忙完了,紧接着就是忙自己的另外两个开源项目,一个是Android客户端框架FlyTour
、和另外一个SpringCloud微服务框架FlyCloud,上个周终于告一段落。

数据也能采集了,而且采集服务保活也做了,有效的避免了数据采集服务在后台被系统杀死的可能,接着就需要把采集到的数据在移动设备上实时的展示出来,这两天我也特意在网上搜索了一下,关于Android车辆运动轨迹平滑移动的文章,还真没有,大部分都是提问的多,问怎么实现的?都是一些只言片语很零碎的一些回答,在实战项目当中没有太大的实用价值。

关于车辆运动,在我们在日常生活中见到最多的就是滴滴打车,想必这款app大家都使用过,当你在app的叫车页面,输入完毕你的目的地,点击叫车,如果有司机接单了,你就清楚的看到车辆会平滑的移动到你所在的位置去接你。今天我就带领大家一步步的实现一个车辆平滑移动的功能。

这个功能是基于高德地图开发的,因此我特意去高德官网查阅了一下,高德的确提供了官方轨迹移动api,我们暂且不用官方API.先用自己的方法去实现一个最简单的功能
我们先来一睹为快:


在这里插入图片描述

单次轨迹回放

已知有一段轨迹数据,点击回放按钮,小车沿着路线自动的往前运动,播放完毕也就结束了

 public class MoveSingleThread extends Thread{
    private List<LatLng> mLatLngList;
    private Marker mCarMarker;
    public MoveSingleThread(List<LatLng> latLngs, Marker marker) {
        super();
        mLatLngList = latLngs;
        mCarMarker = marker;
    }

    @Override
    public void run() {
        super.run();
    }
    public void moveTrack(){
        // 第一个for循环用来计算走了多少部
        int step = 0;
        for (int i = 0; i < mLatLngList.size() - 1; i++) {
            LatLng startPoint = mLatLngList.get(i);
            LatLng endPoint = mLatLngList.get(i + 1);
            double slope = getSlope(startPoint, endPoint);
            // 是不是正向的标示(向上设为正向)
            boolean isReverse = (startPoint.latitude > endPoint.latitude);
            double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
            // 应该对经纬度同时处理
            for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
                    j - xMoveDistance) {
                step++;
            }
        }

        // 通过距离,计算轨迹动画时间间隔
        double mTimeInterval = 0;// 轨迹回放时间戳
        if (!TextUtils.isEmpty(mDistance)) {
            float totalDistance = Float.parseFloat(mDistance) * 1000;
            if (totalDistance <= 500) {
                mTimeInterval = 1000.0 / step;
            } else if (totalDistance > 500 && totalDistance <= 7500) {
                mTimeInterval = 2.0 * totalDistance / step;
            } else {
                mTimeInterval = 15000.0 / step;
            }
        }

        // while (true) {
        for (int i = 0; i < mLatLngList.size() - 1; i++) {
            if (stopFlag) {
                stopFlag = false;
                break;
            }
            mIsCarMoveing = true;
            LatLng startPoint = mLatLngList.get(i);
            LatLng endPoint = mLatLngList.get(i + 1);
            mCarMarker.setPosition(startPoint);
            mCarMarker.setRotateAngle((float) getAngle(startPoint, endPoint));
            double slope = getSlope(startPoint, endPoint);
            // 是不是正向的标示(向上设为正向)
            boolean isReverse = (startPoint.latitude > endPoint.latitude);
            double intercept = getInterception(slope, startPoint);
            double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
            // 应该对经纬度同时处理
            double mSleep = 0;
            for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
                    j - xMoveDistance) {
                LatLng latLng = null;
                if (slope != Double.MAX_VALUE) {
                    latLng = new LatLng(j, (j - intercept) / slope);
                    // latLng = new LatLng(j, k);
                } else {
                    latLng = new LatLng(j, startPoint.longitude);
                }
                mCarMarker.setPosition(latLng);
                // 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
                if (mTimeInterval < 1) {
                    mSleep += mTimeInterval;
                    if (mSleep >= 1) {
                        SystemClock.sleep((long) mSleep);
                        mSleep = 0;
                    }
                } else
                    SystemClock.sleep((long) mTimeInterval);
            }
        }
    }
}

实时轨迹数据排队问题

如果要显示实时轨迹怎么办 ,上面的代码就有问题了,Thread.start()方法调用后,就会立马执行他的run方法,run方法执行完毕,线程也就结束了,也是说上面的代码只能跑一次轨迹数据,如果每间隔五秒从后台取一次轨迹数据,就需要一数据队列来存储这些数据,每跑完一次数据,就从数据队列里面去取,如果有就取来接着跑,如果没有就处于等待状态。 我们创建异步消息处理线程,这一问题就可以迎刃而解,来数据了我们就可以通过handler把数据post给我们的子线程,Handler自带数据队列,它处于排队状态,如果有数据了就开始跑轨迹,如果没有数据就处于等待状态,直到有数据的到来,如果对异步消息处理线程不熟悉,请查看我的另外一篇文章Android实战开发Handler机制深度解析https://menxindiaolong.blog.csdn.net/article/details/86560330

一个标准的异步消息处理线程应该怎么写?
方法1:

class LooperThread extends Thread {
      public Handler mHandler;
 
      public void run() {
          Looper.prepare();
          mHandler = new Handler() {
              public void handleMessage(Message msg) {
                  // process incoming messages here
              }
          };
          Looper.loop();
      }
}

方法2:

  // Step 1: 创建并启动HandlerThread线程,内部包含Looper
    HandlerThread handlerThread = new HandlerThread("gityuan.com");
    handlerThread.start();
 
    // Step 2: 创建Handler
    Handler handler = new Handler(handlerThread.getLooper()) {
       public void handleMessage(Message msg) {
         // process incoming messages here
       }
     };
    // Step 3: 发送消息
     handler.post(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread id="+Thread.currentThread().getId());
        }
    });

上面就是Android系统中异步消息处理线程的通用写法

运动轨迹的暂停、继续问题

由于运动轨迹是在子线程里面完成的,我们自然而然会想到线程的等待、唤醒,也就是wait、notify的问题了
因此我们在运动过程加上就如下代码就可以了

if (pause) {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

怎么让他恢复运动呢?notify一下即可

public void reStartMove() {
        synchronized (lock) {
            pause = false;
            lock.notify();
        }
}

完整的代码如下:

在这里插入图片描述
/**
 * Description: <MoveCarCustomThread><br>
 * Author:      mxdl<br>
 * Date:        2019/7/10<br>
 * Version:     V1.0.0<br>
 * Update:     <br>
 */
public class MoveCarCustomThread extends Thread {
    public static final String TAG = MoveCarCustomThread.class.getSimpleName();
    private Handler moveCarHandler;//发送数据的异步消息处理器
    private Object lock = new Object();//线程锁
    private boolean moveing = false;//是否线程正在移动
    private boolean pause = false;//暂停状态,为true则暂停
    private boolean stop = false;//停止状态,为true则停止移动
    private WeakReference<MainActivity> mActivityWeakReference;//防止内存Activity导致的内容泄漏
    private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;

    public void setCurrMoveState(MOVE_STATE currMoveState) {
        this.currMoveState = currMoveState;
    }

    public MOVE_STATE getCurrMoveState() {
        return currMoveState;
    }
    public MoveCarCustomThread(MainActivity activity) {
        mActivityWeakReference = new WeakReference<>(activity);
    }
    //暂停移动
    public void pauseMove() {
        pause = true;
    }
    //设置暂停之后,再次移动调用它
    public void reStartMove() {
        synchronized (lock) {
            pause = false;
            lock.notify();
        }
    }

    public void stopMove() {
        stop = true;
        if(moveCarHandler != null){
            moveCarHandler.removeCallbacksAndMessages(null);
        }
        if(mActivityWeakReference.get() != null){
            mActivityWeakReference.get().mLatLngList.clear();
            mActivityWeakReference.get().mMainHandler.removeCallbacksAndMessages(null);
        }
    }

    public Handler getMoveCarHandler() {
        return moveCarHandler;
    }
    public boolean isMoveing() {
        return moveing;
    }


    @Override
    public void run() {
        super.run();
        //设置该线程为loop线程
        Looper.prepare();
        moveCarHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                //通过锁保证发过来的数据同步入列
                synchronized (lock) {
                    if (msg.obj != null && msg.obj instanceof List) {
                        List<LatLng> latLngList = (List<LatLng>) msg.obj;
                        moveCoarseTrack(latLngList);
                    }
                }
            }
        };
        //启动loop线程
        Looper.loop();
    }

    private void moveCoarseTrack(List<LatLng> latLngList) {
        if (latLngList == null || latLngList.size() == 0 || latLngList.size() == 1) {
            return;
        }
        Log.v(TAG, "moveCoarseTrack start.........................................................");
        long startTime = System.currentTimeMillis();
        Log.v(TAG, "startTime:" + startTime);
        int step = TrackMoveUtil.getStep(latLngList);// 通过距离,计算轨迹动画运动步数
        Log.v(TAG, "move step:" + step);
        float distance = TrackMoveUtil.getDistance(latLngList);
        Log.v(TAG, "move distance:" + distance);
        double mTimeInterval = TrackMoveUtil.getMoveTime(distance, step);// 通过距离,计算轨迹动画时间间隔
        mTimeInterval = 10;// 每走一步停止10毫秒
        Log.v(TAG, "move mTimeInterval:" + mTimeInterval);

        moveing = true;
        for (int i = 0; i < latLngList.size() - 1; i++) {
            // 暂停状态,线程停止了
            if (pause) {
                movePause();
            }
            if (stop) {
                break;
            }
            moveing = true;
            LatLng startLatLng = latLngList.get(i);
            LatLng endLatLng = latLngList.get(i + 1);
            MainActivity mainActivity = mActivityWeakReference.get();
            moveCar(startLatLng, endLatLng, mainActivity);
            moveLine(startLatLng, mainActivity);
            moveCamera(startLatLng, mainActivity);

            double slope = TrackMoveUtil.getSlope(startLatLng, endLatLng);// 计算两点间的斜率
            double intercept = TrackMoveUtil.getInterception(slope, startLatLng);// 根据点和斜率算取截距
            boolean isReverse = (startLatLng.latitude > endLatLng.latitude);// 是不是正向的标示(向上设为正向)
            double xMoveDistance = isReverse ? TrackMoveUtil.getXMoveDistance(slope) : -1 * TrackMoveUtil.getXMoveDistance(slope);
            // 应该对经纬度同时处理
            double sleep = 0;
            int flag = 0;
            for (double j = startLatLng.latitude; !((j >= endLatLng.latitude) ^ isReverse); j = j - xMoveDistance) {
                // 非暂停状态地图才进行跟随移动
                if (pause) {
                    movePause();
                }
                if (stop) {
                    break;
                }
                moveing = true;
                flag++;
                if (slope != Double.MAX_VALUE) {
                    startLatLng = new LatLng(j, (j - intercept) / slope);
                } else {
                    startLatLng = new LatLng(j, startLatLng.longitude);
                }
                moveCar(startLatLng, mainActivity);
                moveLine(startLatLng, mainActivity);
                if (flag % 100 == 0) {
                    moveCamera(startLatLng, mainActivity);
                }
                // 如果间隔时间小于1毫秒,则略过当前休眠,累加直到休眠时间到1毫秒:会损失精度
                if (mTimeInterval < 1) {
                    sleep += mTimeInterval;
                    if (sleep >= 1) {
                        Log.v(TAG, "sleep:" + sleep);
                        SystemClock.sleep((long) sleep);
                        sleep = 0;
                    }
                } else {
                    SystemClock.sleep((long) mTimeInterval);
                }

            }
        }
        long endTime = System.currentTimeMillis();
        moveing = false;
        Log.v(TAG, "endTime:" + endTime);
        Log.v(TAG, "run mTimeInterval:" + (endTime - startTime));
        Log.v(TAG, "moveCoarseTrack end.........................................................");
    }

    private void moveLine(LatLng startLatLng, MainActivity mainActivity) {
        mainActivity.mLatLngList.add(startLatLng);// 向轨迹集合增加轨迹点
        mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始
    }

    private void moveCar(LatLng startLatLng, LatLng endLatLng, MainActivity mainActivity) {
        moveCar(startLatLng,mainActivity);
        if (mainActivity.mCarMarker != null) {
            mainActivity.mCarMarker.setRotateAngle((float) TrackMoveUtil.getAngle(startLatLng, endLatLng));// 设置小车车头的方向
        }
    }
    private void moveCar(LatLng startLatLng,MainActivity mainActivity) {
        if (mainActivity.mCarMarker != null) {
            mainActivity.mCarMarker.setPosition(startLatLng);// 小车移动
        }
    }
    private void movePause() {
        try {
            lock.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void moveCamera(LatLng startLatLng, MainActivity mainActivity) {
        Message message = Message.obtain();
        message.what = MainActivity.EventType.MapMove;
        message.obj = startLatLng;
        mainActivity.mMainHandler.sendMessage(message);
    }

}

核心算法工具类

 * <h1>轨迹平滑所需要的工具方法</h1> Date: 2016-10-27 Created by mxdl
 */
public class TrackMoveUtil {
  private static double DISTANCE = 0.0001;

  /**
   * 根据两点算斜率
   */
  public static double getSlope(LatLng fromPoint, LatLng toPoint) {
    if (fromPoint == null || toPoint == null) {
      return 0;
    }
    if (toPoint.longitude == fromPoint.longitude) {
      return Double.MAX_VALUE;
    }
    double slope =
        ((toPoint.latitude - fromPoint.latitude) / (toPoint.longitude - fromPoint.longitude));
    return slope;

  }

  /**
   * 根据两点算取图标转的角度
   */
  public static double getAngle(LatLng fromPoint, LatLng toPoint) {
    if (fromPoint == null || toPoint == null) {
      return 0;
    }
    double slope = getSlope(fromPoint, toPoint);
    if (slope == Double.MAX_VALUE) {
      if (toPoint.latitude > fromPoint.latitude) {
        return 0;
      } else {
        return 180;
      }
    }
    float deltAngle = 0;
    if ((toPoint.latitude - fromPoint.latitude) * slope < 0) {
      deltAngle = 180;
    }
    double radio = Math.atan(slope);
    double angle = 180 * (radio / Math.PI) + deltAngle - 90;
    return angle;
  }

  /**
   * 根据点和斜率算取截距
   */
  public static double getInterception(double slope, LatLng point) {
    if (point == null) {
      return 0;
    }
    return point.latitude - slope * point.longitude;
  }

  /**
   * 计算x方向每次移动的距离
   */
  public static double getXMoveDistance(double slope) {
    if (slope == Double.MAX_VALUE) {
      return DISTANCE;
    }
    return Math.abs((DISTANCE * slope) / Math.sqrt(1 + slope * slope));
  }

  /**
   * 根据轨迹线段计算小车走了多少步
   * 
   * @param latLngList
   * @return
   */
  public static int getStep(List<LatLng> latLngList) {
    int step = 0;
    if (latLngList != null && latLngList.size() > 1) {
      for (int i = 0; i < latLngList.size() - 1; i++) {
        try {
          LatLng startPoint = latLngList.get(i);
          LatLng endPoint = latLngList.get(i + 1);
          double slope = getSlope(startPoint, endPoint);
          // 是不是正向的标示(向上设为正向)
          boolean isReverse = (startPoint.latitude > endPoint.latitude);
          double xMoveDistance = isReverse ? getXMoveDistance(slope) : -1 * getXMoveDistance(slope);
          // 应该对经纬度同时处理
          for (double j = startPoint.latitude; !((j >= endPoint.latitude) ^ isReverse); j =
              j - xMoveDistance) {
            step++;
          }
        } catch (Exception e) {
          e.printStackTrace();
        }
      }

    }
    return step;
  }

  /**
   * 根据总距离和步数计算运动时间
   * 
   * @param distance
   * @param step
   * @return
   */
  public static double getMoveTime(float distance, int step) {
    double timeInterval = 0;
    if (distance > 0) {
      float totalDistance = distance * 1000;
      if (totalDistance <= 500) {
        timeInterval = 1000.0 / step;
      } else if (totalDistance > 500 && totalDistance <= 7500) {
        timeInterval = 2.0 * totalDistance / step;
      } else {
        timeInterval = 15000.0 / step;
      }
    }
    return timeInterval;
  }

  /**
   * 根据轨迹点集合计算总距离
   * 
   * @param latLngList
   * @return
   */
  public static float getDistance(List<LatLng> latLngList) {
    float distance = 0;
    if (latLngList != null && latLngList.size() > 1) {
      for (int i = 0; i < latLngList.size() - 1; i++) {
        try {
          distance += AMapUtils.calculateLineDistance(latLngList.get(i), latLngList.get(i + 1));
        } catch (Exception e) {
          e.printStackTrace();
        }
      }
    }
    return distance;
  }

  // latitude - 地点的纬度,在-90 与90 之间的double 型数值。
  // longitude - 地点的经度,在-180 与180 之间的double 型数值。
  /**
   * 根据一个经纬度字符串求一个经纬度集合a|b|c|d;
   * 
   * @param latlonStr
   * @return
   */
  public static List<LatLng> getListLatLng(String latlonStr) {
    if (!TextUtils.isEmpty(latlonStr)) {
      String[] trackArr = latlonStr.split("\\|");
      if (trackArr != null && trackArr.length > 0) {
        List<LatLng> latLngList = new ArrayList<LatLng>();
        for (int i = 0; i < trackArr.length - 1; i = i + 2) {
          try {
            String lat = trackArr[i + 1];
            String lng = trackArr[i];
            // Logger.v(TAG,"trackArr index:" + i);
            // Logger.v(TAG,"trackArr lat:" + lat);
            // Logger.v(TAG,"trackArr lng:" + lng);
            if (!TextUtils.isEmpty(lat) && !TextUtils.isEmpty(lng)) {
              Double dLat = Double.valueOf(lat);
              Double dLng = Double.valueOf(lng);
              if (dLat >= -90 && dLat <= 90 && dLng >= -180 && dLng <= 180
                  && !(dLat == 0 && dLng == 0)) {
                LatLng latLng = new LatLng(dLat, dLng);
                latLngList.add(latLng);
              }
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
        return latLngList;
      }
    }
    return null;
  }
}

高德API实现的实时运动轨迹

/**
 * Description: <MoveCarCustomThread><br>
 * Author:      mxdl<br>
 * Date:        2019/7/10<br>
 * Version:     V1.0.0<br>
 * Update:     <br>
 */
public class MoveCarSmoothThread implements IMoveCar {
    public static final String TAG = MoveCarSmoothThread.class.getSimpleName();
    private MovingPointOverlay mMovingPointOverlay;
    private WeakReference<MainActivity> mActivityWeakReference;
    private boolean isfirst = true;
    private MOVE_STATE currMoveState = MOVE_STATE.START_STATUS;

    public void setCurrMoveState(MOVE_STATE currMoveState) {
        this.currMoveState = currMoveState;
    }

    public MOVE_STATE getCurrMoveState() {
        return currMoveState;
    }

    public MoveCarSmoothThread(MainActivity activity) {
        mActivityWeakReference = new WeakReference<>(activity);
    }
    @Override
    public void startMove(List<LatLng> latLngs) {
        if (latLngs == null || latLngs.size() == 0) {
            return;
        }

        Log.v("MYTAG","startMove start:"+Thread.currentThread().getName());
        Log.v(TAG, "moveCoarseTrack start.........................................................");
        long startTime = System.currentTimeMillis();
        Log.v(TAG, "startTime:" + startTime);
        final MainActivity mainActivity = mActivityWeakReference.get();
        if (mMovingPointOverlay == null) {
            mMovingPointOverlay = new MovingPointOverlay(mainActivity.mAMap, mainActivity.mCarMarker);
            mMovingPointOverlay.setTotalDuration(5);
            mMovingPointOverlay.setMoveListener(new MovingPointOverlay.MoveListener() {
                @Override
                public void move(double v) {
                    if(isfirst){
                        isfirst = false;
                        Log.v("MYTAG","MoveCarSmoolthThread move start:"+Thread.currentThread().getName());
                    }

                    LatLng position = mMovingPointOverlay.getPosition();
                    mainActivity.mLatLngList.add(position);// 向轨迹集合增加轨迹点
                    mainActivity.mMovePolyline.setPoints(mainActivity.mLatLngList);// 轨迹画线开始

                    Message message = Message.obtain();
                    message.what = MainActivity.EventType.MapMove;
                    message.obj = position;
                    message.arg1 = (int)v;
                    mainActivity.mMainHandler.sendMessage(message);
                }
            });
        }
        mMovingPointOverlay.setPoints(latLngs);
        mMovingPointOverlay.startSmoothMove();
        long endTime = System.currentTimeMillis();
        Log.v(TAG, "endTime:" + endTime);
        Log.v(TAG, "moveCoarseTrack end.........................................................");
    }

    @Override
    public void reStartMove() {
        if(mMovingPointOverlay != null){
            mMovingPointOverlay.startSmoothMove();
        }
    }
    @Override
    public void pauseMove(){
        if(mMovingPointOverlay != null){
            mMovingPointOverlay.stopMove();
        }
    }
    @Override
    public void stopMove(){
        if(mMovingPointOverlay != null){
            mMovingPointOverlay.destroy();
            mMovingPointOverlay = null;
        }
        if(mActivityWeakReference.get() != null){
            mActivityWeakReference.get().mLatLngList.clear();
        }
    }

}

最后我把整个项目的的完整代码传到GitHub上了https://github.com/geduo83/android-amap-movecar

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

推荐阅读更多精彩内容