Android UI實現拖拽及滑動處理

一.簡介

???????本文主角是ItemTouchHelper,它是RecyclerView對于item交互處理的一個輔助類,主要用于拖拽以及滑動處理。關于RecyclerView的分析可參考文章RecyclerView顯示及緩存機制
???????以接口實現的方式,起到了配置簡單、邏輯解耦、職責分明的效果,并且支持所有的布局方式。
???????功能包括如下:

image.png

二.功能實現

2.1.實現接口

???????自定義一個類,實現ItemTouchHelper.Callback接口,然后在實現方法中根據需求簡單配置即可,代碼如下:

public class TaskItemTouchHelper extends ItemTouchHelper.Callback {
}

???????ItemTouchHelper.Callback必須實現的3個方法:
???????? getMovementFlags
???????? onMove
???????? onSwiped
???????其他方法還有onSelectedChanged、clearView、getSwipeThreshold等
???????接下來對上述方法進行一一描述分析:

2.1.1.getMovementFlags

???????用于創建交互方式,交互方式分為兩種:
???????a.拖拽:網格布局支持上下左右,列表只支持上下(UP、DOWN)
???????b.滑動:支持上下左右滑動(LEFT、UP、RIGHT、DOWN)
???????方法實現如下:

/**
 * Called first when Callback, it is used to determine action and direction for current
 * function: drag and swipe action
 */
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    //direction:up,down,left,right
    //constants
    // ItemTouchHelper.UP    0x0001
    // ItemTouchHelper.DOWN  0x0010
    // ItemTouchHelper.LEFT  0x0100
    // ItemTouchHelper.RIGHT 0x1000

    //listen for direction of drag
    int dragFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
    //listen for direction of swipe
    int swipeFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;

    return makeMovementFlags(dragFlags, swipeFlags);
}

???????通過makeMovementFlags把結果返回回去,makeMovementFlags接收兩個參數,dragFlagsswipeFlags,即拖拽和滑動組合的標志位。

2.1.2.onMove

???????拖拽時回調,這里我們主要對起始位置和目標位置的item做一個數據交換,然后刷新視圖顯示。

@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder srcHolder,RecyclerView.ViewHolder targetHolder) {
    // call adapter.notifyItemMoved(from,to) continuously when drag process
    if (srcHolder.getItemViewType() != targetHolder.getItemViewType()) {
        return false;
    }
    // call adapter.notifyItemMoved(from,to) continuously when drag process,
    // callback onItemMove to implements class
    return moveListener.onItemMove(srcHolder.getAdapterPosition(),
            targetHolder.getAdapterPosition());
}

???????通過獲取起始位置,不斷調用adapter的notifyItemMoved()對UI進行刷新。

2.1.3.onSwiped

???????滑動時回調,這個回調方法里主要是做數據和視圖的更新操作。

@Override
public void onSwiped(RecyclerView.ViewHolder holder, int direction) {
    //listen for swipe up:1.delete data;2.call adapter.notifyItemRemove(position);
    moveListener.onItemRemove(holder.getAdapterPosition());
}}

???????比如:滑動刪除某個item,監聽到滿足swipe()的閾值后進行刪除操作;

2.2.關聯RecyclerView

???????上面接口實現部分已經實現了,那么如何將ItemTouchHelper與RecyclerView建立關聯呢?
???????接下來就是把這個輔助類綁定到RecyclerView,代碼實現如下:

mTaskAdapter = new TaskAdapter(this, availableList);
ItemTouchHelper.Callback callback = new TaskItemTouchHelper(mTaskAdapter);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
itemTouchHelper.attachToRecyclerView(mRecyclerView);

???????關聯只需要調用attachToRecyclerView就好了。

2.3.設置分割線

???????通過RecyclerView的抽象靜態內部類ItemDecoration來實現;

public class SpaceItemDecoration extends RecyclerView.ItemDecoration {
    private int mItemSpace;
    private int mTopSpace;

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        calculateOutRect(outRect, view, parent);
    }

    public SpaceItemDecoration(int space, int topSpace) {
        this.mItemSpace = space;
        this.mTopSpace = topSpace;
    }

    private void calculateOutRect(Rect outRect, View view, RecyclerView parent) {
        RecyclerView.Adapter adapter = parent.getAdapter();
        if (adapter != null) {
            int count = adapter.getItemCount();
            int position = parent.getChildPosition(view);
            switch (count) {
                case 1:
                    outRect.top = (SCREEN_HEIGHT - ITEM_HEIGHT) / 2;
                    outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
                    break;
                case 2:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    outRect.right = (SCREEN_WIDTH - ITEM_WIDTH) / 2;
                    break;
                case 3:
                case 4:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    if (position < 2) {
                        outRect.right = (SCREEN_WIDTH - ITEM_WIDTH * 2 - mItemSpace) / 2;
                    }
                    outRect.left = mItemSpace;
                    break;
                default:
                    if (position % 2 == 0) {
                        outRect.top = mTopSpace;
                    } else {
                        outRect.top = 0;
                    }
                    if (position < 2) {
                        outRect.right = mItemSpace;
                    }
                    outRect.left = mItemSpace;
                    break;
            }
        }
    }
}

???????代碼實現也比較簡單,通過addItemDecoration()來建立關聯就可以了;

TaskGridLayoutManager gridLayoutManager = new TaskGridLayoutManager(this, 2,
                RecyclerView.HORIZONTAL, true);
mRecyclerView.setLayoutManager(gridLayoutManager);
mRecyclerView.addItemDecoration(new SpaceItemDecoration(100, 150));

???????傳入不同Item之間的間隔,比如:top和gap,在加載Item時會回調getItemOffsets來獲取該Item對應的Rect,然后再內部進行處理最終確定Item對應的Rect就可以了。

2.4.UI強調

???????在平時的滑動或拖拽交互中,為了區分選中的Item,會對其進行強調,比如:選中的item放大、背景高亮等。
???????此處會用到ItemTouchHelper.Callback中的其他兩個方法,onSelectedChangedclearView,在選中時改變視圖顯示,結束時再進行恢復。

2.4.1.onSelectedChanged

???????拖拽或滑動發生改變時回調,這時我們可以修改item的視圖;

@Override
public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
    //judge state for selected
    if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
        swipeAnimation(viewHolder, true);
    }
    super.onSelectedChanged(viewHolder, actionState);
}

???????actionState對應三種state:
???????ACTION_STATE_IDLE:空閑狀態
???????ACTION_STATE_SWIPE:滑動狀態
???????ACTION_STATE_DRAG:拖拽狀態

2.4.2.clearView

???????拖拽或滑動結束時回調,這時我們要把改變后的item視圖恢復到初始狀態

@Override
 public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
    swipeAnimation(viewHolder, false);
    super.clearView(recyclerView, viewHolder);
}

???????在拖拽或滑動開始和結束時,通過swipeAnimation()進行強調:

private void swipeAnimation(RecyclerView.ViewHolder viewHolder, boolean isStart) {
    float[] floatPram;
    if (isStart) {
        floatPram = new float[]{1.0f, 1.05f};
    } else {
        floatPram = new float[]{1.05f, 1.0f};
    }
    AnimatorSet animatorSet = new AnimatorSet();
    ObjectAnimator scaleX = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleX", floatPram);
    ObjectAnimator scaleY = ObjectAnimator.ofFloat(viewHolder.itemView, "scaleY", floatPram);
    scaleX.setDuration(200);
    scaleY.setDuration(200);
    animatorSet.playTogether(scaleX,scaleY);
    animatorSet.start();
}

三.源碼分析

???????在前面的功能實現代碼中,可以看到,在創建完ItenTouchHelper及Callback后,會通過attachToRecyclerView()建立關聯,本文就從該方法開始分析:

3.1.attachToRecyclerView

public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
    if (mRecyclerView == recyclerView) {
        return; // nothing to do
    }
    if (mRecyclerView != null) {
        destroyCallbacks();
    }
    mRecyclerView = recyclerView;
    if (recyclerView != null) {
        final Resources resources = recyclerView.getResources();
        mSwipeEscapeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
        mMaxSwipeVelocity = resources.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
        setupCallbacks();
    }
}

???????從該方法可以看到,主要操作如下:
???????a.首先進行重復判斷,如果是相同的recyclerview,直接返回;
???????b.如果mRecyclerView不為空,調用了destroyCallbacks,在destroyCallbacks里面對mRecyclerView進行了一些移除和回收操作,說明只能綁定到一個RecyclerView;同時,注意這里判斷的主體是mRecyclerView,不是我們傳進來的recyclerView,而且我們傳進來的recyclerView是支持Nullable的,所以我們可以傳個空值走到destroyCallbacks里來做解綁操作
???????c.最后當傳入的recyclerView不為空時,調用setupCallbacks();
???????前面分析到,destroyCallbacks會進行一些移除和回收操作,那么setupCallbacks()應該是執行初始化操作,一起看一下具體實現:

3.2.setupCallbacks

private void setupCallbacks() {
    ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
    mSlop = vc.getScaledTouchSlop();        
    mRecyclerView.addItemDecoration(this);        
    mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);       
    mRecyclerView.addOnChildAttachStateChangeListener(this);       
    startGestureDetection();    
}

???????可以看到,在setupCallbacks()內部會執行了四個方法:
???????1.執行addItemDecoration(this)將自身加入到RecyclerView中的mItemDecorations進行管理,注意:ItemTouchHelper是繼承了RecyclerView.ItemDecoration;
???????2.執行addOnItemTouchListener()對RecyclerView的Item觸摸事件進行監聽處理;
???????3.執行addOnChildAttachStateChangeListener對child View是否移除Window進行回調監聽;
???????4.執行startGestureDetection()來進行手勢識別監聽;

3.3.mOnItemTouchListener

private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
    @Override
    public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
            @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        final int action = event.getActionMasked();
        if (action == MotionEvent.ACTION_DOWN) {
            .................
            if (mSelected == null) {
                final RecoverAnimation animation = findAnimation(event);
                if (animation != null) {
                    .................
                    select(animation.mViewHolder, animation.mActionState);
                    updateDxDy(event, mSelectedFlags, 0);
                }
            }
       } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            mActivePointerId = ACTIVE_POINTER_ID_NONE;
            select(null, ACTION_STATE_IDLE);
        } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
            final int index = event.findPointerIndex(mActivePointerId);
            if (index >= 0) {
                checkSelectForSwipe(action, event, index);
            }
        }
        
   }

    @Override
    public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
        mGestureDetector.onTouchEvent(event);
        ..................
        final int action = event.getActionMasked();
        final int activePointerIndex = event.findPointerIndex(mActivePointerId);
        if (activePointerIndex >= 0) {
            checkSelectForSwipe(action, event, activePointerIndex);
        }
        ViewHolder viewHolder = mSelected;
        if (viewHolder == null) {
             return;
        }
        switch (action) {
            case MotionEvent.ACTION_MOVE: {
                // Find the index of the active pointer and fetch its position
                if (activePointerIndex >= 0) {
                    moveIfNecessary(viewHolder);
                }
                break;
            }
        }

    @Override
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
        if (!disallowIntercept) {
            return;
        }
        select(null, ACTION_STATE_IDLE);
    }
};

???????該方法主要功能就是監聽MotionEvent進行判斷處理,然后執行對不同的手勢進行不同的處理,主要有以下三個方法:
???????a.select
???????b.checkSelectForSwipe
???????c.moveIfNecessary

3.3.1.select

    void select(@Nullable ViewHolder selected, int actionState) {
        if (selected == mSelected && actionState == mActionState) {
            return;
        }
        .........        
        if (mSelected != null) {
            if (prevSelected.itemView.getParent() != null) {
                final float targetTranslateX, targetTranslateY;
                switch (swipeDir) {
                    case LEFT:
                    case RIGHT:
                    case START:
                    case END:
                        targetTranslateY = 0;
                        targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();                        break;
                    ............
                }
                ...........
            } else {
                removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);                  mCallback.clearView(mRecyclerView, prevSelected);
            }
        }
        ..........        
        mCallback.onSelectedChanged(mSelected, mActionState);
        mRecyclerView.invalidate();
    }

???????在該方法內,這里面主要是在拖拽或滑動時對translateX/Y的計算和處理,然后通過mCallback.clearView和mCallback.onSelectedChanged進行回調,最后調用invalidate()實時刷新。

3.3.2.checkSelectForSwipe

void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
    ............//進行過濾判斷
    final ViewHolder vh = findSwipedView(motionEvent);
    
    final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);

    final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
                >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
    if (swipeFlags == 0) {
        return;
    }
    ......................
    //最終滿足swipe的操作
    select(vh, ACTION_STATE_SWIPE);
}

???????該方法時對滑動處理的check,最后也是收斂到select()方法統一處理。

3.3.3.moveIfNecessary

void moveIfNecessary(ViewHolder viewHolder) {
    if (mRecyclerView.isLayoutRequested()) {
        return;
    }
    if (mActionState != ACTION_STATE_DRAG) {
        return;
    }
    .............
    //最終滿足onMove交換的操作
    if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
        // keep target visible
        mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,target, toPosition, x, y);
    }
}

???????該方法時檢查拖拽時是否需要交換item,通過mCallback.onMoved進行回調。

3.4.startGestureDetection

private void startGestureDetection() {
    mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
    mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
                mItemTouchHelperGestureListener);
}

3.4.1.ItemTouchHelperGestureListener

private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
    ...........
    @Override
    public void onLongPress(MotionEvent e) {
        .......
        View child = findChildView(e);
        if (child != null) {
            ViewHolder vh = mRecyclerView.getChildViewHolder(child);
            if (vh != null) {
                ...........
                if (pointerId == mActivePointerId) {
                    ...............
                    if (mCallback.isLongPressDragEnabled()) {
                        select(vh, ACTION_STATE_DRAG);
                    }
                }
            }
        }
    }
}

???????此處主要是對長按事件的處理,最后也是收斂到select()方法統一處理。

簡單總結

???????a.綁定RecyclerView
???????b.注冊觸摸手勢監聽
???????c.根據手勢,先是內部處理各種校驗、位置計算、動畫處理、刷新等,然后回調給ItemTouchHelper.Callback

3.5.ItemDecoration

???????前面講到,本地繼承RecyclerView.ItemDecoration,重寫getItemOffsets可以設置分割線,接下來看一下具體的邏輯實現:

3.5.1.addItemDecoration

public void addItemDecoration(@NonNull ItemDecoration decor) {
    addItemDecoration(decor, -1);
}
public void addItemDecoration(@NonNull ItemDecoration decor, int index) {
    if (mLayout != null) {
        mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"+ " layout");
    }
    if (mItemDecorations.isEmpty()) {
        setWillNotDraw(false);
    }
    if (index < 0) {
        mItemDecorations.add(decor);
    } else {
        mItemDecorations.add(index, decor);
    }
    markItemDecorInsetsDirty();
    requestLayout();
}

???????可以看到,在執行addItemDecoration()會將該decor加入到mItemDecorations中進行管理,從此處可以看到,ItemDecoration可以是多個,可以單獨對Rect的各個區域進行單獨處理,在存儲之后,看一下是如何用到的?
???????我們知道,ItemDecoration是在RecyclerView進行加載ViewHolder時進行使用,因為要確定各個ViewHolder的顯示位置及顯示大小,那根據這個思路就好分析了,這里要說明一下,RecyclerView的ViewHolder的顯示是由LayoutManager來進行管理的,中間的過程就不一一陳述了,可以直接閱讀源碼,此處直接從LayoutManager的measureChild進行分析:

3.5.2.measureChild

public void measureChild(@NonNull View child, int widthUsed, int heightUsed) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    final Rect insets = mRecyclerView.getItemDecorInsetsForChild(child);
    widthUsed += insets.left + insets.right;
    heightUsed += insets.top + insets.bottom;
    final int widthSpec = getChildMeasureSpec(getWidth(), getWidthMode(),
                    getPaddingLeft() + getPaddingRight() + widthUsed, lp.width,
                    canScrollHorizontally());
    final int heightSpec = getChildMeasureSpec(getHeight(), getHeightMode(),
                    getPaddingTop() + getPaddingBottom() + heightUsed, lp.height,
                    canScrollVertically());
    if (shouldMeasureChild(child, widthSpec, heightSpec, lp)) {
        child.measure(widthSpec, heightSpec);
    }
}

???????可以看到,在該方法內會調用RecyclerView的getItemDecorInsetsForChild()來獲取到child view對應的Rect,然后來計算child view需要顯示的區域;

3.5.3.getItemDecorInsetsForChild

Rect getItemDecorInsetsForChild(View child) {
    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
    .................
    final Rect insets = lp.mDecorInsets;
    insets.set(0, 0, 0, 0);
    final int decorCount = mItemDecorations.size();
    for (int i = 0; i < decorCount; i++) {
        mTempRect.set(0, 0, 0, 0);
        mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
        insets.left += mTempRect.left;
        insets.top += mTempRect.top;
        insets.right += mTempRect.right;
        insets.bottom += mTempRect.bottom;
    }
    lp.mInsetsDirty = false;
    return insets;
}

???????可以看到,在該方法內會遍歷mItemDecorations,通過ItemDecoration的getItemOffsets來獲取對應的mTempRect,在賦值給insets,最終返回;

3.5.4.getItemOffsets

???????在本地實現getItemOffsets時,會先調用super.getItemOffsets(),主要用來對Rect進行置空,主要作用是確保每個Item不會相互影響,即每個item的顯示區域可以隨意定義;

@Deprecated
public void getItemOffsets(@NonNull Rect outRect, int itemPosition,@NonNull RecyclerView parent) {
    outRect.set(0, 0, 0, 0);
}

public void getItemOffsets(@NonNull Rect outRect, @NonNull View view,@NonNull RecyclerView parent, @NonNull State state) {
   getItemOffsets(outRect, ((LayoutParams)view.getLayoutParams()).getViewLayoutPosition(),
                    parent);
}

???????以上就是RecycleView UI實現拖拽即滑動處理的實現及部分源碼實現分析!

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

推薦閱讀更多精彩內容