【W(wǎng)indow系列】——PopupWindow的前世今生

本系列博客基于android-28版本
【W(wǎng)indow系列】——Toast源碼解析
【W(wǎng)indow系列】——PopupWindow的前世今生
【W(wǎng)indow系列】——Dialog源碼解析
【W(wǎng)indow系列】——Window中的Token

前言

上一篇博客分析了Toast的源碼,一提到Window必然少不了本篇博客分析的PopupWindow酪呻,本來我以為是一樣的流程段磨,創(chuàng)建Window履植,設置View到DecorView,加入Window倒槐,完事兒...但卻發(fā)現(xiàn)PopupWindow卻沒有按照這種實現(xiàn)方式實現(xiàn)的热幔。

大綱

本篇博客會分析一下幾點:

  1. PopupWindow的實現(xiàn)原理源碼
  2. PopupWindow關于BackgroundDrawable的版本差異導致的問題
  3. PopupWindow的觸摸事件處理

源碼分析

我們平時使用PopupWindow主要涉及以下三個核心方法:

PopupWindow window = new PopupWindow();
window.setContentView(...);
window.showAsDropDown(...);

所以首先看一下構(gòu)造函數(shù)

public PopupWindow(View contentView, int width, int height, boolean focusable) {
        if (contentView != null) {
            mContext = contentView.getContext();
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        setContentView(contentView);
        setWidth(width);
        setHeight(height);
        setFocusable(focusable);
    }

如果在構(gòu)造函數(shù)設置了ContentView蛉谜,那么直接獲取Context對象和WindowManager,調(diào)用setContentView方法姻采,設置寬高雅采,和Focusable,這里要注意一下Focusable這個變量,后面會講到這個變量在PopupWindow中的作用。
如果我們調(diào)用的是最基礎的構(gòu)造函數(shù)婚瓜,一般我們下一步會調(diào)用setContentView方法設置我們的布局宝鼓,那么這里我們就來看一下這個方法。

public void setContentView(View contentView) {
        if (isShowing()) {
            return;
        }
        //保存ContentView
        mContentView = contentView;

        if (mContext == null && mContentView != null) {
            mContext = mContentView.getContext();
        }

        if (mWindowManager == null && mContentView != null) {
            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
        }

        // Setting the default for attachedInDecor based on SDK version here
        // instead of in the constructor since we might not have the context
        // object in the constructor. We only want to set default here if the
        // app hasn't already set the attachedInDecor.
        if (mContext != null && !mAttachedInDecorSet) {
            // Attach popup window in decor frame of parent window by default for
            // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current
            // behavior of not attaching to decor frame for older SDKs.
            setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
                    >= Build.VERSION_CODES.LOLLIPOP_MR1);
        }

    }

可以看到巴刻,和剛才看到的構(gòu)造函數(shù)基本相同愚铡,保存了ContentView變量后,獲取ContextWindowManger對象胡陪。
可以看到上面兩個步驟基本上都是做的準備工作沥寥,那么接下來看一下最核心的展示方法showAsDropDown

public void showAsDropDown(View anchor) {
        showAsDropDown(anchor, 0, 0);
    }
    
public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
        if (isShowing() || !hasContentView()) {
            return;
        }

        TransitionManager.endTransitions(mDecorView);
        //綁定監(jiān)聽,設置變量
        attachToAnchor(anchor, xoff, yoff, gravity);

        mIsShowing = true;
        mIsDropdown = true;
        //創(chuàng)建布局參數(shù)
        final WindowManager.LayoutParams p =
                createPopupLayoutParams(anchor.getApplicationWindowToken());
        //包裹布局督弓,構(gòu)建布局層級
        preparePopup(p);

        final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
                p.width, p.height, gravity, mAllowScrollingAnchorParent);
        updateAboveAnchor(aboveAnchor);
        p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
        //添加布局到Window中
        invokePopup(p);
    }

可以看到营曼,這個方法其實還是利用了重載,實現(xiàn)了很多方法愚隧,最終都是到了最后這個方法里蒂阱。
上面大概分了四部分,我分別寫了注釋狂塘,這里來單獨看一下录煤。

protected void attachToAnchor(View anchor, int xoff, int yoff, int gravity) {
        detachFromAnchor();

        final ViewTreeObserver vto = anchor.getViewTreeObserver();
        if (vto != null) {
            vto.addOnScrollChangedListener(mOnScrollChangedListener);
        }
        anchor.addOnAttachStateChangeListener(mOnAnchorDetachedListener);

        final View anchorRoot = anchor.getRootView();
        anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
        anchorRoot.addOnLayoutChangeListener(mOnLayoutChangeListener);
        //弱引用
        mAnchor = new WeakReference<>(anchor);
        mAnchorRoot = new WeakReference<>(anchorRoot);
        mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();
        mParentRootView = mAnchorRoot;

        mAnchorXoff = xoff;
        mAnchorYoff = yoff;
        mAnchoredGravity = gravity;
    }

可以看到這個方法主要是設置我們傳入到參數(shù)的,但是這里要注意的是Google在這里使用了弱引用荞胡,這個我感覺是比較少見的妈踊,目前我所了解的FrameWork層的源碼里,很少看到Google使用弱引用泪漂,這里利用弱引用保存了傳入的布局和頂層父布局廊营。

protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();

        // These gravity settings put the view at the top left corner of the
        // screen. The view is then positioned to the appropriate location by
        // setting the x and y offsets to match the anchor's bottom-left
        // corner.
        p.gravity = computeGravity();
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;
        //設置Token
        p.token = token;
        p.softInputMode = mSoftInputMode;
        //設置動畫
        p.windowAnimations = computeAnimationResource();

        if (mBackground != null) {
            p.format = mBackground.getOpacity();
        } else {
            p.format = PixelFormat.TRANSLUCENT;
        }
        //設置寬高
        if (mHeightMode < 0) {
            p.height = mLastHeight = mHeightMode;
        } else {
            p.height = mLastHeight = mHeight;
        }

        if (mWidthMode < 0) {
            p.width = mLastWidth = mWidthMode;
        } else {
            p.width = mLastWidth = mWidth;
        }

        p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
                | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;

        // Used for debugging.
        p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));

        return p;
    }

createPopupLayoutParams是用來創(chuàng)建一個LayoutParam,這里注重注意一下token這個變量萝勤,看過前一篇博客的應該都記得露筒,Toast組件也需要一個token變量,這里這個token可以看到是用anchor.getApplicationWindowToken()獲取的敌卓,也就是父布局的token慎式。關于token后面會抽出一篇博客來專門分析一下,token對于Window類型的影響趟径。

private void preparePopup(WindowManager.LayoutParams p) {
        if (mContentView == null || mContext == null || mWindowManager == null) {
            throw new IllegalStateException("You must specify a valid content view by "
                    + "calling setContentView() before attempting to show the popup.");
        }

        if (p.accessibilityTitle == null) {
            p.accessibilityTitle = mContext.getString(R.string.popup_window_default_title);
        }

        // The old decor view may be transitioning out. Make sure it finishes
        // and cleans up before we try to create another one.
        if (mDecorView != null) {
            mDecorView.cancelTransitions();
        }

        // When a background is available, we embed the content view within
        // another view that owns the background drawable.
        //設置Background包裹
        if (mBackground != null) {
            mBackgroundView = createBackgroundView(mContentView);
            mBackgroundView.setBackground(mBackground);
        } else {
            mBackgroundView = mContentView;
        }
        //再用DecorView包裹
        mDecorView = createDecorView(mBackgroundView);
        mDecorView.setIsRootNamespace(true);
        //設置elevation
        // The background owner should be elevated so that it casts a shadow.
        mBackgroundView.setElevation(mElevation);

        // We may wrap that in another view, so we'll need to manually specify
        // the surface insets.
        p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);

        mPopupViewInitialLayoutDirectionInherited =
                (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
    }

這個方法可以說是popupwindow的最核心的方法了瘪吏,首先我們可以看到,對mBackgroud變量進行了判空蜗巧,如果設置了backgroud掌眠,則執(zhí)行createBackgroundView方法。

private PopupBackgroundView createBackgroundView(View contentView) {
        final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
        final int height;
        if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
            height = WRAP_CONTENT;
        } else {
            height = MATCH_PARENT;
        }

        final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext);
        final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(
                MATCH_PARENT, height);
        backgroundView.addView(contentView, listParams);

        return backgroundView;
    }

這里可以看到幕屹,構(gòu)建了一個寬高相同的布局參數(shù)扇救,并且創(chuàng)建了一個PopupBackgroundView刑枝,利用addView方法,將我們的ContentView包裹了起來迅腔。

private class PopupBackgroundView extends FrameLayout {
        public PopupBackgroundView(Context context) {
            super(context);
        }

        @Override
        protected int[] onCreateDrawableState(int extraSpace) {
            if (mAboveAnchor) {
                final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
                View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
                return drawableState;
            } else {
                return super.onCreateDrawableState(extraSpace);
            }
        }
    }

這里的PopupBackgroundView其實就是一個FrameLayout装畅,單純的只是為了設置Backgroud
接下來執(zhí)行createDecorView方法沧烈。

private PopupDecorView createDecorView(View contentView) {
        final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
        final int height;
        if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
            height = WRAP_CONTENT;
        } else {
            height = MATCH_PARENT;
        }

        final PopupDecorView decorView = new PopupDecorView(mContext);
        decorView.addView(contentView, MATCH_PARENT, height);
        decorView.setClipChildren(false);
        decorView.setClipToPadding(false);

        return decorView;
    }

可以看到和剛才大同小異掠兄,哪這回為什么又要包裹一層呢?這里就要看一下PopupDecorView

private class PopupDecorView extends FrameLayout {
        /** Runnable used to clean up listeners after exit transition. */
        private Runnable mCleanupAfterExit;

        public PopupDecorView(Context context) {
            super(context);
        }

        @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
                //對返回鍵做了特殊處理
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                if (getKeyDispatcherState() == null) {
                    return super.dispatchKeyEvent(event);
                }

                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
                        dismiss();
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            } else {
                return super.dispatchKeyEvent(event);
            }
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();

            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                //觸摸位置在外部锌雀,則直接dismiss()
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
...
} 

這里就內(nèi)容很多了蚂夕,首先這個還是一個繼承了FrameLayout的布局,唯一不同的是腋逆,這里重寫了兩個關鍵方法dispatchKeyEventonTouchEvent婿牍,所以我們應該知道這里對鍵盤事件和觸摸事件做了特殊處理,當是返回鍵時或者觸摸位置在View的外部的時候則調(diào)用dismiss()方法惩歉。
這也就是為什么Popupwindow點擊外部可以消失的原因等脂,也就是觸摸事件處理
這里還有一個地方值得我們注意

@Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

可以看到這里還重寫了dispatchTouchEvent方法撑蚌,熟悉Android事件分發(fā)流程的應該清楚上遥,這里是事件分發(fā)的頂層,這里多出了一個mTouchInterceptor這個概念争涌,其實就是一個攔截器粉楚,也就是說,對于PopupWindow亮垫,我們是可以自定義事件的處理的模软。
做完這所有的準備后,就是最后一個方法了饮潦。

private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();
        //通過WindowManger加入View
        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }

終于看到了最核心的顯示方法燃异,我們可以確定PopupWindow是通過WindowMangeraddView方法加入的『蓿可以發(fā)現(xiàn)特铝,其實PopupWindow并沒有重新創(chuàng)建新的Window,而是在當前Window上暑中,利用WindowManger.addView加入的壹瘟。,這可以說就是PopupWindow的顯示原理鳄逾。

PopupWindow關于BackgroundDrawable的版本差異導致的問題

最開始學習PopupWindow的使用方法的時候稻轨,我們經(jīng)常會看到這樣的一個注釋。

// 如果不設置PopupWindow的背景,就會出現(xiàn)一個問題:無論是點擊外部區(qū)域還是Back鍵都無法dismiss彈框
popupWindow.setBackgroundDrawable(new ColorDrawable());

通過上面的源碼分析雕凹,我們本沒有發(fā)現(xiàn)BackgroundDrawable會有這么大的影響殴俱,只是單純的印象一個包裝View的背景政冻,這里就要說一下PopupWindow的版本差異了,本篇博客是基于android-28,通過源碼我們能知道backgrounddrawable不會有這樣的影響线欲。但是我們來看一下Android4.2.2的源碼

private void preparePopup(WindowManager.LayoutParams p) {
        if (mContentView == null || mContext == null || mWindowManager == null) {
            throw new IllegalStateException("You must specify a valid content view by "
                    + "calling setContentView() before attempting to show the popup.");
        }

        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
            if (layoutParams != null &&
                    layoutParams.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                height = ViewGroup.LayoutParams.WRAP_CONTENT;
            }

            // when a background is available, we embed the content view
            // within another view that owns the background drawable
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackgroundDrawable(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }
        mPopupViewInitialLayoutDirectionInherited =
                (mPopupView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
        mPopupWidth = p.width;
        mPopupHeight = p.height;
    }

可以看到這里在preparePopup方法里明场,就有了不同,這里如果設置了mBackground就會使用PopupViewContainer保存李丰。

private class PopupViewContainer extends FrameLayout {
1542        private static final String TAG = "PopupWindow.PopupViewContainer";
1543
1544        public PopupViewContainer(Context context) {
1545            super(context);
1546        }
1547
1548        @Override
1549        protected int[] onCreateDrawableState(int extraSpace) {
1550            if (mAboveAnchor) {
1551                // 1 more needed for the above anchor state
1552                final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
1553                View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
1554                return drawableState;
1555            } else {
1556                return super.onCreateDrawableState(extraSpace);
1557            }
1558        }
1559
1560        @Override
1561        public boolean dispatchKeyEvent(KeyEvent event) {
1562            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
1563                if (getKeyDispatcherState() == null) {
1564                    return super.dispatchKeyEvent(event);
1565                }
1566
1567                if (event.getAction() == KeyEvent.ACTION_DOWN
1568                        && event.getRepeatCount() == 0) {
1569                    KeyEvent.DispatcherState state = getKeyDispatcherState();
1570                    if (state != null) {
1571                        state.startTracking(event, this);
1572                    }
1573                    return true;
1574                } else if (event.getAction() == KeyEvent.ACTION_UP) {
1575                    KeyEvent.DispatcherState state = getKeyDispatcherState();
1576                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
1577                        dismiss();
1578                        return true;
1579                    }
1580                }
1581                return super.dispatchKeyEvent(event);
1582            } else {
1583                return super.dispatchKeyEvent(event);
1584            }
1585        }
1586
1587        @Override
1588        public boolean dispatchTouchEvent(MotionEvent ev) {
1589            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
1590                return true;
1591            }
1592            return super.dispatchTouchEvent(ev);
1593        }
1594
1595        @Override
1596        public boolean onTouchEvent(MotionEvent event) {
1597            final int x = (int) event.getX();
1598            final int y = (int) event.getY();
1599
1600            if ((event.getAction() == MotionEvent.ACTION_DOWN)
1601                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
1602                dismiss();
1603                return true;
1604            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
1605                dismiss();
1606                return true;
1607            } else {
1608                return super.onTouchEvent(event);
1609            }
1610        }
1611
1612        @Override
1613        public void sendAccessibilityEvent(int eventType) {
1614            // clinets are interested in the content not the container, make it event source
1615            if (mContentView != null) {
1616                mContentView.sendAccessibilityEvent(eventType);
1617            } else {
1618                super.sendAccessibilityEvent(eventType);
1619            }
1620        }
1621    }
1622

可以看到苦锨,這里就直接處理的鍵盤事件和觸摸事件,那么就意味著如果我們沒有設置Background那么在低版本的情況下將會出現(xiàn)無法點擊外部消失這個功能趴泌,雖然后面的修復了這個問題舟舒,但是Google也留了一個很大的坑啊,而且為了包裝Background在展示上的一致性嗜憔,在高版本無奈只能選擇使用兩次包裹來實現(xiàn)秃励,也是費盡心思了。吉捶。夺鲜。

總結(jié)

本篇博客主要分析了PopupWindow的實現(xiàn)原理,總的來看帚稠,PopupWindow主要是以下幾個步驟:

  1. 設置ContentView
  2. 利用自定義View包裹我們的ContentView谣旁,自定義View重寫了鍵盤事件和觸摸事件分發(fā),實現(xiàn)了點擊外部消失
  3. 最終利用WindowManger的addView加入布局滋早,并沒有創(chuàng)建新的Window
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末榄审,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子杆麸,更是在濱河造成了極大的恐慌搁进,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,332評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昔头,死亡現(xiàn)場離奇詭異饼问,居然都是意外死亡,警方通過查閱死者的電腦和手機揭斧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,508評論 3 385
  • 文/潘曉璐 我一進店門莱革,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人讹开,你說我怎么就攤上這事盅视。” “怎么了旦万?”我有些...
    開封第一講書人閱讀 157,812評論 0 348
  • 文/不壞的土叔 我叫張陵闹击,是天一觀的道長。 經(jīng)常有香客問我成艘,道長赏半,這世上最難降的妖魔是什么贺归? 我笑而不...
    開封第一講書人閱讀 56,607評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮断箫,結(jié)果婚禮上拂酣,老公的妹妹穿的比我還像新娘。我一直安慰自己仲义,他們只是感情好踱葛,可當我...
    茶點故事閱讀 65,728評論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著光坝,像睡著了一般尸诽。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盯另,一...
    開封第一講書人閱讀 49,919評論 1 290
  • 那天性含,我揣著相機與錄音,去河邊找鬼鸳惯。 笑死商蕴,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的芝发。 我是一名探鬼主播绪商,決...
    沈念sama閱讀 39,071評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼辅鲸!你這毒婦竟也來了格郁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,802評論 0 268
  • 序言:老撾萬榮一對情侶失蹤独悴,失蹤者是張志新(化名)和其女友劉穎例书,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刻炒,經(jīng)...
    沈念sama閱讀 44,256評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡决采,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,576評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了坟奥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片树瞭。...
    茶點故事閱讀 38,712評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖爱谁,靈堂內(nèi)的尸體忽然破棺而出晒喷,到底是詐尸還是另有隱情,我是刑警寧澤管行,帶...
    沈念sama閱讀 34,389評論 4 332
  • 正文 年R本政府宣布厨埋,位于F島的核電站邪媳,受9級特大地震影響捐顷,放射性物質(zhì)發(fā)生泄漏荡陷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,032評論 3 316
  • 文/蒙蒙 一迅涮、第九天 我趴在偏房一處隱蔽的房頂上張望废赞。 院中可真熱鬧,春花似錦叮姑、人聲如沸唉地。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽耘沼。三九已至,卻和暖如春朱盐,著一層夾襖步出監(jiān)牢的瞬間群嗤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,026評論 1 266
  • 我被黑心中介騙來泰國打工兵琳, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留狂秘,地道東北人。 一個月前我還...
    沈念sama閱讀 46,473評論 2 360
  • 正文 我出身青樓躯肌,卻偏偏與公主長得像者春,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子清女,可洞房花燭夜當晚...
    茶點故事閱讀 43,606評論 2 350

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