安卓两个页面组件的无缝衔接part2(移动View)

上篇文章安卓两个页面组件的无缝衔接part1(共享元素)
介绍了如何使用安卓系统提供的共享元素来实现多个页面之间元素的无缝衔接,本文讨论第二个方案:利用属性动画移动一个全局View,先看效果

2-1.gif

该方案的原理是利用ViewAttr类记录第一个页面和第二个页面的需要衔接的View的属性(x、y、width、height),然后在在页面切换时对View执行属性动画。这个方案的效果和上篇文章的效果差不多,页面之间View元素之间的衔接很丝滑,播放视频没有卡帧、跳帧现象,而且音频也没有被中断

一 第一个页面MainAct

MainAct.kt

class MainAct : AppCompatActivity() {
    lateinit var bind: ActivityMain2Binding
    var playPaused: Boolean = false
    var clickGotoPage2 = false
    lateinit var playViewLp: ViewGroup.LayoutParams
    var attr: ViewAttr? = null
    var mPlayer: SysMediaPlayer = SysMediaPlayer.getInstance()
    var mSurface: Surface? = null
    var seamlessCb = object : SeamlessObserver.Callback {
        override fun onEvent(type: Int, currentAttr: ViewAttr?) {
            Log.w("zzh", "on event on mainact type=${type}")
            if (type == 2) {
                // var target = TextureView(this@MainAct)
                // ViewMgr.getInstance().target = target
                var target = ViewMgr.getInstance().target
                val parent: ViewParent? = target.getParent()
                if (parent != null) {
                    (parent as ViewGroup).removeView(target)
                }
                // initTextureListener(target as? TextureView)
                bind.root.addView(target, playViewLp)
                Log.w("zzh", "back to main act currentAttr=${currentAttr}")
                Log.w("zzh", "back to main act attr=${attr}")
            }
            clickGotoPage2 = false
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bind = ActivityMain2Binding.inflate(layoutInflater)
        supportActionBar?.title = "MainActInSeamless"
        setContentView(bind.root)
        SeamlessObserver.getInstance().register(seamlessCb)
        playViewLp = bind.playView.layoutParams

        bind.btnGotoSingleSeamlessAct.setOnClickListener { v -> onClickGotoSingleSeamless(v) }

        initTextureListener(bind.playView)
    }

    private fun onClickGotoSingleSeamless(v: View) {
        clickGotoPage2 = true
        ViewMgr.getInstance().target = bind.playView // important
        val intent = Intent(this@MainAct, SingleSeamlessListAct::class.java)
        if (attr == null) {
            attr = ViewAttr()
            val location = IntArray(2)
            bind.playView.getLocationInWindow(location)
            // attr?.setX(location[0])
            // attr?.setY(location[1])
            attr?.setX(bind.playView.x.toInt())
            attr?.setY(bind.playView.y.toInt())
            attr?.setWidth(bind.playView.getMeasuredWidth())
            attr?.setHeight(bind.playView.getMeasuredHeight())
        }
        Log.w("zzh", "goto single seamless act attr=${attr}")
        intent.putExtra("attr", attr)
        startActivity(intent)
        overridePendingTransition(0, 0)
    }

    private fun initTextureListener(textureView: TextureView?) {
        textureView?.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
            override fun onSurfaceTextureAvailable(surface: SurfaceTexture, w: Int, h: Int) {
                Log.w("zzh", "surface log available in main act w=$w h=$h thread=${Thread.currentThread()}")
                if (mSurface == null) {
                    mPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
                    mPlayer!!.setDataSource("https://hw-v.cztv.com/cztv/vod/2023/11/11/0b1098f4e2dc7e4ba4e8a0ceb39ccadf/0b1098f4e2dc7e4ba4e8a0ceb39ccadf_h264_800k_mp4.mp4_playlist.m3u8")
                    mPlayer!!.mInternalMediaPlayer.setOnPreparedListener(object : MediaPlayer.OnPreparedListener {
                        override fun onPrepared(mp: MediaPlayer?) {
                            mPlayer!!.start()
                        }
                    })
                    mPlayer!!.mInternalMediaPlayer.prepare()
                }
                mSurface = Surface(surface)
                mPlayer!!.setSurface(mSurface)
            }

            override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, w: Int, h: Int) {
                Log.w("zzh", "surface log size changed in main activity w=$w h=$h")
            }

            override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
                Log.w("zzh", "surface log destroy in main activity")
                return false
            }

            override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {
            }
        }
    }

    override fun onPause() {
        super.onPause()
        if (!clickGotoPage2) {
            mPlayer.pause()
            playPaused = true
        }
    }

    override fun onResume() {
        if (playPaused) {
            mPlayer.start()
        }
        clickGotoPage2 = false
        super.onResume()
    }

    override fun onDestroy() {
        SeamlessObserver.getInstance().unregister(seamlessCb)
        super.onDestroy()
    }
}

activity_main2.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".singleseamless.MainAct">
    <TextureView
        android:id="@+id/playView"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_width="match_parent"
        android:layout_height="160dp" />

    <Button
        android:id="@+id/btnGotoSingleSeamlessAct"
        android:layout_marginTop="20dp"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="GotoSingleSeamlessAct"
        app:layout_constraintTop_toBottomOf="@id/playView"
        app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

在函数onClickGotoSingleSeamless里,我们记录了要移动的View的属性数据(x、y、width、height)存储到ViewAttr并将数据通过Intent传递给下个页面,在下个页面进行属性动画的执行。

二 第二个页面SingleSeamlessListAct

SingleSeamlessListAct.kt

class SingleSeamlessListAct : AppCompatActivity() {
    lateinit var bind: ActivitySingleSeamlessListBinding
    lateinit var mAdapter: SingleAdapter
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        bind = ActivitySingleSeamlessListBinding.inflate(layoutInflater)
        supportActionBar?.title = "SingleSeamlessListAct"
        setContentView(bind.root)

        var lm = LinearLayoutManager(this)
        lm.orientation = RecyclerView.VERTICAL
        bind.recycler.layoutManager = lm

        var attr: ViewAttr? = intent.getParcelableExtra("attr")
        mAdapter = SingleAdapter(this)
        mAdapter.setAttr(attr)
        mAdapter.setRoot(bind.root)
        bind.recycler.adapter = mAdapter
    }

    override fun onBackPressed() {
        SeamlessObserver.getInstance().execute(2, mAdapter.getCurAttr())
        ViewMoveHelper(ViewMgr.getInstance().target, mAdapter.getCurAttr(), mAdapter.getAttr(), ViewMgr.DURATION, object :
            ViewMoveHelper.EndCallback {
            override fun onEnd() {
            }
        }).startAnim()
        super.onBackPressed()
        overridePendingTransition(0, 0)
    }
}

activity_single_seamless_list.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SecondListAct">
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recycler"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

SingleAdapter.java

class SingleAdapter : RecyclerView.Adapter<SingleAdapter.ThisVH> {
    private val mDatas: ArrayList<String> = ArrayList<String>()
    private var mSelected = -1
    private var mLayoutId = -1
    private var currentAttr: ViewAttr? = null
    private var attr: ViewAttr? = null
    private var root: ViewGroup? = null
    var mPlayer: SysMediaPlayer = SysMediaPlayer.getInstance()
    var mSurface: Surface? = null

    constructor(ctx: Context, defaultIndex: Int = 2, layoutId: Int = R.layout.list_item_single) {
        for (i in 0..6) {
            mDatas.add("index:$i")
        }
        mSelected = defaultIndex
        mLayoutId = layoutId
    }

    public fun setRoot(vg: ViewGroup) {
        root = vg
    }

    public fun setAttr(a: ViewAttr?) {
        attr = a
    }

    public fun getAttr(): ViewAttr? {
        return attr
    }

    public fun getCurAttr(): ViewAttr? {
        return currentAttr
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ThisVH {
        val view: View =
            LayoutInflater.from(parent.context).inflate(mLayoutId, parent, false)
        return ThisVH(parent, view)
    }

    override fun getItemCount(): Int {
        return mDatas.size
    }

    override fun onBindViewHolder(holder: ThisVH, position: Int) {
        holder.bind(position)
    }

    inner class ThisVH(parent: ViewGroup, itemView: View) : RecyclerView.ViewHolder(itemView) {
        var container: FrameLayout

        init {
            container = itemView.findViewById(R.id.container)
        }

        fun bind(pos: Int) {
            Log.w("zzh", "bind view holder pos=$pos holder=$this")
            if (mSelected == pos) {
                container.viewTreeObserver.addOnPreDrawListener(object :
                    ViewTreeObserver.OnPreDrawListener {
                    override fun onPreDraw(): Boolean {
                        container.viewTreeObserver.removeOnPreDrawListener(this)
                        var target = ViewMgr.getInstance().target
                        val parent: ViewParent? = target.getParent()
                        if (parent != null) {
                            (parent as ViewGroup).removeView(target)
                        }
                        root!!.addView(target, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
                        currentAttr = ViewAttr()
                        val location = IntArray(2)
                        container.getLocationInWindow(location)
                        currentAttr!!.setX(location[0])
                        // currentAttr!!.setY(location[1])
                        currentAttr!!.setY(itemView.y.toInt())
                        currentAttr!!.setWidth(container.measuredWidth)
                        currentAttr!!.setHeight(container.measuredHeight)
                        Log.w("zzh", "on pre draw attr0=${attr}")
                        Log.w("zzh", "on pre draw attr1=${currentAttr} newparent=${target.parent}")
                        ViewMoveHelper(target, attr, currentAttr, ViewMgr.DURATION, object : EndCallback {
                            override fun onEnd() {
                                Log.w("zzh", "on pre draw attr2=${currentAttr} newparent=${target.parent}")
                            }
                        }).startAnim()
                        // val animation = AlphaAnimation(0f, 1f)
                        // animation.duration = ViewMgr.DURATION
                        // llContent.setAnimation(animation)
                        // animation.start()
                        return true
                    }
                })
            } else {
            }

        }
    }
}

list_item_single.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/teal_700"
    android:layout_marginBottom="5dp">
    <FrameLayout
        android:id="@+id/container"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        android:layout_width="match_parent"
        android:layout_height="160dp"/>
</androidx.constraintlayout.widget.ConstraintLayout>

在SingleAdapter的onBindViewHolder中,对View执行属性动画,开始的属性是View在在第一个页面的属性,结束的属性是在第二个页面即当前页面的属性,两个页面共用一个播放器实例,保证视频和音频的流畅播放和丝滑衔接。

三 其余辅助类源码

ViewMgr.java

public class ViewMgr {
    public static final long DURATION = 250;
    private View mTarget;
    private ViewMgr() {
    }

    private static class Holder {
        private static ViewMgr INSTANCE = new ViewMgr();
    }

    public static ViewMgr getInstance() {
        return Holder.INSTANCE;
    }

    public View getTarget() {
        return mTarget;
    }

    public void setTarget(View v) {
        mTarget = v;
    }
}

SysMediaPlayer.java

public class SysMediaPlayer {
    public MediaPlayer mInternalMediaPlayer;

    private enum PlayerState {
        STATE_ERROR,
        STATE_IDLE,
        STATE_INITIALIZED,
        STATE_PREPARING,
        STATE_PREPARED,
        STATE_STARTED,
        STATE_PAUSED,
        STATE_STOPPED,
        STATE_COMPLETE,
    }
    private PlayerState mPlayerState;

    public SysMediaPlayer(Context context, int resId) {
        mInternalMediaPlayer = MediaPlayer.create(context, resId);
    }

    private SysMediaPlayer() {
        mInternalMediaPlayer = new MediaPlayer();
    }

    private static class Holder {
        private static SysMediaPlayer INSTANCE = new SysMediaPlayer();
    }
    public static SysMediaPlayer getInstance() {
        return Holder.INSTANCE;
    }

    public void setDataSource(String s) {
        try {
            Map<String, String> headers = new HashMap<String, String>();
            mInternalMediaPlayer.setDataSource(s);
        } catch (Exception e) {
            Log.e("zzh", "set data source error", e);
        }
    }

    public void setDataSource(Context context, Uri s) {
        try {
            mInternalMediaPlayer.setDataSource(context, s);
        } catch (Exception e) {
            Log.e("zzh", "set data source2 error", e);
        }
    }

    public void setAudioStreamType(int type) {
        mInternalMediaPlayer.setAudioStreamType(type);
    }

    public void setSurface(Surface surface) {
        mInternalMediaPlayer.setSurface(surface);
    }

    public void prepareAsync() throws IllegalStateException {
        if (mPlayerState == PlayerState.STATE_INITIALIZED || mPlayerState == PlayerState.STATE_STOPPED) {
            mInternalMediaPlayer.prepareAsync();
            mPlayerState = PlayerState.STATE_PREPARING;
        }
    }

    public void prepare() {
        try {
            mInternalMediaPlayer.prepare();
            mPlayerState = PlayerState.STATE_PREPARED;
        } catch (IOException e) {
            Log.e("zzh", "prepare error", e);
        }
    }

    public void start() throws IllegalStateException {
        // if (mPlayerState == PlayerState.STATE_PREPARED || mPlayerState == PlayerState.STATE_COMPLETE || mPlayerState == PlayerState.STATE_PAUSED) {
        //     // 必须在onPrepared后设置,如果onPrepared时设置相当于调用start/pause,所以移到start接口
        //     // realSetSpeed();
        //     mInternalMediaPlayer.start();
        // }
        mInternalMediaPlayer.start();
        mPlayerState = PlayerState.STATE_STARTED;
    }

    public void pause() throws IllegalStateException {
        if (mPlayerState == PlayerState.STATE_STARTED) {
        }
        mPlayerState = PlayerState.STATE_PAUSED;
        mInternalMediaPlayer.pause();
    }

    public void stop() throws IllegalStateException {
        if (mPlayerState == PlayerState.STATE_STARTED ||
                mPlayerState == PlayerState.STATE_PREPARED ||
                mPlayerState == PlayerState.STATE_PAUSED ||
                mPlayerState == PlayerState.STATE_COMPLETE) {
            mInternalMediaPlayer.stop();
            mPlayerState = PlayerState.STATE_STOPPED;
        }
    }

    public void release() {
        mInternalMediaPlayer.release();
    }
}

SeamlessObserver.java

public class SeamlessObserver {
    private List<Callback> callbacks = new ArrayList<>();
    private SeamlessObserver(){}

    private static class Holder {
        private static SeamlessObserver INSTANCE = new SeamlessObserver();
    }

    public static SeamlessObserver getInstance() {
        return Holder.INSTANCE;
    }

    public void register(Callback cb) {
        if (cb == null || callbacks.contains(cb)) {
            return;
        }
        callbacks.add(cb);
    }

    public void unregister(Callback cb) {
        if (!callbacks.contains(cb)) {
            return;
        }
        callbacks.remove(cb);
    }

    public void execute(int type, ViewAttr attr) {
        if (callbacks.size() > 0) {
            Callback callback = callbacks.get(callbacks.size() - 1);
            callback.onEvent(type, attr);
        }
        // for (Callback callback : callbacks) {
        // }
    }

    public interface Callback {
        void onEvent(int type, ViewAttr attr);
    }
}

ViewAttr.java

public class ViewAttr implements Parcelable {
    public static final Creator<ViewAttr> CREATOR = new Creator<ViewAttr>() {
        @Override
        public ViewAttr createFromParcel(Parcel in) {
            return new ViewAttr(in);
        }

        @Override
        public ViewAttr[] newArray(int size) {
            return new ViewAttr[size];
        }
    };
    private int x;
    private int y;
    private int width;
    private int height;

    public ViewAttr() {
    }

    protected ViewAttr(Parcel in) {
        x = in.readInt();
        y = in.readInt();
        width = in.readInt();
        height = in.readInt();
    }

    public int getX() {
        return x;
    }

    public void setX(int x) {
        this.x = x;
    }

    public int getY() {
        return y;
    }

    public void setY(int y) {
        this.y = y;
    }

    public int getWidth() {
        return width;
    }

    public void setWidth(int width) {
        this.width = width;
    }

    public int getHeight() {
        return height;
    }

    public void setHeight(int height) {
        this.height = height;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(x);
        dest.writeInt(y);
        dest.writeInt(width);
        dest.writeInt(height);
    }

    @Override
    public String toString() {
        return "ViewAttr{" +
                "x=" + x +
                ", y=" + y +
                ", width=" + width +
                ", height=" + height +
                '}';
    }
}

ViewMoveHelper.java

public class ViewMoveHelper {
    private View targetView;
    private ViewAttr fromViewInfo;
    private ViewAttr toViewInfo;
    private long duration;
    private EndCallback mEndCallback = new EndCallback() {
    };

    /**
     * @param targetView   目标布局
     * @param fromViewInfo 起始view坐标信息
     * @param toViewInfo   目标view坐标信息
     * @param duration     动画时长
     */
    public ViewMoveHelper(View targetView, ViewAttr fromViewInfo, ViewAttr toViewInfo, long duration, @NonNull EndCallback callback) {
        this.targetView = targetView;
        this.fromViewInfo = fromViewInfo;
        this.toViewInfo = toViewInfo;
        this.duration = duration;
        mEndCallback = callback;
    }

    public void startAnim() {
        ObjectAnimator xAnim = ObjectAnimator.ofFloat(targetView, "x", fromViewInfo.getX(), toViewInfo.getX());
        ObjectAnimator yAnim = ObjectAnimator.ofFloat(targetView, "y", fromViewInfo.getY(), toViewInfo.getY());
        ValueAnimator widthAnim = ValueAnimator.ofInt(fromViewInfo.getWidth(), toViewInfo.getWidth());
        ValueAnimator heightAnim = ValueAnimator.ofInt(fromViewInfo.getHeight(), toViewInfo.getHeight());
        widthAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                ViewGroup.LayoutParams param = targetView.getLayoutParams();
                param.width = (int) valueAnimator.getAnimatedValue();
                targetView.setLayoutParams(param);
            }
        });
        heightAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator valueAnimator) {
                ViewGroup.LayoutParams param = targetView.getLayoutParams();
                param.height = (int) valueAnimator.getAnimatedValue();
                targetView.setLayoutParams(param);
            }
        });

        AnimatorSet animation = new AnimatorSet();
        animation.addListener(new Animator.AnimatorListener() {

            @Override
            public void onAnimationStart(@NonNull Animator animation) {
            }

            @Override
            public void onAnimationEnd(@NonNull Animator animation) {
                mEndCallback.onEnd();
            }

            @Override
            public void onAnimationCancel(@NonNull Animator animation) {
            }

            @Override
            public void onAnimationRepeat(@NonNull Animator animation) {
            }
        });
        animation.playTogether(xAnim, yAnim, widthAnim, heightAnim);
        animation.setDuration(duration);
        animation.setInterpolator(new DecelerateInterpolator());
        animation.start();
    }

    public interface EndCallback {
        default void onEnd() {
        }
    }
}

对比上个文章里介绍的共享元素的方案,还是推荐共享元素的方案,原因:

  1. 代码少。动画通过系统的Scene和Transition实现,不用像第二种方案里那样手搓实现属性动画,不用计算和维护x、y、width、height等属性;
  2. 效果好。其实对比下,还是共享元素的效果比较好,因为用到了一个GhostView的东西,它的作用是在不改变view的parent的情况下,将view绘制在另一个parent下,这就避免了第二种方案里的来回removeView和addView的操作。






参考文献:

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

推荐阅读更多精彩内容