ViewDragHelper詳解

2013年谷歌i/o大會(huì)上介紹了兩個(gè)新的layout: SlidingPaneLayout和DrawerLayout,現(xiàn)在這倆個(gè)類被廣泛的運(yùn)用,其實(shí)研究他們的源碼你會(huì)發(fā)現(xiàn)這兩個(gè)類都運(yùn)用了ViewDragHelper來(lái)處理拖動(dòng)。ViewDragHelper是framework中不為人知卻非常有用的一個(gè)工具

ViewDragHelper解決了android中手勢(shì)處理過(guò)于復(fù)雜的問(wèn)題,在DrawerLayout出現(xiàn)之前,側(cè)滑菜單都是由第三方開(kāi)源代碼實(shí)現(xiàn)的,其中著名的當(dāng)屬?MenuDrawer?,MenuDrawer重寫(xiě)onTouchEvent方法來(lái)實(shí)現(xiàn)側(cè)滑效果,代碼量很大,實(shí)現(xiàn)邏輯也需要很大的耐心才能看懂。如果每個(gè)開(kāi)發(fā)人員都從這么原始的步奏開(kāi)始做起,那對(duì)于安卓生態(tài)是相當(dāng)不利的。所以說(shuō)ViewDragHelper等的出現(xiàn)反映了安卓開(kāi)發(fā)框架已經(jīng)開(kāi)始向成熟的方向邁進(jìn)。

本文先介紹ViewDragHelper的基本用法,然后介紹一個(gè)能真正體現(xiàn)ViewDragHelper實(shí)用性的例子。

ViewDragHelper

其實(shí)ViewDragHelper并不是第一個(gè)用于分析手勢(shì)處理的類,gesturedetector也是,但是在和拖動(dòng)相關(guān)的手勢(shì)分析方面gesturedetector只能說(shuō)是勉為其難。

關(guān)于ViewDragHelper有如下幾點(diǎn):

? ?ViewDragHelper.Callback是連接ViewDragHelper與view之間的橋梁(這個(gè)view一般是指擁子view的容器即parentView);

? ?ViewDragHelper的實(shí)例是通過(guò)靜態(tài)工廠方法創(chuàng)建的;

? ?你能夠指定拖動(dòng)的方向;

? ?ViewDragHelper可以檢測(cè)到是否觸及到邊緣;

? ?ViewDragHelper并不是直接作用于要被拖動(dòng)的View,而是使其控制的視圖容器中的子View可以被拖動(dòng),如果要指定某個(gè)子view的行為,需要在Callback中想辦法;

? ?ViewDragHelper的本質(zhì)其實(shí)是分析onInterceptTouchEvent和onTouchEvent的MotionEvent參數(shù),然后根據(jù)分析的結(jié)果去改變一個(gè)容器中被拖動(dòng)子View的位置(?通過(guò)offsetTopAndBottom(int offset)和offsetLeftAndRight(int offset)方法?),他能在觸摸的時(shí)候判斷當(dāng)前拖動(dòng)的是哪個(gè)子View;

? ?雖然ViewDragHelper的實(shí)例方法 ViewDragHelper create(ViewGroup forParent, Callback cb) 可以指定一個(gè)被ViewDragHelper處理拖動(dòng)事件的對(duì)象 。

用法:

下面部分內(nèi)容基本是Each Navigation Drawer Hides a ViewDragHelper一文的翻譯。

1.ViewDragHelper的初始化

ViewDragHelper一般用在一個(gè)自定義ViewGroup的內(nèi)部,比如下面自定義了一個(gè)繼承于LinearLayout的DragLayout,DragLayout內(nèi)部有一個(gè)子view mDragView作為成員變量:

public?class?DragLayout?extends?LinearLayout?{

private?final?ViewDragHelper?mDragHelper;

private?View?mDragView;

public?DragLayout(Context?context)?{

??this(context,?null);

}

public?DragLayout(Context?context,?AttributeSet?attrs)?{

??this(context,?attrs,?0);

}

public?DragLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

}

創(chuàng)建一個(gè)帶有回調(diào)接口的ViewDragHelper

public?DragLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

??mDragHelper?=?ViewDragHelper.create(this,?1.0f,?new?DragHelperCallback());

}

其中1.0f是敏感度參數(shù)參數(shù)越大越敏感。第一個(gè)參數(shù)為this,表示該類生成的對(duì)象,他是ViewDragHelper的拖動(dòng)處理對(duì)象,必須為ViewGroup。

要讓ViewDragHelper能夠處理拖動(dòng)需要將觸摸事件傳遞給ViewDragHelper,這點(diǎn)和gesturedetector是一樣的:

@Override

public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{

??final?int?action?=?MotionEventCompat.getActionMasked(ev);

??if?(action?==?MotionEvent.ACTION_CANCEL?||?action?==?MotionEvent.ACTION_UP)?{

??????mDragHelper.cancel();

??????return?false;

??}

??return?mDragHelper.shouldInterceptTouchEvent(ev);

}

@Override

public?boolean?onTouchEvent(MotionEvent?ev)?{

??mDragHelper.processTouchEvent(ev);

??return?true;

}

接下來(lái),你就可以在回調(diào)中處理各種拖動(dòng)行為了。

2.拖動(dòng)行為的處理

處理橫向的拖動(dòng):

在DragHelperCallback中實(shí)現(xiàn)clampViewPositionHorizontal方法, 并且返回一個(gè)適當(dāng)?shù)臄?shù)值就能實(shí)現(xiàn)橫向拖動(dòng)效果,clampViewPositionHorizontal的第二個(gè)參數(shù)是指當(dāng)前拖動(dòng)子view應(yīng)該到達(dá)的x坐標(biāo)。所以按照常理這個(gè)方法原封返回第二個(gè)參數(shù)就可以了,但為了讓被拖動(dòng)的view遇到邊界之后就不在拖動(dòng),對(duì)返回的值做了更多的考慮。

@Override

public?int?clampViewPositionHorizontal(View?child,?int?left,?int?dx)?{

??Log.d("DragLayout",?"clampViewPositionHorizontal?"?+?left?+?","?+?dx);

??final?int?leftBound?=?getPaddingLeft();

??final?int?rightBound?=?getWidth()?-?mDragView.getWidth();

??final?int?newLeft?=?Math.min(Math.max(left,?leftBound),?rightBound);

??return?newLeft;

}

同上,處理縱向的拖動(dòng):

在DragHelperCallback中實(shí)現(xiàn)clampViewPositionVertical方法,實(shí)現(xiàn)過(guò)程同clampViewPositionHorizontal

@Override

public?int?clampViewPositionVertical(View?child,?int?top,?int?dy)?{

??final?int?topBound?=?getPaddingTop();

??final?int?bottomBound?=?getHeight()?-?mDragView.getHeight();

??final?int?newTop?=?Math.min(Math.max(top,?topBound),?bottomBound);

??return?newTop;

}

clampViewPositionHorizontal 和 clampViewPositionVertical必須要重寫(xiě),因?yàn)槟J(rèn)它返回的是0。事實(shí)上我們?cè)谶@兩個(gè)方法中所能做的事情很有限。 個(gè)人覺(jué)得這兩個(gè)方法的作用就是給了我們重新定義目的坐標(biāo)的機(jī)會(huì)。

通過(guò)DragHelperCallback的tryCaptureView方法的返回值可以決定一個(gè)parentview中哪個(gè)子view可以拖動(dòng),現(xiàn)在假設(shè)有兩個(gè)子views (mDragView1和mDragView2) ?,如下實(shí)現(xiàn)tryCaptureView之后,則只有mDragView1是可以拖動(dòng)的。

@Override

public?boolean?tryCaptureView(View?child,?int?pointerId)?{

??return?child?==?mDragView1;

}

滑動(dòng)邊緣:

分為滑動(dòng)左邊緣還是右邊緣:EDGE_LEFT和EDGE_RIGHT,下面的代碼設(shè)置了可以處理滑動(dòng)左邊緣:

mDragHelper.setEdgeTrackingEnabled(ViewDragHelper.EDGE_LEFT);

假如如上設(shè)置,onEdgeTouched方法會(huì)在左邊緣滑動(dòng)的時(shí)候被調(diào)用,這種情況下一般都是沒(méi)有和子view接觸的情況。

@Override

public?void?onEdgeTouched(int?edgeFlags,?int?pointerId)?{

????super.onEdgeTouched(edgeFlags,?pointerId);

????Toast.makeText(getContext(),?"edgeTouched",?Toast.LENGTH_SHORT).show();

}

如果你想在邊緣滑動(dòng)的時(shí)候根據(jù)滑動(dòng)距離移動(dòng)一個(gè)子view,可以通過(guò)實(shí)現(xiàn)onEdgeDragStarted方法,并在onEdgeDragStarted方法中手動(dòng)指定要移動(dòng)的子View

@Override

public?void?onEdgeDragStarted(int?edgeFlags,?int?pointerId)?{

????mDragHelper.captureChildView(mDragView2,?pointerId);

}

ViewDragHelper讓我們很容易實(shí)現(xiàn)一個(gè)類似于YouTube視頻瀏覽效果的控件,效果如下:

代碼中的關(guān)鍵點(diǎn):

1.tryCaptureView返回了唯一可以被拖動(dòng)的header view;

2.拖動(dòng)范圍drag range的計(jì)算是在onLayout中完成的;

3.注意在onInterceptTouchEvent和onTouchEvent中使用的ViewDragHelper的若干方法;

4.在computeScroll中使用continueSettling方法(因?yàn)閂iewDragHelper使用了scroller)?

5.smoothSlideViewTo方法來(lái)完成拖動(dòng)結(jié)束后的慣性操作。

需要注意的是代碼仍然有很大改進(jìn)空間。

activity_main.xml

????????xmlns:android="http://schemas.android.com/apk/res/android"

????????android:layout_width="match_parent"

????????android:layout_height="match_parent">


????????????android:id="@+id/listView"

????????????android:layout_width="match_parent"

????????????android:layout_height="match_parent"

????????????android:tag="list"

????????????/>


????????????android:layout_width="match_parent"

????????????android:layout_height="match_parent"

????????????android:id="@+id/youtubeLayout"

????????????android:orientation="vertical"

????????????android:visibility="visible">


????????????????android:id="@+id/viewHeader"

????????????????android:layout_width="match_parent"

????????????????android:layout_height="128dp"

????????????????android:fontFamily="sans-serif-thin"

????????????????android:textSize="25sp"

????????????????android:tag="text"

????????????????android:gravity="center"

????????????????android:textColor="@android:color/white"

????????????????android:background="#AD78CC"/>


????????????????android:id="@+id/viewDesc"

????????????????android:tag="desc"

????????????????android:textSize="35sp"

????????????????android:gravity="center"

????????????????android:text="Loreum?Loreum"

????????????????android:textColor="@android:color/white"

????????????????android:layout_width="match_parent"

????????????????android:layout_height="match_parent"

????????????????android:background="#FF00FF"/>


YoutubeLayout.java

public?class?YoutubeLayout?extends?ViewGroup?{

private?final?ViewDragHelper?mDragHelper;

private?View?mHeaderView;

private?View?mDescView;

private?float?mInitialMotionX;

private?float?mInitialMotionY;

private?int?mDragRange;

private?int?mTop;

private?float?mDragOffset;

public?YoutubeLayout(Context?context)?{

??this(context,?null);

}

public?YoutubeLayout(Context?context,?AttributeSet?attrs)?{

??this(context,?attrs,?0);

}

@Override

protected?void?onFinishInflate()?{

????mHeaderView?=?findViewById(R.id.viewHeader);

????mDescView?=?findViewById(R.id.viewDesc);

}

public?YoutubeLayout(Context?context,?AttributeSet?attrs,?int?defStyle)?{

??super(context,?attrs,?defStyle);

??mDragHelper?=?ViewDragHelper.create(this,?1f,?new?DragHelperCallback());

}

public?void?maximize()?{

????smoothSlideTo(0f);

}

boolean?smoothSlideTo(float?slideOffset)?{

????final?int?topBound?=?getPaddingTop();

????int?y?=?(int)?(topBound?+?slideOffset?*?mDragRange);

????if?(mDragHelper.smoothSlideViewTo(mHeaderView,?mHeaderView.getLeft(),?y))?{

????????ViewCompat.postInvalidateOnAnimation(this);

????????return?true;

????}

????return?false;

}

private?class?DragHelperCallback?extends?ViewDragHelper.Callback?{

??@Override

??public?boolean?tryCaptureView(View?child,?int?pointerId)?{

????????return?child?==?mHeaderView;

??}

????@Override

??public?void?onViewPositionChanged(View?changedView,?int?left,?int?top,?int?dx,?int?dy)?{

??????mTop?=?top;

??????mDragOffset?=?(float)?top?/?mDragRange;

????????mHeaderView.setPivotX(mHeaderView.getWidth());

????????mHeaderView.setPivotY(mHeaderView.getHeight());

????????mHeaderView.setScaleX(1?-?mDragOffset?/?2);

????????mHeaderView.setScaleY(1?-?mDragOffset?/?2);

????????mDescView.setAlpha(1?-?mDragOffset);

????????requestLayout();

??}

??@Override

??public?void?onViewReleased(View?releasedChild,?float?xvel,?float?yvel)?{

??????int?top?=?getPaddingTop();

??????if?(yvel?>?0?||?(yvel?==?0?&&?mDragOffset?>?0.5f))?{

??????????top?+=?mDragRange;

??????}

??????mDragHelper.settleCapturedViewAt(releasedChild.getLeft(),?top);

??}

??@Override

??public?int?getViewVerticalDragRange(View?child)?{

??????return?mDragRange;

??}

??@Override

??public?int?clampViewPositionVertical(View?child,?int?top,?int?dy)?{

??????final?int?topBound?=?getPaddingTop();

??????final?int?bottomBound?=?getHeight()?-?mHeaderView.getHeight()?-?mHeaderView.getPaddingBottom();

??????final?int?newTop?=?Math.min(Math.max(top,?topBound),?bottomBound);

??????return?newTop;

??}

}

@Override

public?void?computeScroll()?{

??if?(mDragHelper.continueSettling(true))?{

??????ViewCompat.postInvalidateOnAnimation(this);

??}

}

@Override

public?boolean?onInterceptTouchEvent(MotionEvent?ev)?{

??final?int?action?=?MotionEventCompat.getActionMasked(ev);

??if?((?action?!=?MotionEvent.ACTION_DOWN))?{

??????mDragHelper.cancel();

??????return?super.onInterceptTouchEvent(ev);

??}

??if?(action?==?MotionEvent.ACTION_CANCEL?||?action?==?MotionEvent.ACTION_UP)?{

??????mDragHelper.cancel();

??????return?false;

??}

??final?float?x?=?ev.getX();

??final?float?y?=?ev.getY();

??boolean?interceptTap?=?false;

??switch?(action)?{

??????case?MotionEvent.ACTION_DOWN:?{

??????????mInitialMotionX?=?x;

??????????mInitialMotionY?=?y;

????????????interceptTap?=?mDragHelper.isViewUnder(mHeaderView,?(int)?x,?(int)?y);

??????????break;

??????}

??????case?MotionEvent.ACTION_MOVE:?{

??????????final?float?adx?=?Math.abs(x?-?mInitialMotionX);

??????????final?float?ady?=?Math.abs(y?-?mInitialMotionY);

??????????final?int?slop?=?mDragHelper.getTouchSlop();

??????????if?(ady?>?slop?&&?adx?>?ady)?{

??????????????mDragHelper.cancel();

??????????????return?false;

??????????}

??????}

??}

??return?mDragHelper.shouldInterceptTouchEvent(ev)?||?interceptTap;

}

@Override

public?boolean?onTouchEvent(MotionEvent?ev)?{

??mDragHelper.processTouchEvent(ev);

??final?int?action?=?ev.getAction();

????final?float?x?=?ev.getX();

????final?float?y?=?ev.getY();

????boolean?isHeaderViewUnder?=?mDragHelper.isViewUnder(mHeaderView,?(int)?x,?(int)?y);

????switch?(action?&?MotionEventCompat.ACTION_MASK)?{

??????case?MotionEvent.ACTION_DOWN:?{

??????????mInitialMotionX?=?x;

??????????mInitialMotionY?=?y;

??????????break;

??????}

??????case?MotionEvent.ACTION_UP:?{

??????????final?float?dx?=?x?-?mInitialMotionX;

??????????final?float?dy?=?y?-?mInitialMotionY;

??????????final?int?slop?=?mDragHelper.getTouchSlop();

??????????if?(dx?*?dx?+?dy?*?dy?<?slop?*?slop?&&?isHeaderViewUnder)?{

??????????????if?(mDragOffset?==?0)?{

??????????????????smoothSlideTo(1f);

??????????????}?else?{

??????????????????smoothSlideTo(0f);

??????????????}

??????????}

??????????break;

??????}

??}

??return?isHeaderViewUnder?&&?isViewHit(mHeaderView,?(int)?x,?(int)?y)?||?isViewHit(mDescView,?(int)?x,?(int)?y);

}

private?boolean?isViewHit(View?view,?int?x,?int?y)?{

????int[]?viewLocation?=?new?int[2];

????view.getLocationOnScreen(viewLocation);

????int[]?parentLocation?=?new?int[2];

????this.getLocationOnScreen(parentLocation);

????int?screenX?=?parentLocation[0]?+?x;

????int?screenY?=?parentLocation[1]?+?y;

????return?screenX?>=?viewLocation[0]?&&?screenX?<?viewLocation[0]?+?view.getWidth()?&&

????????????screenY?>=?viewLocation[1]?&&?screenY?<?viewLocation[1]?+?view.getHeight();

}

@Override

protected?void?onMeasure(int?widthMeasureSpec,?int?heightMeasureSpec)?{

????measureChildren(widthMeasureSpec,?heightMeasureSpec);

????int?maxWidth?=?MeasureSpec.getSize(widthMeasureSpec);

????int?maxHeight?=?MeasureSpec.getSize(heightMeasureSpec);

????setMeasuredDimension(resolveSizeAndState(maxWidth,?widthMeasureSpec,?0),

????????????resolveSizeAndState(maxHeight,?heightMeasureSpec,?0));

}

@Override

protected?void?onLayout(boolean?changed,?int?l,?int?t,?int?r,?int?b)?{

??mDragRange?=?getHeight()?-?mHeaderView.getHeight();

????mHeaderView.layout(

????????????0,

????????????mTop,

????????????r,

????????????mTop?+?mHeaderView.getMeasuredHeight());

????mDescView.layout(

????????????0,

????????????mTop?+?mHeaderView.getMeasuredHeight(),

????????????r,

????????????mTop??+?b);

}

不管是menudrawer 還是本文實(shí)現(xiàn)的DragLayout都體現(xiàn)了一種設(shè)計(jì)哲學(xué),即可拖動(dòng)的控件都是封裝在一個(gè)自定義的Layout中的,為什么這樣做?為什么不直接將ViewDragHelper.create(this, 1f, new DragHelperCallback())中的this替換成任何已經(jīng)布局好的容器,這樣這個(gè)容器中的子View就能被拖動(dòng)了,而往往是單獨(dú)定義一個(gè)Layout來(lái)處理?個(gè)人認(rèn)為如果在一般的布局中去拖動(dòng)子view并不會(huì)出現(xiàn)什么問(wèn)題,只是原本規(guī)則的世界被打亂了,而單獨(dú)一個(gè)Layout來(lái)完成拖動(dòng),無(wú)非是說(shuō),他本來(lái)就沒(méi)有什么規(guī)則可言,拖動(dòng)一下也無(wú)妨。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評(píng)論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評(píng)論 3 418
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 176,463評(píng)論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 63,009評(píng)論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評(píng)論 6 410
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 55,218評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 42,436評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 34,659評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 35,917評(píng)論 1 286
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,687評(píng)論 3 392
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評(píng)論 2 374

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