Android自定义控件之从0到1轻松实现侧滑按钮


一、前言


二、构想图

EasySwipeMenuLayout构想图.jpg
  • 我们这次要实现的控件叫做EasySwipeMenuLayout,内部主要分为三部分:
    1、内容区域
    2、左边菜单按钮区域
    2、右边菜单按钮区域
  • 当我们向右滑时,通过scroller将左边按钮区域滚动出来
  • 当我们向左滑时,通过scroller将右边按钮区域滚动出来
  • 实现的思路滤清了,那么我们就开始动手吧

三、具体实现

  • 首先,网上类似的轮子有很多,但为什么我们还要自己写一下呢,当然是为了学习,所谓知其然而知其所以然也,轮子只是满足了大部分人的需求,试想某一天,有些效果网上是找不到的,那么此时就只能靠自己了。
  • 当然,你也可以说,我就是想自己写,哈哈。
  • 在开始前,我还想再说一点,网上有很多类似的轮子,但是我发现个特点,他们要求控件内的子布局的顺序相对呆板,不够灵活,也就是所谓通过约定来实现。
  • but,我这次想通过配置来实现,那么如何配置呢,其实我们可以通过控件的id进行绑定,参考了google官方控件的部分思想。

布局文件配置效果

  • 首先,我想实现的配置效果是这样子的

      <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="内容区域" />
            </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是现实内容的,哪个是左边的菜单布局,哪个是右边的菜单布局。

       <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">
    
  • 为什么要这样子设计的,我的想法是,这样子更灵活,我不用规定里面的子布局的顺序。

  • 以上仅代表个人观点,当然,肯定有更好的设计方案。

  • Ok,既然要通过id来配置,那么就会用到自定义控件属性的知识,其实很简单,就是在res/values下创建一个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对象,根据定义的名字获取值即可
          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,自定义控件的自定义属性问题就这样解决了,接下来我们就开始分析实现代码吧


  • 首先我们的EasySwipeMenuLayout通过继承ViewGroup进行实现,里面的构造方法通过不断的调用自身的构造方法,最终会调用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、肯定是获取我们自定义的属性了,因为我们要根据用户配置的属性进行处理嘛

  • 2、前面也说了,侧滑用到了scroller,我们的scroller对象的初始化也可以放在这里

  • 3、一些辅助类的初始化

      /**
       * 初始化方法 * * @param context
        * @param attrs
        * @param defStyleAttr
        */
      private void init(Context context, AttributeSet attrs, int defStyleAttr) {
          //创建辅助对象
        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、开始遍历,并用变量存储用户配置的数据,包括菜单布局的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();
          }
    
      }
    
  • 初始化之后,根据View的创建流程,下一步当然是测量了

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        
        //1、获取childView的个数
      int count = getChildCount();
        //参考frameLayout测量代码
        //2、判断我们的EasySwipeMenuLayout的宽高是明确的具体数值还是匹配或者包裹父布局,为什么要处理呢,还不大清楚的可以看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、设置我们的EasySwipeMenuLayout的具体宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
                resolveSizeAndState(maxHeight, heightMeasureSpec,
                        childState << MEASURED_HEIGHT_STATE_SHIFT));
        
        //11、EasySwipeMenuLayout的宽高已经知道了,前面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、以下是重新设置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,布局已经测量好了,我们只需要把它按设计摆上去即可

      @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、根据我们配置的id获取对象的View对象,里面我们自动帮用户设置了setClickable(true);当然你也可以让用户自己去配置,这样做是为了响应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, "找到内容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事件进行实现,当然你也可以重写onTouchEvent事件进行实现

      @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,参考了网上一个作者的思想,通过类单例来控制每次只有一个菜单被打开
                  if (mViewCache != null) {
                      if (mViewCache != this) {
                          //4、当此时点击的view不实已开大菜单的view,我们就关闭已打开的菜单
                          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、当用户松开时,判断当前状态,比如左滑菜单出现一半了,此时松开我们应该让菜单自动滑出来
        State result = isShouldOpen(getScrollX());
                  handlerSwipeMenu(result);
                  break;
              }
              default: {
                  break;
              }
          }
    
          return super.dispatchTouchEvent(ev);
    
      }
    
  • Ok,之后我们再考虑点细节问题就差不多了

  • 比如,假如你在recyclerView中使用,那么当你侧滑出菜单的时候,肯定不希望他出发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值时,认为应该进行滚动,拦截子控件的事件
        return true;
                  }
                  break;
    
              }
    
          }
          return super.onInterceptTouchEvent(event);
      }
    
  • Ok,到这里我们就基本完工了。


总结

  • 自定义View三部曲,测量、布局、绘制的掌握是关键
  • 与用户交互,重写dispatchTouchEvent或者onTouchEvent等,根据实际情况而定
  • 做好一定的touch事件拦截处理
  • 重点还是要掌握自定义View的三部曲以及touch事件的分发机制,再加上一些动画的处理,基本能满足大部分的业务需求了,重点还是要掌握根本的东西,厚积而薄发,加油。
  • 希望通过本次的内容分析能够给予你一些帮助,谢谢!
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容