RecyclerView 滑動多選的分析與實(shí)現(xiàn)(二)

方案三: AndroidDragSelect

前文說到,方案三就是分析了方案一的缺點(diǎn)之后食呻,給出了自己的基于 OnItemTouchListener 的實(shí)現(xiàn)方案近范,耦合度低,可以很容易集成進(jìn)現(xiàn)有的項(xiàng)目當(dāng)中集峦。

從自定義 RecyclerView 的方案中可以看到伏社,它是在事件分發(fā)的時候進(jìn)行處理。事實(shí)上塔淤,在這個方法里做計(jì)算感覺上就有點(diǎn)不對摘昌,從源碼來看,RecyclerView 本身是沒有重寫 dispatchTouchEvent() 方法的高蜂,而方案一通過重寫此方法并在這里完成自動滾動的計(jì)算處理聪黎,顯得有些重。

回顧一下事件分發(fā)機(jī)制备恤,其中 dispatchTouchEvent() 用來進(jìn)行事件的分發(fā)稿饰,onInterceptTouchEvent() 被前一個方法調(diào)用锦秒,用來進(jìn)行判斷是否進(jìn)行攔截,真正地處理點(diǎn)擊事件則是在 onTouchEvent() 當(dāng)中湘纵。所以方案三就是利用了 RecyclerView 的 OnItemTouchListener 來對觸摸事件進(jìn)行攔截處理脂崔。

在查看方案三的源碼之前,我們先來看一下 RecyclerView 中的這個 OnItemTouchListener 接口:

OnItemTouchListener

從源碼注釋可以看出梧喷,三個方法是在與 RecyclerView 同一視圖層級上對事件進(jìn)行處理的砌左,也就是在分發(fā)給子 View 之前。

public static interface OnItemTouchListener {
    // public boolean onInterceptTouchEvent(MotionEvent e)
    public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e);
    // public boolean onTouchEvent(MotionEvent e)
    public void onTouchEvent(RecyclerView rv, MotionEvent e);
    // public void requestDisallowInterceptTouchEvent(boolean disallowIntercept)
    public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
}

其中注釋是 ViewGroup 中這三個方法的定義铺敌,可以看到除了 onRequestDisallowInterceptTouchEvent() 方法之外汇歹,其他兩個都有一點(diǎn)小差別。

onInterceptTouchEvent() 這個方法參數(shù)不一樣偿凭,onTouchEvent() 除了參數(shù)不一樣产弹,返回值也變了,變成了無返回值弯囊。那么也就可以猜測痰哨,如果 OnItemTouchListener 處理了點(diǎn)擊事件,就不會再交由父 View 再進(jìn)行處理了匾嘱。到底是不是這樣子呢斤斧,我們通過 RecyclerView 的源碼查看一下。

@Override
public boolean onInterceptTouchEvent(MotionEvent e) {
    // 省略代碼……
    if (dispatchOnItemTouchIntercept(e)) {
        cancelTouch();
        return true;
    }
    // 省略代碼……
}

再進(jìn)一步查看 dispatchOnItemTouchIntercept() 可以看到霎烙,如果添加的 OnItemTouchListener 它攔截了 MotionEvent 事件撬讽,那么就返回 true,此時 RecyclerView 也返回 true 表明攔截了此次事件不再由子 View 進(jìn)行處理悬垃。

再去看看 RecyclerView 的 onTouchEvent() 方法游昼,看是不是同樣地把這個事件交由 OnItemTouchListener 來處理。

@Override
public boolean onTouchEvent(MotionEvent e) {
    // 省略代碼……
    if (dispatchOnItemTouch(e)) {
        cancelTouch();
        return true;
    }
    // 省略代碼……
}

dispatchOnItemTouchIntercept() 類似的尝蠕,如果添加的 OnItemTouchListener 它攔截了 MotionEvent 事件烘豌,那么就由它在 onTouchEvent() 中進(jìn)行處理。這里再稍微看一下 dispatchOnItemTouch() 來解決一個實(shí)踐中的小困惑:OnItemTouchListener 里在 onInterceptTouchEvent() 中對于 MotionEvent.ACTION_DOWN 無論是否返回 true看彼,都不會在 onTouchEvent 里收到此 MotionEvent扇谣。

private boolean dispatchOnItemTouch(MotionEvent e) {
    if (mActiveOnItemTouchListener != null) {
        if (action == MotionEvent.ACTION_DOWN) {
            // Stale state from a previous gesture, we're starting a new one. Clear it.
            mActiveOnItemTouchListener = null;
        } else {
            mActiveOnItemTouchListener.onTouchEvent(this, e);
            if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                // Clean up for the next gesture.
                mActiveOnItemTouchListener = null;
            }
            return true;
        }
    }
    // 省略代碼……
}

可以看到 OnItemTouchListener 中的 onInterceptTouchEvent() 是無法接收到 MotionEvent.ACTION_DOWN 的。

接下來對方案三進(jìn)行分析闲昭,在正式分析之前,先吐槽一句靡挥,大家還是比較喜歡 Start 可以直接拿來用的庫序矩,因?yàn)檫@個方案三 weidongjian/AndroidDragSelect-SimulateGooglePhoto:19 ★ 是個 Demo,參數(shù)跋破、設(shè)定比較粗糙簸淀,導(dǎo)致了星星好少瓶蝴,但其設(shè)計(jì)思路是很好的。而方案二 MFlisar/DragSelectRecyclerView:267 ★ 就是在其基礎(chǔ)上進(jìn)行的改進(jìn)租幕,兩者的共同點(diǎn)就是 OnItemTouchListener舷手,它們幾乎是一樣的。而方案三的滑動多選也就只是通過這一個類來實(shí)現(xiàn)的劲绪,所以下文以方案二代碼來具體分析男窟,它的代碼更規(guī)范一點(diǎn),但是方案二代碼里面大括號是放在行首以及 if 代碼塊沒有大括號讓我很難受……

滾動區(qū)的定義

方案三的滾動區(qū)設(shè)定比較簡單贾富,我就直接上圖了歉眷,其實(shí)這個圖也不對,可能原作者是這樣子想的颤枪,但是源碼里的那個 mTopBound 設(shè)定得不對汗捡。

方案三滾動區(qū)

方案二與方案一的滾動區(qū)設(shè)定一模一樣,只是名稱改了一下畏纲。

方案二滾動區(qū)

自動滾動實(shí)現(xiàn)

方案一使用的是一種通過 Handler 的 postDelayed 方法的延時策略扇住,可以在大約每 25ms 時滾動一下,這里使用大約就是因?yàn)?Handler 的調(diào)度也是需要時間的盗胀。在本方案中艘蹋,使用 Scroller 來實(shí)現(xiàn)流暢地滾動,Scroller 的使用读整、講解可以看《Android 開發(fā)藝術(shù)探索》及網(wǎng)上資料來學(xué)習(xí)簿训。具體就見下面的代碼:

public void startAutoScroll() {
    if (recyclerView == null) {
        return;
    }
    // 創(chuàng)建 Scroller
    if (scroller == null) {
        scroller = ScrollerCompat.create(recyclerView.getContext(),
                new LinearInterpolator());
    }
    if (scroller.isFinished()) {
        recyclerView.removeCallbacks(scrollRun);
        // 設(shè)置參數(shù),這里只有100000是有意義的米间,它代表
        // 手指在滾動區(qū)完全靜止不動時最多可持續(xù)滾動100s
        scroller.startScroll(0, scroller.getCurrY(), 0, 5000, 100000);
        ViewCompat.postOnAnimation(recyclerView, scrollRun);
    }
}

public void stopAutoScroll() {
    if (scroller != null && !scroller.isFinished()) {
        recyclerView.removeCallbacks(scrollRun);
        scroller.abortAnimation();
    }
}

private Runnable scrollRun = new Runnable() {
    @Override
    public void run() {
        if (scroller != null && scroller.computeScrollOffset()) {
            scrollBy(scrollDistance);
            ViewCompat.postOnAnimation(recyclerView, scrollRun);
        }
    }
};

private void scrollBy(int distance) {
    int scrollDistance;
    // 限制滾動速度
    if (distance > 0) {
        scrollDistance = Math.min(distance, MAX_SCROLL_DISTANCE);
    } else {
        scrollDistance = Math.max(distance, -MAX_SCROLL_DISTANCE);
    }
    recyclerView.scrollBy(0, scrollDistance);
    // 自動滾動時的選擇范圍的更新在這里强品,因?yàn)橹辉谧詣訚L動時這兩個才有合法值
    if (lastX != Float.MIN_VALUE && lastY != Float.MIN_VALUE) {
        updateSelectedRange(recyclerView, lastX, lastY);
    }
}

觸摸事件的處理

onInterceptTouchEvent

首先是 onInterceptTouchEvent() 方法,簡單來說屈糊,在這里判斷一下滑動選擇功能是否激活的榛,只在激活時候才攔截觸摸事件;事實(shí)上逻锐,由于長按才 active夫晌,所以攔截不到 MotionEvent.ACTION_DOWN 事件,而它將在長按之后處理接收到的第一個 MotionEvent.ACTION_MOVE 事件昧诱,在這里進(jìn)行參數(shù)的初始化晓淀。后續(xù)再接收到的 MotionEvent 就全部都由 onTouchEvent() 方法來處理了。

@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
    if (!mIsActive || rv.getAdapter().getItemCount() == 0)
        return false;

    int action = MotionEventCompat.getActionMasked(e);
    switch (action) {
        // 事實(shí)上盏档,由于長按才active凶掰,所以以下兩個case是不會收到的
        case MotionEvent.ACTION_POINTER_DOWN:
        case MotionEvent.ACTION_DOWN:
            reset();
            break;
    }
    // 參數(shù)設(shè)定
    mRecyclerView = rv;
    int height = rv.getHeight();
    mTopBoundFrom = mTouchRegionTopOffset;
    mTopBoundTo = mTouchRegionTopOffset + mAutoScrollDistance;
    mBottomBoundFrom = height - mTouchRegionBottomOffset - mAutoScrollDistance;
    mBottomBoundTo = height - mTouchRegionBottomOffset;
    return true;
}

onTouchEvent

這里就是對 Move 事件進(jìn)行自動滾動、更新選擇范圍的處理。

@Override
public void onTouchEvent(RecyclerView rv, MotionEvent e) {
    if (!mIsActive) {
        return;
    }

    int action = MotionEventCompat.getActionMasked(e);
    switch (action) {
        case MotionEvent.ACTION_MOVE:
            // 將此方法提前懦窘,因?yàn)椴榭捶椒梢灾浪惶幚頋L動區(qū)內(nèi)的事件前翎,
            // 包括自動滾動、更新選擇范圍
            processAutoScroll(e);
            if (!mInTopSpot && !mInBottomSpot) {
                // 不在滾動區(qū)內(nèi)的只要更新選擇范圍
                updateSelectedRange(rv, e);
            }
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
        case MotionEvent.ACTION_POINTER_UP:
            // 退出時重置狀態(tài)
            reset();
            break;
    }
}

自動滾動的實(shí)現(xiàn)方式上面已經(jīng)提過了畅涂,這里的的自動滾動處理主要是解決三個問題:

  • 記錄手指最后位置港华,以便在手指不動時還可以更新選擇范圍
  • 手指是否在滾動區(qū)的判斷,以及是否允許滾動區(qū)之上的滾動
  • 根據(jù)手指在滾動區(qū)的位置更新“速度”值
private void processAutoScroll(MotionEvent event) {
    int y = (int) event.getY();
    if (y >= mTopBoundFrom && y <= mTopBoundTo) {
        // 嚴(yán)格位于上滾動區(qū)內(nèi)
        mLastX = event.getX();
        mLastY = event.getY();
        // 計(jì)算速度 = maxSpeed * (手指離上滾動區(qū)下邊界的距離 / 上滾動區(qū)的高度)
        // 往上滾速度為負(fù)數(shù)
        mScrollSpeedFactor = (mTopBoundTo - y) / (float)mAutoScrollDistance;
        mScrollDistance = (int) (mMaxScrollDistance * mScrollSpeedFactor * -1f);
        if (!mInTopSpot) {
            mInTopSpot = true;
            startAutoScroll();
        }
    } else if (mScrollAboveTopRegion && y < mTopBoundFrom) {
        // 允許在上滾動區(qū)之上進(jìn)行自動滾動
        mLastX = event.getX();
        mLastY = event.getY();
        mScrollDistance = mMaxScrollDistance * -1;
        if (!mInTopSpot) {
            mInTopSpot = true;
            startAutoScroll();
        }
    } else if (y >= mBottomBoundFrom && y <= mBottomBoundTo) {
        mLastX = event.getX();
        mLastY = event.getY();
        mScrollSpeedFactor = ((y - mBottomBoundFrom)) / (float)mAutoScrollDistance;
        mScrollDistance = (int) ((float) mMaxScrollDistance * mScrollSpeedFactor);
        if (!mInBottomSpot) {
            mInBottomSpot = true;
            startAutoScroll();
        }
    } else if (mScrollBelowTopRegion && y > mBottomBoundTo) {
        mLastX = event.getX();
        mLastY = event.getY();
        mScrollDistance = mMaxScrollDistance;
        if (!mInBottomSpot) {
            mInBottomSpot = true;
            startAutoScroll();
        }
    } else {
        // 不在滾動區(qū)內(nèi)重置數(shù)據(jù)
        mInBottomSpot = false;
        mInTopSpot = false;
        mLastX = Float.MIN_VALUE;
        mLastY = Float.MIN_VALUE;
        stopAutoScroll();
    }
}

選擇范圍的更新與回調(diào)

上面看到午衰,在自動滾動時進(jìn)行選擇范圍的更新立宜。先來簡單看一下更新范圍的更新方法:

private void updateSelectedRange(RecyclerView rv, float x, float y) {
    View child = rv.findChildViewUnder(x, y);
    if (child != null) {
        int position = rv.getChildAdapterPosition(child);
        if (position != RecyclerView.NO_POSITION && mEnd != position) {
            mEnd = position;
            // 在手指到達(dá)新的條目時再通知更新
            notifySelectRangeChange();
        }
    }
}

可見重點(diǎn)在于 notifySelectRangeChange() 方法。這段代碼可以結(jié)合圖來理解苇经。

坐標(biāo)圖.png

首先明確一些條件:

  • 手指按下的地方為 start赘理,手指當(dāng)前的地方為 end滥朱。但它們的大小關(guān)系不定鞠呈。
  • start 與 end 之間的條目一定是被選中的。
  • newStart 代表現(xiàn)在 start 與 end 兩者中較小者厦幅,newEnd 代表較大者
  • lastStart 和 lastEnd 與 newStart 和 newEnd 含義相同蜘澜,但指的是未更新前的位置

事實(shí)上施流,如果是列表型,那么因?yàn)檫@個范圍不會跳變鄙信,所以 lastStart 和 lastEnd 與 newStart 和 newEnd 只會相差 1瞪醋。但如果是網(wǎng)格型列表,可以上下行滑動時范圍就會跳變装诡。

private void notifySelectRangeChange() {
    if (mSelectListener == null)
        return;
    if (mStart == RecyclerView.NO_POSITION || mEnd == RecyclerView.NO_POSITION)
        return;

    int newStart, newEnd;
    newStart = Math.min(mStart, mEnd);
    newEnd = Math.max(mStart, mEnd);
    if (mLastStart == RecyclerView.NO_POSITION || mLastEnd == RecyclerView.NO_POSITION) {
        if (newEnd - newStart == 1)
            mSelectListener.onSelectChange(newStart, newStart, true);
        else
            mSelectListener.onSelectChange(newStart, newEnd, true);
    } else {
        // 重點(diǎn)看這四句银受,對照著坐標(biāo)圖可以看懂的
        if (newStart > mLastStart)
            mSelectListener.onSelectChange(mLastStart, newStart - 1, false);
        else if (newStart < mLastStart)
            // 此條件下如圖,應(yīng)該把它們之間的選中鸦采。而lastStart之前已經(jīng)選中了宾巍。
            mSelectListener.onSelectChange(newStart, mLastStart - 1, true);
        if (newEnd > mLastEnd)
            mSelectListener.onSelectChange(mLastEnd + 1, newEnd, true);
        else if (newEnd < mLastEnd)
            // 此條件下如圖,應(yīng)該把它們之間的取消選中渔伯。而lastEnd之前已經(jīng)選中了也要取消顶霞。
            mSelectListener.onSelectChange(newEnd + 1, mLastEnd, false);
    }

    mLastStart = newStart;
    mLastEnd = newEnd;
}

那么這個范圍就是通過回調(diào)來通知監(jiān)聽者的。

public interface OnDragSelectListener {
    /**
     * @param start      the newly (un)selected range start
     * @param end        the newly (un)selected range end
     * @param isSelected true, it range got selected, false if not
     */
    void onSelectChange(int start, int end, boolean isSelected);
}

在我的理解中 start 與 end 之間的條目的選中狀態(tài)是指一種狀態(tài)锣吼,它可以代表是選擇條目的狀態(tài)选浑,也可以是不選擇條目的狀態(tài),具體來說就是選中與未選中是兩種狀態(tài)玄叠,我們指定 true 代表某一種狀態(tài)古徒,從而使用 false 代表另一種狀態(tài),因此方法 void onSelectChange(int start, int end, boolean isSelected) 的參數(shù) 3 確切地說應(yīng)該命名為 state读恃。這樣子再重新理解一下上面 notifySelectRangeChange 中的那重要的四句話就會明白它指的是:

  • 指定 start 與 end 之間的條目的狀態(tài)為 A
  • 根據(jù)坐標(biāo)圖描函,將 newStart 和 newEnd 之間的狀態(tài)也置為 A崎苗,另外的則更新狀態(tài)為非 A

以上說有這個狀態(tài)相關(guān)內(nèi)容,如果不是太理解舀寓,可以看看我的實(shí)現(xiàn)方案,它是在對方案二進(jìn)行再次修訂而成的肌蜻,對于此內(nèi)容會有更好的理解互墓。

方案一回調(diào)為 selectRange(initialSelection, lastDraggedIndex, minReached, maxReached) 有 4 個參數(shù),相當(dāng)于把方案二的 lastStart蒋搜、lastEnd篡撵、newStart 和 newEnd 全部傳回來。但實(shí)際上豆挽,傳回之后也是采用同樣的處理方式育谬,因此將選擇與反選的操作放到 OnItemTouchListener 里會更方便。

方案三的使用與效果

到目前為止帮哈,基于這一個單純的回調(diào)膛檀,就可以完成 Google 的選擇策略了。實(shí)現(xiàn)也非常地簡單:

touchListener.setSelectListener(new DragSelectTouchListener.onSelectListener() {
    @Override
    public void onSelectChange(int start, int end, boolean isSelected) {
        //選擇的范圍回調(diào)
        adapter.selectRangeChange(start, end, isSelected);
        actionBar.setTitle(String.valueOf(adapter.getSelectedSize()) + " selected");
    }
});

是不是特別地簡潔娘侍?但是這里有兩點(diǎn)要注意咖刃,

  • 一個是由于回調(diào) onSelectChange() 非常頻繁,所以在 Adapter 里的相應(yīng)的選擇的方法 selectRangeChange 一定要注意判斷一下條目的原先的狀態(tài)憾筏,也就是說如果狀態(tài)沒有改變嚎杨,那么就什么都不做,如果狀態(tài)更改了氧腰,才去更新狀態(tài):notifyItemChanged()枫浙。
  • 另一個是,由于 Item Change 時默認(rèn)帶著動畫古拴,所以在滾動時如果速度比較快箩帚、條目比較寬,就會看到明顯的殘影斤富。如果沒有自定義的動畫可以采用以下方法去除默認(rèn)的 Change 動畫即可:
((SimpleItemAnimator)recyclerView.getItemAnimator()).setSupportsChangeAnimations(false);
// 或者
recyclerView.getItemAnimator().setChangeDuration(0);

如果有動畫的話可能不行膏潮,動畫的內(nèi)容我后續(xù)會進(jìn)行實(shí)踐,并且還會看看使用 OnItemTouchListener 實(shí)現(xiàn)的
Click 事件回調(diào)方案與此滑動方案的兼容性满力。大家可以自行測試焕参、處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末油额,一起剝皮案震驚了整個濱河市叠纷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌潦嘶,老刑警劉巖涩嚣,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡航厚,警方通過查閱死者的電腦和手機(jī)顷歌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來幔睬,“玉大人眯漩,你說我怎么就攤上這事÷槎ィ” “怎么了赦抖?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辅肾。 經(jīng)常有香客問我队萤,道長,這世上最難降的妖魔是什么矫钓? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任要尔,我火速辦了婚禮,結(jié)果婚禮上份汗,老公的妹妹穿的比我還像新娘盈电。我一直安慰自己,他們只是感情好杯活,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布匆帚。 她就那樣靜靜地躺著,像睡著了一般旁钧。 火紅的嫁衣襯著肌膚如雪吸重。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天歪今,我揣著相機(jī)與錄音嚎幸,去河邊找鬼。 笑死寄猩,一個胖子當(dāng)著我的面吹牛嫉晶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播田篇,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼替废,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了泊柬?” 一聲冷哼從身側(cè)響起椎镣,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兽赁,沒想到半個月后状答,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冷守,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年惊科,在試婚紗的時候發(fā)現(xiàn)自己被綠了拍摇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡馆截,死狀恐怖授翻,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情孙咪,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布巡语,位于F島的核電站翎蹈,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏男公。R本人自食惡果不足惜荤堪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望枢赔。 院中可真熱鬧澄阳,春花似錦、人聲如沸踏拜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽速梗。三九已至肮塞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間姻锁,已是汗流浹背枕赵。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留位隶,地道東北人拷窜。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像涧黄,于是被迫代替她去往敵國和親篮昧。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

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