Android 原生 Picture in Picture 画中画功能避坑指南

最近生活有些变动所以断更好久,不过虽迟到但永远不会缺席。ChatGPT 浪潮还在持续扩大,各位同学一定要体验体验丫~

这篇主要介绍最近需求中遇到的问题,希望能帮助后来者少踩坑。先说结论:Android 原生画中画功能并不完善,如果可以接受 APP 有两个任务栈则可以使用;否则趁早自己用浮窗自定义实现画中画的功能吧。

1. PiP 简介

Android PiP 模式也称之为画中画模式,允许用户在使用应用程序的同时,在屏幕的一角或一侧浮动显示另一个应用程序或视频。这使得用户可以同时进行多项任务,而不必切换应用程序或中断正在进行的任务。如下所示:

图 1 PiP示例

(注:B站的 PiP 是自定义实现的,未使用系统 PiP)

2. 准备工作,跑通 Demo

官方文档:https://developer.android.google.cn/guide/topics/ui/picture-in-picture?hl=zh-cn
官方Demo:https://github.com/android/media-samples/tree/main/PictureInPictureKotlin

打开官方 Demo,首先得改一下 minSdkVersion,demo 里设置的是 API 31(Android 12.0),不满足实际应用需求,这里改为 23(Android 6.0). 但 PiP 功能只能在 Android8.0 及以上的系统上使用,所以用到一些方法时,需要注明 @RequiresApi(Build.VERSION_CODES.O)。所以,如果需要在 Android 8.0 以下的设备支持 PiP,只能使用自定义悬浮窗实现

还需要注释掉 setAutoEnterEnabled(true)setSeamlessResizeEnabled(false) 这两个方法。因为它们只能在 Android 12.0 及以上系统使用,且对于 PiP 的主体功能没有影响。setAutoEnterEnabled 用于设置 Activity 在退到后台时是否自动进入 PiP 模式,当设置为 true,则在用户点击 Home 键回到主屏幕时,Activity 可自动进入 PiP 模式,而不用开发者手动调用 enterPictureInPictureMode 方法;setSeamlessResizeEnabled 用于设置非视频画中画时的动画效果,不影响功能。

按照上述的内容设置完后就可以将 Demo 跑通了。

3. 示例代码分析

仅分析查看了 Demo 中的 MovieActivity 中的 PiP 相关的代码。比较重要的代码如下:

// code 1
    @RequiresApi(Build.VERSION_CODES.O)
    private fun minimize() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

调用 enterPictureInPictureMode(@NonNull PictureInPictureParams params) 方法就可以进入 PiP,声明如下:

// code 2
    public boolean enterPictureInPictureMode(@NonNull PictureInPictureParams params) { 
        ···
    }

方法简介:它是 Activity 类中的方法,需要传递一个 PictureInPictureParams 类型对象。当系统成功将该 Activity 切换到 PiP 模式或已经处于 PiP,则返回值为 true;如果设备不支持 PiP 则返回 false。

再来看下构建 PictureInPictureParams 类型对象的 updatePictureInPictureParams() 方法:

// code 3
@RequiresApi(Build.VERSION_CODES.O)
    private fun updatePictureInPictureParams(): PictureInPictureParams {
        // 1、计算出 PiP 小窗的宽高比,这里直接使用播放视频的控件宽和高计算
        val aspectRatio = Rational(binding.movie.width, binding.movie.height)
        // 2、将播放视频的控件binding.movie设置为 PiP 中要展示的部分
        val visibleRect = Rect()
        binding.movie.getGlobalVisibleRect(visibleRect)
        val params = PictureInPictureParams.Builder()
            .setAspectRatio(aspectRatio)
            // 3、指定进入画中画的屏幕部分。系统根据这个可实现平滑动画效果。这里就把之前生成的 visibleRect 传值过去
            .setSourceRectHint(visibleRect)
            .build()
        setPictureInPictureParams(params)
        return params
    }

updatePictureInPictureParams 方法作用是构建出进入 PiP 的一些参数,比如进入小窗的控件,小窗的宽高比等。注释很清楚,源码直接拿来套用就行。需要注意的点:只能指定 PiP 模式的宽高比,并不能直接设置宽和高的具体值,系统会根据设置的宽高比自己计算具体值。

如果在播放器控件上层有其他的操作按钮等,还需要在 onPictureInPictureModeChanged 回调中进行处理,即进入 PiP 后隐藏这些按钮;退出后恢复这些按钮的状态。 如下是 Demo 中的实现:

// code 4
    override fun onPictureInPictureModeChanged(
        isInPictureInPictureMode: Boolean, newConfig: Configuration
    ) {
        super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
        if (isInPictureInPictureMode) {
            // Hide the controls in picture-in-picture mode.
            binding.movie.hideControls()
        } else {
            // Show the video controls if the video is not playing
            if (!binding.movie.isPlaying) {
                binding.movie.showControls()
            }
        }
    }

通过这个方法可以监听 PiP 的进入和退出。

还有一些是 PiP 模式下的播放/暂停、上一个/下一个 操作按钮,即下图红框中的这三个按钮,相关的使用方式 Demo 中已有示例,这里不再赘述。

图 1 PiP 按钮

除此之外,还要在需要进入 PiP 的 Activity 的 AndroidManifest 中设置支持 PiP 的属性以及处理布局配置更改。这样一来,如果在 PiP 模式转换期间出现布局更改,该 Activity 就不会重新启动。

// code 5
<activity android:name="VideoActivity"
    android:supportsPictureInPicture="true"
    android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
    ...

这些官方文档中就有,这里也不再多说。

4. 功能实现及踩坑汇总

4.1 实现点击 Back 键及 Home 键自动进入 PiP

用户在观看视频时,点击返回键或 Home 键,当前 Activity 需要进入 PiP 继续播放,这是个常见的功能,实现起来也比较简单:

// code 6
    // 实现点击返回键进入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onBackPressed() {
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

    // 实现点击 Home 键进入 PiP
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onUserLeaveHint() {
        super.onUserLeaveHint()
        enterPictureInPictureMode(updatePictureInPictureParams())
    }

如果设置了之前提到的 setAutoEnterEnabled(true) 方法,则可以不用在 onUserLeaveHint() 回调里主动调用 enterPictureInPictureMode 方法进入 PiP。但建议还是不用 setAutoEnterEnabled,因为它只能在 Android 12 上使用。。。

onUserLeaveHint() 方法也是 Activity 中的方法,当 Activity 进入后台时就会调用它,比如用户点击 Home 键就会回调它。但有来电时,来电的 Activity 会自动带到前台,这时被退到后台的 Activity 的 onUserLeaveHint 方法并不会被调用。onUserLeaveHint 的调用时机是在 onPause 方法之前,这点需要注意。

4.2 实现 Activity 处于 PiP 时再次进入更新视频

假设 MovieActivity 已处于 PiP 并正在播放视频,用户点击另外一个视频又要跳转到 MovieActivity 的情形。如果不进行处理就会出现有两个 MovieActivity 同时播放视频的情况,即小窗播放的同时,还有一个另一个 MovieActivity 也在播放。如下所示,本来只有一个 PiP 在播放视频,然后点击 WATCH VIDEO TWO 按钮又进入了 MovieActivity,此时有两个视频同时在播放:

图 2

查看堆栈信息确实有两个 MovieActivity:

图 3

这种情况下是需要将 MovieActivity 由 PiP 恢复到正常状态并播放新的视频,如果视频内容没有变则接着播放原视频。官方 Demo 也有说明如何处理,需要两个步骤:
1)将 MovieActivity 的 launchMode 设置为 singleTask
2)在 MovieActivity 的 onNewIntent 方法里处理更新数据等逻辑;

比如我在打开 MovieActivity 时通过 Intent 传递不同的 video 来播放不同的视频,那么在 onNewIntent 中就需要接收传递的参数并更新:

// code 7
// MainActivity.kt    通过 Intent 传入不同的视频
        binding.btnWatchVid1.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
            startActivity(intent)
        }
        binding.btnWatchVid2.setOnClickListener {
            val intent = Intent(this, MovieActivity::class.java)
            intent.putExtra(MovieActivity.KEY_VIDEO_ID, R.raw.vid_dajiang)
            startActivity(intent)
        }
// code 8
// MovieActivity.kt    onNewIntent 接收并更新
    override fun onNewIntent(intent: Intent?) {
        super.onNewIntent(intent)
        val newVideoId = intent?.getIntExtra(KEY_VIDEO_ID, R.raw.vid_bigbuckbunny)
        newVideoId?.let {
            // 更新视频
            binding.movie.setVideoResourceId(it)
        }
    }

在实际中可能更加复杂,但大体思路是一致的。

4.3 实现跳转其他 Activity 时,当前 Activity 自动进入 PiP

场景:正在 MovieActivity 里播放视频,用户点击某个按钮跳转到其他 Activity,MovieActivity 此时需进入 PiP,用户可以在新打开的 Activity 页面进行操作。

官方文档里并没有针对这一场景进行说明和提示,所以一开始以为很简单,直接跟之前一样调用 enterPictureInPictureMode(updatePictureInPictureParams()) 不就可以了么?于是就有了下面的代码:

// code 9
        binding.btnJumpTestOne.setOnClickListener {
            enterPictureInPictureMode(updatePictureInPictureParams())
            startActivity(Intent(this@MovieActivity, TestOneActivity::class.java))
        }

想着先将当前的 MovieActivity 进入 PiP,再跳转到其他的 Activity,结果 MovieActivity 直接退出了,也没有错误信息。看栈信息发现其实要跳转的新 Activity —— TestOneActivity 已经打开了。。。

打开失败的动图:

图 4 跳转失败示例1

在 MovieActivity 中点击 JUMP TO TESTONEACTIVITY 按钮跳转之后,堆栈信息如下,可以看到 pid = 21126 的进程就是 Demo 程序,TestOneActivity 确实打开了,MovieActivity 已退出:

图 5

加延时再试:

// code 10
        binding.btnJumpTestOne.setOnClickListener {
            lifecycleScope.launch {
                enterPictureInPictureMode(updatePictureInPictureParams())
                delay(1000)
                startActivity(Intent(this@MovieActivity, TestOneActivity::class.java))
            }
        }

确实进入 PiP 了,但后面跳转的 TestOneActivity 也在 PiP 了。。

图 6 跳转失败示例2

如果是先跳转 TestOneActivity 再进入 PiP ,经测试只会跳转并不会进入 PiP,这里就不再展示了。

经分析和实践发现,只能先进入 PiP 再进行跳转,之所以会出现在 PiP 里跳转,是因为后面跳转的 TestOneActivity 进入了 MovieActivity 所在的任务栈。Activity 在没有设置 taskAffinity 属性时,都会放在默认的同一个任务栈中。

所以想到的第一个方法就是,修改 MovieActivity 的 launchMode,改为 singleInstance。这样既可以保证任务栈中只有一个 MovieActivity 的实例,也可以将 MovieActivity 放在独立的任务栈中。试了下果然可以了,但会在多任务切换页里出现同一个 App 有两个任务栈的现象:

图7 一个 App 出现多个任务栈

这是第一个问题,这个问题直到最后也无法解决,在 AndroidManifest 文件中添加 autoRemoveFromRecentsexcludeFromRecents 都没用,还是会在多任务切换页出现两个栈。

还有一个问题即问题二,还是 singleInstance 引起的。当 MovieActivity 正在以非小窗模式播放视频时,先进入多任务切换页,再按 Home 键回到主屏幕,然后再点击 App 图标进入时,发现进入的不是 MovieActivity,而是 MovieActivity 的上个页面,即 MainActivity,此时再进入多任务切换页面,会发现 MovieActivity 所在那个任务栈已经消失了。这里其实有两个问题:
1)回到主屏幕后再点击 App 图标应该回到 MovieActivity;
2)用户并没有关闭 MovieActivity,但进入多任务切换页面后无法找到 MovieActivity 了。如下动图:

图 8 问题二

问题二的两个问题得先解决 2)才能解决 1)。2)之所以会出现是因为一个 App 出现了两个任务栈,这两个任务栈的 taskAffinity参数默认是一样的,一山不容二虎,那么点击桌面图标后,就会把之前的任务栈移到前台,然后会把另一个任务栈干掉。
所以首先要保留这两个任务栈,给 MovieActivity 设置一个单独的 taskAffinity名称,这就可以得以保留,问题 2)就解决了。只有先保留任务栈,才能解决问题 1)。

导致问题1)的原因是因为用户在点击 App 图标时,会将 MainActivity 所在的栈移到前台,那么首先可以想到的方法是,在点击 App 图标时,将含有 MovieActivity 的栈移到前台显示。所以我们可以注册一个生命周期监听,在 onResume 时,去遍历 App 的所有任务栈,找到含有 MovieActivity 的栈并将其移到前台即可:

// code 11   DemoApplication.kt  onCreate方法中
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks{
    ···
    override fun onActivityResumed(activity: Activity) {
        val appCompatActivity = if (activity is AppCompatActivity) {
            activity
        } else {
            return
        }
        // 限制条件:所有的 activity 必须为 AppCompatActivity 或其子类
        val activityManager = appCompatActivity
            .getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        for (i in activityManager.appTasks.indices) {
            val appTask = activityManager.appTasks[i]
            val taskInfo = appTask.taskInfo
            if (taskInfo.topActivity == null) continue
            val topActivityName = taskInfo.topActivity?.className
            if ((!topActivityName.isNullOrBlank() && topActivityName.contains(
                    MovieActivity::class.java.simpleName
                )) && i != 0
            ) {
                // 如果存在视频播放页且所在的 Task 不在前台,则需要将其移到前台
                activityManager.moveTaskToFront(
                    taskInfo.id,
                    ActivityManager.MOVE_TASK_NO_USER_ACTION
                )
            }
        }
    }
    ···
})

很明显这个方法并不好,App 中每个 Activity 在调用 onResume 时都会走一遍这个逻辑;且 App 中所有的 Activity 必须为 AppCompatActivity 或它的子类。还得需要申请 REORDER_TASKS 权限:

// code 12   AndroidManifest.xml
<!--  申请可排序任务栈权限  -->
<uses-permission android:name="android.permission.REORDER_TASKS" />

并且这里还遇到一个问题:当在 MovieActivity 跳转到 TestOneActivity 时,进入 PiP,此时点击 PiP 中的关闭按钮关闭 PiP,然后点击 Home 回到桌面,再点击 App 图标会发现进入的是 MovieActivity 页,而并不是 TestOneActivity:

图 9 点击App进入页面不对的问题

经分析,原因是 PiP 的关闭按钮点击后,只是将 MovieActivity 退到了后台,并没有销毁。。。所以退到后台,再点击 App 图标时,会将包含 MovieActivity 的任务栈显示到前台,而不显示 TestOneActivity 所在的任务栈。那么我们就需要在关闭 PiP 按钮的回调中直接关闭 Activity,但我们开发者拿不到关闭按钮的回调,所以就有了下面的问题:

如何在用户点击 PiP 里的关闭按钮时,关闭 PiP 所在的 Activity?
经多次实验得知,PiP 虽然没有关闭小窗的回调,但会先调用 onStop 然后会调用 onPictureInPictureModeChanged 方法。所以可以根据是否回调了 onStop 来间接判断是否点击了 PiP 小窗里的关闭按钮。

// code 13
// MovieActivity 的 ViewModel
class MovieViewModel: ViewModel() {
    //进入或退出画中画模式所在Activity的事件 true: 进入; false: 退出
    val enterOrExitPiPMode = MutableLiveData<Boolean>()
}

// MovieActivity.kt
@RequiresApi(Build.VERSION_CODES.O)
override fun onPictureInPictureModeChanged(
    isInPictureInPictureMode: Boolean, newConfig: Configuration
) {
    super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
    if (isInPictureInPictureMode) {
        ···
    } else {
        ···
        // PiP没有关闭小窗的回调,但会先回调 onStop 然后回调 onPictureInPictureModeChanged 方法。可以根据
        // 是否回调了 onStop 来间接判断是否点击了PiP小窗里的关闭按钮,这里需要在用户主动关闭小窗后 finish 掉
        // MovieActivity
        if (lifecycle.currentState < Lifecycle.State.STARTED) {
            movieViewModel.enterOrExitPiPMode.value = false
        }
    }
}

// MovieActivity.kt
// 进入 or 退出画中画模式
movieViewModel.enterOrExitPiPMode.observe(this) {
    if (it) {
        // 这里暂没有操作
    } else {
        // 关闭画中画模式事件,需要直接 finish 掉 MovieActivity
        finish()
    }
}

这里使用 LiveData 是因为在其他的页面可能也需要关闭 PiP,所以可以先获得 ViewModel,通过更新 enterOrExitPiPMode 的值去关闭 PiP。

4.4 去掉 PiP 下方自带的三个按钮

PiP 小窗上的 6个按钮只有底部的三个按钮可自定义,另外的三个按钮无法修改,这也是为什么无法拿到关闭按钮回调的原因。如果需要针对底部的三个按钮进行自定义,通过设置 PictureInPictureParams 参数实现,但最多只能自定义 3个,我们这里不需要这三个按钮,就可以设置一个透明按钮间接去掉:

// code 14
// 第一步:新建一个 RemoteAction list
@RequiresApi(Build.VERSION_CODES.O)
private fun initPiPActions(): List<RemoteAction> {
    //去掉原生小窗中默认自带的 上一个、暂停、下一个 三个按钮
    val actions = mutableListOf<RemoteAction>()
    val emptyIntent = PendingIntent.getBroadcast(requireContext(), 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
    actions.add(RemoteAction(Icon.createWithResource(requireContext(), R.drawable.divider_transparent), "", "", emptyIntent))
    return actions
}

// 第二步:设置到 PictureInPictureParams 参数中
val params = PictureInPictureParams.Builder()
    .setAspectRatio(aspectRatio)
    // Specify the portion of the screen that turns into the picture-in-picture mode.
    // This makes the transition animation smoother.
    .setSourceRectHint(visibleRect)
    .setActions(initPiPActions())
    .build()

自定义底部三个按钮的方法有两种:一是通过实现 RemoteAction 的方法;二是官方 Demo 中的方法。关于这个内容参考文献2 更加详实,可以借鉴。

5. 难以解决的问题

以上的坑基本趟完了,但下面的坑实在是难以解决,这里也欢迎大佬们能给出建议。

5.1 App 出现两个任务栈

为了实现从 MovieActivity 跳转到其他 Activity 时,MovieActivity 自身进入 PiP,必须将 MovieActivity 放到独立的任务栈中,所以就会出现这个问题。以上文中也有说明。

5.2 PiP 模式下跳转一个 singleTask 的 Activity 会在 PiP 中跳转

官方 Demo 中将 MovieActivity 的 launchMode 设置为 singleTask 且不设置 taskAffinity 时,当 MovieActivity 正处于 PiP 模式下,跳转到另一个 Activity 时,目标 Activity 的 launchMode 不能为 singleTask,否则目标 Activity 会在 PiP 中跳转。

图10

这里将 TestOneActivity 的 launchMode 设置为 singleTask,然后从 MovieActivity 跳到 TestOneActivity 时,TestOneActivity 出现在了 PiP 中。而通常项目中会有许多 Activity 的 launchMode 设置为了 singleTask,所以原生 PiP 方案最终被否。。。
github 上也有 issue:https://github.com/android/media-samples/issues/85

6. 小知识点汇总

6.1 ActivityManager.MOVE_TASK_NO_USER_ACTION 的作用

常用于 activityManager.moveTaskToFront 方法中,意思是不把当前的操作看作是用户触发的行为,即不会调用当前 Activity 的 onUserLeaveHint 方法。还有一个是 ActivityManager.MOVE_TASK_WITH_HOME ,这个就会调用当前 Activity 的 onUserLeaveHint 方法。实际应用中貌似很少用到。

6.2 autoRemoveFromRecents 和 excludeFromRecents 的用法

6.2.1 android:autoRemoveFromRecents 用法

android:autoRemoveFromRecents 是在任务栈中的最后一个 Activity 完成之前,由具有此属性的 Activity 启动的任务栈是否保留在多任务切换页面中。即 autoRemoveFromRecents 指定了当 Activity 被系统回收时,是否保留在多任务切换页面中。默认值为 false。

当设置为 true 时: 当 Activity 被系统回收时,从最近使用的多任务切换页中移除该 Activity 所在的任务栈;当设置为 false 时: 当 Activity 被系统回收时,不从最近使用的多任务切换页中移除该 Activity 所在的任务栈。

这个属性主要用于:
1)一些临时 Activity,当它们被销毁后,不希望它们出现在多任务切换页中,可以设置为 true;
2)一些没有重要数据的 Activity,如果设置为 true,当内存不足被系统回收后,由于它已经从多任务切换页移除,用户不太可能再去恢复它,及时移除有利于内存回收;
3)一些包含敏感数据的 Activity,为了安全考虑,不希望它出现在多任务切换页中,可以设置为 true。
所以,总体来说,这个属性主要是出于内存管理和安全考虑,控制 Activity 在被系统回收后是否从多任务切换页中移除。

6.2.2 android:excludeFromRecents 用法

android:excludeFromRecents 也是一个 Activity 属性,它指定了是否从多任务切换页中排除该 Activity 所在的任务栈。默认值为 false。

当设置为 true 时:该 Activity 所在的任务栈不会出现在多任务切换页中;当设置为 false 时:不从多任务切换页中排除该 Activity 所在的任务栈。

这个属性与 android:autoRemoveFromRecents 很像,它们的区别是:
android:autoRemoveFromRecents 是当 Activity 被系统回收时,所在栈是否从多任务切换页中移除;
android:excludeFromRecents 是 Activity 所在的栈从一开始就不会出现在任务列表中。

更多内容,欢迎关注公众号:修之竹
或者查看 修之竹的 Android 专辑

赞人玫瑰,手留余香!欢迎点赞、转发~ 转发请注明出处~

参考文献

  1. 官方文档:https://developer.android.google.cn/guide/topics/ui/picture-in-picture?hl=zh-cn
  2. 总结系列-Android画中画模式-看这篇就够啦; ZhangQiang-; https://blog.csdn.net/u011200604/article/details/104701266
  3. Android 面试黑洞——当我按下 Home 键再切回来,会发生什么?; 扔物线朱凯; https://www.bilibili.com/video/BV1CA41177Se/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容