InputFilter 和 TextWatcher

Android 成長在于積累和分享

版權聲明:本文為參考博主編寫的原創(chuàng)文章馁启,原文: https://blog.csdn.net/u014606081/article/details/53101629

[TOC]

UCS 移動端技術分享

技術形式的一種分享蚓让,發(fā)現(xiàn)在寫過程中參考別人博客的同時發(fā)現(xiàn)確實他寫的更完善點 ,所以在他博客的基礎上添加了一部分自己理解的東西鉴嗤。也是給大家看一下盟劫,如果有不對的地方航缀,希望我們一起修正和學習商架。

導讀

InputFilter源碼解析、TextWatcher源碼解析

前言

Android中 InputFilterTextWatcher 的功能和作用非常相似芥玉,都可以做到對 EditText 輸入內(nèi)容的監(jiān)聽及控制蛇摸。那兩者具體有什么區(qū)別,又是如何實現(xiàn)對輸入內(nèi)容進行監(jiān)聽的灿巧。下面我們就從源碼的角度一起分析一下赶袄。

分析源碼之前先打一下基礎,EditText是繼承自TextView抠藕,90%的功能跟TextView是一致的饿肺,只有4個私有方法,剩下8個是重寫TextView的方法盾似。所以EditText的大部分功能都是在TextView中完成的敬辣,具體邏輯也都是在TextView中。

InputFilter

InputFilter里面只有一個方法filter(),返回值為CharSequence溉跃,用于過濾或者輸入/插入的字符串村刨, 當返回值不為null時,使用返回結果替換原有輸入/插入的字符串撰茎。

package android.text;

/**
 * InputFilters can be attached to {@link Editable}s to constrain the
 * changes that can be made to them.
 */
public interface InputFilter {

        /**
         * @param source  將要插入的字符串嵌牺,來自鍵盤輸入、粘貼
         * @param start  source的起始位置乾吻,為0(暫時沒有發(fā)現(xiàn)其它值的情況)輸入-0髓梅,刪除-0
         * @param end  source的長度: 輸入-文字的長度拟蜻,刪除-0
         * @param dest  EditText中已經(jīng)存在的字符串绎签,原先顯示的內(nèi)容
         * @param dstart  插入點的位置:輸入-原光標位置,刪除-光標刪除結束位置
         * @param dend  輸入-原光標位置酝锅,刪除-光標刪除開始位置
        */
       public CharSequence filter(CharSequence source, int start, int end,
                               Spanned dest, int dstart, int dend);
       ...略...
}

TextView類中的setText()方法诡必,會調(diào)用filter()方法,得到過濾后的字符串搔扁,部分源碼如下:

setText()方法有很多重載方法爸舒,但是最終都會調(diào)用下面這個。這個方法很重要稿蹲,只需要看關鍵邏輯處的注釋扭勉。

/**
 * @param text 將要設置的新內(nèi)容
 * @param type 內(nèi)容的設置類型(static text, styleable/spannable text, or editable text)
 * @param notifyBefore 是否需要觸發(fā)TextWacther的before
 * @param oldlen 新增加內(nèi)容的長度
*/
private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        mTextFromResource = false;
        if (text == null) {
            text = "";
        }

        ...略...

        // 使用InputFilter處理text
        int n = mFilters.length;
        for (int i = 0; i < n; i++) {
            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
            if (out != null) {
                text = out;
            }
        }

        ...略...

        // 如果是EditText,就new一個Editable
        if (type == BufferType.EDITABLE || getKeyListener() != null ||
                needEditableForNotification) {
            createEditorIfNeeded();
            Editable t = mEditableFactory.newEditable(text);
            text = t;
            setFilters(t, mFilters); // filter的另外一處過濾調(diào)用處
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null) imm.restartInput(this);
        } else if (type == BufferType.SPANNABLE || mMovement != null) {
            text = mSpannableFactory.newSpannable(text);
        } else if (!(text instanceof CharWrapper)) {
            text = TextUtils.stringOrSpannedString(text);
        }

        ... 略 ...
    }

mFilters是一個InputFilter數(shù)組苛聘,因為有一個或多個過濾器涂炎。通過for循環(huán),把text按照所有過濾條件全部過濾一遍设哗,最終得到“合格”的text唱捣。

除了setText方法,在鍵盤鍵入的過程中同樣會過濾网梢,TextViewsetFilters方法給Editable設置了filters震缭,并在Editablereplace方法中進行過濾。

private void setFilters(Editable e, InputFilter[] filters) {
        if (mEditor != null) {
            final boolean undoFilter = mEditor.mUndoInputFilter != null;
            final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
            int num = 0;
            if (undoFilter) num++;
            if (keyFilter) num++;
            if (num > 0) {
                InputFilter[] nf = new InputFilter[filters.length + num];

                System.arraycopy(filters, 0, nf, 0, filters.length);
                num = 0;
                if (undoFilter) {
                    nf[filters.length] = mEditor.mUndoInputFilter;
                    num++;
                }
                if (keyFilter) {
                    nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
                }

                e.setFilters(nf);
                return;
            }
        }
        e.setFilters(filters);

舉例SpannableStringBuilderreplace方法的實現(xiàn)战虏。

 // Documentation from interface
    public SpannableStringBuilder replace(final int start, final int end,
            CharSequence tb, int tbstart, int tbend) {
        checkRange("replace", start, end);

        int filtercount = mFilters.length;
        for (int i = 0; i < filtercount; i++) {
            CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);

            if (repl != null) {
                tb = repl;
                tbstart = 0;
                tbend = repl.length();
            }
        }

     ... 略...
}

知道了InputFilter是如何起作用的拣宰,那么剩下的就是搞清楚filter()方法中的各個參數(shù)的含義,寫出自己需要的InputFilter烦感。

SDK提供了兩個實現(xiàn):AllCapsLengthFilter巡社,下面以LengthFilter解讀InputFilter的用法,源碼片段如下:

public static class LengthFilter implements InputFilter {

        private final int mMax;

        public LengthFilter(int max) {
            mMax = max;
        }

        //參數(shù)source:將要插入的字符串啸盏,來自鍵盤輸入重贺、粘貼
        //參數(shù)start:source的起始位置,為0(暫時沒有發(fā)現(xiàn)其它值的情況)輸入-0,刪除-0
        //參數(shù)end:source的長度: 輸入-文字的長度气笙,刪除-0
        //參數(shù)dest:EditText中已經(jīng)存在的字符串次企,原先顯示的內(nèi)容
        //參數(shù)dstart:插入點的位置:輸入-原光標位置,刪除-光標刪除結束位置
        //參數(shù)dend:輸入-原光標位置潜圃,刪除-光標刪除開始位置
        public CharSequence filter(CharSequence source, int start, int end, Spanned dest,
                                   int dstart, int dend) {
            int keep = mMax - (dest.length() - (dend - dstart));
            if (keep <= 0) {
                // 如果超出字數(shù)限制缸棵,就返回“”
                return "";
            } else if (keep >= end - start) {
                // 如果完全滿足限制,就返回null(如果返回值為null谭期,TextView中就會使用原始source)
                return null; // keep original
            } else {
                keep += start;
                if (Character.isHighSurrogate(source.charAt(keep - 1))) {
                    // 如果最后一位字符是HighSurrogate(高編碼堵第,占2個字符位),就把kepp減1隧出,保證不超出字數(shù)限制
                    --keep;
                    if (keep == start) {
                        return "";
                    }
                }
                return source.subSequence(start, keep);
            }
        }

        /**
         * @return the maximum length enforced by this input filter
         */
        public int getMax() {
            return mMax;
        }
    }

TextWatcher

類如其名踏志,用于觀察Text的輸入刪除等變化。

package android.text;

/**
 * When an object of a type is attached to an Editable, its methods will
 * be called when the text is changed.
 */
public interface TextWatcher extends NoCopySpan {
    /**
     * @param s原內(nèi)容
     * @param start  被替換內(nèi)容起點坐標
     * @param count 被替換內(nèi)容的長度
     * @param after 新增加內(nèi)容的長度
     */
    public void beforeTextChanged(CharSequence s, int start,
                                  int count, int after);
    /**
     * @param s 發(fā)生改變后的內(nèi)容
     * @param start  被替換內(nèi)容的起點坐標
     * @param before 被替換內(nèi)容的長度
     * @param count 新增加的內(nèi)容的長度
     */
    public void onTextChanged(CharSequence s, int start, int before, int count);

    /**
     * @param s 發(fā)生改變后的內(nèi)容(對s編輯同樣會觸發(fā)TextWatcher)
     */
    public void afterTextChanged(Editable s);
}

兩種情況會使TextView里面的內(nèi)容發(fā)生變化胀瞪,從而通知監(jiān)聽器针余,第一種就是setText()方法,第二種就是從鍵盤輸入凄诞。兩種都會調(diào)用sendBeforeTextChanged方法發(fā)送通知圆雁,如下(舉例beforeTextChanged):

private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
        if (mListeners != null) {
            final ArrayList<TextWatcher> list = mListeners;
            final int count = list.size();
            for (int i = 0; i < count; i++) {
                list.get(i).beforeTextChanged(text, start, before, after);
            }
        }

        // The spans that are inside or intersect the modified region no longer make sense
        removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
        removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
    }

首先,給大家介紹下Google的設計理念帆谍,說實話伪朽,我不知道原文出自哪,這個是從某個大佬博客上摘過來的汛蝙。


Google對于“改變字符串”的設計理念就是“替換”烈涮。如果是刪內(nèi)容,就是用空字符串替換需要刪除的字符串患雇;如果是增加內(nèi)容跃脊,就是用新字符串替換空字符串。所以要先搞清楚下面幾個概念:

  1. 原內(nèi)容:發(fā)生改變前TextView中的內(nèi)容苛吱;
  2. 被替換內(nèi)容起點坐標:編輯一段內(nèi)容時酪术,有可能是直接添加新內(nèi)容,也有可能是選中一段原有內(nèi)容翠储,用新內(nèi)容把它替換掉绘雁;
  3. 被替換內(nèi)容的長度:如果是直接添加新內(nèi)容,被替換內(nèi)容的長度就是0援所;
  4. 新增加的內(nèi)容:對于setText()來說庐舟,就是方法中的參數(shù),對于鍵盤輸入來說住拭,就是鍵盤輸入的內(nèi)容

再來分析這兩種情況挪略。

情況一:setText()

同樣的历帚,先看下TextViewsetText()方法對相關事件的處理

/**
 * @param text 將要設置的新內(nèi)容
 * @param type 內(nèi)容的設置類型(static text, styleable/spannable text, or editable text)
 * @param notifyBefore 是否需要觸發(fā)TextWacther的before
 * @param oldlen 新增加內(nèi)容的長度
*/
private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        mTextFromResource = false;
        if (text == null) {
            text = "";
        }

        ...略...

        // 先過濾,再確認是否發(fā)送TextChange
        int n = mFilters.length;
        for (int i = 0; i < n; i++) {
            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
            if (out != null) {
                text = out;
            }
        }

        // char[]類型時會提前發(fā)送sendBeforeTextChanged杠娱,此處的notifyBefore即為false
        if (notifyBefore) {
            // 通知調(diào)用TextWatcher的beforeTextChanged()方法
            if (mText != null) {
                oldlen = mText.length();
                sendBeforeTextChanged(mText, 0, oldlen, text.length());
            } else {
                sendBeforeTextChanged("", 0, 0, text.length());
            }
        }

        boolean needEditableForNotification = false;

        if (mListeners != null && mListeners.size() != 0) {
            needEditableForNotification = true;
        }

        if (type == BufferType.EDITABLE || getKeyListener() != null
                || needEditableForNotification) {
            createEditorIfNeeded();
            mEditor.forgetUndoRedo();
            Editable t = mEditableFactory.newEditable(text); // 創(chuàng)建Editable挽牢,后面鍵盤輸入會用到相關監(jiān)聽
            text = t;
            setFilters(t, mFilters);
            InputMethodManager imm = InputMethodManager.peekInstance();
            if (imm != null) imm.restartInput(this);
        } else if (type == BufferType.SPANNABLE || mMovement != null) {
            text = mSpannableFactory.newSpannable(text);
        } else if (!(text instanceof CharWrapper)) {
            text = TextUtils.stringOrSpannedString(text);
        }

        ...略...

        if (text instanceof Spannable && !mAllowTransformationLengthChange) {
            Spannable sp = (Spannable) text;

            // Remove any ChangeWatchers that might have come from other TextViews.
            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
            final int count = watchers.length;
            for (int i = 0; i < count; i++) {
                sp.removeSpan(watchers[i]);
            }

            if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();

            // 設置鍵盤輸入TextWatcher監(jiān)聽(可以看一下ChangeWatcher源碼)
            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
                    | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));

            if (mEditor != null) mEditor.addSpanWatchers(sp);

            if (mTransformation != null) {
                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }

        ...略...

        }

        ...略...

        // 通知調(diào)用TextWatcher的onTextChanged()方法
        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);

        // 通知view刷新
        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

        // 通知調(diào)用TextWatcher的afterTextChanged()方法
        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
        } else {
            // Always notify AutoFillManager - it will return right away if autofill is disabled.
            notifyAutoFillManagerAfterTextChangedIfNeeded();
        }

        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
        if (mEditor != null) mEditor.prepareCursorControllers();

}

這里可以看到,如果調(diào)用setText方法是一定會觸發(fā)TextWatcher相關事件的摊求,所以盡量不要TextWatcheronTextChanged()方法中對文字進行過濾禽拔,然后再調(diào)用setText()方法重置字符串,
如果一定要在TextWatcher的onTextChanged()方法中調(diào)用setText()方法(某些很難受的需求)室叉,注意防止死循環(huán)睹栖。因為setText()方法又會回調(diào)onTextChanged()方法,會形成死循環(huán)茧痕。

情況二:鍵盤輸入

TextView的構造方法中野来,會獲取android:text屬性的值,調(diào)用setText()方法設置初始內(nèi)容凿渊。其中梁只,就會判斷BufferType的類型缚柳,如果是EditText埃脏,就會創(chuàng)建Editable(此段邏輯見上面setText()源碼)。

最終new出SpannableStringBuilder對象秋忙,SpannableStringBuilder實現(xiàn)了Editable彩掐、Appendable接口。Appendable提供了一個接口(有三個重載的):append()灰追,用來把新內(nèi)容(來自鍵盤輸入)添加到原內(nèi)容中堵幽。所以我們?nèi)pannableStringBuilder里看看append()方法的具體實現(xiàn)。

三個重載的接口弹澎,就有三個具體實現(xiàn)朴下,但原理都一樣,最終都會調(diào)用replace()方法苦蒿。下面以其中一個append()實現(xiàn)來分析:

// 鍵盤輸入有兩種:一種是正常輸入殴胧;另一種是先選中一段內(nèi)容,再從鍵盤輸入佩迟,新內(nèi)容會替換掉選中的內(nèi)容团滥;
// 這個方法是正常輸入時調(diào)用
public SpannableStringBuilder append(CharSequence text, int start, int end) {
    // length就是插入點的位置
    int length = length();
    // 最終都會調(diào)用replace()方法來“增加”內(nèi)容。從命名可以看出报强,Google對于字符串改變的設計思路就是“替換”灸姊,如果是刪內(nèi)容,就是用空內(nèi)容替換原內(nèi)容秉溉,如果是增加內(nèi)容力惯,就是用新內(nèi)容替換某個內(nèi)容
    return replace(length, length, text, start, end);
}

public SpannableStringBuilder replace(final int start, final int end,
            CharSequence tb, int tbstart, int tbend) {
        checkRange("replace", start, end);

    // 與setText()一樣碗誉,都會對新增內(nèi)容進行過濾
    int filtercount = mFilters.length;
    for (int i = 0; i < filtercount; i++) {
        CharSequence repl = mFilters[i].filter(tb, tbstart, tbend, this, start, end);

        if (repl != null) {
            tb = repl;
            tbstart = 0;
            tbend = repl.length();
        }
    }

    // 由于是正常鍵盤輸入,end等于start父晶,所以origLen等于0
    final int origLen = end - start;
    // 新增內(nèi)容的長度
    final int newLen = tbend - tbstart;

    if (origLen == 0 && newLen == 0 && !hasNonExclusiveExclusiveSpanAt(tb, tbstart)) {
        return this;
    }

    TextWatcher[] textWatchers = getSpans(start, start + origLen, TextWatcher.class);
    // 通知TextWatcher調(diào)用beforeTextChanged()方法诗充,邏輯跟TextView中的一樣,就不再貼代碼了
    sendBeforeTextChanged(textWatchers, start, origLen, newLen);

    boolean adjustSelection = origLen != 0 && newLen != 0;
    int selectionStart = 0;
    int selectionEnd = 0;
    if (adjustSelection) {
        selectionStart = Selection.getSelectionStart(this);
        selectionEnd = Selection.getSelectionEnd(this);
    }

    change(start, end, tb, tbstart, tbend);

    if (adjustSelection) {
        if (selectionStart > start && selectionStart < end) {
            final int offset = (selectionStart - start) * newLen / origLen;
            selectionStart = start + offset;

            setSpan(false, Selection.SELECTION_START, selectionStart, selectionStart,
                        Spanned.SPAN_POINT_POINT);
            }
        if (selectionEnd > start && selectionEnd < end) {
            final int offset = (selectionEnd - start) * newLen / origLen;
            selectionEnd = start + offset;

            setSpan(false, Selection.SELECTION_END, selectionEnd, selectionEnd,
                        Spanned.SPAN_POINT_POINT);
        }
    }

    // 通知TextWatcher調(diào)用onTextChanged()诱建、afterTextChanged()方法蝴蜓。可以看到俺猿,這兩個方法是一起調(diào)用的茎匠,這點跟setText()有點細微差別,總體來說是一樣的
    sendTextChanged(textWatchers, start, origLen, newLen);
    sendAfterTextChanged(textWatchers);

    sendToSpanWatchers(start, end, newLen - origLen);

    return this;
}

通過分析押袍,大概可以得出如下結論:(通過鍵盤輸入的源碼分析可以確認該結論)

// s:原內(nèi)容
// start:被替換內(nèi)容起點坐標诵冒,因為setText()是將原內(nèi)容全部替換掉,所以起點是0
// count:被替換內(nèi)容的長度谊惭,因為setText()是將原內(nèi)容全部替換掉汽馋,所以就是mText.length()
// after:新增加內(nèi)容的長度
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}

// s:發(fā)生改變后的內(nèi)容
// start:被替換內(nèi)容的起點坐標
// before:被替換內(nèi)容的長度
// count:新增加的內(nèi)容的長度
public void onTextChanged(CharSequence s, int start, int before, int count) {
}

// s:發(fā)生改變后的內(nèi)容
public void afterTextChanged(Editable s) {
}

總結

  • 最好使用InputFilter對字符串進行控制、過濾圈盔。

  • 盡量不要在TextWatcheronTextChanged()方法中對文字進行過濾豹芯,然后再調(diào)用setText()方法重置字符串,效率明顯比InputFilter低驱敲。

  • 如果一定要在TextWatcheronTextChanged()方法中調(diào)用setText()方法铁蹈,注意防止死循環(huán)。因為setText()方法又會回調(diào)onTextChanged()方法众眨,會形成死循環(huán)握牧。

  • TextWatcher主要功能是進行監(jiān)聽,從Google對該類的命名就可以看出來娩梨。沿腰。


鳴謝

thinkreduce
原文鏈接: https://blog.csdn.net/u014606081/article/details/53101629

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市狈定,隨后出現(xiàn)的幾起案子颂龙,更是在濱河造成了極大的恐慌,老刑警劉巖掸冤,帶你破解...
    沈念sama閱讀 212,294評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厘托,死亡現(xiàn)場離奇詭異,居然都是意外死亡稿湿,警方通過查閱死者的電腦和手機铅匹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,493評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來饺藤,“玉大人包斑,你說我怎么就攤上這事流礁。” “怎么了罗丰?”我有些...
    開封第一講書人閱讀 157,790評論 0 348
  • 文/不壞的土叔 我叫張陵神帅,是天一觀的道長。 經(jīng)常有香客問我萌抵,道長找御,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,595評論 1 284
  • 正文 為了忘掉前任绍填,我火速辦了婚禮霎桅,結果婚禮上,老公的妹妹穿的比我還像新娘讨永。我一直安慰自己滔驶,他們只是感情好,可當我...
    茶點故事閱讀 65,718評論 6 386
  • 文/花漫 我一把揭開白布卿闹。 她就那樣靜靜地躺著揭糕,像睡著了一般。 火紅的嫁衣襯著肌膚如雪锻霎。 梳的紋絲不亂的頭發(fā)上著角,一...
    開封第一講書人閱讀 49,906評論 1 290
  • 那天,我揣著相機與錄音量窘,去河邊找鬼雇寇。 笑死,一個胖子當著我的面吹牛蚌铜,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播嫩海,決...
    沈念sama閱讀 39,053評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼冬殃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了叁怪?” 一聲冷哼從身側響起审葬,我...
    開封第一講書人閱讀 37,797評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎奕谭,沒想到半個月后涣觉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,250評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡血柳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,570評論 2 327
  • 正文 我和宋清朗相戀三年官册,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片难捌。...
    茶點故事閱讀 38,711評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡膝宁,死狀恐怖鸦难,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情员淫,我是刑警寧澤合蔽,帶...
    沈念sama閱讀 34,388評論 4 332
  • 正文 年R本政府宣布,位于F島的核電站介返,受9級特大地震影響拴事,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜圣蝎,卻給世界環(huán)境...
    茶點故事閱讀 40,018評論 3 316
  • 文/蒙蒙 一挤聘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧捅彻,春花似錦组去、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,796評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至缭裆,卻和暖如春键闺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背澈驼。 一陣腳步聲響...
    開封第一講書人閱讀 32,023評論 1 266
  • 我被黑心中介騙來泰國打工辛燥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缝其。 一個月前我還...
    沈念sama閱讀 46,461評論 2 360
  • 正文 我出身青樓挎塌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親内边。 傳聞我的和親對象是個殘疾皇子榴都,可洞房花燭夜當晚...
    茶點故事閱讀 43,595評論 2 350

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