写个Android事件分发实际用例(持续更新)

一,概述

感兴趣的读者,如果对Android事件分发还有不了解的地方,可以阅读笔者写的文章再谈android事件分发机制

本文的主要目的,是结合前文所分享事件分发相关原理,在实际案例中使用。

二,Recycler嵌套滑动问题

1,问题描述

假设一个布局中有两个RecyclerView,宽高与父布局一样,两者垂直并排,如下图所示。当滑动RecyclerView1时,要求RecyclerView1响应内部的滑动或其它事件(点击、长按等)。当RecyclerView1内部滑动至底部时,如果继续滑动,就拉起RecyclerView2并且执行滑动。自反同理,RecyclerView2滑动至顶部,就下拉RecycerView1。其RecyclerView1和RecyclerView2内的布局样式完全一致,效果看起来就仿佛只使用了一个RecyclerView一样。怎么做到?

效果图如下,

事件分发-RecyclerView嵌套滑动

2,效果实现

笔者以一种方式实现了此效果,如果读者有更简单的方式,笔者愿在评论区虚心学习。


/**
 * @author :tree.fqyy
 * @date :Created in 2024/1/31 12:39
 * <p>
 * 两个RecyclerView的拉起测试View
 */
public class ScrollRecyclerViewCase extends FrameLayout {


    private RecyclerView recyclerView1;
    private RecyclerView recyclerView2;
    private LinearLayoutManager layout1;
    private LinearLayoutManager layout2;

    private float pointDownPositionY = 0F;
    private boolean isDrag = false;

    public ScrollRecyclerViewCase(@NonNull Context context) {
        super(context);

        initTestCase();
    }

    public ScrollRecyclerViewCase(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initTestCase();
    }

    public ScrollRecyclerViewCase(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initTestCase();
    }

    public ScrollRecyclerViewCase(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        initTestCase();
    }


    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        final int action = ev.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            //所有的DOWN事件都不拦截,透传给子View,
            pointDownPositionY = ev.getY();
            return false;
        }

        if (action == MotionEvent.ACTION_MOVE) {
            //当滑动过程中,遇到MOVE时间,这时通过重写onTouchEvent方法,控制整体滑动
            //一旦返回true,子view将收到CANCEL事件,且后续所有的事件全部都会传给此View
            return true;
        }

        if (action == MotionEvent.ACTION_UP) {
            //UP是否传给子View,判断是否拖拽
            //如果返回false,子类可结合DOWN事件响应如点击、长按、双击等事件。
            return isDrag;
        }
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = event.getAction();
        if (action == MotionEvent.ACTION_DOWN) {
            pointDownPositionY = event.getY();
        }

        if (action == MotionEvent.ACTION_MOVE) {
            //check,父类滚动条件
            if (!isDrag) {
                isDrag = true;
            }
            final float pointMovePosition = event.getY();
            float offsetY = pointMovePosition - pointDownPositionY;
            //以下是滑动调度,结合滑动方向判断recyclerView是否可滑动
            pointDownPositionY = pointMovePosition;
            boolean isUpFlow = offsetY < 0;
            if (isUpFlow) {
                if (recyclerView1.canScrollVertically(1)) {
                    //如果第一个RecyclerView可滑动,直接调用其scrollBy方法,
                    recyclerView1.scrollBy(0, (int) -offsetY);
                } else if (recyclerView2.getTop() >= 0) {
                    //当不可滑动,且第二个RecyclerView未到顶部,那么执行整体偏移
                    dispatchOffsetTopAndBottom(offsetY);
                } else if (recyclerView2.canScrollVertically(1)) {
                    //同样的道理,recyclerView2滑动到了顶部,且可滑动,调用其scrollBy方法,
                    recyclerView2.scrollBy(0, (int) -offsetY);
                }
            } else {
                //以下是镜像,笔者不赘述
                if (recyclerView2.canScrollVertically(-1)) {
                    recyclerView2.scrollBy(0, (int) -offsetY);
                } else if (recyclerView1.getTop() <= 0) {
                    dispatchOffsetTopAndBottom(offsetY);
                } else {
                    recyclerView1.scrollBy(0, (int) -offsetY);
                }
            }
            return true;
        }

        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            pointDownPositionY = 0f;
            if (!isDrag) {
            }
            isDrag = false;
        }

        return true;
    }

    private void dispatchOffsetTopAndBottom(float offsetY) {
        //所有Child View的整体偏移
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View childAt = getChildAt(i);
            childAt.offsetTopAndBottom((int) offsetY);
        }
    }

    /**
     * 简单放入两个RecyclerView,进行测试初始化。
     */
    private void initTestCase() {
        final LayoutParams layoutParams = generateDefaultLayoutParams();
        recyclerView1 = new RecyclerView(getContext());
        recyclerView2 = new RecyclerView(getContext());
        addView(recyclerView2, layoutParams);
        addView(recyclerView1, layoutParams);
        final TestAdapter adapter = new TestAdapter();
        recyclerView1.setAdapter(adapter);
        final TestAdapter2 adapter2 = new TestAdapter2();
        recyclerView2.setAdapter(adapter2);
        layout1 = new LinearLayoutManager(getContext());
        recyclerView1.setLayoutManager(layout1);
        layout2 = new LinearLayoutManager(getContext());
        recyclerView2.setLayoutManager(layout2);
        recyclerView1.post(() -> recyclerView2.offsetTopAndBottom(recyclerView1.getHeight()));
    }


    public static class TestHolder1 extends RecyclerView.ViewHolder {

        public TestHolder1(@NonNull View itemView) {
            super(itemView);
        }
    }

    public static class TestAdapter extends RecyclerView.Adapter<MainActivity.TestHolder> {

        @NonNull
        @Override
        public MainActivity.TestHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            return new MainActivity.TestHolder(new TextView(parent.getContext()));
        }

        @Override
        public void onBindViewHolder(@NonNull MainActivity.TestHolder holder, @SuppressLint("RecyclerView") int position) {
            ((TextView) holder.itemView).setText("recycler1 Position " + position);
            holder.itemView.setOnClickListener(view -> Snackbar.make(view, "click position " + position, Snackbar.LENGTH_SHORT).show());
            holder.itemView.setOnLongClickListener(view -> {
                Snackbar.make(view, "long click position " + position, Snackbar.LENGTH_SHORT).show();
                return true;
            });
        }

        @Override
        public int getItemCount() {
            return 100;
        }
    }


    public static class TestAdapter2 extends RecyclerView.Adapter<MainActivity.TestHolder> {

        @NonNull
        @Override
        public MainActivity.TestHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
            return new MainActivity.TestHolder(new TextView(parent.getContext()));
        }

        @Override
        public void onBindViewHolder(@NonNull MainActivity.TestHolder holder, int position) {
            ((TextView) holder.itemView).setText("recycler2 Position " + position);
            holder.itemView.setOnClickListener(view -> Snackbar.make(view, "click recycler2 position " + position, Snackbar.LENGTH_SHORT).show());
            holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
                @Override
                public boolean onLongClick(View view) {
                    Snackbar.make(view, "long click recycler2 position " + position, Snackbar.LENGTH_SHORT).show();
                    return true;
                }
            });
        }

        @Override
        public int getItemCount() {
            return 100;
        }
    }
}

笔者来看下,为何MOVE时返回true后,所有的后续事件全部给了当前ViewGroup呢?

首先,调用onInterceptTouchEvent方法条件是当前事件是ACTION_DOWN,或已经有子View消耗了事件(mFIrstTouchTarget不为null)。ViewGroup#dispatchTouchEvent中,父View如果拦截,则intercepted为true,进一步导致cancelChild为true,如下图所示。

之后,会从mFIrstTouchTarget中遍历后续所有TouchTarget节点,并且将CANCEL事件下发。最后,mFirstTouchTarget最终变成null,下一次MOVE事件到来时,如下

child参数传入null,表示执行ViewGroup的super.dispatchTouchEvent,事件便进一步被分发到ViewGroup#onTouchEvent中,由于笔者写的ScrollRecyclerViewCase重写了onTouchEvent方法,因此实现了整体滚动效果。

三,两个重叠按钮的响应顺序。

1,问题描述

在一个ViewGroup中,有两个按钮重叠排布,效果如下。通过直觉与经验可知,肯定是Button2响应。那么,为什么呢?

答案仍在ViewGroup#dispatchTouchEvent中,由于下发事件给子View时,是从后向前遍历。因此后添加的View在视图层的顶层,优先级更高。

  • 14
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值