2020-06-22

自定义侧滑控件 SlideSlipView

功能介绍

本控件是模仿QQ的消息侧滑功能来开发的,实现的效果基本跟QQ的侧滑效果一致。而且本控件基本不依赖其它控件,尽量降低耦合性。同时使用起来也非常简单,支持自定义侧滑内容,通过布局文件的方式就可以实现。可以作为RecyclerView的item使用,也可以单独添加到布局中使用。


slide_slip.gif

实现方案

重写ViewGroup的onLayout方法,对每一个子view合理布局、利用View的事件分发机制判断每一个事件到来时需要做的功能(核心模块)、通过Scroller和VelocityTracker配合使用达到自动滑动跟惯性滑动的效果。

使用指南

虽然本控件实现了想要的效果,但是多少还是有一些不太完美的地方需要大家在使用时注意一下。

  1. 该组件继承自FrameLayout,所以在使用的时候将子view包裹到该组件内就可以了,但是子view的布局方式是按照横向顺序排布的,跟横向的LinearLayout效果是一样的。
  2. 为了区分子view属于折叠内容还是属于非折叠内容,每一个子view都需要添加android:tag="1"属性, 属性值必须是整型变量,[0-100]代表是非折叠内容,[101-200]代表折叠内容。
  3. 本组件(不是指子view)的宽度只支持match_parent、固定值,不支持wrap_content;高度则没有限制。
  4. 建议大家使用的时候将折叠内容跟非折叠内容分别用ViewGroup(比如LinearLayout、RelativeLayout、ConstraintLayout等等)包裹起来再放到该控件中,一方面可以减少tag的使用,另一方面可以实现更复杂的布局效果。
  5. 为了解决多指触摸RecyclerView导致多个item同时发生侧滑的问题,在使用该组件的时候最好跟TouchRecyclerview配合使用。

注:非折叠内容是指组件没有发生侧滑时用户看到的view构成的部分,折叠内容是指组件发生侧滑时用户看到的之前隐藏起来的部分。

实现代码

虽然代码很多,但是核心功能代码都在onInterceptTouchEvent、onTouchEvent方法中,其它的都是一些工具方法,基本上不用太关心,代码中都添加了详细的注释,就不再这里继续分析代码了。另外TouchRecyclerview代码很简单就不在这里展示了,最后会贴出源码地址,欢迎大家去git上start!

/**
 * CZL 自定义View模仿qq侧滑删除
 */
public class SlideSlipView extends FrameLayout {

    /**
     * 为了更好地分析代码中的逻辑,下面一些概念在此声明:
     * 折叠状态:置顶、标记为未读、删除这部分隐藏起来的时候
     * 非折叠状态:置顶、标记为未读、删除这部分显示出来的时候
     * 原始内容:折叠状态下item可见内容部分
     * 隐藏内容:置顶、标记为未读、删除这部分构成的内容部分
     */

    private final String TAG = SlideSlipView.this.getClass().getSimpleName();
    private Scroller scroller;//辅助滚动工具
    private int mTouchSlop;
    private final float VELOCITY_SLOP = 600;//惯性滑动最小速度值
    private float mLastX;//记录上次触摸点x坐标
    private float mLastY;//记录上次触摸点y坐标
    //特殊触摸标记,含义:当前被点击的item之外是否还有其它item是展开的,true:是   false:否
    // 当为true的时候会进行‘拦截一切’的举动,也就是对事件只拦截但是什么滑动都不处理(这参考了qq的实现方式)
    private boolean specialTouch = false;
    //特殊触摸标记,含义:是否需要将当前view变成‘折叠状态’,true:是   false:否
    //当前view为'非折叠状态'时,手指点击‘原始内容’区域并立即抬起后将自动折叠view
    private boolean specialTouch2 = false;
    private boolean fold = true;//折叠标记,判断当前view是否是折叠状态,true:是  false:否
    private VelocityTracker velocityTracker;//速度模拟类
    private RecyclerView recyclerView;//如果在RV中使用当前view,需要处理-滑动冲突、多个item同时展开的问题,这会用到RV

    public SlideSlipView(Context context) {
        this(context, (AttributeSet) null);
    }

    public SlideSlipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public SlideSlipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        scroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        mTouchSlop = configuration.getScaledTouchSlop();
        setClipToPadding(false);//设置该属性后,该view设置的padding部分可以随内容一起滑动
    }

    /**
     * 重写布局方法,支持子view设置margin、padding等属性
     * 子view必须设置tag属性,否者直接抛出异常
     * 可见子view设置的tag取值范围0-100;隐藏子view设置tag取值范围100-Integer最大值。
     * 这里的子view是指直接子view,可见子view是指正常情况下可以看见的子view,隐藏子view是指正常情况下不可见的子view,当滑动以后才能看到的view
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        final int childCount = getChildCount();
        final int mPaddingLeft = Math.max(0, getPaddingLeft());
        final int mPaddingRight = Math.max(0, getPaddingRight());
        final int mPaddingTop = Math.max(0, getPaddingTop());
        int widthSum = mPaddingLeft;//可见内容部分初始左边界位置
        int widthSum2 = getMeasuredWidth();//隐藏内容部分初始左边界位置
        int heightSum = 0;
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            int tag;
            try {
                tag = Integer.valueOf((String) childView.getTag());//这里设计通过tag区分可见子view跟隐藏子view,所以使用该控件必需设置子view的tag
            } catch (Exception e) {
                throw new IllegalArgumentException("SlideSlipView的子View需要设置tag,显示View的tag 0-100,隐藏View的tag 101-Integer.MAX_VALUE");
            }

            heightSum = mPaddingTop + getMargin(2, childView);
            if (tag <= 100 && tag >= 0) {//可见子view,tag 0-100
                widthSum += getMargin(0, childView);//获取childView左边界的mleft位置
                if (widthSum >= (getMeasuredWidth() - mPaddingRight)) {//当可见子View累计宽度大于可用宽度时,不再显示剩余‘可见子view’
                    continue;
                }
                int rightPosition = Math.min(getMeasuredWidth() - mPaddingRight, widthSum + childView.getMeasuredWidth());//当子view的右边界大于
                childView.layout(widthSum, heightSum, rightPosition, heightSum + childView.getMeasuredHeight());
                widthSum += childView.getMeasuredWidth();//计算子view的横向位置
                widthSum += getMargin(1, childView);//计算子view的横向位置
            } else if (tag > 100) {//隐藏子view, tag 100-Integer.MAX_VALUE
                Log.e(TAG, "onLayout: currentViewWidth=" + getMeasuredWidth() + "\tchildWidth=" + childView.getMeasuredWidth() + "\ttag=" + tag);
                widthSum2 += getMargin(0, childView);//隐藏子view有多少就布局多少个
                childView.layout(widthSum2, heightSum, widthSum2 + childView.getMeasuredWidth(), heightSum + childView.getMeasuredHeight());
                widthSum2 += childView.getMeasuredWidth();//计算子view的横向位置
                widthSum2 += getMargin(1, childView);//计算子view的横向位置
            }
        }
    }

    /**
     * 侧滑关闭策略:
     * 1、点击的item是‘展开’状态,则将item折叠(包括当前item及可能存在的其它item,因为RV如果多个手指同时滑动多个item时,都会进行侧滑,这跟qq不一样,是个小bug)
     * 具体实现方案:在onInterceptTouchEvent方法的ACTION_UP事件,并更新状态,同时拦截该事件(不允许调用item的onClick方法)
     * 2、被点击的item是‘折叠’状态,如果其它item有‘展开’则‘折叠’其它item(在actiondown的时候就),如果其他item都是‘折叠’则正常处理点击事件
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted;
        float currentX = ev.getX();
        float currentY = ev.getY();
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN: {
                if (existExpandChildren()) {//这行代码作用:当有其它item展开的时候,点击当前item关闭其它展开的item,并将该事件停止下传
//                    Log.e(TAG, "---------------------------onInterceptTouchEvent: 特殊事件ACTION_DOWN");
                    if (getRecyclerView() != null) {//xz
                        getRecyclerView().requestDisallowInterceptTouchEvent(true);//这个方法一旦调用,除非再调用一次,否则该view永远无法拦截事件
                    }
                    specialTouch = true;//特殊事件标记,当是true的时候拦截所有的事件,但是不对事件做处理(即rv不进行滚动、slideview不进行任何滑动响应)
                }
                if (!isFold()) {//如果当前item是‘展开’状态
                    specialTouch = false;//如果当前item也是‘非折叠’状态的话,停止‘拦截一切’的举动
                    if (needFold(ev)) {
                        specialTouch2 = true;
                        intercepted = true;
                        break;
                    }
                }
                if (specialTouch) {//把特殊事件标记放这里是为了让第二个标记的判断也能走一遍
                    intercepted = true;
                    break;
                }
                //当既没有其它item展开,点击的也不是‘原始内容’时,那么将触摸事件分发给slideslipview的子view
                mLastX = currentX;
                mLastY = currentY;
                intercepted = false;
                if (getRecyclerView() != null) {//xz
                    getRecyclerView().requestDisallowInterceptTouchEvent(true);
                }
            }
            break;
            case MotionEvent.ACTION_MOVE:
                if (specialTouch) {//特殊事件、特殊处理
//                    Log.e(TAG, "---------------------------onInterceptTouchEvent: 特殊事件ACTION_MOVE");
                    intercepted = true;
                    break;
                }
                //当最开始点击的是‘隐藏内容’时会执行到这里(点击‘隐藏内容’后slideview需要判断后续的动作,如果是滑动的话那么对事件进行拦截)
                if (Math.abs(currentX - mLastX) > Math.abs(currentY - mLastY)) {//横向滑动
                    mLastX = currentX;
                    mLastY = currentY;
                    intercepted = true;//拦截事件
                    if (getRecyclerView() != null) {//xz
                        getRecyclerView().requestDisallowInterceptTouchEvent(true);//如果是横向滑动的话,禁止RV拦截接下来的事件,否则RV会拦截接下来的事件,造成滑动冲突
                    }
                } else if (Math.abs(currentY - mLastY) > Math.abs(currentX - mLastX)) {//竖直滑动
                    intercepted = false;//不拦截事件
                    if (getRecyclerView() != null) {//xz
                        getRecyclerView().requestDisallowInterceptTouchEvent(false);//允许RV拦截滑动事件,RV一旦拦截事件的话接下来所有的事件都会交给RV处理
                    }
                } else {
                    intercepted = false;
                    if (getRecyclerView() != null) {//xz
                        getRecyclerView().requestDisallowInterceptTouchEvent(false);
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_CANCEL:
                intercepted = false;//不对up事件进行拦截,因为一旦拦截的话,那么子view的点击(包括长按)功能就不管用了
                break;
            default:
                intercepted = false;
        }
//        Log.e(TAG, "onInterceptTouchEvent: intercept=" + intercepted);
        return intercepted;
    }

    /**
     * 逻辑方法
     * 这个方法用来判断当手指点击‘非折叠状态’下‘原始内容’区域时是否需要自动折叠
     * true:自动折叠    false:不折叠
     */
    private boolean needFold(MotionEvent ev) {
        boolean result = false;
        for (int i = 0; i < getChildCount(); i++) {
            View childView = getChildAt(i);
            int tag = Integer.valueOf((String) childView.getTag());
            if (!(tag >= 0 && tag <= 100)) {//这里的判断是过滤掉‘隐藏内容’view
                continue;
            }
            Rect rect = new Rect();
            childView.getGlobalVisibleRect(rect);
//          Log.e(TAG, "onInterceptTouchEvent: x=" + ev.getX() + "\ty=" + ev.getY() + "\tleft=" + rect.left + "\ttop=" + rect.top + "\tright=" + rect.right + "\tbottom=" + rect.bottom);
            Rect rect1 = new Rect(rect.left, 0, rect.right, rect.bottom - rect.top);
            if (rect1.contains(Math.round(ev.getX()), Math.round(ev.getY()))) {//判断点击区域是否在‘原始内容’部分内
//              Log.e(TAG, "onInterceptTouchEvent: 点击第一个子view");
                result = true;//特殊事件标记,表示手指按下部分是否属于‘原始内容’,true:是   false:不是
                if (getRecyclerView() != null) {//xz
                    getRecyclerView().requestDisallowInterceptTouchEvent(true);
                }
                break;
            }
        }
        return result;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (specialTouch) {//特殊事件-是为了处理点击折叠状态的item,关闭展开状态的item后RV不处理触摸事件(也就是不滑动)
            boolean result;
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    Log.e(TAG, "---------------------------onTouchEvent: 特殊事件ACTION_DOWN");
                    result = true;
                    break;
                case MotionEvent.ACTION_MOVE:
                    Log.e(TAG, "---------------------------onTouchEvent: 特殊事件ACTION_MOVE");
                    result = true;
                    break;
                case MotionEvent.ACTION_UP: {
                    Log.e(TAG, "---------------------------onTouchEvent: 特殊事件ACTION_UP");
                    specialTouch = false;
                    getRecyclerView().requestDisallowInterceptTouchEvent(false);
                    result = true;
                }
                break;
                case MotionEvent.ACTION_CANCEL: {
                    Log.e(TAG, "---------------------------onTouchEvent: 特殊事件ACTION_CANCEL");
                    specialTouch = false;
                    getRecyclerView().requestDisallowInterceptTouchEvent(false);
                    result = true;
                }
                break;
                default:
                    specialTouch = false;
                    getRecyclerView().requestDisallowInterceptTouchEvent(false);
                    result = false;
            }
            return result;
        }

        boolean result = false;//为了配合特殊事件2
        float currentX = event.getX();
        float currentY = event.getY();
        addVelocityTracker(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN: {
                mLastX = currentX;
                mLastY = currentY;
                result = true;//为了配合特殊事件2
            }
            break;
            case MotionEvent.ACTION_MOVE:
                if (specialTouch2 && Math.abs(currentX - mLastX) > mTouchSlop && Math.abs(currentX - mLastX) > Math.abs(currentY - mLastY)) {
                    //这里的条件语句目的只有一个,判断 “从用户手指点击‘原始内容’部分到手指离开” 这是一个点击事件还是滑动事件
                    //当手指移动范围较大的时候看成滑动事件,否者看成点击事件;(当手指按下触摸屏幕的时候,保持不动,对用户来说他认为没有移动手指所以没有移动,
                    // 但是对系统来说即使再微小的移动也能捕获,而且确实当你手指从按下那一刻就在不停的移动,只是人很难察觉到)
                    //至于为什么要区分事件,是因为对不同事件需要做不同处理,点击事件:手指抬起时需要对展开的item折叠 ,滑动事件:手指抬起时需要对item进行自动滑动(折叠起来还是展开)
                    //而作为区分的标记就是specialTouch2,这个值最终会在action_up时用到,所以这里的设计当时也是耗费了一些时间才想到的。
                    specialTouch2 = false;//特殊事件2,一旦开始移动的话就不再算作特殊事件,也就是将特殊事件看做失效
                }
                //当用户手指滑动slideview的时候,让内容滑动起来
                if (Math.abs(currentX - mLastX) > Math.abs(currentY - mLastY)) {//注意点:这里没有使用mTouchSlop进行判断,是因为使用mTouchSlop会让滑动不流畅
                    if (currentX - mLastX > 0) {
                        //向右滑动
                        if (getScrollX() <= 0) {
                            //停止滑动,因为已经滑动到边界
                        } else {
                            scrollBy(-Math.min(getScrollX(), Math.round(currentX - mLastX)), 0);
                        }
                    } else {
                        //向左滑动
                        int childCount = getChildCount();
                        int rightBorder = getChildAt(childCount - 1).getRight();
                        Log.e(TAG, "onTouchEvent: rightBorder=" + rightBorder);
//                        if (getScrollX() >= rightBorder - getChildAt(0).getRight()) {
                        if (getScrollX() >= (rightBorder - getMeasuredWidth())) {//当滚动距离大于‘隐藏子view’区域(间距+宽度)宽度的时候停止滑动
                            //停止滑动,因为已经滑动到边界
                        } else {//当滚动距离小于‘隐藏子view’区域的宽度时,继续进行滑动(这里进行了滑动判断,防止手指移动距离大于内容可滑动距离)
//                            scrollBy(Math.min(rightBorder - getChildAt(0).getRight() - getScrollX(), Math.round(mLastX - currentX)), 0);
                            scrollBy(Math.min(rightBorder - getMeasuredWidth() - getScrollX(), Math.round(mLastX - currentX)), 0);
                        }
                    }
                    mLastX = currentX;
                    mLastY = currentY;
                }
                break;
            case MotionEvent.ACTION_UP: {
                if (specialTouch2) {
                    //如果用户点击的是‘原始内容’,折叠item
                    Log.e(TAG, "---------------------------------onTouchEvent: 特殊事件2");
                    setFold(true);
                    smoothScrollToStart();
                    if (getRecyclerView() != null) {
                        getRecyclerView().requestDisallowInterceptTouchEvent(false);
                    }
                    break;
                }
                //如果用户滑动的item,手指离开的时候让item自动滑动(这里面有用到惯性滑动)
                int childCount = getChildCount();
                int rightBorder = getChildAt(childCount - 1).getRight();
                float x_velocity = getXVelocity();
                if (x_velocity > VELOCITY_SLOP) {//向右滑动(惯性滑动)
                    Log.e(TAG, "---------------------------------onTouchEvent: 快速向右滑动");
                    setFold(true);
                    scroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 200);
                    invalidate();//遗忘点,这句代码如果不加的话导致view不会惯性滑动
                } else if (x_velocity < -VELOCITY_SLOP) {//左滑动(惯性滑动)
                    Log.e(TAG, "---------------------------------onTouchEvent: 快速向左滑动");
                    setFold(false);
//                    scroller.startScroll(getScrollX(), 0, rightBorder - getChildAt(0).getRight() - getScrollX(), 0, 200);
                    scroller.startScroll(getScrollX(), 0, rightBorder - getMeasuredWidth() - getScrollX(), 0, 200);
                    invalidate();//遗忘点,这句代码如果不加的话导致view不会惯性滑动
                } else {//根据滑动距离判断是折叠还是展开
                    Log.e(TAG, "---------------------------------onTouchEvent: 自由滑动");
//                    if (getScrollX() > (rightBorder - getChildAt(0).getRight()) / 2) {
                    if (getScrollX() > (rightBorder - getMeasuredWidth()) / 2) {//当滑动距离超过‘隐藏内容’一半宽度时,手指离开屏幕后隐藏内容剩余部分自动展开
                        setFold(false);
//                        scroller.startScroll(getScrollX(), 0, rightBorder - getChildAt(0).getRight() - getScrollX(), 0, 500);
                        scroller.startScroll(getScrollX(), 0, rightBorder - getMeasuredWidth() - getScrollX(), 0, 500);
                        invalidate();
                    } else {//当滑动距离小于‘隐藏内容’一半宽度时,手指离开屏幕后隐藏内容已展开部分自动折叠
                        setFold(true);
                        scroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
                        invalidate();
                    }
                }
                recycleVelocityTracker();
                releaseRecyclerView();//XZ
            }
            break;
        }
        return super.onTouchEvent(event) || result;
    }

    /**
     * 工具方法(View自带方法,所有view都有该方法)
     * 页面每次重绘都会调用该方法,默认是空实现,一般都是跟scroller配合使用
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            scrollTo(scroller.getCurrX(), scroller.getCurrY());
            postInvalidate();
        }
    }

    /**
     * 工具方法
     * 获取惯性滑动的竖直速度
     */
    private void addVelocityTracker(MotionEvent event) {
        if (velocityTracker == null) {
            velocityTracker = VelocityTracker.obtain();
        }
        velocityTracker.addMovement(event);
    }

    /**
     * 工具方法
     * 获取惯性滑动的横向速度
     */
    private float getXVelocity() {
        if (velocityTracker != null) {
            velocityTracker.computeCurrentVelocity(1000);
            return velocityTracker.getXVelocity();
        }
        return 0;
    }

    /**
     * 工具方法
     * VelocityTracker常规使用
     */
    private void recycleVelocityTracker() {
        if (velocityTracker != null) {
            velocityTracker.recycle();//出错点,这里不需要再添加clear代码,否者会报错
        }
        velocityTracker = null;
    }

    /**
     * 工具方法
     * 获取该view所在列表的recyclerview对象
     */
    private RecyclerView getRecyclerView() {
        if (recyclerView != null)
            return recyclerView;
        ViewParent viewParent = this;
        while (!(viewParent.getParent() instanceof RecyclerView)) {
            viewParent = viewParent.getParent();
            if (viewParent == null) {
                Log.e(TAG, "getRecyclerView: 没有找到recyclerview");
                break;
            }
        }
        recyclerView = (viewParent == null ? null : (RecyclerView) viewParent.getParent());
        if (recyclerView != null) {
            Log.e(TAG, "getRecyclerView: 找到了recyclerview");
        }
        return recyclerView;
    }

    private void releaseRecyclerView() {
        recyclerView = null;
    }

    /**
     * view折叠状态
     * 默认为true
     * 当删除、置顶部分隐藏的时候是折叠状态,显示的时候是非折叠状态
     */
    public boolean isFold() {
        return fold;
    }

    public void setFold(boolean fold) {
        this.fold = fold;
    }

    /**
     * 工具方法
     * 遍历RV可见范围内是否有其它item是非折叠状态
     */
    private boolean existExpandChildren() {
        boolean result = false;
        if (getRecyclerView() != null) {
            int childCount = getRecyclerView().getChildCount();
            for (int i = 0; i < childCount; i++) {
                View itemView = getRecyclerView().getChildAt(i);
                SlideSlipView slideSlipView = getExpandSlideSlipView(itemView);
                if (slideSlipView != null) {
                    slideSlipView.smoothScrollToStart();
                    slideSlipView.setFold(true);
                    result = true;
                }
            }
        }
        return result;
    }

    /**
     * 工具方法
     * scroller滑动常规使用
     */
    private void smoothScrollToStart() {
        scroller.startScroll(getScrollX(), 0, -getScrollX(), 0, 500);
        postInvalidate();
    }

    /**
     * 工具方法
     * 递归获取slideslipview对象
     */
    private SlideSlipView getExpandSlideSlipView(View parentView) {
        if (parentView == null) {
            return null;
        }
        if (parentView == this) {
            return null;
        }
        if (parentView instanceof SlideSlipView) {
            if (((SlideSlipView) parentView).isFold()) {
                return null;
            } else {
                return (SlideSlipView) parentView;
            }
        }
        if (!(parentView instanceof ViewGroup)) {
            return null;
        }
        final int childCount = ((ViewGroup) parentView).getChildCount();
        for (int i = 0; i < childCount; i++) {
            SlideSlipView slipView = getExpandSlideSlipView(((ViewGroup) parentView).getChildAt(i));
            if (slipView != null) {
                if (slipView.isFold()) {
                    return null;
                } else {
                    return slipView;
                }
            }
        }
        return null;
    }

    /**
     * 工具方法
     * 获取子view的间距
     */
    private int getMargin(int type, View childView) {
        ViewGroup.LayoutParams layoutParams = childView.getLayoutParams();
        if (!(layoutParams instanceof MarginLayoutParams)) {
            return 0;
        }
        int result;
        switch (type) {
            case 0:
                result = Math.max(0, ((MarginLayoutParams) layoutParams).leftMargin);
                break;
            case 1:
                result = Math.max(0, ((MarginLayoutParams) layoutParams).rightMargin);
                break;
            case 2:
                result = Math.max(0, ((MarginLayoutParams) layoutParams).topMargin);
                break;
            default:
                result = 0;
        }
        return result;
    }
}

源码地址:https://github.com/AndroidFirstDeveloper/DevelopProject

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。