Android View 的滾動原理和 Scroller、VelocityTracker 類的使用

Android 開發(fā)中經(jīng)常涉及 View 的滾動础嫡,例如類似于 ScrollView 的滾動手勢和滾動動畫指么,例如用 ListView 模仿 iOS 上的左滑刪除 item,例如 ListView 的下拉刷新榴鼎。這些都是常見的需求伯诬,同時也都涉及 View 滾動的相關知識。

本文將解析 Android 中 View 的滾動原理檬贰,并介紹與滾動相關的兩個輔助類 ScrollerVelocityTracker姑廉,并通過 3 個逐漸深入的例子來加深理解。

注:

  1. 本文沒有嘗試實現(xiàn)上述幾種功能翁涤,只闡述基本原理和基礎類的使用方法桥言。
  2. 文中的例子只是截取了與 View 相關的代碼,完整的示例代碼請見DEMO
  3. 本文的源碼分析基于 Android API Level 21葵礼,并省略掉部分與本文關系不大的代碼号阿。

View 的滾動原理

在了解 View 的滾動原理之前,我們先來想象一個場景:我們坐在一個房間里鸳粉,透過一扇窗戶看窗外的風景扔涧。窗戶是有大小限制的,而風景是沒有大小限制的届谈。

把上述的場景對應到 Android 的 View 顯示原理上來:當一個 View 顯示在界面上枯夜,它的上下左右邊緣就圍成了這個 View 的可視區(qū)域,我們可以稱這個區(qū)域為“可視窗口”艰山,我們平時看到的 View 的內容湖雹,都是透過這個可視窗口中看到的“風景”。View 的大小內容可以無窮大曙搬,不受可視窗口大小的限制摔吏。

另外,如果在窗外的風景中纵装,有一個人出現(xiàn)在窗戶右邊很遠的地方征讲,那么我們在房間里就看不到那個人;如果那個人站在窗戶正對著出去的地方橡娄,那么我們就可以透過窗戶看到他诗箍。對應到 View 上面來,只有出現(xiàn)在“可視窗口”中的那部分內容可以被看到挽唉。

View 的 scroll 相關

在 View 類中扳还,有兩個變量 mScrollXmScrollY才避,它們記錄的是 View 的內容的偏移值。mScrollXmScrollY 的默認值都是 0氨距,即默認不偏移。另外我們需要知道一點棘劣,向左滑動俏让,mScrollX 為正數(shù),反正為負數(shù)茬暇。假設我們令 mScrollX = 10首昔,那么該 View 的內容會相對于原來向左偏移 10px。 看看系統(tǒng)的 View 類中的源碼:

// View.java
public class View {

  /**
  * The offset, in pixels, by which the content of this view is scrolled
  * horizontally.
  * {@hide}
  */
  protected int mScrollX;
  
  /**
  * The offset, in pixels, by which the content of this view is scrolled
  * vertically.
  * {@hide}
  */
  protected int mScrollY;
  
  // ...
}

通常我們比較少直接設置 mScrollXmScrollY糙俗,而是通過 View 提供的兩個方法來設置勒奇。

// 瞬時滾動到某個位置
public void scrollTo(int x, int y)
// 瞬時滾動某個距離
public void scrollBy(int x, int y)

看看兩個方法的源碼:

// View.java
/**
* Set the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the x position to scroll to
* @param y the y position to scroll to
*/
public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

/**
* Move the scrolled position of your view. This will cause a call to
* {@link #onScrollChanged(int, int, int, int)} and the view will be
* invalidated.
* @param x the amount of pixels to scroll by horizontally
* @param y the amount of pixels to scroll by vertically
*/
public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

首先看 scrollTo(int x, int y) 方法,它除了設置 mScrollXmScrollY 兩個變量巧骚,還會觸發(fā)自己重新繪制赊颠,另外還會通過 onScrollChanged 觸發(fā)回調。而 scrollBy 方法其實也是調用 scrollTo 方法劈彪。

明顯竣蹦,兩個方法的區(qū)別在于 scrollTo 方法是滾動到特定位置,參數(shù) x沧奴、y 代表“絕對位置”痘括,而 scrollBy 方法是在當前位置基礎上滾動特定距離,參數(shù) x滔吠、y 代表“相對位置”纲菌。

另外,View 還提供了 mScrollXmScrollY 的 getter:

// 獲取 mScrollX
public final int getScrollX()
// 獲取 mScrollY
public final int getScrollY()

看看源碼中這兩個方法的注釋疮绷,可以更好地理解 scroll 的概念翰舌。

// View.java
/**
* Return the scrolled left position of this view. This is the left edge of
* the displayed part of your view. You do not need to draw any pixels
* farther left, since those are outside of the frame of your view on
* screen.
*
* @return The left edge of the displayed part of your view, in pixels.
*/
public final int getScrollX() {
    return mScrollX;
}
/**
* Return the scrolled top position of this view. This is the top edge of
* the displayed part of your view. You do not need to draw any pixels above
* it, since those are outside of the frame of your view on screen.
*
* @return The top edge of the displayed part of your view, in pixels.
*/
public final int getScrollY() {
    return mScrollY;
}

例子1

為了更好地理解 mScrollXmScrollY,也為后續(xù)介紹的知識做準備矗愧,我們先看一個例子:

/**
* 示例:自定義 ViewGroup灶芝,包含幾個一字排開的子 View,

* 每個子 View 都與該 ViewGroup 一樣大唉韭。
* 調用 moveToIndex 方法會調用 scrollTo 方法夜涕,從而瞬時滾動到某一位置
*/
public class Case1ViewGroup extends ViewGroup {

    public static final int CHILD_NUMBER = 6;
    private int mCurrentIndex = 0;

    public Case1ViewGroup(Context context) {
        super(context);
        init();
    }

    public Case1ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public Case1ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        // 添加幾個子 View
        for (int i = 0; i < CHILD_NUMBER; i++) {
            TextView child = new TextView(getContext());
            int color;
            switch (i % 3) {
                case 0:
                    color = 0xffcc6666;
                    break;
                case 1:
                    color = 0xffcccc66;
                    break;
                case 2:
                default:
                    color = 0xff6666cc;
                    break;
            }
            child.setBackgroundColor(color);
            child.setGravity(Gravity.CENTER);
            child.setTextSize(TypedValue.COMPLEX_UNIT_SP, 46);
            child.setTextColor(0x80ffffff);
            child.setText(String.valueOf(i));
            addView(child);
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        // 每個子 View 都與自己一樣大
        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
            View childView = getChildAt(i);
            childView.measure(
                    MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
        }

        setMeasuredDimension(width, height);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 子 View 一字排開
        for (int i = 0, childCount = getChildCount(); i < childCount; i++) {
            View childView = getChildAt(i);
            childView.layout(getWidth() * i, 0, getWidth() * (i + 1), b - t);
        }
    }

    /**
    * 瞬時滾動到第幾個子 View
    * @param targetIndex 要移動到第幾個子 View
    */
    public void moveToIndex(int targetIndex) {
        if (!canMoveToIndex(targetIndex)) {
            return;
        }
        scrollTo(targetIndex * getWidth(), getScrollY());
        mCurrentIndex = targetIndex;
        invalidate();
    }

    /**
    * 判斷移動的子 View 下標是否合法
    * @param index 要移動到第幾個子 View
    * @return index 是否合法
    */
    public boolean canMoveToIndex(int index) {
        return index < CHILD_NUMBER && index >= 0;
    }

    public int getCurrentIndex() {
        return mCurrentIndex;
    }
}

將以上這個自定義的 ViewGroup 放到 Activity 中,調用它的 moveToIndex(int targetIndex) 就可以實現(xiàn)瞬時滾動到第 n 個子 View 了属愤。(完整示例代碼見DEMO

Scroller 類 —— 計算滾動位置的輔助類

到目前為止女器,我們已經(jīng)能通過 View 提供的方法設置 mScrollXmScrollY住诸,來使 View “滾動”驾胆。但這種滾動都是瞬時的涣澡,換句話說,這種滾動都是無動畫的丧诺。實際上我們想要做到的滾動是平滑的入桂、有動畫的,就像我們不希望窗戶外面的那個人突然出現(xiàn)在窗戶中間驳阎,這樣會嚇到我們抗愁,我們更希望那個人能有一個“慢慢走進視覺范圍”的過程。

Scroller 類就是幫助我們實現(xiàn) View 平滑滾動的一個輔助類呵晚,使用方法通常是在 View 中作為一個成員變量蜘腌,用 Scroller 類來記錄/計算 View 的滾動位置,再從 Scroller 類中讀取出計算結果饵隙,設置到 View 中撮珠。這里注意一點:在 Scroller 中設置和計算 View 的滾動位置并不會影響 View 的滾動,只有從 Scroller 中取出計算結果并設置到 View 中時金矛,滾動才會實際生效芯急。

Scroller 提供了一系列方法來執(zhí)行滾動、計算滾動位置绷柒,以下列出幾個重要方法:

// 開始滾動志于,并記下當前時間點作為開始滾動的時間點
public void startScroll(int startX, int startY, int dx, int dy, int duration)
// 停止?jié)L動
public void abortAnimation()
// 計算當前時間點對應的滾動位置,并返回動畫是否還在進行
public boolean computeScrollOffset()
// 獲取上一次 computeScrollOffset 執(zhí)行時的滾動 x 值
public final int getCurrX()
// 獲取上一次 computeScrollOffset 執(zhí)行時的滾動 y 值
public final int getCurrY()
// 根據(jù)當前的時間點废睦,判斷動畫是否已結束
public final boolean isFinished()

有了這幾個方法榴都,我們容易想到如何實現(xiàn) View 的平滑滾動動畫:

  • 在開始動畫時調用 startScroll 方法断凶,傳入動畫開始位置、移動距離、動畫時長录平;
  • 每隔一段時間前硫,調用 computeScrollOffset 方法听哭,計算當前時間點對應的滾動位置痊远;
  • 如果上一步返回 true,代表動畫仍在進行刚陡,則調用 getCurrXgetCurrY 方法獲取當前位置惩妇,并調用 View 的 scrollTo 方法使 View 滾動;
  • 不斷循環(huán)進行第 2 步筐乳,直到返回 false歌殃,代表動畫結束。

這里提到“每隔一段時間”蝙云,從直覺上我們可能覺得應該有個循環(huán)氓皱,但實際上我們可以借助 View 的 computeScroll 方法來實現(xiàn)。先看看 computeScroll 方法的源碼:

// View.java
/**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
* object.
*/
public void computeScroll() {
}

看注釋可知該方法天生就是用來計算 View 的 mScrollXmScrollY 值,該方法會在父 View 調用該 View 的 draw 方法之前被自動調用波材,View 類中默認沒有實現(xiàn)任何內容股淡,我們需要自己實現(xiàn)。所以我們只需要在該方法中廷区,用 Scroller 計算并設置 mScrollXmScrollY 的值唯灵,并判斷如果動畫沒結束則讓該 View 失效(調用 postInvalidate() 方法),觸發(fā)下一次 computeScroll隙轻,就可以實現(xiàn)上述循環(huán)早敬。

例子2

這個例子的 ViewGroup 繼承自例子 1 的 ViewGroup,擁有同樣的子 View大脉,區(qū)別只在于例子 2 是通過 Scroller 來滾動,實現(xiàn)了滾動的動畫水孩,而不再是瞬時滾動镰矿。

/**
* 示例:自定義一個 ViewGroup,包含幾個一字排開的子 View俘种,

* 每個子 View 都與該 ViewGroup 一樣大秤标。
* 通過 Scroller 實現(xiàn)滾動。
* 調用 moveToIndex 方法會觸發(fā) Scroller 的 startScroller宙刘,開始動畫苍姜,并使 View 失效。
* 并在 computeScroll 方法中判斷動畫是否在進行悬包,進而計算當前滾動位置衙猪,并觸發(fā)下一次 View 失效。
*/
public class Case2ViewGroup extends Case1ViewGroup {

    // 滾動器
    protected Scroller mScroller;

    public Case2ViewGroup(Context context) {
        super(context);
        initScroller();
    }

    public Case2ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        initScroller();
    }

    public Case2ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initScroller();
    }

    private void initScroller() {
        mScroller = new Scroller(getContext());
    }

    /**
    * 通過動畫滾動到第幾個子 View
    * @param targetIndex 要移動到第幾個子 View
    */
    @Override
    public void moveToIndex(int targetIndex) {
        if (!canMoveToIndex(targetIndex)) {
            return;
        }
        mScroller.startScroll(
                getScrollX(), getScrollY(),
                targetIndex * getWidth() - getScrollX(), getScrollY());
        mCurrentIndex = targetIndex;
        invalidate();
    }

    public void stopMove() {
        if (!mScroller.isFinished()) {
            int currentX = mScroller.getCurrX();
            int targetIndex = (currentX + getWidth() / 2) / getWidth();
            mScroller.abortAnimation();
            this.scrollTo(targetIndex * getWidth(), 0);
            mCurrentIndex = targetIndex;
        }
    }

    /**
    * 在 ViewGroup.dispatchDraw() -> ViewGroup.drawChild() -> View.draw(Canvas,ViewGroup,long) 時被調用
    * 任務:計算 mScrollX & mScrollY 應有的值布近,然后調用scrollTo/scrollBy
    */
    @Override
    public void computeScroll() {
        boolean isNotFinished = mScroller.computeScrollOffset();
        if (isNotFinished) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }

}

將以上這個自定義的 ScrollerViewGroup 放到 Activity 中垫释,調用它的 moveToIndex(int targetIndex) 就可以實現(xiàn)滾動到第 n 個子 View 了。(在 Activity 中使用的完整示例代碼見DEMO

VelocityTracker —— 計算滾動速度的輔助類

到目前為止撑瞧,我們已經(jīng)可以實現(xiàn) View 平滑的滾動動畫棵譬,那么如果我們還想根據(jù)用戶手指在 View 上滑動的速度和距離來控制 View 的滾動,應該怎么做预伺?Android 系統(tǒng)提供了另一個輔助類 VelocityTracker 來實現(xiàn)類似功能订咸。

VelocityTracker 是一個速度跟蹤器,通過用戶操作時(通常在 View 的 onTouchEvent 方法中)傳進去一系列的 Event酬诀,該類就可以計算出用戶手指滑動的速度脏嚷,開發(fā)者可以方便地獲取這些參數(shù)去做其他事情×侠模或者手指滑動超過一定速度并松手然眼,就觸發(fā)翻頁。

看看 VelocityTracker 類提供的幾個常用的方法葵腹,這些方法分為幾類:

  • 初始化和銷毀:

    // 由系統(tǒng)分配一個 VelocityTracker 對象高每,而不是 new 一個
    static public VelocityTracker obtain()
    
    - // 使用完畢時調用該方法回收 VelocityTracker 對象
    public void recycle()
    
  • 添加 Event 以供追蹤:

    // 不斷調用該方法傳入一系列 event屿岂,記錄用戶的操作
    public void addMovement(MotionEvent event)
    
  • 計算速度:

    // 計算調用該方法的時刻對應的速度,傳入的是速度的計時單位
    public void computeCurrentVelocity(int units)
    
    // 調用 computeCurrentVelocity 方法后就可以通過該方法獲取之前計算的 x 方向速度
    public float getXVelocity()
    
    // 調用 computeCurrentVelocity 方法后就可以通過該方法獲取之前計算的 y 方向速度
    public float getYVelocity()
    

例子3

下面通過一個例子來看看 VelocityTracker 的用法鲸匿。該例子的 ViewGroup 繼承自例子 2 的 ViewGroup爷怀,擁有同樣的子 View,區(qū)別在于除了可以用動畫來滾動带欢,還可以用手勢來拖動滾動运授。重點看該 ViewGroup 的 onTouchEvent 方法:

/**
* 示例:自定義一個 ViewGroup,包含幾個一字排開的子 View乔煞,

* 每個子 View 都與該 ViewGroup 一樣大吁朦。
* 通過 VelocityTracker 監(jiān)控手指滑動速度。
*/
public class Case3ViewGroup extends Case2ViewGroup {

    // 速度監(jiān)控器
    private VelocityTracker mVelocityTracker;

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

    public Case3ViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public Case3ViewGroup(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 非滑動狀態(tài)
    private static final int TOUCH_STATE_REST = 0;
    // 滑動狀態(tài)
    private static final int TOUCH_STATE_SCROLLING = 1;
    // 表示當前狀態(tài)
    private int mTouchState = TOUCH_STATE_REST;

    // 上一次事件的位置
    private float mLastMotionX;
    // 觸發(fā)滾動的最小滑動距離渡贾,手指滑動超過該距離才認為是要拖動逗宜,防止手抖
    private int mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
    // 最小滑動速率,手指滑動超過該速度時才會觸發(fā)翻頁
    private static final int VELOCITY_MIN = 600;

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();

        //表示已經(jīng)開始滑動了空骚,不需要走該 ACTION_MOVE 方法了纺讲。
        if ((action == MotionEvent.ACTION_MOVE) && (mTouchState != TOUCH_STATE_REST)) {
            return true;
        }

        final float x = ev.getX();

        switch (action) {
            case MotionEvent.ACTION_DOWN:
                mLastMotionX = x;
                mTouchState = mScroller.isFinished() ? TOUCH_STATE_REST : TOUCH_STATE_SCROLLING;
                break;

            case MotionEvent.ACTION_MOVE:
                final int xDiff = (int) Math.abs(mLastMotionX - x);
                //超過了最小滑動距離,就可以認為開始滑動了
                if (xDiff > mTouchSlop) {
                    mTouchState = TOUCH_STATE_SCROLLING;
                }
                break;

            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                mTouchState = TOUCH_STATE_REST;
                break;
        }
        return mTouchState != TOUCH_STATE_REST;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        // 速度監(jiān)控器囤屹,監(jiān)控每一個 event
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);

        // 觸摸點
        final float eventX = event.getX();

        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:

                // 如果滾動未結束時按下熬甚,則停止?jié)L動
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                // 記錄按下位置
                mLastMotionX = eventX;
                break;
            case MotionEvent.ACTION_MOVE:
                // 手指移動的位移
                int deltaX = (int)(eventX - mLastMotionX);
                // 滾動內容,前提是不超出邊界
                int targetScrollX = getScrollX() - deltaX;
                if (targetScrollX >= 0 &&
                        targetScrollX <= getWidth() * (CHILD_NUMBER - 1)) {
                    scrollTo(targetScrollX, 0);
                }
                // 記下手指的新位置
                mLastMotionX = eventX;
                break;
            case MotionEvent.ACTION_UP:
                // 計算速度
                mVelocityTracker.computeCurrentVelocity(1000);
                float velocityX = mVelocityTracker.getXVelocity();
                if (velocityX > VELOCITY_MIN && canMoveToIndex(getCurrentIndex() - 1)) {
                    // 自動向右邊繼續(xù)滑動
                    moveToIndex(getCurrentIndex() - 1);
                } else if (velocityX < -VELOCITY_MIN && canMoveToIndex(getCurrentIndex() + 1)) {
                    // 自動向左邊繼續(xù)滑動
                    moveToIndex(getCurrentIndex() + 1);
                } else {
                    // 手指速度不夠或不允許再滑

                    int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                    moveToIndex(targetIndex);
                }
                // 回收速度監(jiān)控器
                if (mVelocityTracker != null) {
                    mVelocityTracker.recycle();
                    mVelocityTracker = null;
                }
                //修正 mTouchState 值
                mTouchState = TOUCH_STATE_REST;
                break;
            case MotionEvent.ACTION_CANCEL:
                mTouchState = TOUCH_STATE_REST;
                break;
        }

        return true;
    }
}

在該例子中肋坚,在 View 的 onTouchEvent 方法中乡括,在 ACTION_MOVE 手指移動中不斷調用 scrollTo 方法,實現(xiàn) View 跟隨手指移動冲簿;同時粟判,將 Event 不斷地添加到 mVelocityTracker 速度監(jiān)控器中,并在 ACTION_UP 手指抬起時從速度監(jiān)控器中獲取速度峦剔,當速度達到某一閾值時自動滾動到上一頁或下一頁档礁。

總結

至此,我們已經(jīng)了解了 View 的滾動原理吝沫,并兩個輔助類來幫助控制 View 的滾動位置和滾動速度呻澜。總結一下:

  • View 的顯示可以理解為透過“視覺窗口”來看內容惨险,內容可以無限大羹幸,改變 View 的 mScrollXmScrollY 可以看到不同的內容,實現(xiàn)瞬時滾動辫愉。
  • 調用 View 的 scrollToscrollBy 方法可以瞬時滾動 View栅受。
  • Scroller 輔助類可以協(xié)助實現(xiàn) View 的滾動動畫,實現(xiàn)方法是:調用 startScroll 方法開始滾動,并在 View 的 computeScroll 方法中不斷改變 mScrollXmScrollY 來滾動 View屏镊。
  • VelocityTracker 輔助類可以協(xié)助追蹤 View 的滾動速度依疼,通常是在 View 的 onTouchEvent 方法中將 Event 傳進該類中來追蹤。調用該類的 computeCurrentVelocity 方法之后而芥,就可以調用 getXVelocitygetYVelocity 方法分別獲取 x 方向和 y 方向的速度律罢。

有了上述的知識和工具后,我們就能實現(xiàn)很多與滾動相關的效果棍丐。

以上误辑,感謝閱讀。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末歌逢,一起剝皮案震驚了整個濱河市巾钉,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌秘案,老刑警劉巖睛琳,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異踏烙,居然都是意外死亡,警方通過查閱死者的電腦和手機历等,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門讨惩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人寒屯,你說我怎么就攤上這事荐捻。” “怎么了寡夹?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵处面,是天一觀的道長。 經(jīng)常有香客問我菩掏,道長魂角,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任智绸,我火速辦了婚禮野揪,結果婚禮上,老公的妹妹穿的比我還像新娘瞧栗。我一直安慰自己斯稳,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布迹恐。 她就那樣靜靜地躺著挣惰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上憎茂,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天珍语,我揣著相機與錄音,去河邊找鬼唇辨。 笑死廊酣,一個胖子當著我的面吹牛,可吹牛的內容都是我干的赏枚。 我是一名探鬼主播亡驰,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼饿幅!你這毒婦竟也來了凡辱?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤栗恩,失蹤者是張志新(化名)和其女友劉穎透乾,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體磕秤,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡乳乌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了市咆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汉操。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蒙兰,靈堂內的尸體忽然破棺而出磷瘤,到底是詐尸還是另有隱情,我是刑警寧澤搜变,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布采缚,位于F島的核電站,受9級特大地震影響挠他,放射性物質發(fā)生泄漏扳抽。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一殖侵、第九天 我趴在偏房一處隱蔽的房頂上張望摔蓝。 院中可真熱鬧,春花似錦愉耙、人聲如沸贮尉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽猜谚。三九已至败砂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間魏铅,已是汗流浹背昌犹。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留览芳,地道東北人斜姥。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像沧竟,于是被迫代替她去往敵國和親铸敏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內容