Android應用內(nèi)懸浮窗的實現(xiàn)方案

1凛捏、懸浮窗的基本介紹

懸浮窗,大家應該也不陌生芹缔,凌駕于應用之上的一個小彈窗坯癣,實現(xiàn)上很簡單,就是添加一個系統(tǒng)級別的窗口最欠,Android中通過WindowManagerService( WMS)來管理所有的窗口坡锡,對于WMS來說,管你是Activity窒所、Toast、Dialog帆锋,都不過是通過WindowManagerGlobal.addView()添加的一個個View吵取。
Android中的窗口分為三個級別:

  • 1.1 應用窗口,比如Activity的窗口;
  • 1.2 子窗口锯厢,依賴于父窗口皮官,比如PopupWindow;
  • 1.3 系統(tǒng)窗口,比如狀態(tài)欄实辑、Toast捺氢,目標懸浮窗就是系統(tǒng)窗口.

2、根據(jù)產(chǎn)品需求進行設計

先了解一下大概的產(chǎn)品需求:
1剪撬、懸浮窗需要跨越整個應用
2摄乒、需要與懸浮窗進行交互
3、懸浮窗得移動
4残黑、點擊跳轉(zhuǎn)特定的頁面
5馍佑、消息提示的拖拽小紅點

需求很簡單,但是如果估算沒錯梨水,不下一周產(chǎn)品經(jīng)理會添加新的需求拭荤,所以為了更好的后續(xù)擴展,需要進行合理的設計疫诽,主要分為以下幾點:
1舅世、懸浮窗自定義一個FrameLayout布局FloatLayout,里面進行拖動及點擊響應處理;
2奇徒、FloatMonkService雏亚,是一個服務,開啟服務的時候創(chuàng)建懸浮窗;
3逼龟、FloatCallBack评凝,交互接口,在FloatMonkService里面實現(xiàn)接口腺律,用于交互;
4奕短、FloatWindowManager宜肉,懸浮窗的管理,因為后續(xù)懸浮窗布局可能有好幾個翎碑,可以在這里面進行切換;
5谬返、HomeWatcherReceiver,廣播接收者日杈,因為在應用內(nèi)展示遣铝,需要監(jiān)聽用戶在點擊Home鍵和切換鍵的時候隱藏懸浮窗,需要FloatMonkService里頭動態(tài)注冊;
6莉擒、FloatActionController酿炸,其實就是代理,其它模塊需要通過它來和懸浮窗進行交互涨冀,真正干活的是實現(xiàn)FloatCallBack接口的FloatMonkService;
7填硕、FloatPermissionManager,需要適配各個機型的權限鹿鳖,慶幸網(wǎng)上已有大佬分享扁眯,只需要單獨對7.0系統(tǒng)進行一些適配就行,懸浮窗權限適配;
8翅帜、拖拽控件DraggableFlagView姻檀,直接拿來在懸浮窗上出現(xiàn)很奇怪的問題,所以需要改造一下下才能達到圖中效果涝滴。

3绣版、具體實現(xiàn)

float_littlemonk_layout.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dfv="http://schemas.android.com/apk/res-auto"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center"
    android:orientation="vertical">

    <RelativeLayout
        android:id="@+id/monk_relative_root"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            android:id="@+id/float_id"
            android:layout_width="70dp"
            android:layout_height="80dp"
            android:layout_gravity="center_vertical|end"
            android:scaleType="center"
            android:src="@drawable/little_monk" />

    </RelativeLayout>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <floatwindow.xishuang.float_lib.view.DraggableFlagView
            android:id="@+id/main_dfv"
            android:layout_width="17dp"
            android:layout_height="17dp"
            android:layout_gravity="end"
            dfv:color1="#FF3B30" />
    </FrameLayout>
</FrameLayout>

簡單的布局,就是一張圖片+右上角放一個自定義的小紅點歼疮。

FloatLayout.java

@Override
    public boolean onTouchEvent(MotionEvent event) {
        // 獲取相對屏幕的坐標僵娃,即以屏幕左上角為原點
        int x = (int) event.getRawX();
        int y = (int) event.getRawY();
        //下面的這些事件,跟圖標的移動無關腋妙,為了區(qū)分開拖動和點擊事件
        int action = event.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                startTime = System.currentTimeMillis();
                mTouchStartX = event.getX();
                mTouchStartY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                //圖標移動的邏輯在這里
                float mMoveStartX = event.getX();
                float mMoveStartY = event.getY();
                // 如果移動量大于3才移動
                if (Math.abs(mTouchStartX - mMoveStartX) > 3
                        && Math.abs(mTouchStartY - mMoveStartY) > 3) {
                    // 更新浮動窗口位置參數(shù)
                    mWmParams.x = (int) (x - mTouchStartX);
                    mWmParams.y = (int) (y - mTouchStartY);
                    mWindowManager.updateViewLayout(this, mWmParams);
                    return false;
                }
                break;
            case MotionEvent.ACTION_UP:
                endTime = System.currentTimeMillis();
                //當從點擊到彈起小于半秒的時候,則判斷為點擊,如果超過則不響應點擊事件
                if ((endTime - startTime) > 0.1 * 1000L) {
                    isclick = false;
                } else {
                    isclick = true;
                }
                break;
        }
        //響應點擊事件
        if (isclick) {
            Toast.makeText(mContext, "我是大傻叼", Toast.LENGTH_SHORT).show();
        }
        return true;
    }

為了把懸浮窗的view操作抽離出來默怨,自定義了這個布局,主要進行兩部分功能骤素,懸浮窗的移動和點擊處理匙睹,重點是通過mWindowManager.updateViewLayout(this, mWmParams)來進行懸浮窗的位置移動,我這個Demo里面只是簡單的通過時間來判斷點擊事件济竹,有必要的話點擊事件需要添加特定View范圍判斷來響應點擊痕檬。

// 如果移動量大于3才移動
if (Math.abs(mTouchStartX - mMoveStartX) > 3 && Math.abs(mTouchStartY - mMoveStartY) > 3) 

這個判斷是為了避免點擊懸浮窗不在重心位置會出現(xiàn)移動的現(xiàn)象。

FloatMonkService.java

/**
 * 懸浮窗在服務中創(chuàng)建送浊,通過暴露接口FloatCallBack與Activity進行交互
 */
public class FloatMonkService extends Service implements FloatCallBack {
    /**
     * home鍵監(jiān)聽
     */
    private HomeWatcherReceiver mHomeKeyReceiver;

    @Override
    public void onCreate() {
        super.onCreate();
        FloatActionController.getInstance().registerCallLittleMonk(this);
        //注冊廣播接收者
        mHomeKeyReceiver = new HomeWatcherReceiver();
        final IntentFilter homeFilter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        registerReceiver(mHomeKeyReceiver, homeFilter);
        //初始化懸浮窗UI
        initWindowData();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    /**
     * 初始化WindowManager
     */
    private void initWindowData() {
        FloatWindowManager.createFloatWindow(this);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        //移除懸浮窗
        FloatWindowManager.removeFloatWindowManager();
        //注銷廣播接收者
        if (null != mHomeKeyReceiver) {
            unregisterReceiver(mHomeKeyReceiver);
        }
    }

    /////////////////////////////////////////////////////////實現(xiàn)接口////////////////////////////////////////////////////
    @Override
    public void guideUser(int type) {
        FloatWindowManager.updataRedAndDialog(this);
    }


    /**
     * 懸浮窗的隱藏
     */
    @Override
    public void hide() {
        FloatWindowManager.hide();
    }

    /**
     * 懸浮窗的顯示
     */
    @Override
    public void show() {
        FloatWindowManager.show();
    }

    /**
     * 添加可領取的數(shù)量
     */
    @Override
    public void addObtainNumer() {
        FloatWindowManager.addObtainNumer(this);
        guideUser(4);
    }

    /**
     * 減少可領取的數(shù)量
     */
    @Override
    public void setObtainNumber(int number) {
        FloatWindowManager.setObtainNumber(this, number);
    }
}

服務開啟的時候通過FloatWindowManager.createFloatWindow(this)來創(chuàng)建懸浮窗梦谜,實現(xiàn)FloatCallBack 實現(xiàn)需要交互的接口。下面看一下創(chuàng)建懸浮窗的真正操作是怎樣的。

FloatWindowManager.java

/**
     * 創(chuàng)建一個小懸浮窗唁桩。初始位置為屏幕的右下角位置闭树。
     */
    public static void createFloatWindow(Context context) {
        wmParams = new WindowManager.LayoutParams();
        WindowManager windowManager = getWindowManager(context);
        mFloatLayout = new FloatLayout(context);
        if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/
            wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動開啟懸浮窗權限*/
            String packname = context.getPackageName();
            PackageManager pm = context.getPackageManager();
            boolean permission = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.SYSTEM_ALERT_WINDOW", packname));
            if (permission) {
                wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            } else {
                wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
            }
        }

        //設置圖片格式,效果為背景透明
        wmParams.format = PixelFormat.RGBA_8888;
        //設置浮動窗口不可聚焦(實現(xiàn)操作除浮動窗口外的其他可見窗口的操作)
        wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
        //調(diào)整懸浮窗顯示的突脑瑁靠位置為左側(cè)置頂
        wmParams.gravity = Gravity.START | Gravity.TOP;

        DisplayMetrics dm = new DisplayMetrics();
        //取得窗口屬性
        mWindowManager.getDefaultDisplay().getMetrics(dm);
        //窗口的寬度
        int screenWidth = dm.widthPixels;
        //窗口高度
        int screenHeight = dm.heightPixels;
        //以屏幕左上角為原點报辱,設置x、y初始值单山,相對于gravity
        wmParams.x = screenWidth;
        wmParams.y = screenHeight;

        //設置懸浮窗口長寬數(shù)據(jù)
        wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
        wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
        mFloatLayout.setParams(wmParams);
        windowManager.addView(mFloatLayout, wmParams);
        mHasShown = true;
        //是否展示小紅點展示
        checkRedDot(context);
    }

/**
     * 返回當前已創(chuàng)建的WindowManager碍现。
     */
    private static WindowManager getWindowManager(Context context) {
        if (mWindowManager == null) {
            mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        }
        return mWindowManager;
    }

核心代碼其實就是mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE),其中的context不能是Activity的米奸,一開始就說了昼接,Activity會返回它專享的WindowManager,而Activity的窗口級別是屬于應用層的悴晰。進行一些初始化操作之后 windowManager.addView(mFloatLayout, wmParams)把布局添加進去就ok了辩棒。

 if (Build.VERSION.SDK_INT >= 24) { /*android7.0不能用TYPE_TOAST*/
            wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
        } else { /*以下代碼塊使得android6.0之后的用戶不必再去手動開啟懸浮窗權限*/
            String packname = context.getPackageName();
            PackageManager pm = context.getPackageManager();
            boolean permission = (PackageManager.PERMISSION_GRANTED == pm.checkPermission("android.permission.SYSTEM_ALERT_WINDOW", packname));
            if (permission) {
                wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
            } else {
                wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
            }
        }

說一下這段代碼的意義,當WindowManager.LayoutParams.type設置為WindowManager.LayoutParams.TYPE_TOAST的時候膨疏,是可以跳過權限申請的,但是為毛又單獨適配各個機型呢钻弄,因為我們有小米Android系統(tǒng)佃却,魅族Android系統(tǒng),還有華為等等Android系統(tǒng)窘俺,特別是產(chǎn)品經(jīng)理的魅族饲帅,一些特殊機型上是沒有效果的,所以為了更保險瘤泪,得再加一份權限申請灶泵,還有一點得提一下,那就是7.0上WindowManager.LayoutParams.TYPE_TOAST对途,懸浮窗只能持續(xù)一秒的時間赦邻,所以7.0不設這個type,谷歌爸爸最叼实檀,7.0以上老老實實申請權限惶洲。

FloatActionController.java


/**
 * Author:xishuang
 * Date:2017.08.01
 * Des:與懸浮窗交互的控制類,真正的實現(xiàn)邏輯不在這
 */
public class FloatActionController {

    private FloatActionController() {
    }

    public static FloatActionController getInstance() {
        return LittleMonkProviderHolder.sInstance;
    }

    // 靜態(tài)內(nèi)部類
    private static class LittleMonkProviderHolder {
        private static final FloatActionController sInstance = new FloatActionController();
    }

    private FloatCallBack mCallLittleMonk;

    /**
     * 開啟服務懸浮窗
     */
    public void startMonkServer(Context context) {
        Intent intent = new Intent(context, FloatMonkService.class);
        context.startService(intent);
    }

    /**
     * 關閉懸浮窗
     */
    public void stopMonkServer(Context context) {
        Intent intent = new Intent(context, FloatMonkService.class);
        context.stopService(intent);
    }

    /**
     * 注冊監(jiān)聽
     */
    public void registerCallLittleMonk(FloatCallBack callLittleMonk) {
        mCallLittleMonk = callLittleMonk;
    }

    /**
     * 懸浮窗的顯示
     */
    public void show() {
        if (mCallLittleMonk == null) return;
        mCallLittleMonk.show();
    }

    /**
     * 懸浮窗的隱藏
     */
    public void hide() {
        if (mCallLittleMonk == null) return;
        mCallLittleMonk.hide();
    }
}

這就是暴露出來的接口膳犹,按需添加恬吕,效果大概是這樣的。

HomeWatcherReceiver.java

/**
 * Author:xishuang
 * Date:2017.08.01
 * Des:一些Home建與切換鍵的廣播監(jiān)聽须床,需要動態(tài)注冊
 */

public class HomeWatcherReceiver extends BroadcastReceiver {
    private static final String TAG = "HomeWatcherReceiver";
    private static final String SYSTEM_DIALOG_FROM_KEY = "reason";
    private static final String SYSTEM_DIALOG_FROM_RECENT_APPS = "recentapps";
    private static final String SYSTEM_DIALOG_FROM_HOME_KEY = "homekey";
    private static final String SYSTEM_DIALOG_FROM_LOCK = "lock";

    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        Log.i(TAG, "onReceive: action: " + action);
        //根據(jù)不同的信息進行一些個性操作
        if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
            String from = intent.getStringExtra(SYSTEM_DIALOG_FROM_KEY);
            Log.i(TAG, "from: " + from);
            if (SYSTEM_DIALOG_FROM_HOME_KEY.equals(from)) { //短按Home鍵
                Log.i(TAG, "Home Key");
                //按home鍵會直接關閉懸浮窗
                FloatActionController.getInstance().stopMonkServer(context);
            } else if (SYSTEM_DIALOG_FROM_RECENT_APPS.equals(from)) { //長按Home鍵或是Activity切換鍵
                Log.i(TAG, "long press home key or activity switch");
            } else if (SYSTEM_DIALOG_FROM_LOCK.equals(from)) { //鎖屏操作
                Log.i(TAG, "lock");
            }
        }
    }

}

這個就是一個廣播接收者铐料,需要監(jiān)聽系統(tǒng)的一些操作,然后根據(jù)不同的操作實現(xiàn)自己想要的邏輯,Demo中我只是針對Home鍵進行了簡單的處理钠惩,點擊Home退到主頁會直接銷毀服務柒凉,看具體要求進行擴展。

接下來看一下具體的使用妻柒,先看下Activity的布局
activity_main.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:orientation="vertical"
    tools:context="xishuang.floatwindow.MainActivity">

    <Button
        android:id="@+id/open_float"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="開啟懸浮窗" />

    <Button
        android:id="@+id/red_dot"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="打開小紅點" />

</LinearLayout>

就是兩個按鈕扛拨,一個用來開啟懸浮窗,一個用來進行簡單的交互举塔,展示小紅點绑警。

Mainactivity.java

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btOpenFloat = (Button) findViewById(R.id.open_float);
        Button btRedDot = (Button) findViewById(R.id.red_dot);

        assert btOpenFloat != null;
        btOpenFloat.setOnClickListener(this);
        assert btRedDot != null;
        btRedDot.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.open_float) {
            boolean isPermission = FloatPermissionManager.getInstance().applyFloatWindow(this);
            //有對應權限或者系統(tǒng)版本小于7.0
            if (isPermission || Build.VERSION.SDK_INT < 24) {
                //開啟懸浮窗
                FloatActionController.getInstance().startMonkServer(this);
            }
        } else if (v.getId() == R.id.red_dot) {
            //開啟小紅點
            FloatActionController.getInstance().setObtainNumber(1);
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //關閉懸浮窗
        FloatActionController.getInstance().stopMonkServer(this);
    }
}

具體使用看起來也還簡單,因為邏輯都已經(jīng)盡量封裝和解耦了央渣,就是在開啟懸浮窗的時候计盒,7.0版本以上必須先申請權限才能開啟,7.0以下可以直接開啟芽丹,因為前面已經(jīng)設置WindowManager.LayoutParams.TYPE_TOAST北启,雖然有些特殊機型也必須申請權限,但起碼先保證我的懸浮窗在大多數(shù)手機上可以先展示出來拔第。

boolean isPermission = FloatPermissionManager.getInstance().applyFloatWindow(this);

這段代碼說明咕村,無論在哪種情況,我會先進行權限檢查蚊俺,雙重保險懈涛。

大概效果如下:


20170201.gif

Demo:代碼地址感興趣可以看看完整的演示代碼。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末泳猬,一起剝皮案震驚了整個濱河市批钠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌得封,老刑警劉巖埋心,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異忙上,居然都是意外死亡拷呆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門疫粥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洋腮,“玉大人,你說我怎么就攤上這事手形∩豆” “怎么了?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵库糠,是天一觀的道長伙狐。 經(jīng)常有香客問我涮毫,道長,這世上最難降的妖魔是什么贷屎? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任罢防,我火速辦了婚禮,結(jié)果婚禮上唉侄,老公的妹妹穿的比我還像新娘咒吐。我一直安慰自己,他們只是感情好属划,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布恬叹。 她就那樣靜靜地躺著,像睡著了一般同眯。 火紅的嫁衣襯著肌膚如雪绽昼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天须蜗,我揣著相機與錄音硅确,去河邊找鬼。 笑死明肮,一個胖子當著我的面吹牛菱农,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播柿估,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼循未,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了官份?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤烙丛,失蹤者是張志新(化名)和其女友劉穎舅巷,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體河咽,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡钠右,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了忘蟹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片飒房。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖媚值,靈堂內(nèi)的尸體忽然破棺而出狠毯,到底是詐尸還是另有隱情,我是刑警寧澤褥芒,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布嚼松,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏献酗。R本人自食惡果不足惜寝受,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望罕偎。 院中可真熱鬧很澄,春花似錦、人聲如沸颜及。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽器予。三九已至浪藻,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間乾翔,已是汗流浹背爱葵。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留反浓,地道東北人萌丈。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓克懊,卻偏偏與公主長得像渠缕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子窒悔,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容