從源碼來看ItemTouchHelper實(shí)現(xiàn)RecyclerView列表的拖拽和側(cè)滑

RecyclerView是一個用來替換之前的ListView和GridView的控件,使用的時候叫胖,雖然比以前的ListView看起來麻煩幢痘,但是其實(shí)作為一個高度解耦的控件景鼠,復(fù)雜一點(diǎn)點(diǎn)換來極大的靈活性,豐富的可操作性陈症,何樂而不為呢。不過今天主要說說它的一個輔助類ItemTouchHelper來實(shí)現(xiàn)列表的拖動滑動刪除

RecyclerView用法(ListView)

1.導(dǎo)入控件包

compile 'com.android.support:support-v13:25.+'

2.布局文件加入控件

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_test"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></android.support.v7.widget.RecyclerView>

3.定義Adapter

public class TestAdapter extends RecyclerView.Adapter implements TouchCallbackListener {
    /**
     * 數(shù)據(jù)源列表
     */
    private List<String> mData;

    /**
     * 構(gòu)造方法傳入數(shù)據(jù)
     * @param mData
     */
    public TestAdapter(List<String> mData) {
        this.mData = mData;
    }

    /**
     * 創(chuàng)建用于復(fù)用的ViewHolder
     * @param parent
     * @param viewType
     * @return
     */
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        ViewHolder vh = new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item,parent,false));
        return vh;
    }

    /**
     * 對ViewHolder的控件進(jìn)行操作
     * @param holder
     * @param position
     */
    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if(holder instanceof ViewHolder){
            ViewHolder holder1 = (ViewHolder) holder;
            holder1.tv_test.setText(mData.get(position));
        }
    }

    /**
     *
     * @return 數(shù)據(jù)的總數(shù)
     */
    @Override
    public int getItemCount() {
        return mData.size();
    }

    /**
     * 長按拖拽時的回調(diào)
     * @param fromPosition 拖拽前的位置
     * @param toPosition 拖拽后的位置
     */
    @Override
    public void onItemMove(int fromPosition, int toPosition) {
        Collections.swap(mData, fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);//通知Adapter更新
    }

    /**
     * 滑動時的回調(diào)
     * @param position 滑動的位置
     */
    @Override
    public void onItemSwipe(int position) {
        mData.remove(position);
        notifyItemRemoved(position);////通知Adapter更新
    }

    /**
     * 自定義的ViewHolder內(nèi)部類凡纳,必須繼承RecyclerView.ViewHolder(這里用不用static存在爭議,沒有專門的測試喊暖,
     * 從內(nèi)存占用來看微乎其微惫企,但是不知道有沒有內(nèi)存泄露的問題)
     */
    public class ViewHolder extends RecyclerView.ViewHolder{

        private TextView tv_test;
        public ViewHolder(View itemView) {
            super(itemView);
            tv_test = (TextView) itemView.findViewById(R.id.tv_test);
        }
    }
}

這里定義RecyclerView的Adapter適配器,必須繼承自RecyclerView.Adapter,而且需要在內(nèi)部定義ViewHolder類狞尔,這個跟我們之前使用ListView是一樣的丛版,不過在RecyclerView里面這個是必須實(shí)現(xiàn)的。還有就是這里我并沒有用static偏序,不影響復(fù)用页畦,但是內(nèi)存會不會泄漏呢?

然后里面還有兩個在拖拽和滑動時的回調(diào)研儒,這里是我們自己定義的一個接口TouchCallbackListener

TouchCallbackListener

public interface TouchCallbackListener {
    /**
     * 長按拖拽時的回調(diào)
     * @param fromPosition 拖拽前的位置
     * @param toPosition 拖拽后的位置
     */
    void onItemMove(int fromPosition, int toPosition);
    /**
     * 滑動時的回調(diào)
     * @param position 滑動的位置
     */
    void onItemSwipe(int position);
}

4.使用ItemTouchHelper實(shí)現(xiàn)上下拖拽和滑動刪除功能

ItemTouchHelper的構(gòu)造方法需要傳入ItemTouchHelper.Callback來自己定義各種動作時的處理豫缨,我們自定義的類如下:

TouchCallback

public class TouchCallback extends ItemTouchHelper.Callback {
    /**
     * 自定義的監(jiān)聽接口
     */
    private TouchCallbackListener mListener;

    public TouchCallback(TouchCallbackListener listener) {
        this.mListener = listener;
    }

    /**
     * 定義列表可以怎么滑動(上下左右)
     * @param recyclerView
     * @param viewHolder
     * @return
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        //上下滑動
        int dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        //左右滑動
        int swipeFlag = ItemTouchHelper.LEFT| ItemTouchHelper.RIGHT;
        //使用此方法生成標(biāo)志返回
        return makeMovementFlags(dragFlag, swipeFlag);
    }

    /**
     * 拖拽移動時調(diào)用的方法
     * @param recyclerView 控件
     * @param viewHolder 移動之前的條目
     * @param target 移動之后的條目
     * @return
     */
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        mListener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    /**
     * 滑動時調(diào)用的方法
     * @param viewHolder 滑動的條目
     * @param direction 方向
     */
    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
        mListener.onItemSwipe(viewHolder.getAdapterPosition());
    }

    /**
     * 是否允許長按拖拽
     * @return true or false
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return true;
    }

    /**
     * 是否允許滑動
     * @return true or false
     */
    @Override
    public boolean isItemViewSwipeEnabled() {
        return true;
    }
}

5.使用RecyclerView綁定Adapter和ItemTouchHelper

最后在Activity中來使用RecyclerView

public class MainActivity extends AppCompatActivity{

    private RecyclerView mRecyclerView;
    private TestAdapter mTestAdapter;
    private List<String> mData;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initData();
        mRecyclerView = (RecyclerView) findViewById(R.id.rv_test);
        mRecyclerView.setAdapter(mTestAdapter);
        //定義布局管理器,這里是ListView端朵。GridLayoutManager對應(yīng)GridView
        LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this);
        //ListView的方向,縱向
        linearLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
        mRecyclerView.setLayoutManager(linearLayoutManager);
        //添加每一行的分割線
//        mRecyclerView.addItemDecoration(new DividerItemDecoration(this, DividerItemDecoration.VERTICAL_LIST));
        ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
        helper.attachToRecyclerView(mRecyclerView);
    }

    /**
     * 初始化模擬數(shù)據(jù)
     */
    private void initData() {
        mData = new ArrayList<>();
        String temp;
        for(int i = 0; i < 99; ++i){
            temp = i + "*";
            mData.add(temp);
        }
        mTestAdapter = new TestAdapter(mData);
    }

6.添加分割線

RecyclerView默認(rèn)每一行是沒有分割線的好芭,如果需要分割線的話要自己去定義ItemDecoration,這個類可以為每個條目添加額外的視圖與效果冲呢,我們自己定義的代碼如下:
DividerItemDecoration

public class DividerItemDecoration extends RecyclerView.ItemDecoration{
    private static final int[] ATTRS = new int[]{
            android.R.attr.listDivider//Android默認(rèn)的分割線效果
    };
    public static final int HORIZONTAL_LIST = LinearLayoutManager.HORIZONTAL;

    public static final int VERTICAL_LIST = LinearLayoutManager.VERTICAL;

    private Drawable mDivider;

    private int mOrientation;

    public DividerItemDecoration(Context context, int oritation) {
        final TypedArray a = context.obtainStyledAttributes(ATTRS);
        mDivider = a.getDrawable(0);
        a.recycle();
        setOrientation(oritation);
    }

    public void setOrientation(int orientation) {
        if(orientation != HORIZONTAL_LIST && orientation != VERTICAL_LIST){
            throw new IllegalArgumentException("invalid orientation");
        }
        this.mOrientation = orientation;
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == VERTICAL_LIST){
            drawVertical(c, parent);
        }else {
            drawHorizontal(c,parent);
        }
    }

    /**
     * 縱向的列表
     * @param c
     * @param parent
     */
    public void drawVertical(Canvas c, RecyclerView parent){
        final int left = parent.getPaddingLeft();
        final int right = parent.getWidth() - parent.getPaddingRight();

        final int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i);
            RecyclerView v = new RecyclerView(parent.getContext());
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int top = child.getBottom() + params.bottomMargin;
            final int bottom = top + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    /**
     * 橫向的列表
     * @param c
     * @param parent
     */
    public void drawHorizontal(Canvas c, RecyclerView parent){
        final int top = parent.getPaddingTop();
        final int bottom = parent.getHeight() - parent.getPaddingBottom();

        final int childCount = parent.getChildCount();

        for(int i = 0; i < childCount; i++){
            final View child = parent.getChildAt(i);
            final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
            final int left = child.getRight() + params.rightMargin;
            final int right = left + mDivider.getIntrinsicHeight();
            mDivider.setBounds(left, top, right, bottom);
            mDivider.draw(c);
        }
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if(mOrientation == VERTICAL_LIST){
            outRect.set(0,0,0,mDivider.getIntrinsicHeight());
        }else {
            outRect.set(0,0,mDivider.getIntrinsicWidth(), 0);
        }
    }
}

到此就實(shí)現(xiàn)了一個支持長按拖拽和滑動刪除的列表舍败,很簡單,效果就不截圖了敬拓。

ItemTouchHelper原理

實(shí)現(xiàn)拖拽和滑動刪除的過程的很簡單邻薯,并且還有非常流暢的動畫。只需要給ItemTouchHelper傳入一個我們自己定義的回調(diào)即可乘凸,但是它的內(nèi)部是怎么實(shí)現(xiàn)的呢厕诡?來一步一步看看代碼。

首先看看它的類定義:

public class ItemTouchHelper extends RecyclerView.ItemDecoration
        implements RecyclerView.OnChildAttachStateChangeListener

繼承自RecyclerView.ItemDecoration营勤,跟分割線一樣灵嫌,也是通過繼承這個類來給每個條目添加效果

然后從它的在外層的使用開始:

ItemTouchHelper helper = new ItemTouchHelper(new TouchCallback(mTestAdapter));
helper.attachToRecyclerView(mRecyclerView);

RecyclerView和ItemTouchHelper的關(guān)聯(lián)是ItemTouchHelper的attachToRecyclerView方法,進(jìn)入這個方法:

ItemTouchHelper.attachToRecyclerView

    public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
        if (mRecyclerView == recyclerView) {
            return; // nothing to do
        }
        if (mRecyclerView != null) {
            destroyCallbacks();
        }
        mRecyclerView = recyclerView;
        if (mRecyclerView != null) {
            final Resources resources = recyclerView.getResources();
            mSwipeEscapeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
            mMaxSwipeVelocity = resources
                    .getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
            setupCallbacks();
        }
    }

首先判斷傳入的RecyclerView是否跟已經(jīng)綁定的相等冀偶,如果相等醒第,就直接返回,不過不相等进鸠,銷毀之前的回調(diào)稠曼,然后將傳入的RecyclerView賦值給全局變量,設(shè)置速率客年,最后調(diào)用setupCallbacks初始化

ItemTouchHelper.setupCallbacks

    private void setupCallbacks() {
        ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
        mSlop = vc.getScaledTouchSlop();
        mRecyclerView.addItemDecoration(this);
        mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
        mRecyclerView.addOnChildAttachStateChangeListener(this);
        initGestureDetector();
    }

前兩句是獲取TouchSlop的值霞幅,這個值用于判斷是滑動還是點(diǎn)擊,然后給RecyclerView添加ItemDecoration(也就是自己)量瓜,條目的觸摸監(jiān)聽司恳,條目的關(guān)聯(lián)狀態(tài)監(jiān)聽。這里最主要的就是看看mOnItemTouchListener的實(shí)現(xiàn):

ItemTouchHelper.mOnItemTouchListener

    private final OnItemTouchListener mOnItemTouchListener
            = new OnItemTouchListener() {
        @Override
        public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            if (DEBUG) {
                Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
            }
            //用于處理多點(diǎn)觸控
            final int action = MotionEventCompat.getActionMasked(event);
            if (action == MotionEvent.ACTION_DOWN) {
                mActivePointerId = event.getPointerId(0);
                mInitialTouchX = event.getX();
                mInitialTouchY = event.getY();
                obtainVelocityTracker();
                if (mSelected == null) {
                    final RecoverAnimation animation = findAnimation(event);
                    if (animation != null) {
                        mInitialTouchX -= animation.mX;
                        mInitialTouchY -= animation.mY;
                        endRecoverAnimation(animation.mViewHolder, true);
                        if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
                            mCallback.clearView(mRecyclerView, animation.mViewHolder);
                        }
                        select(animation.mViewHolder, animation.mActionState);
                        updateDxDy(event, mSelectedFlags, 0);
                    }
                }
            } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mActivePointerId = ACTIVE_POINTER_ID_NONE;
                select(null, ACTION_STATE_IDLE);
            } else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                // in a non scroll orientation, if distance change is above threshold, we
                // can select the item
                final int index = event.findPointerIndex(mActivePointerId);
                if (DEBUG) {
                    Log.d(TAG, "pointer index " + index);
                }
                if (index >= 0) {
                    checkSelectForSwipe(action, event, index);
                }
            }
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(event);
            }
            return mSelected != null;
        }

        @Override
        public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
            mGestureDetector.onTouchEvent(event);
            if (DEBUG) {
                Log.d(TAG,
                        "on touch: x:" + mInitialTouchX + ",y:" + mInitialTouchY + ", :" + event);
            }
            if (mVelocityTracker != null) {
                mVelocityTracker.addMovement(event);
            }
            if (mActivePointerId == ACTIVE_POINTER_ID_NONE) {
                return;
            }
            final int action = MotionEventCompat.getActionMasked(event);
            final int activePointerIndex = event.findPointerIndex(mActivePointerId);
            if (activePointerIndex >= 0) {
                checkSelectForSwipe(action, event, activePointerIndex);
            }
            ViewHolder viewHolder = mSelected;
            if (viewHolder == null) {
                return;
            }
            switch (action) {
                case MotionEvent.ACTION_MOVE: {
                    // Find the index of the active pointer and fetch its position
                    if (activePointerIndex >= 0) {
                        updateDxDy(event, mSelectedFlags, activePointerIndex);
                        moveIfNecessary(viewHolder);
                        mRecyclerView.removeCallbacks(mScrollRunnable);
                        mScrollRunnable.run();
                        mRecyclerView.invalidate();
                    }
                    break;
                }
                case MotionEvent.ACTION_CANCEL:
                    if (mVelocityTracker != null) {
                        mVelocityTracker.clear();
                    }
                    // fall through
                case MotionEvent.ACTION_UP:
                    select(null, ACTION_STATE_IDLE);
                    mActivePointerId = ACTIVE_POINTER_ID_NONE;
                    break;
                case MotionEvent.ACTION_POINTER_UP: {
                    final int pointerIndex = MotionEventCompat.getActionIndex(event);
                    final int pointerId = event.getPointerId(pointerIndex);
                    if (pointerId == mActivePointerId) {
                        // This was our active pointer going up. Choose a new
                        // active pointer and adjust accordingly.
                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                        mActivePointerId = event.getPointerId(newPointerIndex);
                        updateDxDy(event, mSelectedFlags, pointerIndex);
                    }
                    break;
                }
            }
        }

這里主要重寫了兩個方法onInterceptTouchEventonTouchEvent绍傲,先來看看onInterceptTouchEvent扔傅,攔截屏幕事觸控的事件耍共,首先是判斷單點(diǎn)按下

if (action == MotionEvent.ACTION_DOWN) {
                //現(xiàn)在追蹤的觸摸事件
                mActivePointerId = event.getPointerId(0);
                //獲取最開始按下的坐標(biāo)值
                mInitialTouchX = event.getX();
                mInitialTouchY = event.getY();
                //獲取速度追蹤器(此方法避免重復(fù)創(chuàng)建)
                obtainVelocityTracker();
                //如果選擇的條目為空
                if (mSelected == null) {
                    //查找對應(yīng)的動畫(避免重復(fù)動畫)
                    final RecoverAnimation animation = findAnimation(event);
                    //執(zhí)行動畫,
                    if (animation != null) {
                        //更新初始值
                        mInitialTouchX -= animation.mX;
                        mInitialTouchY -= animation.mY;
                        //從動畫列表里移除條目對應(yīng)的動畫
                        endRecoverAnimation(animation.mViewHolder, true);
                        //從回收列表里移除條目視圖
                        if (mPendingCleanup.remove(animation.mViewHolder.itemView)) {
                            mCallback.clearView(mRecyclerView, animation.mViewHolder);
                        }
                        //執(zhí)行選擇動畫
                        select(animation.mViewHolder, animation.mActionState);
                        //更新移動距離x猎塞,y的值
                        updateDxDy(event, mSelectedFlags, 0);
                    }
                }
            }

然后是判斷取消單點(diǎn)抬起

else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
                mActivePointerId = ACTIVE_POINTER_ID_NONE;
                select(null, ACTION_STATE_IDLE);//清除動畫

最后執(zhí)行下面判斷點(diǎn)擊狀態(tài)為空:

else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
                // 移動距離超過了臨界值试读,判斷是否滑動選擇的條目
                final int index = event.findPointerIndex(mActivePointerId);
                if (DEBUG) {
                    Log.d(TAG, "pointer index " + index);
                }
                if (index >= 0) {
                    //判斷是否滑選擇的條目
                    checkSelectForSwipe(action, event, index);
                }
            }

最后如果選擇的條目不等于null,返回true荠耽,表示攔截觸摸事件钩骇,接下來執(zhí)行onTouchEvent方法,只看對觸摸動作的判斷:

1.按下移動手指

case MotionEvent.ACTION_MOVE: {
                    // 如果點(diǎn)擊序號大于0铝量,表示有點(diǎn)擊事件
                    if (activePointerIndex >= 0) {
                        //更新移動距離
                        updateDxDy(event, mSelectedFlags, activePointerIndex);
                        //移動ViewHolder
                        moveIfNecessary(viewHolder);
                        //先移除動畫
                        mRecyclerView.removeCallbacks(mScrollRunnable);
                        //執(zhí)行動畫
                        mScrollRunnable.run();
                        //重繪RecyclerView
                        mRecyclerView.invalidate();
                    }
                    break;
                }

這里來看看mScrollRunnable.run()

    final Runnable mScrollRunnable = new Runnable() {
        @Override
        public void run() {
            if (mSelected != null && scrollIfNecessary()) {
                if (mSelected != null) { //it might be lost during scrolling
                    moveIfNecessary(mSelected);
                }
                mRecyclerView.removeCallbacks(mScrollRunnable);
                //遞歸調(diào)用
                ViewCompat.postOnAnimation(mRecyclerView, this);
            }
        }
    };

這里的run方法相當(dāng)于是一個死循環(huán)倘屹,在里面又不斷調(diào)用自己,不斷的執(zhí)行動畫慢叨,因?yàn)檫x中的條目需要不停的跟隨手指的移動纽匙,直到判斷條件返回FALSE停止執(zhí)行,然后回到onTouchEvent繼續(xù)判斷

2.當(dāng)用戶保持按下操作拍谐,并從你的控件轉(zhuǎn)移到外層控件時哄辣,會觸發(fā)ACTION_CANCEL:

case MotionEvent.ACTION_CANCEL:
                    if (mVelocityTracker != null) {
                        //清除速度追蹤器
                        mVelocityTracker.clear();
                    }

3.抬起手指

case MotionEvent.ACTION_UP:
                    //清理選擇動畫
                    select(null, ACTION_STATE_IDLE);
                    //手指狀態(tài)置空
                    mActivePointerId = ACTIVE_POINTER_ID_NONE;
                    break;

4.多點(diǎn)觸控抬起

case MotionEvent.ACTION_POINTER_UP: {
                    final int pointerIndex = MotionEventCompat.getActionIndex(event);
                    final int pointerId = event.getPointerId(pointerIndex);
                    if (pointerId == mActivePointerId) {
                        //選擇一個新的手指活動點(diǎn),并且更新x赠尾,y的距離
                        final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                        mActivePointerId = event.getPointerId(newPointerIndex);
                        updateDxDy(event, mSelectedFlags, pointerIndex);
                    }
                    break;
                }

根據(jù)對OnItemTouchListener的源碼分析,我們知道了跟隨手指的動畫是怎么來實(shí)現(xiàn)的毅弧,簡單來說气嫁,就是檢測手指的動作,然后不斷的重繪够坐,最終就展現(xiàn)在我們面前寸宵,在長按上下拖拽時,按住的條目隨著手指移動元咙,左右滑動時梯影,條目“飛”出屏幕。不過在實(shí)際的項(xiàng)目中庶香,這種側(cè)滑刪除的操作肯定不是直接側(cè)滑就執(zhí)行刪除甲棍,需要右邊有一個刪除的按鈕來確認(rèn),這個也可以在ItemTouchHelper的基礎(chǔ)上來改進(jìn)赶掖,后面再說吧感猛。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市奢赂,隨后出現(xiàn)的幾起案子陪白,更是在濱河造成了極大的恐慌,老刑警劉巖膳灶,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件咱士,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)序厉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進(jìn)店門锐膜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人脂矫,你說我怎么就攤上這事枣耀。” “怎么了庭再?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵捞奕,是天一觀的道長。 經(jīng)常有香客問我拄轻,道長颅围,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任恨搓,我火速辦了婚禮院促,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斧抱。我一直安慰自己常拓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布辉浦。 她就那樣靜靜地躺著弄抬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宪郊。 梳的紋絲不亂的頭發(fā)上掂恕,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天,我揣著相機(jī)與錄音弛槐,去河邊找鬼懊亡。 笑死,一個胖子當(dāng)著我的面吹牛乎串,可吹牛的內(nèi)容都是我干的店枣。 我是一名探鬼主播,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼叹誉,長吁一口氣:“原來是場噩夢啊……” “哼艰争!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起桂对,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤甩卓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后蕉斜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逾柿,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡缀棍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了机错。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爬范。...
    茶點(diǎn)故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖弱匪,靈堂內(nèi)的尸體忽然破棺而出青瀑,到底是詐尸還是另有隱情,我是刑警寧澤萧诫,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布斥难,位于F島的核電站,受9級特大地震影響帘饶,放射性物質(zhì)發(fā)生泄漏哑诊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一及刻、第九天 我趴在偏房一處隱蔽的房頂上張望镀裤。 院中可真熱鬧,春花似錦缴饭、人聲如沸暑劝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽铃岔。三九已至,卻和暖如春峭火,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背智嚷。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工卖丸, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人盏道。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓稍浆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親猜嘱。 傳聞我的和親對象是個殘疾皇子衅枫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,828評論 2 345

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