View繪制流程(二)

View繪制流程(一)

View的布局

ViewRootImplperformTraversalsperformMeasure執行完成以后會接著執行performLayoutViewRootImpl調用performLayout執行Window對應的View的布局。

  • ViewRootImpl的performLayout。
  • DecorView(FrameLayout)的layout方法。
  • DecorView(FrameLayout)的onLayout方法。
  • DecorView(FrameLayout)的layoutChildren方法。
  • DecorView(FrameLayout)的所有子View的Layout。
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    /*******部分代碼省略**********/

    //View的布局
    private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth,
            int desiredWindowHeight) {
        /*******部分代碼省略**********/
        final View host = mView;
        /*******部分代碼省略**********/
        try {
            //調用View的Layout方法進行布局
            host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
            mInLayout = false;
            //在ViewRootImpl進行布局的期間,Window內的View自己進行requestLayout
            int numViewsRequestingLayout = mLayoutRequesters.size();
            if (numViewsRequestingLayout > 0) {           
                    for (int i = 0; i < numValidRequests; ++i) {
                        final View view = validLayoutRequesters.get(i);
                        Log.w("View", "requestLayout() improperly called by " + view +
                                " during layout: running second layout pass");
                        //請求對該View布局,最終回調到ViewRootImpl的requestLayout進行重新測量、布局、繪制
                        view.requestLayout();
                    }
                    measureHierarchy(host, lp, mView.getContext().getResources(),
                            desiredWindowWidth, desiredWindowHeight);
                    mInLayout = true;
                    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
                    mHandlingLayoutInLayoutRequest = false;

                    // Check the valid requests again, this time without checking/clearing the
                    // layout flags, since requests happening during the second pass get noop'd
                    validLayoutRequesters = getValidLayoutRequesters(mLayoutRequesters, true);
                    if (validLayoutRequesters != null) {
                        getRunQueue().post(new Runnable() {
                            @Override
                            public void run() {
                                int numValidRequests = finalRequesters.size();
                                for (int i = 0; i < numValidRequests; ++i) {
                                    final View view = finalRequesters.get(i);
                                    /*******部分代碼省略**********/
                                    //請求對該View布局,最終回調到ViewRootImpl的requestLayout進行重新測量、布局、繪制
                                    view.requestLayout();
                                }
                            }
                        });
                    }
                }
            }
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        mInLayout = false;
    }
}

layout方法接收四個參數,這四個參數分別代表相對Parent的左、上、右、下坐標。而且還可以看見左上都為0,右下分別為上面剛剛測量的widthheight

public void layout(int l, int t, int r, int b) {
        ......
        //實質都是調用setFrame方法把參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量
        //判斷View的位置是否發生過變化,以確定有沒有必要對當前的View進行重新layout
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        //需要重新layout
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            //回調onLayout
            onLayout(changed, l, t, r, b);
            ......
        }
        ......
}

類似measure過程,layout調用了onLayout方法,這里需要注意的是layout方法可以被子類重寫,下面看一下ViewonLayout方法。

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

竟然是一個空的方法。,那么就看一下ViewGrouplayout方法。

@Override
public final void layout(int l, int t, int r, int b) {
    if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
        if (mTransition != null) {
            mTransition.layoutChange(this);
        }
        //調用View的layout方法
        super.layout(l, t, r, b);
    } else {
        mLayoutCalledWhileSuppressed = true;
    }
}

本質還是調用Viewlayout方法,這里需要注意的是ViewGroup的layout方法是不能被子類重寫的。

接下來看下ViewGrouponLayout方法。

@Override
protected abstract void onLayout(boolean changed,int l, int t, int r, int b);

ViewGrouponLayout()方法竟然是一個抽象方法,這就是說所有ViewGroup的子類都必須重寫這個方法。所以在自定義ViewGroup控件中,onLayout配合onMeasure方法一起使用可以實現自定義View的復雜布局。自定義View首先調用onMeasure進行測量,然后調用onLayout方法動態獲取子View和子View的測量大小,然后進行layout布局。重載onLayout的目的就是安排其children在父View的具體位置,重載onLayout通常做法就是寫一個for循環調用每一個子視圖的layout(l, t, r, b)函數,傳入不同的參數l, t, r, b來確定每個子視圖在父視圖中的顯示位置。

既然ViewonLayout方法為空方法,ViewGrouponLayout方法為抽象方法,下面以ViewGroup的子類LinearLayout為例。

public class LinearLayout extends ViewGroup {
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (mOrientation == VERTICAL) {
            layoutVertical(l, t, r, b);
        } else {
            layoutHorizontal(l, t, r, b);
        }
    }
}

LinearLayoutlayout過程是分VerticalHorizontal的,這個就是xml布局的orientation屬性設置的。

void layoutVertical(int left, int top, int right, int bottom) {
    final int paddingLeft = mPaddingLeft;

    int childTop;
    int childLeft;

    // Where right end of child should go
    //計算父窗口推薦的子View寬度
    final int width = right - left;
    //計算父窗口推薦的子View右側位置
    int childRight = width - mPaddingRight;

    // Space available for child
    //child可使用空間大小
    int childSpace = width - paddingLeft - mPaddingRight;
    //通過ViewGroup的getChildCount方法獲取ViewGroup的子View個數
    final int count = getVirtualChildCount();
    //獲取Gravity屬性設置
    final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
    final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
    //依據majorGravity計算childTop的位置值
    switch (majorGravity) {
       case Gravity.BOTTOM:
           // mTotalLength contains the padding already
           childTop = mPaddingTop + bottom - top - mTotalLength;
           break;

           // mTotalLength contains the padding already
       case Gravity.CENTER_VERTICAL:
           childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
           break;

       case Gravity.TOP:
       default:
           childTop = mPaddingTop;
           break;
    }
    //重點!!!開始遍歷
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            //LinearLayout中其子視圖顯示的寬和高由measure過程來決定的,因此measure過程的意義就是為layout過程提供視圖顯示范圍的參考值
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            //獲取子View的LayoutParams
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();

            int gravity = lp.gravity;
            if (gravity < 0) {
                gravity = minorGravity;
            }
            final int layoutDirection = getLayoutDirection();
            final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
            //依據不同的absoluteGravity計算childLeft位置
            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
                case Gravity.CENTER_HORIZONTAL:
                    childLeft = paddingLeft + ((childSpace - childWidth) / 2)
                            + lp.leftMargin - lp.rightMargin;
                    break;

                case Gravity.RIGHT:
                    childLeft = childRight - childWidth - lp.rightMargin;
                    break;

                case Gravity.LEFT:
                default:
                    childLeft = paddingLeft + lp.leftMargin;
                    break;
            }

            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            //通過垂直排列計算調運child的layout設置child的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child, i);
        }
    }
}

實質都是調用setFrame方法把參數分別賦值給mLeft、mTop、mRight和mBottom這幾個變量。

從上面分析的ViewGroup子類LinearLayout的onLayout實現代碼可以看出,一般情況下layout過程會參考measure過程中計算得到的mMeasuredWidthmMeasuredHeight來安排子View在父View中顯示的位置,但這不是必須的,measure過程得到的結果可能完全沒有實際用處,特別是對于一些自定義的ViewGroup,其子View的個數、位置和大小都是固定的,這時候我們可以忽略整個measure過程,只在layout方法中傳入的4個參數來安排每個子View的具體位置。

到這里就不得不提getWidth()、getHeight()和getMeasuredWidth()、getMeasuredHeight()這兩對方法之間的區別(上面分析measure過程已經說過getMeasuredWidth()、getMeasuredHeight()必須在onMeasure之后使用才有效)。可以看出來getWidth()與getHeight()方法必須在layout(int l, int t, int r, int b)執行之后才有效。那我們看下View源碼中這些方法的實現吧,如下:

public final int getMeasuredWidth() {
    return mMeasuredWidth & MEASURED_SIZE_MASK;
}

public final int getMeasuredHeight() {
    return mMeasuredHeight & MEASURED_SIZE_MASK;
}

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

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

public final int getLeft() {
    return mLeft;
}

public final int getRight() {
    return mRight;
}

public final int getTop() {
    return mTop;
}

public final int getBottom() {
    return mBottom;
}

mMeasuredWidth是一個8位的十六進制數,高兩位代表ModeMEASURED_SIZE_MASK按位&后獲得測量后的寬度。

View布局總結

View.layout方法可被重載,ViewGroup.layoutfinal的不可重載,ViewGroup.onLayoutabstract的,子類必須重載實現自己的位置邏輯。View.onLayout方法是一個空方法。

measure操作完成后得到的是對每個View經測量過的measuredWidth和measuredHeightlayout操作完成之后得到的是對每個View進行位置分配后的mLeft、mTop、mRight、mBottom,這些值都是相對于父View來說的。

onLayout中最終循環調用子ViewsetFrame方法來設置mLeft、mTop、mRight和mBottom的值。

getMeasuredWidth()、getMeasuredHeight()必須在onMeasure之后使用才有效;getWidth()與getHeight()方法必須在layout(int l, int t, int r, int b)執行之后才有效。

View的繪制

ViewRootImpl調用performDraw執行Window對應的View的布局。

  • ViewRootImpl的performDraw
  • ViewRootImpl的draw
  • ViewRootImpl的drawSoftware
  • DecorView(FrameLayout)的draw方法;
  • DecorView(FrameLayout)的dispatchDraw方法;
  • DecorView(FrameLayout)的drawChild方法;
  • DecorView(FrameLayout)的所有子View的draw方法;
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    /*******部分代碼省略**********/
    //View的繪制
    private void performDraw() {
    /*******部分代碼省略**********/
        try {
            draw(fullRedrawNeeded);
        } finally {
            mIsDrawing = false;
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
        /*******部分代碼省略**********/
    }
    //進行繪制
    private void draw(boolean fullRedrawNeeded) {
        /*******部分代碼省略**********/
        //View上添加的Observer進行繪制事件的分發
        mAttachInfo.mTreeObserver.dispatchOnDraw();
        if (!dirty.isEmpty() || mIsAnimating || accessibilityFocusDirty) {
            if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
                /*******部分代碼省略**********/
                //調用Window對應的ViewRootImpl的invalidate方法
                mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);
            } else {
                /*******部分代碼省略**********/
                //繪制Window
                if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) {
                    return;
                }
            }
        }

        if (animating) {
            mFullRedrawNeeded = true;
            scheduleTraversals();
        }
    }

    void invalidate() {
        mDirty.set(0, 0, mWidth, mHeight);
        if (!mWillDrawSoon) {
            scheduleTraversals();
        }
    }

    /**
     * @return true if drawing was successful, false if an error occurred
     */
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
            boolean scalingRequired, Rect dirty) {

        /*******部分代碼省略**********/
        try {
            /*******部分代碼省略**********/
            try {
                canvas.translate(-xoff, -yoff);
                if (mTranslator != null) {
                    mTranslator.translateCanvas(canvas);
                }
                canvas.setScreenDensity(scalingRequired ? mNoncompatDensity : 0);
                attachInfo.mSetIgnoreDirtyState = false;
                //View繪制
                mView.draw(canvas);

                drawAccessibilityFocusedDrawableIfNeeded(canvas);
            } finally {
                if (!attachInfo.mSetIgnoreDirtyState) {
                    // Only clear the flag if it was not set during the mView.draw() call
                    attachInfo.mIgnoreDirtyState = false;
                }
            }
        } finally {
            /*******部分代碼省略**********/
        }
        return true;
    }
}

由于ViewGroup沒有重寫Viewdraw方法,所以直接看View.draw方法:

public void draw(Canvas canvas) {
    ......
    // 1. 繪制背景
    ......
    if (!dirtyOpaque) {
        drawBackground(canvas);
    }
    ......
    //2.繪制View的內容
    if (!dirtyOpaque) onDraw(canvas);

    //3.對當前View的所有子View進行繪制,如果當前的View沒有子View就不需要進行繪制。
    dispatchDraw(canvas);
    ......
    //4.對View的滾動條進行繪制。
    onDrawScrollBars(canvas);
    ......
}
1.對View的背景進行繪制。
private void drawBackground(Canvas canvas) {
    //獲取xml中通過android:background屬性或者代碼中setBackgroundColor()、setBackgroundResource()等方法進行賦值的背景Drawable
    final Drawable background = mBackground;
    ......
    //根據layout過程確定的View位置來設置背景的繪制區域
    if (mBackgroundSizeChanged) {
        background.setBounds(0, 0,  mRight - mLeft, mBottom - mTop);
        mBackgroundSizeChanged = false;
        rebuildOutline();
    }
    ......
        //調用Drawable的draw()方法來完成背景的繪制工作
        background.draw(canvas);
    ......
}

可以看出View背景是一個Drawable,繪制背景最終調用的是Drawable.draw

2.對View的內容進行繪制。
protected void onDraw(Canvas canvas) {
}   

View.onDraw方法為空方法,ViewGroup也沒有重寫該方法。因為每個View的內容部分是各不相同的,所以需要由子類去實現具體邏輯。

3.對當前View的所有子View進行繪制,如果當前的View沒有子View就不需要進行繪制。

View.dispatchDraw()方法是一個空方法,如果View包含子類需要重寫他,所以我們看下ViewGroup.dispatchDraw方法源碼:

@Override
protected void dispatchDraw(Canvas canvas) {
    ......
    final int childrenCount = mChildrenCount;
    final View[] children = mChildren;
    ......
    for (int i = 0; i < childrenCount; i++) {
        ......
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
    // Draw any disappearing views that have animations
    if (mDisappearingChildren != null) {
        ......
        for (int i = disappearingCount; i >= 0; i--) {
            ......
            more |= drawChild(canvas, child, drawingTime);
        }
    }
    ......
}

該方法內部會遍歷每個子View,然后調用drawChild()方法

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
4.對View的滾動條進行繪制。
protected final void onDrawScrollBars(Canvas canvas) {
    final ScrollabilityCache cache = mScrollCache;
    if (cache != null) {
        int state = cache.state;
        if (state == ScrollabilityCache.OFF) {
            return;
        }
        boolean invalidate = false;   
        .........  
        final boolean drawHorizontalScrollBar = isHorizontalScrollBarEnabled();
        final boolean drawVerticalScrollBar = isVerticalScrollBarEnabled()
                && !isVerticalScrollBarHidden();
        // Fork out the scroll bar drawing for round wearable devices.
        if (mRoundScrollbarRenderer != null) {
            if (drawVerticalScrollBar) {
                final Rect bounds = cache.mScrollBarBounds;
                getVerticalScrollBarBounds(bounds, null);
                mRoundScrollbarRenderer.drawRoundScrollbars(
                        canvas, (float) cache.scrollBar.getAlpha() / 255f, bounds);
                if (invalidate) {
                    invalidate();
                }
            }
        } else if (drawVerticalScrollBar || drawHorizontalScrollBar) {
            final ScrollBarDrawable scrollBar = cache.scrollBar;
            if (drawHorizontalScrollBar) {
                scrollBar.setParameters(computeHorizontalScrollRange(),
                        computeHorizontalScrollOffset(),
                        computeHorizontalScrollExtent(), false);
                final Rect bounds = cache.mScrollBarBounds;
                getHorizontalScrollBarBounds(bounds, null);
                onDrawHorizontalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                        bounds.right, bounds.bottom);
                if (invalidate) {
                    invalidate(bounds);
                }
            }
            if (drawVerticalScrollBar) {
                scrollBar.setParameters(computeVerticalScrollRange(),
                        computeVerticalScrollOffset(),
                        computeVerticalScrollExtent(), true);
                final Rect bounds = cache.mScrollBarBounds;
                getVerticalScrollBarBounds(bounds, null);
                onDrawVerticalScrollBar(canvas, scrollBar, bounds.left, bounds.top,
                        bounds.right, bounds.bottom);
                if (invalidate) {
                    invalidate(bounds);
                }
            }
        }
    }
}

可以看見其實任何一個View都是有(水平垂直)滾動條的,只是一般情況下沒讓它顯示而已。

View的invalidate和postInvalidate方法源碼分析

View中的invalidate方法有很多的重載,最終都會調用invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, boolean fullInvalidate)方法(ViewGroup沒有重寫這些方法)

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    ......
        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            //設置刷新區域
            damage.set(l, t, r, b);
            //傳遞調運Parent ViewGroup的invalidateChild方法
            p.invalidateChild(this, damage);
        }
        ......
}

View.invalidateInternal方法實質是將要刷新區域直接傳遞給了父ViewGroup的invalidateChild方法,在該方法中,調用父View的invalidateChild,這是一個從當前向上級父View回溯的過程,每一層的父View都將自己的顯示區域與傳入的刷新Rect做交集 。所以我們看下ViewGroup的invalidateChild方法,源碼如下:

public final void invalidateChild(View child, final Rect dirty) {
    ViewParent parent = this;
    final AttachInfo attachInfo = mAttachInfo;
    ......
    do {
        ......
        //循環層層上級調運,直到ViewRootImpl會返回null
        parent = parent.invalidateChildInParent(location, dirty);
        ......
    } while (parent != null);
}

這個過程最后傳遞到ViewRootImpl的invalidateChildInParent方法結束,所以我們看下ViewRootImpl的invalidateChildInParent方法,如下:

@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    ......
    //View調運invalidate最終層層上傳到ViewRootImpl后最終觸發了該方法
    scheduleTraversals();
    ......
    return null;
}

最終調用了scheduleTraversals方法,該方法會調用ViewRootImpl.performTraversals方法,重新繪制。

invalidate該方法只能在UI Thread中執行,其他線程中需要使用postInvalidate方法

postInvalidate方法最終會調用ViewRootImpl類的dispatchInvalidateDelayed方法,源碼如下:

public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
    mHandler.sendMessageDelayed(msg, delayMilliseconds);
}

ViewRootImpl類的Handler發送了一條MSG_INVALIDATE消息,繼續追蹤這條消息的處理可以發現:

public void handleMessage(Message msg) {
    ......
    switch (msg.what) {
    case MSG_INVALIDATE:
        ((View) msg.obj).invalidate();
        break;
    ......
    }
    ......
}

最終在UI線程中調用了Viewinvalidate方法。

直接調用invalidate方法.請求重新draw,但只會繪制調用者本身。因為其他的View狀態沒有變化的話,是不會執行對應的繪制方法的。

在窗口上的View都有一個ViewRootImpl作為它的Parent,處理View的布局、事件處理等。

Kotlin項目實戰

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

推薦閱讀更多精彩內容