一个能随手势滑动消除的 Android 应用内悬浮全局通知的实现

1. 例子

先来看两个例子。

1.1 小米手机上收到通知时

小米悬浮通知.gif

1.2 锤子手机上复制链接时

锤子悬浮通知.gif

都是在屏幕上方显示了一个悬浮的通知,并且这个通知是在切换 activity 的时候或者按返回键的时候不消失的,当你横向滑动的时候才消失。

下面来实现类似的效果。

本文最后实现的效果如下:

悬浮通知.gif

2. 实现

由于悬浮通知不受到 activity 的控制,因此需要使用悬浮窗来实现。但是先不管悬浮窗,先实现一个可以随手滑动的消失的 view。

这个效果要满足以下几个要求:

  1. 当手指在屏幕上滑动不松开的时候,悬浮通知也随着手指滑动。
  2. 当手指松开的时候,判断滑动方向,来决定是向右还是向左消失。
  3. 当快速滑动(fling)的时候,朝着对应的方向消失。
  4. 滑动的时候,透明度(alpha)也相应地跟着变化。

可以重写 onTouch 方法,但是那样比较麻烦。这里使用一个 GestureDetector 类,可以方便的判断手势。

GestureDetector 的构造方法之一是:

public GestureDetector(Context context, OnGestureListener listener)

传入两个参数,第二个参数是一个手势监听回调;在回调中就可以获取各种位置信息,来进行处理。

创建 GestureDetector 的代码如下:

mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                float x1 = e1.getX();
                float x2 = e2.getX();
                int offsetX = (int) (x2 - x1);
                layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                float fling = e2.getX() - e1.getX();
                if (fling > 0) {
                   disappear(DIRECTION_LEFT);
                } else {
                   disappear(DIRECTION_RIGHT);
                }
                return false;
            }
        });

其中,在 onScroll 方法中,计算出要滑动的位置:

float x1 = e1.getX();
float x2 = e2.getX();
int offsetX = (int) (x2 - x1);

e1 代表的是手指最初按下去的时候的 MotionEvent,e2 代表的是滑动到执行该 onScroll 的时候手指的位置,e1.getX() - e2.getX() 即代表在 x 方向上滑动了多少。

接下来通过 layoutWithAlpha 来重新摆放该 view 的位置并设置 alpha 值,传入 4 个参数分别是左上右下的值。

private void layoutWithAlpha(int l, int t, int r, int b) {
    layout(l, t, r, b);
    float alpha = (screenWidth - Math.abs(getLeft())) / screenWidth;
    setAlpha(alpha);
}

layoutWithAlpha这个方法其实就是调用 View 内部的 layout 来重新摆放位置,然后计算出相应的 alpha 值。

再看一下 onFling 方法。onFling 方法中的 e1 和 e2 与 onScroll 方法中的是一样的。onFling先通过 e2.getX() - e1.getX() 判断滑动的方向,大于 0 表示向左滑动,反之向右。

这里 disappear 方法代码如下:

private void disappear(int direction) {
    final int speed = direction == DIRECTION_LEFT ? 66 : -66;
    Runnable r = new Runnable() {
        @Override
        public void run() {
            int left = getLeft();
            int right = getRight();
            layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
            if (left < screenWidth && right > 0) {
                post(this);
              }
            }
        }
    };
    post(r);
}

这里 66 是我随便设定的值,表示一次性移动多少 px。 disappear 方法就是不断的调用 layout 来进行移动,此时,SwipeNotification 的所有代码如下,但是此时 SwipeNotification 还只能放在 activity 内:

public class SwipeNotification extends LinearLayout {

    private static final String TAG = "SwipeNotification";

    private static final int DIRECTION_LEFT = 1;

    private static final int DIRECTION_RIGHT = 2;

    private static final int SPEED_PX = 66;

    private GestureDetector mDetector;

    private boolean isDisappearing = false;

    private ImageView mImageView;

    private TextView mTextView;

    private static float screenWidth;

    public SwipeNotification(@NonNull Context context) {
        this(context, null);
    }

    public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs,
                              int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        screenWidth = getScreenWidth(context);
        View inflate = LayoutInflater.from(context).inflate(R.layout.item_notification, this);
        mImageView = inflate.findViewById(R.id.image_view);
        mTextView = inflate.findViewById(R.id.text_view);
        mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                float x = e.getX();
                float y = e.getY();
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                Log.d(TAG, "onSingleTapUp: ");
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                float x1 = e1.getX();
                float x2 = e2.getX();
                int offsetX = (int) (x2 - x1);
                layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                float fling = e2.getX() - e1.getX();
                if (fling > 0) {
                    disappear(DIRECTION_LEFT);
                } else {
                    disappear(DIRECTION_RIGHT);
                }
                return false;
            }
        });
    }

    public void setText(CharSequence text) {
        mTextView.setText(text);
    }

    public void setImageResource(int resId) {
        mImageView.setImageResource(resId);
    }

    private void layoutWithAlpha(int l, int t, int r, int b) {
        layout(l, t, r, b);
        float alpha = (screenWidth - Math.abs(getLeft())) / screenWidth;
        Log.d(TAG, "layoutWithAlpha: alpha = " + alpha);
        setAlpha(alpha);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
        if (!mDetector.onTouchEvent(event) && detectedUp) {
            onUp(event);
//            return super.onTouchEvent(event);
        }
        return true;
    }

    private void onUp(MotionEvent event) {
        Log.d(TAG, "onUp: ");
        if (isDisappearing) {
            return;
        }
        autoDisappear();
    }

    private void autoDisappear() {
        Log.d(TAG, "autoDisappear: ");
        if (getLeft() > screenWidth / 5) {
            disappear(DIRECTION_LEFT);
        } else if (getRight() < screenWidth * 4 / 5) {
            disappear(DIRECTION_RIGHT);
        }
    }

    private void disappear(int direction) {
        isDisappearing = true;
        final int speed = direction == DIRECTION_LEFT ? SPEED_PX : -SPEED_PX;
        Log.d(TAG, "disappear: speed=" + speed);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                int left = getLeft();
                int right = getRight();
                layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
                Log.d(TAG, "run: layout");
                if (left < screenWidth && right > 0) {
                    post(this);
                }
            }
        };
        post(r);
    }

    public static int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager)
                context.getSystemService(Context.WINDOW_SERVICE);
        return wm.getDefaultDisplay().getWidth();
    }
}

需要注意的一点是,GestureDetector 没有判断手指抬起的时候的回调,因此需要手动在
onTouchEvent 当中捕获到这个手指抬起的动作,然后自动滑动消失。(注意:onSingleTapUp 只是单击抬起才触发,当滑动后不会再触发这个回调)

@Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
        if (!mDetector.onTouchEvent(event) && detectedUp) {
            onUp(event);
        }
        return true;
    }

这个悬浮通知的 xml 布局如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/shadow"
    android:orientation="horizontal">

    <ImageView
        android:id="@+id/image_view"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:contentDescription="ic_launcher"
        android:src="@mipmap/ic_launcher" />

    <TextView
        android:id="@+id/text_view"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:text="test" />

</LinearLayout>

然后再在 activity 的布局中引用一下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <com.enhao.rxjavatest.SwipeNotification
        android:layout_width="match_parent"
        android:layout_height="50dp" />
        
    <com.enhao.rxjavatest.SwipeNotification
        android:layout_width="match_parent"
        android:layout_height="50dp" />

</LinearLayout>

ok,基本就实现了类似的效果,如下图:

悬浮通知1.gif

但是这个是放在 activity 中的。为了实现在应用的全局内都能显示通知,就是说切换 activity,通知不消失,就要使用悬浮框了。

悬浮窗的权限申请不在本文的范围内,而且众多国产 rom 的对悬浮窗的权限管理不太一样。适配起来需要费一定的劲。

本文假设已经获取到了悬浮窗的权限。

由于这个悬浮窗是应用内全局的,因此打算把它做成一个单例类。

代码如下:

public class SwipeNotificationManager {

    private static final String TAG = "FloatingWindowManager";

    private static SwipeNotificationManager sManager;

    private WindowManager mWindowManager;
    private WindowManager.LayoutParams mParams;

    // 用一个 LinearLayout 作为包含所有通知的容器
    private LinearLayout mNotificationContainer;

    private Context mContext;

    // 用来记录包含通知的 LinearLayout 是否已经添加到窗口里了
    private boolean isAdded;

    // 最大允许显示的通知条目数量
    private int mMaxCount = 3;

    private SwipeNotificationManager(Context context) {
        mContext = context;
        mParams = new WindowManager.LayoutParams();

        // 获取 WindowManager
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);

        // 新建一个包含所有通知的容器
        mNotificationContainer = new LinearLayout(context);
        mNotificationContainer.setOrientation(LinearLayout.VERTICAL);

        // 设定一下 WindowManager.LayoutParams 的一些参数值
        initWindowParams(mWindowManager, mParams);

        // 将 mNotificationContainer 添加到窗口上
        mWindowManager.addView(mNotificationContainer, mParams);
        isAdded = true;
    }

    public static SwipeNotificationManager getInstance(Context context) {
        if (sManager == null) {
            synchronized (SwipeNotificationManager.class) {
                if (sManager == null) {
                    sManager = new SwipeNotificationManager(context.getApplicationContext());
                }
            }
        }
        return sManager;
    }

    public void addNotification(final CharSequence text, int imageId) {
        // 新建一条通知
        final SwipeNotification notification = new SwipeNotification(mContext);
        // 设置通知的文本和图片
        notification.setText(text);
        notification.setImageResource(imageId);
        // 给通知添加点击事件
        notification.setOnClickNotificationListener(
                new SwipeNotification.OnClickNotificationListener() {
                    @Override
                    public void onClickNotification() {
                        Toast.makeText(mContext, text, Toast.LENGTH_SHORT).show();
                    }
                });
        // 若包含通知的 LinearLayout 已经被移除窗口,就加上去
        if (!isAdded) {
            mWindowManager.addView(mNotificationContainer, mParams);
            Log.d(TAG, "addNotification: addView");
            isAdded = true;
        }
        // 获取一共有多少条通知
        int childCount = mNotificationContainer.getChildCount();

        // 已经显示的通知数目大于了最大值,就将第一条移除
        if (childCount == mMaxCount) {
            mNotificationContainer.removeViewAt(0);
        }

        // 将通知添加进 mNotificationContainer
        mNotificationContainer.addView(notification);

        // 通知已经通过滑动移除出屏幕的时候,将通知从 mNotificationContainer 中移除
        notification.setOnDisappearListener(new SwipeNotification.OnDisappearListener() {
            @Override
            public void onDisappear() {
                mNotificationContainer.removeView(notification);
                // 如果 mNotificationContainer 里面已经没有通知了,将 mNotificationContainer 从窗口移除
                if (mNotificationContainer.getChildCount() == 0) {
                    mWindowManager.removeViewImmediate(mNotificationContainer);
                    isAdded = false;
                }
            }
        });

    }

    private void initWindowParams(WindowManager manager, WindowManager.LayoutParams params) {
        // android 8.0 以上,要将 type 设为 TYPE_APPLICATION_OVERLAY
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        } else {
            params.type = WindowManager.LayoutParams.TYPE_PHONE;
        }
        mParams.format = PixelFormat.RGBA_8888;
        params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //调整悬浮窗显示的停靠位置为置顶
        params.gravity = TOP;
        //以屏幕左上角为原点,设置x、y初始值
        params.x = 0;
        params.y = 0;
        //设置悬浮窗口长宽
        params.width = MATCH_PARENT;
        params.height = WRAP_CONTENT;
    }

    /**
     * 设置一下最大的显示通知的数量
     */
    private void setMaxCount(int maxCount) {
        mMaxCount = maxCount;
    }

其中,由于 GestureDetector 消耗掉了触摸事件,因此不能给 SwipeNotification 直接 setOnClickListener;为了使点击事件能够处理,需要在 GestureDetector 的 onSingleTapUp 里面添加一个回调。修改后的 SwipeNotification 代码如下:

public class SwipeNotification extends LinearLayout {

    private static final String TAG = "SwipeNotification";

    private static final int AUTO_DISMISS_TIME = 2000;

    private static final int DIRECTION_LEFT = 1;

    private static final int DIRECTION_RIGHT = 2;

    private static final int SPEED_PX = 66;

    private GestureDetector mDetector;

    private OnDisappearListener mDisappearListener;

    private OnClickNotificationListener mClickNotificationListener;

    private boolean isDisappearing = false;

    private ImageView mImageView;

    private TextView mTextView;

    private static float screenWidth;

    public SwipeNotification(@NonNull Context context) {
        this(context, null);
    }

    public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SwipeNotification(@NonNull Context context, @Nullable AttributeSet attrs,
                             int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    private void init(Context context) {
        screenWidth = getScreenWidth(context);
        View inflate = LayoutInflater.from(context).inflate(R.layout.item_notification, this);
        mImageView = inflate.findViewById(R.id.image_view);
        mTextView = inflate.findViewById(R.id.text_view);
        mDetector = new GestureDetector(context, new GestureDetector.OnGestureListener() {
            @Override
            public boolean onDown(MotionEvent e) {
                float x = e.getX();
                float y = e.getY();
                return false;
            }

            @Override
            public void onShowPress(MotionEvent e) {

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                Log.d(TAG, "onSingleTapUp: ");
                if (mClickNotificationListener != null) {
                    mClickNotificationListener.onClickNotification();
                }
                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                float x1 = e1.getX();
                float x2 = e2.getX();
                int offsetX = (int) (x2 - x1);
                layoutWithAlpha(getLeft() + offsetX, getTop(), getRight() + offsetX, getBottom());
                return false;
            }

            @Override
            public void onLongPress(MotionEvent e) {

            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                Log.d(TAG, "onFling: =" + (e2.getX() - e1.getX()) + " velocityX=" + velocityX);
                float fling = e2.getX() - e1.getX();
                if (fling > 0) {
                    disappear(DIRECTION_LEFT);
                } else {
                    disappear(DIRECTION_RIGHT);
                }
                return false;
            }
        });
    }

    public void setText(CharSequence text) {
        mTextView.setText(text);
    }

    public void setImageResource(int resId) {
        mImageView.setImageResource(resId);
    }

    private void layoutWithAlpha(int l, int t, int r, int b) {
        layout(l, t, r, b);
        float alpha = (1080f - Math.abs(getLeft())) / 1080f;
        Log.d(TAG, "layoutWithAlpha: alpha = " + alpha);
        setAlpha(alpha);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean detectedUp = event.getAction() == MotionEvent.ACTION_UP;
        if (!mDetector.onTouchEvent(event) && detectedUp) {
            onUp(event);
//            return super.onTouchEvent(event);
        }
        return true;
    }

    private void onUp(MotionEvent event) {
        Log.d(TAG, "onUp: ");
        if (isDisappearing) {
            return;
        }
        autoDisappear();
    }

    private void autoDisappear() {
        Log.d(TAG, "autoDisappear: ");
        if (getLeft() > screenWidth / 5) {
            disappear(DIRECTION_LEFT);
        } else if (getRight() < screenWidth * 4 / 5) {
            disappear(DIRECTION_RIGHT);
        }
    }

    private void disappear(int direction) {
        isDisappearing = true;
        final int speed = direction == DIRECTION_LEFT ? SPEED_PX : -SPEED_PX;
        Log.d(TAG, "disappear: speed=" + speed);
        Runnable r = new Runnable() {
            @Override
            public void run() {
                int left = getLeft();
                int right = getRight();
                layoutWithAlpha(left + speed, getTop(), right + speed, getBottom());
                Log.d(TAG, "run: layout");
                if (left < screenWidth && right > 0) {
                    post(this);
                } else {
                    if (mDisappearListener != null) {
                        mDisappearListener.onDisappear();
                    }
                }
            }
        };
        post(r);
    }

    public void setOnDisappearListener(OnDisappearListener listener) {
        mDisappearListener = listener;
    }

    public void setOnClickNotificationListener(OnClickNotificationListener listener) {
        mClickNotificationListener = listener;
    }


    public static int getScreenWidth(Context context) {
        WindowManager wm = (WindowManager)
                context.getSystemService(Context.WINDOW_SERVICE);
        return wm.getDefaultDisplay().getWidth();
    }

    /**
     * 通知滑动出去消失了的回调
     */
    public interface OnDisappearListener {
        void onDisappear();
    }

    /**
     * 点击通知的回调
     */
    public interface OnClickNotificationListener {
        void onClickNotification();
    }
}

在 activity 里面使用:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    SwipeNotificationManager mWindowManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mWindowManager = SwipeNotificationManager.getInstance(this);
        Button addButton = (Button) findViewById(R.id.btn_add_notification);
        Button startActivityButton = (Button) findViewById(R.id.btn_start_activity);
        addButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String text = "通知:" + String.valueOf(Math.random());
                mWindowManager.addNotification(text, R.mipmap.ic_launcher);
            }
        });

        startActivityButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                startActivity(intent);
            }
        });

    }

最终效果图如下:


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

推荐阅读更多精彩内容