CoordinatorLayout與Behavior的一己之見

前言

許多文章都是將CoordinatorLayout弊决、AppbarLayoutCollapsingToolbarLayout魁淳、Toolbar等放在一起介紹飘诗,容易誤解為這幾個布局一定要互相搭配,且僅僅適用于這些場景中界逛。
其實不然昆稿,其中最重要的是CoordinatorLayout喳瓣,我把它稱為協(xié)調布局仿滔。協(xié)調什么布局呢蜈膨?自然是嵌套在其內部的 Child View曙咽。
CoordinatorLayout充當了一個中間層的角色箫荡,一邊接收其他組件的事件绞灼,一邊將接收到的事件通知給內部的其他組件。
Behavior就是CoordinatorLayout傳遞事件的媒介,Behavior 定義了 CoordinatorLayout 中**直接子 View **的行為規(guī)范闲延,決定了當收到不同事件時合愈,應該做怎樣的處理寸莫。
總結來說态坦,Behavior代理以下四種事件攻旦,其大致傳遞流程如下圖:

事件流好像很高深莫測的樣子...,再簡化一點的說法:CoordinatorLayout中的某個或某幾個方法被其他類調用合搅,之后CoordinatorLayout再調用Behavior中的某個或某幾個方法(=赌髓。=好像更抽象了)。總之舌仍,讓這四類事件現(xiàn)在腦子里有個印象就可以了。

接著先介紹一下自定義Behavior的通用流程苏揣。為什么是通用流程呢平匈?因為上面提到了有四種事件流队丝,根據(jù)不同的事件流损晤,是要重寫不同的方法的,會在下面一一說明憾股。

自定義Behavior的通用流程

1. 重寫構造方法

public class CustomBehavior extends CoordinatorLayout.Behavior {

    public CustomBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}

一定要重寫這個構造方法鹿蜀,因為當你在XML中設置該Behavior時箕慧,在 CoordinatorLayout 中會反射調用該方法,并生成該 Behavior 實例茴恰。
2. 綁定到View
綁定的方法有三種:
在 XML 文件中颠焦,設置任意 View 的屬性

app:layout_behavior="你的Behavior的包路徑和類名"

或者在代碼中:

(CoordinatorLayout.LayoutParams)child.getLayoutParams().setBehavior();

再或者當你的View是自定義的View時。
在你的自定義View類上添加@DefaultBehavior(你的Behavior.class)往枣。

@DefaultBehavior(CustomBehavior.class)
public class CustomView extends View {}

3. 判斷依賴對象
CoordinatorLayout 收到某個 view 的變化或者嵌套滑動事件時伐庭,CoordinatorLayout就會嘗試把事件下發(fā)給Behavior,綁定了該 Behavior 的 view 就會對事件做出響應分冈。

下面是這兩個具有依賴的關系的view在Behavior方法中的形參名圾另,方便讀者分辨:
被動變化,也就是綁定了Behavior的view稱為child
主動變化的view在「變化事件」中稱為dependency雕沉;在「嵌套滑動事件」中稱為target集乔。

因為可能會存在很多的Child View可以向CoordinatorLayout發(fā)出消息,也同時存在很多的Child View擁有著不同的Behavior坡椒,那么在CoordinatorLayout將真正的事件傳遞進這個Behavior之前扰路,肯定需要一個方法,告知CoordinatorLayout這兩者的依賴關系是否成立倔叼。如果關系成立汗唱,那么就把事件下發(fā)給你,如果關系不成立缀雳,那咱就到此over渡嚣。
下面以「變化事件」的layoutDependsOn說幾個例子,「嵌套滑動事件」就在onStartNestedScroll中做同樣的判斷肥印。另外的兩種「布局事件」「觸摸事件」就沒有這一步了识椰。
a.根據(jù)id

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency.getId() == R.id.xxx;
}

b.根據(jù)類型

@Override
public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
    return dependency instanceof CustomView;
}

c.根據(jù)id的另一種寫法

<declare-styleable name="Follow">
    <attr name="target" format="reference"/>
</declare-styleable>

先自定義target這個屬性。

<android.support.design.widget.CoordinatorLayout    
    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:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <View
        android:id="@+id/first"
        android:layout_width="match_parent"
        android:layout_height="128dp"
        android:background="@android:color/holo_blue_light"/>

    <View
        android:id="@+id/second"
        android:layout_width="match_parent"
        android:layout_height="128dp"
        app:layout_behavior=".FollowBehavior"
        app:target="@id/first"
        android:background="@android:color/holo_green_light"/>

</android.support.design.widget.CoordinatorLayout>
public class FollowBehavior extends CoordinatorLayout.Behavior {
    private int targetId;

    public FollowBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Follow);
        for (int i = 0; i < a.getIndexCount(); i++) {
            int attr = a.getIndex(i);
            if(a.getIndex(i) == R.styleable.Follow_target){
                targetId = a.getResourceId(attr, -1);
            }
        }
        a.recycle();
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        return true;
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
        return dependency.getId() == targetId;
    }
}

四種不同的事件流

1. 觸摸事件

TouchEvent 最主要的方法就是兩個:

public boolean onInterceptTouchEvent(MotionEvent ev)
public boolean onTouchEvent(MotionEvent ev)

CoordinatorLayoutonInterceptTouchEventonTouchEvent 方法中深碱,會嘗試調用其 Child View 擁有的 Behavior 中的同名方法腹鹉。

public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)
public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev)

如果 Behavior 對觸摸事件進行了攔截,就不會再分發(fā)到 Child View 自身擁有的觸摸事件中敷硅。
這就意味著:在不知道具體View的情況下功咒,就可以重寫它的觸摸事件。
然而有一點我們需要注意到的是:onTouch事件是CoordinatorLayout分發(fā)下來的绞蹦,所以這里的onTouchEvent并不是我們控件自己的onTouch事件力奋,也就是說,你假如手指不在我們的控件上滑動幽七,也會觸發(fā)onTouchEvent景殷。
需要在onTouchEvent方法中的MotionEvent.ACTION_DOWN下添加:

ox = ev.getX();
oy = ev.getY();
if (oy < child.getTop() || oy > child.getBottom() || ox < child.getLeft() || ox > child.getRight()) { 
    return true;
}

對手勢的位置進行過濾,不是我們控件范圍內的,舍棄掉猿挚。

2. 布局事件

視圖布局無非就是這兩個方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
protected void onLayout(boolean changed, int l, int t, int r, int b)

CoordinatorLayoutonMeasureonLayout 方法中咐旧,也會嘗試調用其 Child View 擁有的 Behavior 中對應的方法,分別是:

public boolean onMeasureChild(CoordinatorLayout parent, V child, int parentWidthMeasureSpec, int widthUsed,
                                int parentHeightMeasureSpec, int heightUsed)
public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection)

同樣地绩蜻,CoordinatorLayout 會優(yōu)先處理 Behavior 中所重寫的布局事件铣墨。

3. 變化事件

這個變化是指 View 的位置、尺寸發(fā)生了變化办绝。
CoordinatorLayoutonDraw 方法中伊约,會遍歷全部的 Child View 嘗試尋找是否有相互關聯(lián)的對象。
確定是否關聯(lián)的方式有兩種:
1. Behavior中定義
通過 BehaviorlayoutDependsOn 方法來判斷是否有依賴關系八秃,如果有就繼續(xù)調用 onDependentViewChanged碱妆。FloatActionButton 可以在 Snackbar 彈出時主動上移就通過該方式實現(xiàn)。

/**
 * 判斷是dependency是否是當前behavior需要的對象
 * @param parent CoordinatorLayout
 * @param child 該Behavior對應的那個View
 * @param dependency dependency 要檢查的View(child是否要依賴這個dependency)
 * @return true 依賴, false 不依賴
 */
@Override
public boolean layoutDependsOn(CoordinatorLayout parent, Button child, View dependency) {
    return false;
}

/**
 * 當改變dependency的尺寸或者位置時被調用
 * @param parent CoordinatorLayout
 * @param child  該Behavior對應的那個View
 * @param dependency child依賴dependency
 * @return true 處理了, false 沒處理
 */
@Override
public boolean onDependentViewChanged(CoordinatorLayout parent, Button child, View dependency) {
    return false;
}

/**
 * 在layoutDependsOn返回true的基礎上之后昔驱,通知dependency被移除了
 * @param parent CoordinatorLayout
 * @param child 該Behavior對應的那個View
 * @param dependency child依賴dependency
 */
@Override
public void onDependentViewRemoved(CoordinatorLayout parent, Button child, View dependency) {
    
}

2. XML中設置屬性
通過 XML 中設置的 layout_anchor疹尾,關聯(lián)設置了 layout_anchor 的 Child View 與 layout_anchor 對應的目標 dependency View。隨后調用 offsetChildToAnchor(child, layoutDirection);骤肛,其實就是調整兩者的位置纳本,讓它們可以一起變化。FloatActionButton 可以跟隨 Toolbar 上下移動就是該方式實現(xiàn)腋颠。

app:layout_anchor="@id/dependencyView.id"

4. 嵌套滑動事件

實現(xiàn)NestedScrollingChild
如果一個View想向外界傳遞滑動事件繁成,即通知 NestedScrollingParent ,就必須實現(xiàn)此接口淑玫。
而 Child 與 Parent 的具體交互邏輯巾腕, NestedScrollingChildHelper 輔助類基本已經(jīng)幫我們封裝好了,所以我們只需要調用對應的方法即可絮蒿。
NestedScrollingChild接口的一般實現(xiàn):

public class CustomNestedScrollingChildView extends View implements NestedScrollingChild {

    private NestedScrollingChildHelper mChildHelper = new NestedScrollingChildHelper(this);

    /**
     * 設置當前View能否滑動
     * @param enabled
     */
    @Override
    public void setNestedScrollingEnabled(boolean enabled) {
        mChildHelper.setNestedScrollingEnabled(enabled);
    }

    /**
     * 判斷當前View能否滑動
     * @return
     */
    @Override
    public boolean isNestedScrollingEnabled() {
        return mChildHelper.isNestedScrollingEnabled();
    }

    /**
     * 啟動嵌套滑動事件流
     * 1. 尋找可以接收 NestedScroll 事件的 parent view尊搬,即實現(xiàn)了 NestedScrollingParent 接口的 ViewGroup
     * 2. 通知該 parent view,現(xiàn)在我要把滑動的參數(shù)傳遞給你
     * @param axes
     * @return
     */
    @Override
    public boolean startNestedScroll(int axes) {
        return mChildHelper.startNestedScroll(axes);
    }

    /**
     * 停止嵌套滑動事件流
     */
    @Override
    public void stopNestedScroll() {
        mChildHelper.stopNestedScroll();
    }

    /**
     * 是否存在接收 NestedScroll 事件的 parent view
     * @return
     */
    @Override
    public boolean hasNestedScrollingParent() {
        return mChildHelper.hasNestedScrollingParent();
    }

    /**
     * 在滑動之后土涝,向父view匯報滾動情況佛寿,包括child view消費的部分和child view沒有消費的部分。
     * @param dxConsumed x方向已消費的滑動距離
     * @param dyConsumed y方向已消費的滑動距離
     * @param dxUnconsumed x方向未消費的滑動距離
     * @param dyUnconsumed y方向未消費的滑動距離
     * @param offsetInWindow 如果parent view滑動導致child view的窗口發(fā)生了變化(child View的位置發(fā)生了變化)
     *                       該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       如果你記錄了手指最后的位置但壮,需要根據(jù)參數(shù)offsetInWindow計算偏移量冀泻,
     *                       才能保證下一次的touch事件的計算是正確的。
     * @return 如果parent view接受了它的滾動參數(shù)蜡饵,進行了部分消費弹渔,則這個函數(shù)返回true,否則為false溯祸。
     */
    @Override
    public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
                                        int dyUnconsumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                offsetInWindow);
    }

    /**
     * 在滑動之前捞附,先問一下 parent view 是否需要滑動巾乳,
     * 即child view的onInterceptTouchEvent或onTouchEvent方法中調用您没。
     * 1. 如果parent view滑動了一定距離鸟召,你需要重新計算一下parent view滑動后剩下給你的滑動距離剩余量,
     *      然后自己進行剩余的滑動氨鹏。
     * 2. 該方法的第三第四個參數(shù)返回parent view消費掉的滑動距離和child view的窗口偏移量欧募,
     *      如果你記錄了手指最后的位置,需要根據(jù)第四個參數(shù)offsetInWindow計算偏移量仆抵,
     *      才能保證下一次的touch事件的計算是正確的跟继。
     * @param dx x方向的滑動距離
     * @param dy y方向的滑動距離
     * @param consumed 如果不是null, 則告訴child view現(xiàn)在parent view滑動的情況,
     *                 consumed[0]parent view告訴child view水平方向滑動的距離(dx)
     *                 consumed[1]parent view告訴child view垂直方向滑動的距離(dy)
     * @param offsetInWindow 可選 length=2 的數(shù)組镣丑,
     *                       如果parent view滑動導致child View的窗口發(fā)生了變化(子View的位置發(fā)生了變化)
     *                       該參數(shù)返回x(offsetInWindow[0]) y(offsetInWindow[1])方向的變化
     *                       如果你記錄了手指最后的位置舔糖,需要根據(jù)參數(shù)offsetInWindow計算偏移量,
     *                       才能保證下一次的touch事件的計算是正確的莺匠。
     * @return 如果parent view對滑動距離進行了部分消費金吗,則這個函數(shù)返回true,否則為false趣竣。
     */
    @Override
    public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
        return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
    }

    /**
     * 在嵌套滑動的child view快速滑動之后再調用該函數(shù)向parent view匯報快速滑動情況摇庙。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @param consumed true 表示child view快速滑動了, false 表示child view沒有快速滑動
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
        return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
    }

    /**
     * 在嵌套滑動的child view快速滑動之前告訴parent view快速滑動的情況。
     * @param velocityX 水平方向的速度
     * @param velocityY 垂直方向的速度
     * @return true 表示parent view快速滑動了, false 表示parent view沒有快速滑動
     */
    @Override
    public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
        return mChildHelper.dispatchNestedPreFling(velocityX, velocityY);
    }

實現(xiàn)NestedScrollingParent
如果一個View Group想接收來自 NestedScrollingChild 的滑動事件遥缕,就需要實現(xiàn)該接口卫袒。
同樣有一個 NestedScrollingParentHelper
輔助類,幫我們封裝好了 parent view 與 child view之間的具體交互邏輯单匣。
由 NestedScrollingChild 主動發(fā)出滑動事件傳遞給 NestedScrollingParent夕凝,NestedScrollingParent 做出響應。
之間的調用關系如下表所示:

Child View Parent View
startNestedScroll onStartNestedScroll户秤、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
dispatchNestedFling onNestedFling
dispatchNestedPreFling onNestedPreFling

繼承Behavior
在上面的說明中提到 Parent View 會消費一部分或全部的滑動距離码秉,但其實大部分情況下,我們的 Parent View 自身并不會消費滑動距離虎忌,都是傳遞給 Behavior泡徙,也就是擁有這個 Behavior 的 Child View 才是真正消費滑動距離的實例。
Behavior 擁有與 NestedScrollingParent 接口完全同名的方法膜蠢。在每一個 NestedScrollingParent 的方法中都會調用 Behavior 中的同名方法堪藐。
有這么幾個方法做下特別說明:

/**
 * 開始嵌套滑動的時候被調用
 * 1. 需要判斷滑動的方向是否是我們需要的。
 *      nestedScrollAxes == ViewCompat.SCROLL_AXIS_HORIZONTAL 表示是水平方向的滑動
 *      nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL 表示是豎直方向的滑動
 * 2. 返回 true 表示繼續(xù)接收后續(xù)的滑動事件挑围,返回 false 表示不再接收后續(xù)滑動事件
 */
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child,
                                   View directTargetChild, View target, int nestedScrollAxes) {
}

/**
 * 滑動中調用
 * 1. 正在上滑:dyConsumed > 0 && dyUnconsumed == 0
 * 2. 已經(jīng)到頂部了還在上滑:dyConsumed == 0 && dyUnconsumed > 0
 * 3. 正在下滑:dyConsumed < 0 && dyUnconsumed == 0
 * 4. 已經(jīng)打底部了還在下滑:dyConsumed == 0 && dyUnconsumed < 0
 */
@Override
public void onNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target,
                           int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
}

/**
 * 快速滑動中調用
 */
@Override
public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target,
                             float velocityX, float velocityY, boolean consumed) {
}

總結

總結一下這四種事件流礁竞,和各自需要實現(xiàn)的方法。
根據(jù)在自定義Behavior時是否需要判斷依賴關系杉辙,把Behavior代理的四種情況分成兩類:
事件來自外部父view:
1.布局事件:BehavioronMeasureChild+onLayoutChild
2.觸摸事件:BehavioronInterceptTouchEvent+onTouchEvent
事件來自內部子view:
3.view變化事件:BehaviorlayoutDependsOn+onDependentViewChanged+onDependentViewRemoved
4.嵌套滑動事件:BehavioronStartNestedScroll+onNestedScrollAccepted+onStopNestedScroll+onNestedScroll+onNestedPreScroll+onNestedFling+onNestedPreFling

后記

之前在Google模捂、百度自定義Behavior造輪子的時候,剛開始看一篇,覺得不過如此狂男,就這么點東西综看。再看一篇,咦~實現(xiàn)怎么又不一樣了岖食,再來一篇又不一樣了红碑。
本文就是想起一個大綱的作用,輪子再怎么造泡垃,還是這么些個方法析珊。以后再看別人的輪子或者自己造輪子的時候,可以清晰一些蔑穴。

擴展

sidhu眼中的CoordinatorLayout.Behavior(一)
sidhu眼中的CoordinatorLayout.Behavior(二)
sidhu眼中的CoordinatorLayout.Behavior(三)
Material Design系列忠寻,自定義Behavior支持所有View
CoordinatorLayout的使用如此簡單

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市存和,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌哑姚,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倡蝙,死亡現(xiàn)場離奇詭異,居然都是意外死亡绞佩,警方通過查閱死者的電腦和手機寺鸥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門品山,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人肘交,你說我怎么就攤上這事笆载。” “怎么了涯呻?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長涝登。 經(jīng)常有香客問我效诅,道長趟济,這世上最難降的妖魔是什么咽笼? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任褐荷,我火速辦了婚禮,結果婚禮上叛甫,老公的妹妹穿的比我還像新娘杨伙。我一直安慰自己限匣,他們只是感情好,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布锌历。 她就那樣靜靜地躺著峦筒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪物喷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天扇丛,我揣著相機與錄音尉辑,去河邊找鬼。 笑死卓练,一個胖子當著我的面吹牛堤器,可吹牛的內容都是我干的。 我是一名探鬼主播整吆,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拴测!你這毒婦竟也來了府蛇?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎穷遂,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盅惜,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡忌穿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年掠剑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片沸伏。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡动分,死狀恐怖澜公,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情坟乾,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布明吩,位于F島的核電站印荔,受9級特大地震影響,放射性物質發(fā)生泄漏仍律。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望草则。 院中可真熱鬧,春花似錦娶靡、人聲如沸看锉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掌唾,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間凭语,已是汗流浹背撩扒。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工搓谆, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泉手。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓斩萌,卻偏偏與公主長得像屏轰,于是被迫代替她去往敵國和親倘是。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內容