Android 淺談自定義View(1)

做為一名Android開發者,自定義View應該是我們工作中繞不開的話題,畢竟系統提供的View有限,有時很難滿足我們的需求,此時就需要結合具體的場景來編寫自定義View,通過自定義View不僅可以實現特定的效果,還可以簡化代碼。自定義View的過程,也是我們自己造小輪子的過程,說不定在你的其它項目中就可以用到,對提高生產力還是大大有幫助的。

雖說自定義View是工作中繞不開的話題,但也是Android中最容易把開發者繞進去的知識點,經歷過了從入門到放棄的再重新入門的辛酸過程,也該是用正確的姿勢學習自定義View了。

View是什么呢?......就是Android中所有控件的基類,我們經常聽到的ViewGroup也是View的一個子類。

直接上來就說如何自定義View未免有些空泛,所以我們從View底層的工作原理開始聊起,知其然也要知其所以然。App中我們所用到看到的一個個控件,都要經過measure(測量)、layout(布局)、draw(繪制)三大流程才會呈現在我們的眼前。其中measure用來測量View的大小,layout用來確定被測量后的View最終的位置,draw則是將View渲染繪制出來。其實View的工作流程在我們生活中也能找到類似的原型,比如,我們要畫一個西瓜,首先要確定西瓜的大小,接下來要確定畫在紙上的那個位置,最后才進行繪制。

在分析View的工作流程前,我們先要了解一個重要的知識點---MeasureSpec,MeasureSpec代表一個View的測量規格,它是一個32位的int值,高兩位代表測量模式(SpecMode),低30位代表在對應測量模式下的大小(SpecSize)。通過MeasureSpec的getMode()、getSize()方法可以得到對應View寬\高的測量模式以及大小。
SpecMode,即測量模式有以下三種:

  • EXACTLY:父容器已經檢測出View所需要的精確大小,此時View的大小就是SpecSize,對應于LayoutParams中的具體數值和match_parent兩種類型。
  • AT_MOST:父容器指定了一個可用的大小即SpecSize,View的大小由其具體的實現決定,但不能大于SpecSize,對用于LayoutParams中的wrap_content。
  • UNSPECIFIED:父容器不對View大小做限制,View需要多大就給多大,這種測量模式一般用于系統內容,在我們自定義View中很少用到。

View的MeasureSpec是如何確定的呢?其實是由View自身的LayoutParams和父容器的MeasureSpec共同決定的。具體的細節我們來看源碼,在ViewGroup中有一個measureChildWithMargins方法:

protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

在measureChildWithMargins()方法中,通過getChildMeasureSpec()方法得到了View寬\高對應的測量模式childWidthMeasureSpec 、childHeightMeasureSpec,接下來重點看getChildMeasureSpec()的實現細節:

/**
  * @param spec 父View寬/高的測量規格
  * @param padding 父View在寬/高上已經占用的空間大小
  * @param childDimension 子View的寬/高
  */
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);//得到父View在寬/高上的測量模式
        int specSize = MeasureSpec.getSize(spec);//得到父View在對應測量模式下的寬/高

        int size = Math.max(0, specSize - padding);//計算子View在寬/高上可用的空間大小

        int resultSize = 0;
        int resultMode = 0;
        // 開始根據父View的測量規格以及子View的LayoutParams判斷子View的測量規格
        switch (specMode) {
        // 當父View的測量模式為精確的大小時(包括具體的數值和match_parent兩種)
        case MeasureSpec.EXACTLY:
            // 如果子View的LayoutParams的寬/高是固定的數值,那么它的測量模式為MeasureSpec.EXACTLY,
            // 大小為LayoutParams對應的寬/高數值,這樣測量規格就確定了。
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            // 如果子View的LayoutParams的寬/高為match_parent,那么子View的寬/高和父View尺寸相等,即為size,
            // 因為父View的尺寸已經確定,則子View的測量模式為MeasureSpec.EXACTLY。
            else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            }
            //  如果子View的LayoutParams的寬/高為wrap_content,就是說子View想根據實現方式來自己確定自己的大小,
            // 這個當然可以, 但是寬/高不能超過父View的尺寸,最大為size,則對應的測量模式為MeasureSpec.AT_MOST。
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 當父View的測量模式為最大模式時,即父View目前也不知道自己的具體大小,但不能大于size
        case MeasureSpec.AT_MOST:
            // 既然子View的寬/高已經確定,雖然父View的尺寸尚未確定也要優先滿足子View,
            // 則子View的寬/高為自身大小childDimension,對應的測量模式為MeasureSpec.EXACTLY。
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } 
            // 如果子View的LayoutParams的寬/高為match_parent,雖說父View的大小為size,但具體的數值并不能確定,
            // 所以子View寬/高不能超過父View的最大尺寸,即size,
            // 此時子View的寬高為最大為size,則對應的測量模式為MeasureSpec.AT_MOST
            else if (childDimension == LayoutParams.MATCH_PARENT) 
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } 
            // 如果子View的LayoutParams的寬/高為wrap_content,即子View想自己來決定自己的大小,這個當然可以
            // 同理,因為父View尺寸的不確定性,所以子View最終自我決定的尺寸不能大于size,
            // 對應的測量模式為MeasureSpec.AT_MOST。
            else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // 這種情況下,View的尺寸不受任何限制,主要用于系統內部,在我們日常開發中幾乎用不到,就不分析了。
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                resultSize = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //根據最終子View的測量大小和測量模式得到相應的測量規格。
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }

所以View的測量規格是由自身的LayoutParams和父View的MeasureSpec共同決定的,它們之間具體的組合關系如下圖:


此圖來自互聯網

既然搞清楚了MeasureSpec是怎么回事,接下來具體來看一下View的工作流程measure、layout、draw。

1、measure

首先測量過程要區分是View還是ViewGroup,如果只是單純的View,則是需要測量自身就好了,如果是ViewGroup則需要先測量自身,再去遞歸測量所有的的子View。
1.1、當自定義View是單純的View時
在View類中有如下方法:

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
}

View通過該方法來進行大小測量,但是這是final方法,我們并不能重寫,但是在它內部調用了View類的另外一個方法:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

好熟悉的感覺,這就是我們在自定義View的時候通常重寫的onMeasure()方法。
其中setMeasuredDimension()方法,用來存儲測量后的View的寬/高,存儲之后,我們才可以調用View的getMeasuredWidth()、getMeasuredHeight()的到對應的測量寬/高。
重點看一下其中的getDefaultSize()方法:

/**
 * @param size View的默認尺寸
 * @param measureSpec View的測量規格
 */
public static int getDefaultSize(int size, int measureSpec) {
        int result = size;
        int specMode = MeasureSpec.getMode(measureSpec);
        int specSize = MeasureSpec.getSize(measureSpec);

        switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
        }
        return result;
    }

可以發現,當View的測量模式為MeasureSpec.AT_MOST、MeasureSpec.EXACTLY時,它最終的測量尺寸都為specSize ,竟然相等。再結合上邊的MeasureSpec關系圖對比下,可以看到當View最終的測量規格為MeasureSpec.AT_MOST時,其最終的尺寸為父View的尺寸。所以當自定義View在布局中的使用wrap_content和match_parent時的效果是一樣的,View都將占滿父View剩余的空間,但這并不是我們愿意看到的,所以我們需要在View的布局寬/高為wrap_content時,重新計算View的測量尺寸,其它情況下直接使用系統的測量值即可,重新測量的模板代碼如下:

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        measureChildren(widthMeasureSpec, heightMeasureSpec);

        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);

        //View的布局參數為wrap_content時,需要重新計算的View的測量寬/高
        int measureWidth = 0;
        int measureHeight = 0;

        if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
            //確定measureWidth、measureHeight
            setMeasuredDimension(measureWidth, measureHeight);
        } else if (widthSpecMode == MeasureSpec.AT_MOST) {
            //確定measureWidth
            setMeasuredDimension(measureWidth, heightSpecSize);
        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
            //確定measureHeight
            setMeasuredDimension(widthSpecSize, measureHeight);
        }
    }

至于如何確定measureWidth、measureHeight的值,就需要結合具體的業務需求了。
1.2、當自定義View是一個ViewGroup時
ViewGroup是一個抽象類,繼承與View類,但它沒有重寫onMeasure()方法,所以需要ViewGroup的子類去實現onMeasure()方法以進行具體測量。既然View類對onMeasure()方法方法做了統一的實現,為什么ViewGroup類沒有呢?因為View類不牽扯子View的布局,而ViewGroup中的子View可能有不同的布局情況,實現細節也有差別,所以無法做統一的處理,只能交給子類根據業務需求來重寫。
在ViewGroup類里有一個measureChildren()方法:

protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
        final int size = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < size; ++i) {
            final View child = children[i];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

該方法通過遍歷子View,進而調用measureChild()方法得到子View的測量規格:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();

        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);

        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

可以看到該方法和我們上邊分析的measureChildWithMargins()方法類似,都是通過getChildMeasureSpec()完成對子View規格的測量。所以一般情況下,我們自定義View如果繼承ViewGroup,則需要在重寫onMeasure()方法時首先進行measureChildren()操作來確定子View的測量規格。

1.1中我們提到,如果View在布局中寬/高為wrap_content時,需要重寫onMeasure(),來重新計算View的測量寬/高,同樣的道理,當我們自定義的View是一個ViewGroup的話也需要重新計算ViewGroup的測量寬/高,當然這里的計算一般要考慮子View的數量以及測量規格等情況。

2、layout

layout的作用是來確定View本身的位置,在View類中源碼如下:

public void layout(int l, int t, int r, int b) {
        if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

        int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;

        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;

            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }

        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
    }

其中有這么一段:

boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

用來設置View四個邊的位置,即mLeft、mTop、mBottom、mRight的值,這樣也就確定了View本身的位置。
接下來通過onLayout(changed, l, t, r, b);來確定子View在父View中的位置:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
    }

是個空方法哦,所以當我們自定義ViewGroup時需要重寫onLayout()方法,來確定子View的位置。View類中的layout方法有這么一段注釋:

Derived classes should not override this method.
Derived classes with children should override onLayout.
In that method, they should call layout on each of their children.

大概的意思是這樣的,View的派生類一般不需要重寫layout方法,應該在其派生類中重寫onLayout()方法,并在onLayout()方法中調用layout()方法來確定子View的位置。

其實,這也符合我們平時自定義View時如果繼承ViewGroup時的情況,我們一般都會重寫onLayout()方法,然后通過layout()方法確定子View的具體位置。
當我們自定義的View如果繼承View類的話,一般就不需要重寫onLayout()方法了哦,畢竟沒有子View么。

執行完layout方法后,我們的View具體位置也就確定了,此時可以通過getWidth()、getHeight()方法得到View的最終寬/高:

public final int getWidth() {
        return mRight - mLeft;
    }

public final int getHeight() {
        return mBottom - mTop;
    }

還記得我們在分析measure過程時提到,通過getMeasuredWidth()、getMeasuredHeight()可以得到View的測量寬/高,這兩組方法有什么區別呢?其實一般情況下View的測量寬/高和最終的寬/高相等,只是賦值的時間點不同,但在某些特殊的情況下就有差別了。拿getWidth()方法來說,它的返回值是mRight - mLeft,即View右邊位置和左邊位置的差值,我們假設一個自定義ViewGroup中某個子View的四邊的位置分別為:l、t、r、b,一般情況下我們會這樣確定子View的位置:

childView.layout(l, t, r, b);

這種情況View的測量寬度和最終寬度是相等的,但如果按照如下的寫法:

childView.layout(l, t, r + 100, b);

此時View的最終寬度會比測量寬度大100px的。在measure過程中有一點需要注意,如果View的結構比較復雜,則可能需要多次的進行測量才能得到最終的測量結果,這也會導致我們得到的測量尺寸不準確。所以,所以要得到View最終的正確尺寸,應該通過getWidth()或者getHeight()方法。

3、draw

經歷了measure、layout的過程,View的尺寸和位置已經確定,接下來就差最后一步了,那就是draw,具體的繪制流程是什么樣的呢?查看一下View類中draw方法的源碼:

public void draw(Canvas canvas) {
        final int privateFlags = mPrivateFlags;
        final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
                (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
        mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

        /*
         * Draw traversal performs several drawing steps which must be executed
         * in the appropriate order:
         *
         *      1. Draw the background
         *      2. If necessary, save the canvas' layers to prepare for fading
         *      3. Draw view's content
         *      4. Draw children
         *      5. If necessary, draw the fading edges and restore layers
         *      6. Draw decorations (scrollbars for instance)
         */

        // Step 1, draw the background, if needed
        int saveCount;

        if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // we're done...
            return;
        }
        //此處省略N行代碼......
}

繪制的流程很清晰,基本按照如下幾個步驟:

  • 1、Draw the background 繪制View的背景 drawBackground(canvas)
  • 2、If necessary, save the canvas' layers to prepare for fading 保存畫布層,準備漸變
  • 3、Draw view's content 繪制內容,也就是View自身 onDraw(canvas)
  • 4、Draw children 繪制子View dispatchDraw(canvas)
  • 5、If necessary, draw the fading edges and restore layers 繪制漸變,保存圖層
  • 6、Draw decorations (scrollbars for instance) 繪制裝飾物 onDrawForeground(canvas)
    我們關心的是步驟3、4的onDraw()和dispatchDraw()方法。
    先看onDraw()方法:
protected void onDraw(Canvas canvas) {
    }

是一個空方法,這也可以理解,畢竟不同的View呈現的效果不同,所以需要子類重寫來實現具體的細節。當我們自定義View繼承View類時,通常會重寫onDraw()方法,來繪制線條或各種形狀、圖案等。

再看一下View類的dispatchDraw()方法:

protected void dispatchDraw(Canvas canvas) {
    }

依然是空方法,需要子類去重寫,所以ViewGroup類中重寫了dispatchDraw()方法,遍歷所有的子View,其中有一行代碼是drawChild(canvas, transientChild, drawingTime);正是用來繪制子View的,再看下細節:

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        return child.draw(canvas, this, drawingTime);
    }

其中child.draw(canvas, this, drawingTime);是子View調用了View類的draw()方法,則子View得到了最終的繪制。同樣的道理ViewGroup中的所有子View得到繪制。所以當我們自定義的View是ViewGroup的子類時,必要時可以考慮重寫dispatchDraw()方法來繪制相應的內容。

到這里我們View的工作流程就分析完畢了,掌握這些基本的原理只是第一步,但也是必須的,繼續加油吧。

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

推薦閱讀更多精彩內容