Android自定義控件之從0到1輕松實現(xiàn)側(cè)滑按鈕


一、前言


二、構(gòu)想圖

EasySwipeMenuLayout構(gòu)想圖.jpg
  • 我們這次要實現(xiàn)的控件叫做EasySwipeMenuLayout,內(nèi)部主要分為三部分:
    1、內(nèi)容區(qū)域
    2、左邊菜單按鈕區(qū)域
    2、右邊菜單按鈕區(qū)域
  • 當我們向右滑時,通過scroller將左邊按鈕區(qū)域滾動出來
  • 當我們向左滑時,通過scroller將右邊按鈕區(qū)域滾動出來
  • 實現(xiàn)的思路濾清了,那么我們就開始動手吧

三、具體實現(xiàn)

  • 首先,網(wǎng)上類似的輪子有很多,但為什么我們還要自己寫一下呢,當然是為了學習,所謂知其然而知其所以然也,輪子只是滿足了大部分人的需求,試想某一天,有些效果網(wǎng)上是找不到的,那么此時就只能靠自己了。
  • 當然,你也可以說,我就是想自己寫,哈哈。
  • 在開始前,我還想再說一點,網(wǎng)上有很多類似的輪子,但是我發(fā)現(xiàn)個特點,他們要求控件內(nèi)的子布局的順序相對呆板,不夠靈活,也就是所謂通過約定來實現(xiàn)。
  • but,我這次想通過配置來實現(xiàn),那么如何配置呢,其實我們可以通過控件的id進行綁定,參考了google官方控件的部分思想。

布局文件配置效果

  • 首先,我想實現(xiàn)的配置效果是這樣子的

      <com.guanaj.easyswipemenulibrary.EasySwipeMenuLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:contentView="@+id/content"
        app:leftMenuView="@+id/left"
        app:rightMenuView="@+id/right">
            <LinearLayout
                android:id="@+id/left"
                android:layout_width="100dp"
                android:layout_height="wrap_content"
                android:background="@android:color/holo_blue_dark"
                android:orientation="horizontal"
                android:padding="20dp">
                    <TextView
                          android:layout_width="wrap_content"
                          android:layout_height="wrap_content"
                          android:text="分享" />
              </LinearLayout>
            <LinearLayout
                android:id="@+id/content"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:background="#cccccc"
                android:orientation="vertical"
                android:padding="20dp">
                    <TextView
                          android:layout_width="wrap_content"
                          android:layout_height="wrap_content"
                          android:text="內(nèi)容區(qū)域" />
            </LinearLayout>
            <LinearLayout
                android:id="@+id/right"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/holo_red_light"
                android:orientation="horizontal">
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="@android:color/holo_blue_bright"
                    android:padding="20dp"
                    android:text="刪除" />
                <TextView
                    android:id="@+id/right_menu_2"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:background="@android:color/holo_orange_dark"
                    android:padding="20dp"
                    android:text="收藏" />
            </LinearLayout>
      </com.guanaj.easyswipemenulibrary.EasySwipeMenuLayout>
    
  • 如下可以看到,就是通過id來綁定,讓EasySwipeMenuLayout知道哪個childView是現(xiàn)實內(nèi)容的,哪個是左邊的菜單布局,哪個是右邊的菜單布局。

       <com.guanaj.easyswipemenulibrary.EasySwipeMenuLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:contentView="@+id/content"
                app:leftMenuView="@+id/left"
                app:rightMenuView="@+id/right">
    
  • 為什么要這樣子設(shè)計的,我的想法是,這樣子更靈活,我不用規(guī)定里面的子布局的順序。

  • 以上僅代表個人觀點,當然,肯定有更好的設(shè)計方案。

  • Ok,既然要通過id來配置,那么就會用到自定義控件屬性的知識,其實很簡單,就是在res/values下創(chuàng)建一個attrs.xml文件,在里面以你喜歡的名字定義屬性即可

      xml version="1.0" encoding="utf-8"?>
      <resources>
          /**
          * Created by guanaj on .
          */
          <declare-styleable name="EasySwipeMenuLayout">
              <attr name="leftMenuView" format="reference" />
              <attr name="rightMenuView" format="reference" />
              <attr name="contentView" format="reference" />
              <attr name="canRightSwipe" format="boolean" />
              <attr name="canLeftSwipe" format="boolean" />
              <attr name="fraction" format="float" />
          declare-styleable>
    
      resources>
    
  • 定義好了,我們要怎么獲取呢,其實也很easy的了

              //1、通過上下文context獲取TypedArray對象
              TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EasySwipeMenuLayout, defStyleAttr, 0);
    
      try {
          int indexCount = typedArray.getIndexCount();
          
          //2遍歷TypedArray對象,根據(jù)定義的名字獲取值即可
          for (int i = 0; i < indexCount; i++) {
              int attr = typedArray.getIndex(i);
              if (attr == R.styleable.EasySwipeMenuLayout_leftMenuView) {
                  mLeftViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_leftMenuView, -1);
              } else if (attr == R.styleable.EasySwipeMenuLayout_rightMenuView) {
                  mRightViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_rightMenuView, -1);
              } else if (attr == R.styleable.EasySwipeMenuLayout_contentView) {
                  mContentViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_contentView, -1);
              } else if (attr == R.styleable.EasySwipeMenuLayout_canLeftSwipe) {
                  mCanLeftSwipe = typedArray.getBoolean(R.styleable.EasySwipeMenuLayout_canLeftSwipe, true);
              } else if (attr == R.styleable.EasySwipeMenuLayout_canRightSwipe) {
                  mCanRightSwipe = typedArray.getBoolean(R.styleable.EasySwipeMenuLayout_canRightSwipe, true);
              } else if (attr == R.styleable.EasySwipeMenuLayout_fraction) {
                  mFraction = typedArray.getFloat(R.styleable.EasySwipeMenuLayout_fraction, 0.5f);
              }
          }
    
      } catch (Exception e) {
          e.printStackTrace();
      } finally {
          //3、最后不要忘記回收typedArray對象哦
          typedArray.recycle();
      }
    
  • Ok,自定義控件的自定義屬性問題就這樣解決了,接下來我們就開始分析實現(xiàn)代碼吧


  • 首先我們的EasySwipeMenuLayout通過繼承ViewGroup進行實現(xiàn),里面的構(gòu)造方法通過不斷的調(diào)用自身的構(gòu)造方法,最終會調(diào)用init()方法做一些初始化方面的工作。

      public class EasySwipeMenuLayout extends ViewGroup {
    
          private static final String TAG = "EasySwipeMenuLayout";
          ....
    
          public EasySwipeMenuLayout(Context context) {
              this(context, null);
          }
    
          public EasySwipeMenuLayout(Context context, AttributeSet attrs) {
              this(context, attrs, 0);
          }
    
          public EasySwipeMenuLayout(Context context, AttributeSet attrs, int defStyleAttr) {
              super(context, attrs, defStyleAttr);
              init(context, attrs, defStyleAttr);
    
          }
      }
    
  • 我們想下初始化需要做什么工作呢?其實很簡單

  • 1、肯定是獲取我們自定義的屬性了,因為我們要根據(jù)用戶配置的屬性進行處理嘛

  • 2、前面也說了,側(cè)滑用到了scroller,我們的scroller對象的初始化也可以放在這里

  • 3、一些輔助類的初始化

      /**
       * 初始化方法 * * @param context
        * @param attrs
        * @param defStyleAttr
        */
      private void init(Context context, AttributeSet attrs, int defStyleAttr) {
          //創(chuàng)建輔助對象
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
          mScaledTouchSlop = viewConfiguration.getScaledTouchSlop();
          mScroller = new Scroller(context);
          //1、獲取配置的屬性值
        TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EasySwipeMenuLayout, defStyleAttr, 0);
    
          try {
              int indexCount = typedArray.getIndexCount();
              //2、開始遍歷,并用變量存儲用戶配置的數(shù)據(jù),包括菜單布局的id等
              for (int i = 0; i < indexCount; i++) {
                  int attr = typedArray.getIndex(i);
                  if (attr == R.styleable.EasySwipeMenuLayout_leftMenuView) {
                      
                      mLeftViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_leftMenuView, -1);
                  } else if (attr == R.styleable.EasySwipeMenuLayout_rightMenuView) {
                      mRightViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_rightMenuView, -1);
                  } else if (attr == R.styleable.EasySwipeMenuLayout_contentView) {
                      mContentViewResID = typedArray.getResourceId(R.styleable.EasySwipeMenuLayout_contentView, -1);
                  } else if (attr == R.styleable.EasySwipeMenuLayout_canLeftSwipe) {
                      mCanLeftSwipe = typedArray.getBoolean(R.styleable.EasySwipeMenuLayout_canLeftSwipe, true);
                  } else if (attr == R.styleable.EasySwipeMenuLayout_canRightSwipe) {
                      mCanRightSwipe = typedArray.getBoolean(R.styleable.EasySwipeMenuLayout_canRightSwipe, true);
                  } else if (attr == R.styleable.EasySwipeMenuLayout_fraction) {
                      mFraction = typedArray.getFloat(R.styleable.EasySwipeMenuLayout_fraction, 0.5f);
                  }
              }
    
          } catch (Exception e) {
              e.printStackTrace();
          } finally {
              typedArray.recycle();
          }
    
      }
    
  • 初始化之后,根據(jù)View的創(chuàng)建流程,下一步當然是測量了

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        //1、獲取childView的個數(shù)
      int count = getChildCount();
        //參考frameLayout測量代碼
        //2、判斷我們的EasySwipeMenuLayout的寬高是明確的具體數(shù)值還是匹配或者包裹父布局,為什么要處理呢,還不大清楚的可以看Android之自定義View的死亡三部曲之(Measure) 這篇文章
      final boolean measureMatchParentChildren =
                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
                        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
        mMatchParentChildren.clear();
        int maxHeight = 0;
        int maxWidth = 0;
        int childState = 0;
        //3、開始遍歷childViews進行測量
      for (int i = 0; i < count; i++) {
            View child = getChildAt(i);

            //4、如果view是GONE,那么我們就不需要測量它了,因為它是隱藏的嘛
            if (child.getVisibility() != GONE) {
              
              //5、測量子childView
                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
                MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
                //6、獲取childView中寬的最大值
                maxWidth = Math.max(maxWidth,
                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
                //7、獲取childView中高的最大值
                maxHeight = Math.max(maxHeight,
                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
                childState = combineMeasuredStates(childState, child.getMeasuredState());
                
                //8、如果child中有MATCH_PARENT的,需要再次測量,這里先添加到mMatchParentChildren集合中
                if (measureMatchParentChildren) {
                    if (lp.width == LayoutParams.MATCH_PARENT ||
                            lp.height == LayoutParams.MATCH_PARENT) {
                        mMatchParentChildren.add(child);
                    }
                }
            }
        }
        // Check against our minimum height and width
        //9、我們的EasySwipeMenuLayout的寬度和高度還要考慮背景的大小哦
      maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
        
        //10、設(shè)置我們的EasySwipeMenuLayout的具體寬高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        
        //11、EasySwipeMenuLayout的寬高已經(jīng)知道了,前面MATCH_PARENT的child的值當然我們也能知道了 ,所以這次再次測量它
        count = mMatchParentChildren.size();
        if (count > 1) {
            for (int i = 0; i < count; i++) {
                final View child = mMatchParentChildren.get(i);
                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

                //12、以下是重新設(shè)置child測量所需的MeasureSpec對象
                final int childWidthMeasureSpec;
                if (lp.width == LayoutParams.MATCH_PARENT) {
                    final int width = Math.max(0, getMeasuredWidth()
                            - lp.leftMargin - lp.rightMargin);
                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
                            width, MeasureSpec.EXACTLY);
                } else {
                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
                            lp.leftMargin + lp.rightMargin,
                            lp.width);
                }

                final int childHeightMeasureSpec;
                if (lp.height == FrameLayout.LayoutParams.MATCH_PARENT) {
                    final int height = Math.max(0, getMeasuredHeight()
                            - lp.topMargin - lp.bottomMargin);
                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
                            height, MeasureSpec.EXACTLY);
                } else {
                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
                            lp.topMargin + lp.bottomMargin,
                            lp.height);
                }
              
                //13、重新測量child
                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
            }
        }

    }
  • Ok,布局已經(jīng)測量好了,我們只需要把它按設(shè)計擺上去即可

      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          int count = getChildCount();
          int left = 0 + getPaddingLeft();
          int right = 0 + getPaddingLeft();
          int top = 0 + getPaddingTop();
          int bottom = 0 + getPaddingTop();
          //1、根據(jù)我們配置的id獲取對象的View對象,里面我們自動幫用戶設(shè)置了setClickable(true);當然你也可以讓用戶自己去配置,這樣做是為了響應(yīng)touch事件
          for (int i = 0; i < count; i++) {
              View child = getChildAt(i);
              if (mLeftView == null && child.getId() == mLeftViewResID) {
                  // Log.i(TAG, "找到左邊按鈕view");
        mLeftView = child;
                  mLeftView.setClickable(true);
              } else if (mRightView == null && child.getId() == mRightViewResID) {
                  // Log.i(TAG, "找到右邊按鈕view");
        mRightView = child;
                  mRightView.setClickable(true);
              } else if (mContentView == null && child.getId() == mContentViewResID) {
                  // Log.i(TAG, "找到內(nèi)容View");
        mContentView = child;
                  mContentView.setClickable(true);
              }
    
          }
          //2、布局contentView,contentView是放在屏幕中間的
        int cRight = 0;
          if (mContentView != null) {
              mContentViewLp = (MarginLayoutParams) mContentView.getLayoutParams();
              int cTop = top + mContentViewLp.topMargin;
              int cLeft = left + mContentViewLp.leftMargin;
              cRight = left + mContentViewLp.leftMargin + mContentView.getMeasuredWidth();
              int cBottom = cTop + mContentView.getMeasuredHeight();
              mContentView.layout(cLeft, cTop, cRight, cBottom);
          }
          
          //3、布局mLeftView,mLeftView是在左邊的,一開始是看不到的
          if (mLeftView != null) {
              MarginLayoutParams leftViewLp = (MarginLayoutParams) mLeftView.getLayoutParams();
              int lTop = top + leftViewLp.topMargin;
              int lLeft = 0 - mLeftView.getMeasuredWidth() + leftViewLp.leftMargin + leftViewLp.rightMargin;
              int lRight = 0 - leftViewLp.rightMargin;
              int lBottom = lTop + mLeftView.getMeasuredHeight();
              mLeftView.layout(lLeft, lTop, lRight, lBottom);
          }
          
          //4、布局mRightView,mRightView是在右邊的,一開始也是看不到的
          if (mRightView != null) {
              MarginLayoutParams rightViewLp = (MarginLayoutParams) mRightView.getLayoutParams();
              int lTop = top + rightViewLp.topMargin;
              int lLeft = mContentView.getRight() + mContentViewLp.rightMargin + rightViewLp.leftMargin;
              int lRight = lLeft + mRightView.getMeasuredWidth();
              int lBottom = lTop + mRightView.getMeasuredHeight();
              mRightView.layout(lLeft, lTop, lRight, lBottom);
          }
    
      }
    
  • Ok,弄到這里,我們接下來還有什么沒做呢

  • yes,當然是對于touch事件的交互了

  • 這里采用重寫dispatchTouchEvent事件進行實現(xiàn),當然你也可以重寫onTouchEvent事件進行實現(xiàn)

      @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {
          switch (ev.getAction()) {
              case MotionEvent.ACTION_DOWN: {
                  //   System.out.println(">>>>dispatchTouchEvent() ACTION_DOWN");
    
        isSwipeing = false;
                  //1、記錄最后點擊的位置
                  if (mLastP == null) {
                      mLastP = new PointF();
                  }
                  mLastP.set(ev.getRawX(), ev.getRawY());
                  if (mFirstP == null) {
                      mFirstP = new PointF();
                  }
                  //2、記錄第一次點擊的位置
                  mFirstP.set(ev.getRawX(), ev.getRawY());
                  
                  //3、mViewCache,參考了網(wǎng)上一個作者的思想,通過類單例來控制每次只有一個菜單被打開
                  if (mViewCache != null) {
                      if (mViewCache != this) {
                          //4、當此時點擊的view不實已開大菜單的view,我們就關(guān)閉已打開的菜單
                          mViewCache.handlerSwipeMenu(State.CLOSE);
    
                      }
              
                  }
    
                  break;
              }
              case MotionEvent.ACTION_MOVE: {
                  // System.out.println(">>>>dispatchTouchEvent() ACTION_MOVE getScrollX:" + getScrollX());
        isSwipeing = true;
                
                //5、獲得橫向和縱向的移動距離
                  float distanceX = mLastP.x - ev.getRawX();
                  float distanceY = mLastP.y - ev.getRawY();
                  if (Math.abs(distanceY) > mScaledTouchSlop * 2) {
                      break;
                  }
                  //當處于水平滑動時,禁止父類攔截
        if (Math.abs(distanceX) > mScaledTouchSlop * 2 || Math.abs(getScrollX()) > mScaledTouchSlop * 2) {
                      requestDisallowInterceptTouchEvent(true);
                  }
                  //6、通過使用scrollBy控制view的滑動
                  scrollBy((int) (distanceX), 0);
                  
                   //7、越界修正 
                 if (getScrollX() < 0) {
                      if (!mCanRightSwipe || mLeftView == null) {
                          scrollTo(0, 0);
                      }
                      {//左滑
        if (getScrollX() < mLeftView.getLeft()) {
                              scrollTo(mLeftView.getLeft(), 0);
                          }
    
                      }
                  } else if (getScrollX() > 0) {
                      if (!mCanLeftSwipe || mRightView == null) {
                          scrollTo(0, 0);
                      } else {
                          if (getScrollX() > mRightView.getRight() - mContentView.getRight() - mContentViewLp.rightMargin) {
                              scrollTo(mRightView.getRight() - mContentView.getRight() - mContentViewLp.rightMargin, 0);
                          }
                      }
                  }
    
                  mLastP.set(ev.getRawX(), ev.getRawY());
    
                  break;
              }
              case MotionEvent.ACTION_UP:
              case MotionEvent.ACTION_CANCEL: {
                  //    System.out.println(">>>>dispatchTouchEvent() ACTION_CANCEL OR ACTION_UP");
                   //8、當用戶松開時,判斷當前狀態(tài),比如左滑菜單出現(xiàn)一半了,此時松開我們應(yīng)該讓菜單自動滑出來
        State result = isShouldOpen(getScrollX());
                  handlerSwipeMenu(result);
                  break;
              }
              default: {
                  break;
              }
          }
    
          return super.dispatchTouchEvent(ev);
    
      }
    
  • Ok,之后我們再考慮點細節(jié)問題就差不多了

  • 比如,假如你在recyclerView中使用,那么當你側(cè)滑出菜單的時候,肯定不希望他出發(fā)recyclerView的滾動事件,這時我們可以通過重寫onInterceptTouchEvent方法處理

      @Override
      public boolean onInterceptTouchEvent(MotionEvent event) {
          // Log.d(TAG, "dispatchTouchEvent() called with: " + "ev = [" + event + "]");
    
        switch (event.getAction()) {
              case MotionEvent.ACTION_DOWN: {
                  break;
              }
              case MotionEvent.ACTION_MOVE: {
                  //對左邊界進行處理
        float distance = mLastP.x - event.getRawX();
                  if (Math.abs(distance) > mScaledTouchSlop) {
                      // 當手指拖動值大于mScaledTouchSlop值時,認為應(yīng)該進行滾動,攔截子控件的事件
        return true;
                  }
                  break;
    
              }
    
          }
          return super.onInterceptTouchEvent(event);
      }
    
  • Ok,到這里我們就基本完工了。


總結(jié)

  • 自定義View三部曲,測量、布局、繪制的掌握是關(guān)鍵
  • 與用戶交互,重寫dispatchTouchEvent或者onTouchEvent等,根據(jù)實際情況而定
  • 做好一定的touch事件攔截處理
  • 重點還是要掌握自定義View的三部曲以及touch事件的分發(fā)機制,再加上一些動畫的處理,基本能滿足大部分的業(yè)務(wù)需求了,重點還是要掌握根本的東西,厚積而薄發(fā),加油。
  • 希望通過本次的內(nèi)容分析能夠給予你一些幫助,謝謝!
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,321評論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,559評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,442評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,835評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,581評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,922評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,931評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,096評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,639評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,374評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,591評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,104評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,789評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,196評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,524評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,322評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,554評論 2 379

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