Android ItemDecoration 實現分組索引列表

0
1

我們要實現的效果主要涉及三個部分:

  • 分組 GroupHeader
  • 分割線
  • SideBar

前兩個部分涉及到一個ItemDecoration類,也是我們接下來的重點,該類是RecyclerView的一個抽象靜態內部類,主要作用就是給RecyclerViewItemView繪制額外的裝飾效果,例如給RecyclerView添加分割線。

使用ItemDecoration時需要繼承該類,根據需求可以重寫如下三個方法,其它的方法已經deprecated了:

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

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
    }
}

然后將其添加到RecyclerView中:

recyclerView.addItemDecoration(new GroupHeaderItemDecoration())

了解這個三個方法的作用,這樣才能更好的實現我們想要的功能:

1、getItemOffsets()

給指定的ItemView設置偏移量,具體怎么設置呢,咱們看圖說話:

2

圖中左邊的是原始RecyclerView列表,右邊是設置了ItemView偏移量的列表,其實相當于在ItemView外部添加了一個矩形區域
其中lefttoprightbottom就是ItemView在四個方向的偏移量,對應的設置代碼如下:

outRect.set(left, top, right, bottom)

在我們的分組索引列表中,只需要對ItemView設置頂部的偏移量,其它三個偏移量為0即可。這樣就可以在ItemView頂部預留出一定高度的區域,如下圖:

3

2、onDraw()

getItemOffsets()方法中,我們設置了偏移量,進而得到了對應的偏移區域,接下來在onDraw()中就可以給ItemView繪制裝飾效果了,所以我們在該方法中將分組索引列表中的GroupHeader的內容繪制在ItemView頂部偏移區域里。也就是繪制前邊 gif 圖里的A、B、C... GroupHeader,雖然看起來像一個個獨立的ItemView,但并不是的哦!

注意該繪制操作會在ItemViewonDraw()前完成的!

3、onDrawOver()

該方法同樣也是用來繪制的,但是它在ItemDecorationonDraw()方法和ItemViewonDraw()完成后才執行。所以其繪制的內容會遮擋在RecyclerView上,因此我們可以在該方法中繪制分組索引列表中懸浮的GroupHeader,也就是在列表頂部隨著列表滾動切換的GroupHeader

一、分組GroupHeader

三個方法的作用已經解釋完了,接下來就是代碼實現我們的效果了:

首先保證RecyclerView的數據源已經按照某種規律進行了分組排序,具體什么規律你說了算,我們例子中按照數據源中指定字段的值的首字母升序排列,也就是常見通訊錄的排序方式。然后在每個data中保存需要在GroupHeader上顯示的內容,可以使用tag字段,我們這里保存的是對應的首字母。這里沒必要將整個數據源設置到ItemDecoration里邊,所以我們只需要提取排序后數據源的tag保存到列表中,然后設置到ItemDecoration里邊,后邊的操作就依賴設置的數據源了,根據tag的異同來決定是否繪制GroupHeader等。

上邊已經分析了,GroupHeader只在列表中每組數據對應的第一個ItemView頂部顯示,只需要對ItemView設置頂部的偏移量即可:

public class GroupHeaderItemDecoration extends RecyclerView.ItemDecoration {
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        RecyclerView.LayoutManager manager = parent.getLayoutManager();

        //只處理線性垂直類型的列表
        if ((manager instanceof LinearLayoutManager)
                && LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) {
            return;
        }

        int position = parent.getChildAdapterPosition(view);
        //ItemView的position==0 或者 當前ItemView的data的tag和上一個ItemView的不相等,則為當前ItemView設置top 偏移量
        if (!Utils.listIsEmpty(tags) && (position == 0 || !tags.get(position).equals(tags.get(position - 1)))) {
            outRect.set(0, groupHeaderHeight, 0, 0);
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
    }
}

其中tags就是我們設置到ItemDecoration的數據源,是一個String集合。groupHeaderHeight就是ItemView的頂部偏移量。

之后就是在ItemView的頂部偏移區域繪制GroupHeader了:

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

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        for (int i = 0; i < parent.getChildCount(); i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            String tag = tags.get(position);
            //和getItemOffsets()里的條件判斷類似,開始繪制分組的GroupHeader
            if (!Utils.listIsEmpty(tags) && (position == 0 || !tag.equals(tags.get(position - 1)))) {
                drawGroupHeader(c, parent, view, tag);
            }
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
    }

    private void drawGroupHeader(Canvas c, RecyclerView parent, View view, String tag) {
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int bottom = view.getTop() - params.topMargin;
        int top = bottom - groupHeaderHeight;
        c.drawRect(left, top, right, bottom, mPaint);
        int x = left + groupHeaderLeftPadding;
        int y = top + (groupHeaderHeight + Utils.getTextHeight(mTextPaint, tag)) / 2;
        c.drawText(tag, x, y, mTextPaint);
    }
}

繪制GroupHeader就是Canvasc操作,先繪制一個矩形框,再繪制相應的文字,當然繪制圖片也是沒問題的,其中groupHeaderLeftPadding是個可配置字段,代表繪制的文字或圖片到列表左邊沿的距離,也可以理解為GroupHeader的左padding

最后就是懸浮在頂部的GroupHeader繪制了:

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

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
        if (!show) {
            return;
        }
        //列表第一個可見的ItemView位置
        int position = ((LinearLayoutManager) (parent.getLayoutManager())).findFirstVisibleItemPosition();
        String tag = tags.get(position);
        View view = parent.findViewHolderForAdapterPosition(position).itemView;
        //當前ItemView的data的tag和下一個itemView的不相等,則代表將要重新繪制懸停的GroupHeader
        boolean flag = false;
        if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && !tag.equals(tags.get(position + 1))) {
            //如果第一個可見ItemView的底部坐標小于groupHeaderHeight,則執行Canvas向上位移操作
            if (view.getBottom() <= groupHeaderHeight) {
                c.save();
                flag = true;
                c.translate(0, view.getHeight() + view.getTop() - groupHeaderHeight);
            }
        }

        drawSuspensionGroupHeader(c, parent, tag);

        if (flag) {
            c.restore();
        }
    }

    private void drawSuspensionGroupHeader(Canvas c, RecyclerView parent, String tag) {
        int left = parent.getPaddingLeft();
        int right = parent.getWidth() - parent.getPaddingRight();
        int bottom = groupHeaderHeight;
        int top = 0;
        c.drawRect(left, top, right, bottom, mPaint);
        int x = left + groupHeaderLeftPadding;
        int y = top + (groupHeaderHeight + Utils.getTextHeight(mTextPaint, tag)) / 2;
        c.drawText(tag, x, y, mTextPaint);
    }
}

繪制操作和onDraw中的類似,gif 中有一個懸浮GroupHeader上移的動畫,就是通過Canvas位移來實現的,注意在Canvas位移的前后進行save()restore()操作。

我們給GroupHeaderItemDecoration提供了設置GroupHeader左padding、高度、背景色、文字顏色、尺寸、以及是否顯示頂部懸浮GroupHeader的方法,方便使用。

關于繪制操作需要注意的是,GroupHeader所在的偏移區域和ItemView是相互獨立的,不要把GroupHeader當做ItemView的一部分哦。到這里GroupHeader的功能就實現了,只需要將GroupHeaderItemDecoration添加到RecyclerView即可。

至于如何通過layout或者View來實現GroupHeader,做過一些嘗試,效果都不理想,期待大家的好想法哦!

這里先用一個接口,對外提供自定義繪制GroupHeader的方法:

public interface OnDrawItemDecorationListener {
    /**
     * 繪制GroupHeader 
     * @param c
     * @param paint 繪制GroupHeader區域的paint
     * @param textPaint 繪制文字的paint
     * @param params    共四個值left、top、right、bottom 代表GroupHeader所在區域的四個坐標值
     * @param position  原始數據源中的position
     */
    void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position);
     /**
      * 繪制懸浮在列表頂部的GroupHeader 
      */
    void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position);
}

二、分割線

現在RecyclerView還差一個分割線,當前最笨的辦法可以在ItemView的布局文件中設置,既然系統都提供了ItemDecoration,那用它來優雅的實現為何不可呢,我們只需要給列表中每組數據除了最后一項數據對應的ItemView之外的添加分割線即可,也就是不給每組數據對應的最后一個ItemView添加分割線。很簡單,直接上核心代碼:

public class DivideItemDecoration extends RecyclerView.ItemDecoration {
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        super.getItemOffsets(outRect, view, parent, state);
        RecyclerView.LayoutManager manager = parent.getLayoutManager();

        //只處理線性垂直類型的列表
        if ((manager instanceof LinearLayoutManager)
                && LinearLayoutManager.VERTICAL != ((LinearLayoutManager) manager).getOrientation()) {
            return;
        }

        int position = parent.getChildAdapterPosition(view);
        if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) {
            //當前ItemView的data的tag和下一個ItemView的不相等,則為當前ItemView設置bottom 偏移量
            outRect.set(0, 0, 0, divideHeight);
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDraw(c, parent, state);
        for (int i = 0; i < parent.getChildCount(); i++) {
            View view = parent.getChildAt(i);
            int position = parent.getChildAdapterPosition(view);
            //和getItemOffsets()里的條件判斷類似
            if (!Utils.listIsEmpty(tags) && (position + 1) < tags.size() && tags.get(position).equals(tags.get(position + 1))) {
                drawDivide(c, parent, view);
            }
        }
    }

    @Override
    public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
        super.onDrawOver(c, parent, state);
    }

    private void drawDivide(Canvas c, RecyclerView parent, View view) {
        RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
        int left = parent.getPaddingLeft();
        int right = parent.getWidth();
        int top = view.getBottom() + params.bottomMargin;
        int bottom = top + divideHeight;
        c.drawRect(left, top, right, bottom, mPaint);
    }
}

三、SideBar

SideBar就是 gif 圖右邊的垂直字符條,是一個自定義View。手指觸摸選中一個字符,則列表會滾動到對應的分組頭部位置。實現起來也蠻簡單的,核心代碼如下:

public class SideBar extends View {
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);

        //重新計算SideBar寬高
        if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.AT_MOST) {
            getMaxTextSize();
            if (heightMode == MeasureSpec.AT_MOST) {
                heightSize = (maxHeight + 15) * indexArray.length;
            }

            if (widthMode == MeasureSpec.AT_MOST) {
                widthSize = maxWidth + 10;
            }
        }

        setMeasuredDimension(widthSize, heightSize);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (int i = 0; i < indexArray.length; i++) {
            String index = indexArray[i];
            float x = (mWidth - mTextPaint.measureText(index)) / 2;
            float y = mMarginTop + mHeight * i + (mHeight + Utils.getTextHeight(mTextPaint, index)) / 2;
            //繪制字符
            canvas.drawText(index, x, y, mTextPaint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
                // 選中字符的下標
                int pos = (int) ((event.getY() - mMarginTop) / mHeight);
                if (pos == lastPos) {
                    return true;
                }
                if (pos >= 0 && pos < indexArray.length) {
                    lastPos = pos;
                    setBackgroundColor(TOUCH_COLOR);
                    if (onSideBarTouchListener != null) {
                        for (int i = 0; i < tags.size(); i++) {
                            if (indexArray[pos].equals(tags.get(i))) {
                                onSideBarTouchListener.onTouch(indexArray[pos], i);
                                break;
                            }
                            if (i == tags.size() - 1) {
                                onSideBarTouchListener.onTouch(indexArray[pos], -1);
                            }
                        }
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                lastPos = -1;
                setBackgroundColor(UNTOUCH_COLOR);
                if (onSideBarTouchListener != null) {
                    onSideBarTouchListener.onTouchEnd();
                }
                break;
        }

        return true;
    }
}

onMeasure()方法里,如果SideBar的寬、高測量模式為MeasureSpec.AT_MOST則重新計算SideBar的寬、高。onDraw()方法則是遍歷索引數組,并繪制字符索引。在onTouchEvent()方法里,我們根據手指在SideBar上觸摸坐標點的y值,計算出觸摸的相應字符,以便在OnSideBarTouchListener接口進行后續操作,例如列表的跟隨滾動等等。

四、實例

前邊已經完成了三大核心功能,最后來愉快的使用下吧:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.list);
        SideBar sideBar = (SideBar) findViewById(R.id.side_bar);
        final TextView tip = (TextView) findViewById(R.id.tip);

        final List<ItemData> datas = new ArrayList<>();
        ItemData data = new ItemData("北京");
        datas.add(data);
        ItemData data1 = new ItemData("上海");
        datas.add(data1);
        ItemData data2 = new ItemData("廣州");
        datas.add(data2);
        .
        .
        .
        ItemData data34 = new ItemData("Hello China");
        datas.add(data34);
        ItemData data35 = new ItemData("寧波");
        datas.add(data35);

        SortHelper<ItemData> sortHelper = new SortHelper<ItemData>() {
            @Override
            public String sortField(ItemData data) {
                return data.getTitle();
            }
        };
        sortHelper.sortByLetter(datas);//將數據源按指定字段首字母排序
        List<String> tags = sortHelper.getTags(datas);//提取已排序數據源的tag值

        MyAdapter adapter = new MyAdapter(this, datas, false);
        final LinearLayoutManager layoutManager = new LinearLayoutManager(this);
        layoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        recyclerView.setLayoutManager(layoutManager);
        //添加分割線
        recyclerView.addItemDecoration(new DivideItemDecoration().setTags(tags));
        //添加GroupHeader
        recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this)
                .setTags(tags)//設置tag集合
                .setGroupHeaderHeight(30)//設置GroupHeader高度
                .setGroupHeaderLeftPadding(20));//設置GroupHeader 左padding
        recyclerView.setAdapter(adapter);

        sideBar.setOnSideBarTouchListener(tags, new OnSideBarTouchListener() {
            @Override
            public void onTouch(String text, int position) {
                tip.setVisibility(View.VISIBLE);
                tip.setText(text);
                if ("↑".equals(text)) {
                    layoutManager.scrollToPositionWithOffset(0, 0);
                    return;
                }
                //滾動列表到指定位置
                if (position != -1) {
                    layoutManager.scrollToPositionWithOffset(position, 0);
                }
            }

            @Override
            public void onTouchEnd() {
                tip.setVisibility(View.GONE);
            }
        });
    }
}

這也就是文章開頭的 gif 效果。如果需要自定義ItemView的繪制可以這樣寫:

recyclerView.addItemDecoration(new GroupHeaderItemDecoration(this)
                .setTags(tags)
                .setGroupHeaderHeight(30)
                .setGroupHeaderLeftPadding(20)
                .setOnDrawItemDecorationListener(new OnDrawItemDecorationListener() {
                    @Override
                    public void onDrawGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) {
                        c.drawRect(params[0], params[1], params[2], params[3], paint);

                        int x = params[0] + Utils.dip2px(context, 20);
                        int y = params[1] + (Utils.dip2px(context, 30) + Utils.getTextHeight(textPaint, tags.get(position))) / 2;

                        Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null);
                        Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true);
                        c.drawBitmap(icon1, x, params[1] + Utils.dip2px(context, 5), paint);

                        c.drawText(tags.get(position), x + Utils.dip2px(context, 25), y, textPaint);
                    }

                    @Override
                    public void onDrawSuspensionGroupHeader(Canvas c, Paint paint, TextPaint textPaint, int[] params, int position) {
                        c.drawRect(params[0], params[1], params[2], params[3], paint);
                        int x = params[0] + Utils.dip2px(context, 20);
                        int y = params[1] + (Utils.dip2px(context, 30) + Utils.getTextHeight(textPaint, tags.get(position))) / 2;

                        Bitmap icon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher, null);
                        Bitmap icon1 = Bitmap.createScaledBitmap(icon, Utils.dip2px(context, 20), Utils.dip2px(context, 20), true);
                        c.drawBitmap(icon1, x, params[1] + Utils.dip2px(context, 5), paint);

                        c.drawText(tags.get(position), x + Utils.dip2px(context, 25), y, textPaint);
                    }
                })
        );

坐標計算有點復雜了......0_o......
看下效果:

4

當然不止于此,更多的效果等待著機智的你去創造。


更多代碼細節及用法可參考https://github.com/SheHuan/GroupIndexLib

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

推薦閱讀更多精彩內容