【Android】如何從零開始寫一款書籍閱讀器

一款書籍閱讀器颅围,需要以下功能才能說的上比較完整:

  1. 文字頁面展示伟葫,即書頁;
  2. 頁面之間的跳轉(zhuǎn)動畫院促,即翻頁動作筏养;
  3. 能夠在每一頁上記錄閱讀進(jìn)度,即書簽常拓;
  4. 能夠自由選擇文字并標(biāo)注渐溶,即筆記;
  5. 能夠設(shè)置一些屬性弄抬,如屏幕亮度茎辐,字體大小,主體顏色等掂恕,即個性化設(shè)置拖陆。
書籍閱讀器

這篇文章帶來的就是如何打造這么一款閱讀器。(由于整體代碼量比較大懊亡,所以我只能說說我的實(shí)現(xiàn)思路再加上部分的核心代碼來說明依啰,不會有太多的代碼展示。)

翻頁動作——搭建整個閱讀器的框架

在閱讀器上的翻頁動作無外乎仿真和平移這兩種動畫店枣,翻頁時需要準(zhǔn)備兩張頁面速警,一張是當(dāng)前頁,另一張是需要翻轉(zhuǎn)的下一頁鸯两。翻頁的過程就是對這兩個頁面的剪輯闷旧。

這里就不贅述翻頁的原理了(仿真翻頁可以由貝塞爾曲線計算坐標(biāo)繪制實(shí)現(xiàn),平移翻頁則是簡單坐標(biāo)平移變化)钧唐,這里提供一些參考鏈接忙灼。
實(shí)現(xiàn)書籍翻頁效果
Github上的PageFlip庫

現(xiàn)在要做的就是將翻頁動作與 View 結(jié)合起來,我們新建一個 PageAnimController 內(nèi)部實(shí)現(xiàn)翻頁動畫和動畫切換逾柿,同時設(shè)置 PageCarver 來監(jiān)聽翻頁動作缀棍,目的是為了能夠讓 view 檢測到翻頁動作宅此。

 public interface PageCarver {
 
        void drawPage(Canvas canvas, int index);//繪制頁內(nèi)容
        Integer requestPrePage();//請求翻到上一頁
        Integer requestNextPage();//請求翻到下一頁
        void requestInvalidate();//刷新界面
        Integer getCurrentPageIndex();//獲取當(dāng)前頁

        /**
         * 開始動畫的回調(diào)
         *
         * @param isCancel 是否是取消動畫
         */
        void onStartAnim(boolean isCancel);

        /**
         * 結(jié)束動畫的回調(diào)
         *
         * @param isCancel 是否是取消動畫
         */
        void onStopAnim(boolean isCancel);
    }

新建 BaseReaderView 作為閱讀器的基礎(chǔ)視圖,兩者結(jié)合以便控制閱讀器的翻頁效果爬范。

public abstract class BaseReaderView extends View implements PageAnimController.PageCarver{

    /**
     * 將View的繪制事件傳送給 PageAnimController 實(shí)現(xiàn)動畫繪制過程中
     * @param canvas
     * @return
     */
    @Override
    protected void onDraw(Canvas canvas) {
        if (pageAnimController == null || !pageAnimController.dispatchDrawPage(canvas, this)) {
            drawPage(canvas, currentPageIndex);
        }
    }
    
    /**
     * 將View的觸摸事件傳送給 PageAnimController 以便實(shí)現(xiàn)翻頁動畫 
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        pageAnimController.dispatchTouchEvent(event, this);
        return true;
    }
}

但是在翻頁動畫中是需要無數(shù)次的調(diào)用 drawPage 來繪制界面的父腕,為了減少界面計算的開支必須要有一個 Bitmap 緩存來降低消耗。復(fù)用時可以直接使用已經(jīng)生成的bitmap.

/**
 * <p>
 * 頁面快照青瀑,用來存儲閱讀器每一頁的內(nèi)容
 *
 * @author cpacm 2017/10/9
 */

public class PageSnapshot {
    private int pageIndex;
    private Bitmap mBitmap;
    private Canvas mCanvas;

    public Canvas beginRecording(int width, int height) {
        if (mBitmap == null) {
            mBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
            mCanvas = new Canvas(mBitmap);
        } else {
            mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        }
        return mCanvas;
    }

    public void draw(Canvas canvas) {
        if (null != mBitmap) {
            canvas.drawBitmap(mBitmap, 0, 0, null);
        }
    }

    public void destroy() {
        if (mBitmap != null && !mBitmap.isRecycled()) {
            mBitmap.recycle();
            mBitmap = null;
        }
    }
}

基礎(chǔ)模型如下圖所示:


頁面切換模型

現(xiàn)在我們來總結(jié)一下璧亮,這一部分我們搭建了閱讀器最基礎(chǔ)的框架,包括
(1) 翻頁動畫與閱讀器視圖的結(jié)合斥难,能夠確保在View中正確監(jiān)聽翻頁動作枝嘶,
保證整個翻頁動作的準(zhǔn)確性。
(2) 利用 Bitmap 緩存優(yōu)化繪圖流程哑诊,保證翻頁動畫的流暢性群扶。而后包括文字,圖片等元素的顯示都是繪制在這個 Bitmap 上的镀裤。

書頁——組合模式竞阐,保證閱讀器高度可定制化

閱讀器模塊圖

一般來說,閱讀器獲取數(shù)據(jù)都是一章一章來的暑劝,不管是從網(wǎng)絡(luò)上還是本地骆莹。而獲取過來的數(shù)據(jù)閱讀器要進(jìn)行分頁才能展示。如上圖所示担猛,書頁展示由 PageElement 模塊負(fù)責(zé)幕垦,該模塊接收從 BookReaderView 傳入的章節(jié)數(shù)據(jù),然后再經(jīng)底下的4個模塊計算來分頁傅联。

分頁模塊

  • PageElement先改,分頁模塊:功能包括將傳入的章節(jié)數(shù)據(jù)分成數(shù)個 PageData (生成的 PageData 個數(shù)即為該章節(jié)頁數(shù),PageData 記錄了每一頁開頭文字在章節(jié)的位置蒸走,同時包含該頁面HeaderData, LineData,HeadrDataFooterData 數(shù)據(jù)等盏道。各個 Data 里面記錄了相應(yīng)的文字信息,可以快速的定位到章節(jié)內(nèi)容中载碌。);繪制頁面衅枫;緩存章節(jié)數(shù)據(jù)以便無縫切換章節(jié)嫁艇。
  • HeaderElement,頁頭部分:顯示章節(jié)的標(biāo)題;繪制每一頁的頭部弦撩。
  • LineElement,文字行部分:測量一行文字需要的字?jǐn)?shù)步咪;測量行高;繪制行文字益楼;繪制筆記內(nèi)容猾漫;測量每一個字在屏幕中的位置点晴,用于筆記功能;
  • ImageElement,圖片部分:測量圖片的寬高悯周;繪制圖片粒督。
  • FooterElement,頁尾部分:繪制每一頁的頁尾,包括進(jìn)度,時間和電量禽翼。
    //摘自 PageElement 的 onDraw 方法
    @Override
    public void draw(Canvas canvas) {
        int index = drawPageIndex - startPageIndex;
        if (index < 0 || index >= pages.size()) return;
        BookPageData bookPageData = pages.get(index);
        int offsetX = bookSettingParams.paddingLeft;
        int offsetY = bookSettingParams.paddingTop;
        if (bookPageData == null) return;
        canvas.drawColor(bookSettingParams.getBgColor());
        bookHeaderElement.setChapterTitle(bookPageData.getChapterName());
        bookHeaderElement.setX(offsetX);
        bookHeaderElement.setY(offsetY);
        if (bookPageData.isChapterFirstPage()) {
            bookHeaderElement.drawFirstPage(canvas);
        } else {
            bookHeaderElement.draw(canvas);
        }

        bookFooterElement.setProgress(bookPageData.getPageIndex(), bookPageData.getPageNums());
        bookFooterElement.setX(offsetX);
        bookFooterElement.setY(offsetY + getHeight() - bookFooterElement.getHeight());
        bookFooterElement.draw(canvas);

        for (int i = 0; i < bookPageData.getDataList().size(); i++) {
            BookData bookData = bookPageData.getDataList().get(i);
            if (bookData instanceof BookLineData) {
                BookLineData bookLineData = (BookLineData) bookData;
                bookLineElement.setLineText(bookLineData.getContent());
                bookLineElement.setX(bookLineData.getPosition().x);
                bookLineElement.setY(bookLineData.getPosition().y);
                bookLineElement.drawWithDigests(canvas, bookLineData, bookReaderView.getCurrentDigests(index));
                //bookLineElement.draw(canvas);
            } else if (bookData instanceof BookImageData) {
                BookImageData bookImageData = (BookImageData) bookData;
                bookImageElement.setX(bookImageData.getPosition().x);
                bookImageElement.setY(bookImageData.getPosition().y);
                bookImageElement.syncDrawWithinBitmap(canvas, bookImageData, bookReaderView.getCacheBitmap(drawPageIndex));
            }
        }
    }

將書頁分成幾部分組合起來可以有效的減少代碼的耦合屠橄,而且可以自由的控制每一部分的修改,添加和移除闰挡。比如當(dāng)以后我想要加個批注的功能锐墙,可以再添加一個新的 Element ,再復(fù)寫其測量方法和繪制方法,就可以很方便的使用了长酗。

總結(jié)一下:
(1) PageElement 利用各個 Element 模塊將章節(jié)數(shù)據(jù)進(jìn)行測量分頁溪北,每一頁 PageData 記錄著 LineData,ImageData,HeaderDataFooterData信息。繪圖時需要將各個信息填入 Element
(2) 繪圖時調(diào)用 PageElement 的 draw 方法夺脾,其 draw 方法再調(diào)用 各個 Element 的 draw 方法以完成整個繪圖流程之拨。

另外還需要提到的一點(diǎn)是閱讀器內(nèi)部維護(hù)了一個書頁的隊(duì)列,該隊(duì)列緩存了由三個章節(jié)數(shù)據(jù)轉(zhuǎn)化而來的書頁列表劳翰。比如說你正在閱讀第六章敦锌,那么隊(duì)列里面緩存的就是第五章,第六章和第七章的數(shù)據(jù)佳簸,這樣就能實(shí)現(xiàn)上下章翻頁的無縫切換而不需要在翻至下一章時因?yàn)榈却碌恼鹿?jié)數(shù)據(jù)加載而中斷整個閱讀體驗(yàn)乙墙。

/**
 * <p>
 * 章節(jié)緩存構(gòu)成方案如下:
 * | -6,-5,-4,-3,-2,-1,0 | 1,2,3,4,5,6,7,8,9 | 10,11,12,13,14,15 | = pages
 * |    cacheChapter1    |   cacheChapter2   |   cacheChapter3   |
 * startPageIndex = pageIndex:-6  endPageIndex = pageIndex:16
 * currentChapterStartIndex => pageIndex:1  => pages[7]
 * currentChapterEndIndex => pageIndex:10 =>  pages[16]
 * </p>
 */

書簽,筆記——記錄閱讀進(jìn)度

書簽

書簽的本質(zhì)就是記錄當(dāng)前頁的第一個文字在整章文本的位置生均,然后再加上書籍的id,章節(jié)的id(或序號)就能準(zhǔn)確定位听想。

筆記

要記錄筆記就需要文字選擇器來選擇文字,這個時候就需要知道每一個字在當(dāng)前的坐標(biāo)位置(之前用 LineElement 測量文字時已經(jīng)生成每個文字的位置)马胧。

為了達(dá)到上圖的效果汉买,就必須要處理在當(dāng)前頁的觸摸事件:

文字選擇流程

有些細(xì)節(jié)的處理沒有放到流程中,但大致意思是能明白的

// TextSelectorElement 上的觸摸分發(fā)方法
public boolean dispatchTouchEvent(final MotionEvent ev) {
    int key = ev.getAction();
    currentTouchPoint.set(ev.getX(), ev.getY());
    switch (key) {
        case MotionEvent.ACTION_DOWN:
            isPressInvalid = false;
            hasConsume = true;
            isDown = true;
            mTouchDownPoint.set(ev.getX(), ev.getY());
            // 該方法中會記錄isBookDigestDown的值
            checkIsPressDigests(ev.getX(), ev.getY());
            //判斷是否處于選擇模式
            if (!isSelect) {
                if (isBookDigestDown == 0) {
                    postLongClickPerform(0);//提交長按時間
                }
            } else {
                // 判斷是否觸摸到選擇光標(biāo)上佩脊,若是則可以拖動光標(biāo)移動
                checkCurrentMoveCursor(ev);
            }
            break;
        case MotionEvent.ACTION_MOVE:
            float move = PointF.length(ev.getX() - mTouchDownPoint.x, ev.getY() - mTouchDownPoint.y);
            if (move > moveSlop) {
                isPressInvalid = true;
            }
            if (isPressInvalid) {
                removeLongPressPerform();
                if (isSelect) {
                    // 關(guān)閉彈窗(包括筆記編輯框等)
                    onCloseView();
                    // 移動光標(biāo)
                    onMove(ev);
                } else {
                    //未處于選擇模式下蛙粘,相當(dāng)于一個普通的點(diǎn)擊事件
                    onPress(ev);
                }
            }
            break;
        case MotionEvent.ACTION_UP:
            hasConsume = false;
            removeLongPressPerform();
            if (isSelect) {
                // -1 表示為未觸摸到光標(biāo)
                if (moveCursor == -1) {
                    // 取消選擇模式
                    setSelect(false);
                    hasConsume = true;
                } else {
                    //停止移動時,會打開筆記生成彈框
                    onOpenDigestsView();
                }
                moveCursor = -1;
            } else {
                if (isBookDigestDown == 1) {
                    onOpenNoteView();
                    hasConsume = true;
                } else if (isBookDigestDown == 2) {
                    onOpenEditView();
                    hasConsume = true;
                } else {
                    // 模擬成一個普通的點(diǎn)擊事件威彰,會取消當(dāng)前的選擇模式
                    onPress(ev);
                }
            }
            invalidate();
            break;
        case MotionEvent.ACTION_CANCEL:
            hasConsume = false;
            removeLongPressPerform();
            break;
        default:
            break;
    }
    // 判斷選擇器是否消耗了當(dāng)前事件
    return hasConsume || isSelect;
}

當(dāng)然出牧,筆記也要記錄當(dāng)前選擇的書籍id,章節(jié)id(或序號),文字在章節(jié)中的位置這些信息歇盼,方便定點(diǎn)跳轉(zhuǎn)舔痕。

設(shè)置——為閱讀器添磚加瓦

閱讀器設(shè)置界面

閱讀器的設(shè)置一般包括:界面亮度的調(diào)整,字體大小的調(diào)整,上下章的跳轉(zhuǎn)伯复,書籍目錄筆記和書簽的展示慨代,翻頁動畫的更改,日夜主題的更改啸如。當(dāng)一些設(shè)置需要閱讀器能夠在參數(shù)變化時及時響應(yīng)侍匙,就得需要在設(shè)置變化時能及時更新 BookReaderView 下的各個 Element 模塊。
這里我是通過一個輔助類貫穿整個閱讀器來幫助更新各個模塊组底,該類記錄了閱讀器內(nèi)部所有可設(shè)置的屬性丈积,當(dāng)各個模塊被通知需要更新時重新從該類中讀取參數(shù)并設(shè)置(比如畫筆的顏色,頁面的間距债鸡,字體的大小等)江滨。

// 摘自 PageElement 下的設(shè)置屬性變化方法
// BookSettingParams 即為記錄閱讀器設(shè)置屬性的輔助類
@Override
public void update(ReaderSettingParams params) {
    bookSettingParams = (BookSettingParams) params;
    bookHeaderElement.update(bookSettingParams);
    bookFooterElement.update(bookSettingParams);
    bookLineElement.update(bookSettingParams);
    bookImageElement.update(bookSettingParams);

    initPageElement();
}

語音朗讀——為閱讀器添加輔助功能

語音朗讀

此處的語音朗讀使用的是訊飛的TTS引擎。如何使用引入TTS我這里就不具體描述了厌均,重要的是在TTS的 onSpeakProgress(int progress, int beginPos, int endPos) 方法中可以獲取當(dāng)前句子的朗讀進(jìn)度唬滑。

當(dāng)我們傳入一章文字時,TTS會自動幫助我們分段(會以棺弊,晶密。等標(biāo)點(diǎn)符號切割整篇文字),然后按段落來進(jìn)行朗讀模她。上面 progress 代表該段落在整篇文字的進(jìn)度稻艰,beginPos 代表該段落的起始字符在整篇文字的位置,endPos 代表該段落的末尾字符在整篇文字的位置侈净。

既然能夠知道朗讀的位置尊勿,那就能知道朗讀時文字在屏幕的位置了(之前有說過 LineData 記錄了每個字符在屏幕中的位置),那剩下的就是怎么繪制的問題了畜侦。

/**
 * <p>
 * 聽書tts播放模組
 *
 * @author cpacm 2017/12/13
 */

public class BookSpeechElement extends ResElement implements SynthesizerListener {

    // .... 省略部分代碼
    
    // 從每一頁數(shù)據(jù) PageData 中的 LineData 列表中獲取要繪制的區(qū)域
    private void updateDrawRect(int startPos, int endPos) {
        if (endPos <= offsetPosition || endPos == this.endPos) return;
        this.endPos = endPos;
        this.tempPos = startPos;
        int s = this.startPos + startPos + bookPageData.getStartPos() - offsetPosition;
        int e = this.startPos + endPos + bookPageData.getStartPos() - offsetPosition;
        drawRect.clear();
        for (BookLineData line : lineData) {
            if (line.startPos > e || line.endPos <= s) continue;
            if (line.startPos <= s && line.endPos <= e) {
                Rect startRect = line.getCharArea().get(s);
                Rect endRect = line.getCharArea().get(line.endPos - 1);
                Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
                drawRect.add(rect);
            }
            if (line.startPos > s && line.endPos <= e) {
                Rect startRect = line.getCharArea().get(line.startPos);
                Rect endRect = line.getCharArea().get(line.endPos - 1);
                Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
                drawRect.add(rect);
            }
            if (line.startPos > s && line.endPos > e) {
                Rect startRect = line.getCharArea().get(line.startPos);
                Rect endRect = line.getCharArea().get(e);
                Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
                drawRect.add(rect);
            }
            if (line.startPos <= s && line.endPos > e) {
                Rect startRect = line.getCharArea().get(s);
                Rect endRect = line.getCharArea().get(e);
                Rect rect = new Rect(startRect.left, startRect.top, endRect.right, endRect.bottom);
                drawRect.add(rect);
            }
        }
        // 刷新當(dāng)前書頁
        bookReaderView.flashCurrentPageSnapshot();
    }


    @Override
    public void draw(Canvas canvas) {
        if (!isSpeaking()) return;
        for (Rect rect : drawRect) {
            canvas.drawLine(rect.left, rect.bottom, rect.right, rect.bottom, paint);
        }
    }

    @Override
    public void destroy() {
        exitTts();
    }
   
    /*################## 語音合成的回調(diào) ###################*/
    @Override
    public void onSpeakBegin() {}

    @Override
    public void onBufferProgress(int progress, int beginPos, int endPos, String info) { }

    @Override
    public void onSpeakPaused() {}

    @Override
    public void onSpeakResumed() {}

    @Override
    public void onSpeakProgress(int progress, int beginPos, int endPos) {
        // 根據(jù)朗讀的進(jìn)度更新UI
        updateDrawRect(beginPos, endPos);
    }

    @Override
    public void onCompleted(SpeechError speechError) {}

    @Override
    public void onEvent(int i, int i1, int i2, Bundle bundle) {}
}

總結(jié)

首先聲明一點(diǎn)元扔,整篇文章只是闡述了我自己從零開始做書籍閱讀器時一些思路和使用的一些技巧,并沒有覆蓋到閱讀器的各個角落旋膳。如果你想要自己實(shí)現(xiàn)一款閱讀器澎语,那你必須要有扎實(shí)的基礎(chǔ)知識,比如View的繪制流程和事件分發(fā)流程验懊,Canvas的繪圖知識等擅羞,這篇文章也只是給大家提個思路而已。如果有問題或者新的想法歡迎交流义图!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末祟滴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子歌溉,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件痛垛,死亡現(xiàn)場離奇詭異草慧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)匙头,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進(jìn)店門漫谷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蹂析,你說我怎么就攤上這事舔示。” “怎么了电抚?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵惕稻,是天一觀的道長。 經(jīng)常有香客問我蝙叛,道長俺祠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任借帘,我火速辦了婚禮蜘渣,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肺然。我一直安慰自己蔫缸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布际起。 她就那樣靜靜地躺著拾碌,像睡著了一般。 火紅的嫁衣襯著肌膚如雪加叁。 梳的紋絲不亂的頭發(fā)上倦沧,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機(jī)與錄音它匕,去河邊找鬼展融。 笑死,一個胖子當(dāng)著我的面吹牛豫柬,可吹牛的內(nèi)容都是我干的告希。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼烧给,長吁一口氣:“原來是場噩夢啊……” “哼燕偶!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起础嫡,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤指么,失蹤者是張志新(化名)和其女友劉穎酝惧,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伯诬,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡晚唇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了盗似。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片哩陕。...
    茶點(diǎn)故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖赫舒,靈堂內(nèi)的尸體忽然破棺而出悍及,到底是詐尸還是另有隱情,我是刑警寧澤接癌,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布心赶,位于F島的核電站,受9級特大地震影響扔涧,放射性物質(zhì)發(fā)生泄漏园担。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一枯夜、第九天 我趴在偏房一處隱蔽的房頂上張望弯汰。 院中可真熱鬧,春花似錦湖雹、人聲如沸咏闪。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鸽嫂。三九已至,卻和暖如春征讲,著一層夾襖步出監(jiān)牢的瞬間据某,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工诗箍, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留癣籽,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓滤祖,卻偏偏與公主長得像筷狼,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子匠童,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評論 2 353

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