美文网首页
记录在scroll view中嵌套子布局出现的一些问题

记录在scroll view中嵌套子布局出现的一些问题

作者: 笑对浮华 | 来源:发表于2020-01-17 11:19 被阅读0次

先来看看项目中的需求,直接上图吧:

功能演示

这一块的功能主要包含两个:
功能一:子阶段实现侧滑删除功能;
功能二:点击子阶段,展示子阶段下包含的任务。

功能实现:整个页面外层有一个scroll view,子阶段是一个自定义LinearLayout布局,每个自阶段下包含一个recycler view,用于放置子阶段下的任务,点击子阶段展开时显示recycler view,再次点击隐藏recycler view。
功能实现大概就是上面那样,下面主要说一下开发过程中遇到的两个bug
bug 一:点击子阶段时,下面的recycler view不显示,点击事件未响应;
bug 二:上下滚动屏幕时,初次按下的焦点落在子阶段布局内明显感觉滚动不流畅。
上述两个bug都与自定义LinearLayout布局有关,下面先贴上自定义布局的代码:

public class SwipeLayout extends LinearLayout {
    private static final String TAG = "SwipeLayout";

    private Context mContext;

    private LinearLayout mContentView;

    private RelativeLayout mRightView;

    private Scroller mScroller;
    private OnSlideListener mOnSlideListener;
    private int mHolderWidth = 80;
    private int mLastX = 0;
    private int mLastY = 0;
    private static final int TAN = 2;

    private int mSlideState = 0;

    private int firstClickX;
    private int firstClickY;

    public interface OnRightMenuClickListener {
        void rightClick(View view);
    }

    private OnRightMenuClickListener onRightMenuClickListener;

    public void setOnRightMenuClickListener(OnRightMenuClickListener onRightMenuClickListener) {
        this.onRightMenuClickListener = onRightMenuClickListener;
    }

    public interface OnContentClickListener {
        void contentClick(View view);
    }

    private OnContentClickListener onContentClickListener;

    public void setOnContentClickListener(OnContentClickListener onContentClickListener) {
        this.onContentClickListener = onContentClickListener;
    }

    public interface OnSlideListener {
        int SLIDE_STATUS_OFF = 0;
        int SLIDE_STATUS_START_SCROLL = 1;
        int SLIDE_STATUS_ON = 2;

        void onSlide(View view, int status);
    }

    public SwipeLayout(Context context) {
        super(context);
        initView();
    }

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

    private void initView() {
        mContext = getContext();
        mScroller = new Scroller(mContext);
        setOrientation(LinearLayout.HORIZONTAL);
        View.inflate(mContext, R.layout.swipelayout_right, this);
        mContentView = findViewById(R.id.view_content);
        mRightView = findViewById(R.id.view_right);
        mHolderWidth = Math.round(TypedValue.applyDimension(
                TypedValue.COMPLEX_UNIT_DIP, mHolderWidth, getResources()
                        .getDisplayMetrics()));
        //删除按钮的监听
        mRightView.setOnClickListener(v -> {
            if (onRightMenuClickListener != null)
                onRightMenuClickListener.rightClick(v);
        });
        //内容view的监听
        mContentView.setOnClickListener(v -> {
            if (null != onContentClickListener) {
                onContentClickListener.contentClick(v);
            }
        });
    }

    //将View加入到mContentView中
    public void setContentView(View view) {
        mContentView.addView(view);
    }

    public View getContentView() {
        return mContentView;
    }

    public void setOnSlideListener(OnSlideListener onSlideListener) {
        mOnSlideListener = onSlideListener;
    }

    //将当前状态设置为关闭
    public void shrink() {
        if (getScrollX() != 0) {
            this.smoothScrollTo(0, 0);
        }
    }

    //如果其子View存在消耗点击事件的View,那么SwipeLayout的onTouchEvent不会被执行,
    // 因为在ACTION_MOVE的时候返回true,执行其onTouchEvent方法
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int x = (int) ev.getX();
        int y = (int) ev.getY();
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                firstClickX = (int) ev.getX();
                firstClickY = (int) ev.getY();
                if (!mScroller.isFinished()) {
                    mScroller.abortAnimation();
                }
                if (mOnSlideListener != null) {
                    mOnSlideListener.onSlide(this,
                            OnSlideListener.SLIDE_STATUS_START_SCROLL);
                    mSlideState = OnSlideListener.SLIDE_STATUS_START_SCROLL;
                }
                //这里需要记录mLastX,mLastY的值,不然当SwipeLayout已经处于开启状态时,
                // 用于再次滑动SwipeLayout时,会先立即复原到关闭状态,用户体验不太好
                mLastX = (int) ev.getX();
                mLastY = (int) ev.getY();
                return false;
            case MotionEvent.ACTION_MOVE:
                //返回值为true表示本次触摸事件由自己执行,即执行SwipeLayout的onTouchEvent方法
                int deltaX = x - mLastX;
                int deltaY = y - mLastY;
                //这里作用是来比较X、Y轴的滑动距离,如果X轴的滑动距离小于两倍的Y轴滑动距离,则不执行SwipeLayout的滑动事件
                if (Math.abs(deltaX) < Math.abs(deltaY) * TAN) {
                    return false;
                }
                return true;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        int scrollX = getScrollX();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE: {
                int deltaX = x - mLastX;
                //如果SwipeLayout是在譬如ScrollView、ListView这种可以上下滑动的View中
                //那么当用户的手指滑出SwipeLayout的边界,那么将会触发器ACTION_CANCEL事件
                //如果此情形发生,那么SwipeLayout将会处于停止状态,无法复原。
                //当布局内竖直滑动距离小于水平滑动距离时 自己处理事件
                if (Math.abs(firstClickX - x) > Math.abs(firstClickY - y)) {
                    //增加下面这句代码,就是告诉父控件,不要cancel我的事件,我的事件我继续处理。
                    getParent().requestDisallowInterceptTouchEvent(true);
                    int newScrollX = scrollX - deltaX;
                    if (deltaX != 0) {
                        if (newScrollX < 0) {
                            //最小的滑动距离为0
                            newScrollX = 0;
                        } else if (newScrollX > mHolderWidth) {
                            //最大的滑动距离就是mRightView的宽度
                            newScrollX = mHolderWidth;
                        }
                        this.scrollTo(newScrollX, 0);
                    }
                }else {//当布局内竖直滑动距离大于水平滑动距离时 交给父控件处理事件
                    this.scrollTo(0, 0);//触发上下滚动的时候 让swiperlayout恢复到初态
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            }
            case MotionEvent.ACTION_CANCEL:
                getParent().requestDisallowInterceptTouchEvent(false);
                break;
            case MotionEvent.ACTION_UP: {
                int cuX = (int) event.getX();
                if (cuX == firstClickX) {
                    if (mContentView != null && null != onContentClickListener) {
                        onContentClickListener.contentClick(mContentView);
                    }
                } else {
                    int newScrollX = 0;
                    //如果已滑动的距离满足下面条件,则SwipeLayout直接滑动到最大距离,不然滑动到最小距离0
                    if (scrollX - mHolderWidth * 0.8 > 0) {
                        newScrollX = mHolderWidth;
                    }
                    this.smoothScrollTo(newScrollX, 0);
                    if (newScrollX == 0) {
                        mSlideState = OnSlideListener.SLIDE_STATUS_OFF;
                    } else
                        mSlideState = OnSlideListener.SLIDE_STATUS_ON;
                }

                getParent().requestDisallowInterceptTouchEvent(false);
            }
            break;
            default:
                break;
        }
        mLastX = x;
        mLastY = y;
        return true;
    }

    //获取当前SwipeLayout的滑动状态
    public int getSideState() {
        return mSlideState;
    }

    public void setmSlideState(int state) {
        mSlideState = state;
    }

    private void smoothScrollTo(int destX, int destY) {
        // 缓慢滚动到指定位置
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 3);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();
        }
    }
}

bug解决过程:
bug 一解决思路:最开始的时候,因为子阶段自定义布局内还包裹了一层布局,所以我把点击事件放到了最里面的那层布局上去处理,导致焦点问题不能响应点击事件,后来我在自定义布局中设置一个接口,然后在onTouchEvent()方法中去拦截屏幕的响应事件,在MotionEvent.ACTION_UP(手指抬起时触发)动作中去判断此次事件是否是点击事件,是的话再去调用接口中的方法,具体代码如下:
设置内容view部分的点击监听:

    public interface OnContentClickListener {
        void contentClick(View view);
    }

    private OnContentClickListener onContentClickListener;
case MotionEvent.ACTION_UP: 
                //手指抬起时x坐标
                int cuX = (int) event.getX();
                if (cuX == firstClickX) {//通过判断手指抬起时和按下时x的坐标是否相等来判定此次事件为滑动还是点击
                    //事件为点击的时候  调用监听接口
                    if (mContentView != null && null != onContentClickListener) {
                        onContentClickListener.contentClick(mContentView);
                    }
                } else {///事件为滑动时 按滑动处理
                    int newScrollX = 0;
                    //如果已滑动的距离满足下面条件,则SwipeLayout直接滑动到最大距离,不然滑动到最小距离0
                    if (scrollX - mHolderWidth * 0.8 > 0) {
                        newScrollX = mHolderWidth;
                    }
                    this.smoothScrollTo(newScrollX, 0);
                    if (newScrollX == 0) {
                        mSlideState = OnSlideListener.SLIDE_STATUS_OFF;
                    } else
                        mSlideState = OnSlideListener.SLIDE_STATUS_ON;
                }
break;

以上就是第一个bug的解决过程;
bug 二解决思路:滚动时获取焦点在子布局中X和Y方向上滑动的距离,将二者的值做一个比较,当Y方向上的距离大于X方向上时,那么就将事件交给父布局(也是就scrollview)去处理滚动事件,反之就由子布局去处理它自己的侧滑事件。具体代码如下:

case MotionEvent.ACTION_MOVE: 
                int deltaX = x - mLastX;
                //如果SwipeLayout是在譬如ScrollView、ListView这种可以上下滑动的View中
                //那么当用户的手指滑出SwipeLayout的边界,那么将会触发器ACTION_CANCEL事件
                //如果此情形发生,那么SwipeLayout将会处于停止状态,无法复原。
                //当布局内竖直滑动距离小于水平滑动距离时 自己处理事件
                if (Math.abs(firstClickX - x) > Math.abs(firstClickY - y)) {
                    //增加下面这句代码,就是告诉父控件,不要cancel我的事件,我的事件我继续处理。
                    getParent().requestDisallowInterceptTouchEvent(true);
                    int newScrollX = scrollX - deltaX;
                    if (deltaX != 0) {
                        if (newScrollX < 0) {
                            //最小的滑动距离为0
                            newScrollX = 0;
                        } else if (newScrollX > mHolderWidth) {
                            //最大的滑动距离就是mRightView的宽度
                            newScrollX = mHolderWidth;
                        }
                        this.scrollTo(newScrollX, 0);
                    }
                }else {//当布局内竖直滑动距离大于水平滑动距离时 交给父控件处理事件
                    this.scrollTo(0, 0);//触发上下滚动的时候 让swiperlayout恢复到初态
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
break;        

以上就是解决第二个bug的过程。

总结:写这篇文章来记录一下scrollview与其嵌套的布局在事件冲突问题上的解决过程,希望看到的大佬能指出其中一些理解不对的地方,感谢。

相关文章

网友评论

      本文标题:记录在scroll view中嵌套子布局出现的一些问题

      本文链接:https://www.haomeiwen.com/subject/rjhczctx.html