一次滿足兩個(gè)需求——嵌套滑動(dòng)&下拉刷新

前言

日常開發(fā)中虐块,下拉刷新列表是一個(gè)常見的不能再常見的需求了,github上也有很多成熟了下拉刷新庫(kù)可供學(xué)習(xí)嘉蕾,但如果要刷新的列表被ScrollView或者RecyclerView包圍贺奠,可能很多傳統(tǒng)的下拉刷新的實(shí)現(xiàn)就沒(méi)辦法使用了,那我們?nèi)绾卧谇短谆瑒?dòng)中和諧共處地實(shí)現(xiàn)下拉刷新呢错忱?

先看最終效果圖:


基礎(chǔ)知識(shí)——嵌套滾動(dòng)

按照使用習(xí)慣儡率,用戶會(huì)在手機(jī)屏幕上使用上下或者左右滑動(dòng)手勢(shì)來(lái)滾動(dòng)頁(yè)面或者列表,但如果在同一個(gè)界面有多個(gè)控件可以滑動(dòng)時(shí)以清,如何協(xié)調(diào)多個(gè)控件響應(yīng)用戶操作是一個(gè)頗為復(fù)雜的問(wèn)題儿普。一般來(lái)說(shuō),我們可以在控件中實(shí)現(xiàn)dispatchTouchEvent(), onTouchEvent(), onInterceptTouchEvent()三連擊來(lái)控制滑動(dòng)操作掷倔,但這個(gè)實(shí)現(xiàn)方式有個(gè)漏洞眉孩,如果Touch事件傳遞過(guò)程中,某個(gè)View獲得處理Touch事件機(jī)會(huì)勒葱,那么其他View就再也沒(méi)有機(jī)會(huì)去處理這個(gè)Touch事件了浪汪,直到下一次手指再按下,也就是說(shuō)凛虽,這種方法無(wú)法讓多個(gè)View協(xié)同處理一個(gè)滑動(dòng)事件死遭。
這時(shí)候,Google霸霸就跳出來(lái)給我們指出了一條明路——NestedScrolling機(jī)制凯旋。NestedScrolling可以很好地解決嵌套控件中滑動(dòng)事件的攔截呀潭、分發(fā)和使用的問(wèn)題,使用NestedScrolling后的效果如下:



可以看到至非,頂部的AppBar和下面的ListView都是可以滑動(dòng)的钠署,使用了NestedScrolling后,只有ListView下滑到頂部時(shí)荒椭,AppBar才會(huì)響應(yīng)下滑事件谐鼎。
NestedScrolling的想法并不復(fù)雜,它會(huì)把嵌套的控件分為父View和子View戳杀,控件接收到的每個(gè)滑動(dòng)事件都分開幾個(gè)階段通知父View该面,父View決定事件的處理夭苗,并負(fù)責(zé)把處理完的事件分發(fā)給子View,層層傳遞下去隔缀,直到滑動(dòng)事件消耗完:

場(chǎng)景一:

  • 子View:爸爸题造,我準(zhǔn)備在x軸方向滑動(dòng)50px,有什么吩咐沒(méi)
  • 父View:好的猾瘸,沒(méi)什么吩咐的界赔,你滑吧。
  • 子View:遵命牵触!滑動(dòng)ing...... 爸爸淮悼,我滑完了,總共滑了50px揽思。
  • 父View:好的袜腥,記得每次都要提前匯報(bào)!

場(chǎng)景二:

  • 子View:爸爸钉汗,我準(zhǔn)備在x軸方向滑動(dòng)50px羹令,有什么吩咐沒(méi)
  • 父View:你x軸的50px我要全部沒(méi)收,你別動(dòng)了
  • 子View:納尼 w(?Д?)w 好吧誰(shuí)讓你是爸爸...

具體到代碼實(shí)現(xiàn)损痰,Google提供了NestedScrollingParent和NestedScrollingChild兩個(gè)接口福侈,如果自定義的View要當(dāng)父親來(lái)嵌套子View,那么請(qǐng)實(shí)現(xiàn)NestedScrollingParent卢未,如果要當(dāng)兒子被父View包含肪凛,請(qǐng)實(shí)現(xiàn)NestedScrollingChild,大多數(shù)情況下辽社,我們自定義的View最好同時(shí)繼承NestedScrollingParent和NestedScrollingChild來(lái)提高使用時(shí)的靈活性伟墙。

我們先來(lái)看看NestedScrollingParent和NestedScrollingChild:

public interface NestedScrollingParent {

    boolean onStartNestedScroll(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    void onNestedScrollAccepted(@NonNull View child, @NonNull View target, @ScrollAxis int axes);

    void onStopNestedScroll(@NonNull View target);

    void onNestedScroll(@NonNull View target, int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed);

    void onNestedPreScroll(@NonNull View target, int dx, int dy, @NonNull int[] consumed);

    boolean onNestedFling(@NonNull View target, float velocityX, float velocityY, boolean consumed);

    boolean onNestedPreFling(@NonNull View target, float velocityX, float velocityY);

    @ScrollAxis
    int getNestedScrollAxes();
}
public interface NestedScrollingChild {

    void setNestedScrollingEnabled(boolean enabled);

    boolean isNestedScrollingEnabled();

    boolean startNestedScroll(@ScrollAxis int axes);

    void stopNestedScroll();

    boolean hasNestedScrollingParent();

    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
            int dxUnconsumed, int dyUnconsumed, @Nullable int[] offsetInWindow);

    boolean dispatchNestedPreScroll(int dx, int dy, @Nullable int[] consumed,
            @Nullable int[] offsetInWindow);

    boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);

    boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

可以看出Parent和Child很多方法名都是接近的,Google霸霸怕我們處理不好這些事件的分發(fā)爹袁,還貼心的給我們提供了兩個(gè)對(duì)應(yīng)的輔助類:NestedScrollingParentHelper和NestedScrollingChildHelper远荠,如果要寫一個(gè)簡(jiǎn)單的NestedScrolling對(duì)象矮固,只需要調(diào)用NestedScrollingHelper對(duì)應(yīng)的方法就可以了失息,例如:

class NestedScrollingView extends ViewGroup implements NestedScrollingChild{

    private NestedScrollingChildHelper mChildHelper = new NestedScrollingChildHelper(this);
    
    //...
    
    @Override
    boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
                              int dxUnconsumed, int dyUnconsumed, @Nullable  int[] offsetInWindow){
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed,offsetInWindow)
    }
}

滑動(dòng)事件傳遞簡(jiǎn)要流程圖如下(不包含F(xiàn)ling事件):


關(guān)于NestedScrollingParent和NestedScrollingChild方法參數(shù)和返回值的詳細(xì)說(shuō)明可以參考這里,接下來(lái)會(huì)結(jié)合下拉刷新的需求來(lái)實(shí)現(xiàn)具體代碼档址。
Lollipop及以上版本的所有View都已經(jīng)支持了NestedScroll機(jī)制盹兢,Lollipop之前版本可以通過(guò)Support包進(jìn)行向前兼容。

此外守伸,26.0.0中NestedScroll得到了加強(qiáng)绎秒,對(duì)嵌套滾動(dòng)的API做了些改進(jìn),出現(xiàn)了新的接口NestedScrollingParent2和NestedScrollingChild2尼摹。新的接口在部分方法之上添加了一個(gè)新的參數(shù) type 见芹,type參數(shù)告訴你是什么類型的輸入在驅(qū)動(dòng)scroll事件剂娄,目前可以是這兩種選項(xiàng)之一:ViewCompat.TYPE_TOUCH 和ViewCompat.TYPE_NON_TOUCH。詳細(xì)可以參考這里玄呛,下文將使用新的API阅懦。

實(shí)現(xiàn)需求——下拉刷新

要在NestScrolling的基礎(chǔ)上實(shí)現(xiàn)下拉刷新功能,我們首先定義一個(gè)繼承ViewGroup的CustomRefreshLayout控件徘铝,并重寫其中的onMeasure和onLayout方法耳胎,實(shí)現(xiàn)對(duì)滑動(dòng)控件和刷新控件的寬高測(cè)量和具體布局工作:
注:下面的代碼使用了Kotlin語(yǔ)言編寫,語(yǔ)法不復(fù)雜不會(huì)影響閱讀體驗(yàn)惕它,了解更多關(guān)于Kotlin的內(nèi)容可以看我這篇文章

public class CustomRefreshLayout : ViewGroup {

    private var mTarget: View? = null // 被滑動(dòng)的控件
    private var mSpinner: FrameLayout by Delegates.notNull() // 刷新控件

    init {
        mSpinner = FrameLayout(context)
        addView(mSpinner)
    }

    //...

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        if (mTarget == null) {
            ensureTarget()
        }
        if (mTarget == null) {
            return
        }
        mTarget?.measure(MeasureSpec.makeMeasureSpec(
                measuredWidth - paddingLeft - paddingRight,
                MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(
                measuredHeight - paddingTop - paddingBottom, MeasureSpec.EXACTLY))
        mSpinner.measure(
                MeasureSpec.makeMeasureSpec(measuredWidth, MeasureSpec.AT_MOST),
                MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.AT_MOST))
        mOriginalSpinnerOffsetTop = -(mSpinner.measuredHeight)
        mSpinnerIndex = -1
        // 獲取刷新控件的序號(hào)
        for (index in 0 until childCount) {
            if (getChildAt(index) === mSpinner) {
                mSpinnerIndex = index
                break
            }
        }
    }

    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
        val width = measuredWidth
        val height = measuredHeight
        if (childCount == 0) {
            return
        }
        if (mTarget == null) {
            ensureTarget()
        }
        val child = mTarget ?: return
        val childLeft = paddingLeft
        var childTop = paddingTop
        val childWidth = width - paddingLeft - paddingRight
        val childHeight = height - paddingTop - paddingBottom
        child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight)
        val spinnerWidth = mSpinner.measuredWidth
        val spinnerHeight = mSpinner.measuredHeight
        mSpinner.layout(width / 2 - spinnerWidth / 2, mCurrentSpinnerOffsetTop,
                width / 2 + spinnerWidth / 2, mCurrentSpinnerOffsetTop + spinnerHeight)
    }

    private fun ensureTarget() {
        // 確保要處理滑動(dòng)的控件存在
        if (mTarget == null) {
            for (i in 0 until childCount) {
                val child = getChildAt(i)
                if (child != mSpinner) {
                    mTarget = child
                    break
                }
            }
        }
    }
}

這其中有一個(gè)坑怕午,刷新控件mSpinner的位置可能不是ViewGroup的最后一個(gè),所以在繪制時(shí)可能被滑動(dòng)控件覆蓋淹魄,要解決這問(wèn)題郁惜,可以重寫getChildDrawingOrder方法來(lái)指定繪制順序:

override fun getChildDrawingOrder(childCount: Int, i: Int): Int {
    return when {
        mSpinnerIndex < 0 -> i
        i == childCount - 1 -> // 最后一位繪制
            mSpinnerIndex
        i >= mSpinnerIndex -> // 提早一位繪制
            i + 1
        else -> // 保持原順序繪制
            i
    }
}

有了NestedScrolling后,我們可以不用關(guān)注觸摸事件甲锡,只需要處理滑動(dòng)事件中扳炬,所以讓CustomRefreshLayout實(shí)現(xiàn)NestedScrollingParent2和NestedScrollingChild2兩個(gè)接口。

public class CustomRefreshLayout : ViewGroup, NestedScrollingParent2, NestedScrollingChild2 {

    private val mParentHelper = NestedScrollingParentHelper(this)
    private val mChildHelper = NestedScrollingChildHelper(this)
    private var mStatus: Status = Status.Ready

    //...

    // NestedScrollingParent

    override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int, type: Int): Boolean {
        return (isEnabled && mStatus != Status.Refresh
                && nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0 //只接受垂直方向的滾動(dòng)
                && type == ViewCompat.TYPE_TOUCH)//只接受touch的滾動(dòng)搔体,不接受fling
    }

    override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
        mParentHelper.onNestedScrollAccepted(child, target, axes, type)
        // 分發(fā)事件給Nested Parent
        startNestedScroll(axes and ViewCompat.SCROLL_AXIS_VERTICAL, type)
        // 重置計(jì)數(shù)
        mTotalUnconsumed = 0f
        mStatus = Status.Pull
    }

    override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray?, type: Int) {
        consumed ?: return

        // 如果在下拉過(guò)程中恨樟,直接響應(yīng)并消耗上滑距離,調(diào)整Spinner位置
        if (dy > 0 && mTotalUnconsumed > 0) {
            if (dy > mTotalUnconsumed) {
                consumed[1] = dy - mTotalUnconsumed.toInt()
                mTotalUnconsumed = 0f
            } else {
                mTotalUnconsumed -= dy.toFloat()
                consumed[1] = dy
            }
            moveSpinner(mTotalUnconsumed)
        }

        // 讓Nested Parent來(lái)處理剩下的滑動(dòng)距離
        val parentConsumed = mParentScrollConsumed
        if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null, type)) {
            consumed[0] += parentConsumed[0]
            consumed[1] += parentConsumed[1]
        }
    }

    override fun onStopNestedScroll(target: View, type: Int) {
        mParentHelper.onStopNestedScroll(target)
        // 如果有處理過(guò)滑動(dòng)事件疚俱,執(zhí)行滑動(dòng)停止后的操作
        if (mTotalUnconsumed > 0) {
            finishSpinner(mTotalUnconsumed)
            mTotalUnconsumed = 0f
        }
        // 分發(fā)事件給Nested Parent
        stopNestedScroll(type)
    }

    override fun onNestedScroll(target: View, dxConsumed: Int, dyConsumed: Int,
                                dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
        // 首先分發(fā)事件給Nested Parent
        dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,
                mParentOffsetInWindow, type)

        // 考慮到有時(shí)候可能被兩個(gè)nested scrolling view包圍劝术,這里計(jì)算滑動(dòng)距離時(shí)要加上Nested Parent滑動(dòng)的距離
        // 如果可以刷新,移動(dòng)刷新控件的位置
        val dy = dyUnconsumed + mParentOffsetInWindow[1]
        if (dy < 0 && !canChildScrollUp()) {
            mTotalUnconsumed += Math.abs(dy).toFloat()
            moveSpinner(mTotalUnconsumed)
        }
    }

    // NestedScrollingChild呆奕,全部交由Childer Helper來(lái)處理

    override fun startNestedScroll(axes: Int, type: Int): Boolean {
        return mChildHelper.startNestedScroll(axes, type)
    }

    override fun stopNestedScroll(type: Int) {
        mChildHelper.stopNestedScroll(type)
    }

    override fun hasNestedScrollingParent(type: Int): Boolean {
        return mChildHelper.hasNestedScrollingParent(type)
    }

    override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int,
                                      dyUnconsumed: Int, offsetInWindow: IntArray?, type: Int): Boolean {
        return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed,
                dxUnconsumed, dyUnconsumed, offsetInWindow, type)
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
        return mChildHelper.dispatchNestedPreScroll(
                dx, dy, consumed, offsetInWindow, type)
    }

    /**
     * 移動(dòng)刷新控件的垂直位置
     */
    private fun moveSpinner(overscrollTop: Float) {
        if (mSpinner.visibility != View.VISIBLE) {
            mSpinner.visibility = View.VISIBLE
        }
        val move = if (overscrollTop <= mRefreshSlop) {
            overscrollTop
        } else {
            mRefreshSlop + (overscrollTop - mRefreshSlop) / 2f
        }.toInt()
        val targetOffsetTop = mOriginalSpinnerOffsetTop + move
        setSpinnerOffsetTopAndBottom(targetOffsetTop - mCurrentSpinnerOffsetTop)
    }

    /**
     * 停止下拉后的操作
     */
    private fun finishSpinner(overscrollTop: Float) {
        if (overscrollTop > mRefreshSlop) {
            setRefreshing(true, true /* notify */)
        } else {
            // cancel refresh
            mStatus = Status.Ready
            animateSpinnerToReady()
        }
    }

    /**
     * 設(shè)置刷新狀態(tài)
     * @param refreshing 是否在刷新
     * @param notify 是否通知listener
     */
    private fun setRefreshing(refreshing: Boolean, notify: Boolean) {
        val isRefreshing = mStatus == Status.Refresh
        if (isRefreshing != refreshing) {
            ensureTarget()
            if (refreshing) {
                mStatus = Status.Refresh
                animateSpinnerToRefresh()
                if (notify) {
                    mOnRefreshListener?.onRefresh()
                }
            } else {
                mStatus = Status.Ready
                animateSpinnerToReady()
            }
        }
    }

    /**
     * 設(shè)置Spinner的位置
     */
    private fun setSpinnerOffsetTopAndBottom(offset: Int) {
        mSpinner.bringToFront()
        ViewCompat.offsetTopAndBottom(mSpinner, offset)
        mCurrentSpinnerOffsetTop = mSpinner.top
        if (!mIsSpinnerOver) {
            ViewCompat.offsetTopAndBottom(mTarget, offset)
        }
    }
}

再為下拉刷新添加合適的動(dòng)畫效果养晋,比如說(shuō)簡(jiǎn)單的平移:

    /**
     * 讓Spinner帶動(dòng)畫移動(dòng)到準(zhǔn)備位置
     */
    private fun animateSpinnerToReady() {
        mAnimateFrom = mCurrentSpinnerOffsetTop
        mAnimateToReady.reset()
        mAnimateToReady.duration = ANIMATE_DURATION.toLong()
        mAnimateToReady.interpolator = ANIMATE_INTERPOLATOR
        mSpinner.clearAnimation()
        mSpinner.startAnimation(mAnimateToReady)
    }

    //移動(dòng)到準(zhǔn)備位置的動(dòng)畫
    private val mAnimateToReady = object : Animation() {
        public override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
            val targetTop = mAnimateFrom + ((mOriginalSpinnerOffsetTop - mAnimateFrom) * interpolatedTime).toInt()
            val offset = targetTop - mCurrentSpinnerOffsetTop
            setSpinnerOffsetTopAndBottom(offset)
            updateProgress()
        }
    }

    /**
     * 讓Spinner帶動(dòng)畫移動(dòng)到刷新位置
     */
    private fun animateSpinnerToRefresh() {
        mAnimateFrom = mCurrentSpinnerOffsetTop
        mAnimateToRefresh.reset()
        mAnimateToRefresh.duration = ANIMATE_DURATION.toLong()
        mAnimateToRefresh.interpolator = ANIMATE_INTERPOLATOR
        mSpinner.clearAnimation()
        mSpinner.startAnimation(mAnimateToRefresh)
    }

    //移動(dòng)到刷新位置的動(dòng)畫
    private val mAnimateToRefresh = object : Animation() {
        public override fun applyTransformation(interpolatedTime: Float, t: Transformation) {
            val endTarget = mOriginalSpinnerOffsetTop + mRefreshSlop
            val targetTop = mAnimateFrom + ((endTarget - mAnimateFrom) * interpolatedTime).toInt()
            val offset = targetTop - mCurrentSpinnerOffsetTop
            setSpinnerOffsetTopAndBottom(offset)
            updateProgress()
        }
    }

核心功能完成后,在加上一些對(duì)外暴露的接口就大功告成了梁钾!完整代碼在這里绳泉,最終實(shí)現(xiàn)的效果是這樣的:

后記

文章實(shí)現(xiàn)的下拉刷新控件CustomRefreshLayout的原理其實(shí)和官方提供的SwipeRefreshLayout很類似,區(qū)別只在于SwipeRefreshLayout還處理了Touch事件姆泻,而CustomRefreshLayout沒(méi)有零酪,所以CustomRefreshLayout只可以與NestedScrollView或者RecyclerView等實(shí)現(xiàn)了NestedScrolling的控件協(xié)同使用,有興趣學(xué)習(xí)或者使用的同學(xué)可以根據(jù)需要再擴(kuò)展一下拇勃。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末四苇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子方咆,更是在濱河造成了極大的恐慌月腋,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,884評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異榆骚,居然都是意外死亡片拍,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,755評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門妓肢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)穆碎,“玉大人,你說(shuō)我怎么就攤上這事职恳∷鳎” “怎么了?”我有些...
    開封第一講書人閱讀 158,369評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵放钦,是天一觀的道長(zhǎng)色徘。 經(jīng)常有香客問(wèn)我,道長(zhǎng)操禀,這世上最難降的妖魔是什么褂策? 我笑而不...
    開封第一講書人閱讀 56,799評(píng)論 1 285
  • 正文 為了忘掉前任,我火速辦了婚禮颓屑,結(jié)果婚禮上斤寂,老公的妹妹穿的比我還像新娘。我一直安慰自己揪惦,他們只是感情好遍搞,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,910評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著器腋,像睡著了一般溪猿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上纫塌,一...
    開封第一講書人閱讀 50,096評(píng)論 1 291
  • 那天诊县,我揣著相機(jī)與錄音,去河邊找鬼措左。 笑死依痊,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的怎披。 我是一名探鬼主播胸嘁,決...
    沈念sama閱讀 39,159評(píng)論 3 411
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼钳枕!你這毒婦竟也來(lái)了缴渊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,917評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤鱼炒,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后蝌借,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體昔瞧,經(jīng)...
    沈念sama閱讀 44,360評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡指蚁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,673評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了自晰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凝化。...
    茶點(diǎn)故事閱讀 38,814評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖酬荞,靈堂內(nèi)的尸體忽然破棺而出搓劫,到底是詐尸還是另有隱情,我是刑警寧澤混巧,帶...
    沈念sama閱讀 34,509評(píng)論 4 334
  • 正文 年R本政府宣布枪向,位于F島的核電站,受9級(jí)特大地震影響咧党,放射性物質(zhì)發(fā)生泄漏秘蛔。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,156評(píng)論 3 317
  • 文/蒙蒙 一傍衡、第九天 我趴在偏房一處隱蔽的房頂上張望深员。 院中可真熱鬧,春花似錦蛙埂、人聲如沸倦畅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)滔迈。三九已至,卻和暖如春被辑,著一層夾襖步出監(jiān)牢的瞬間燎悍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,123評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工盼理, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谈山,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,641評(píng)論 2 362
  • 正文 我出身青樓宏怔,卻偏偏與公主長(zhǎng)得像奏路,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子臊诊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,728評(píng)論 2 351

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