写个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在视图层的顶层,优先级更高。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/362383.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SourceTree 不显示新添加文件

最近遇到了在项目中新添加了文件&#xff0c;但是在提交的时候SourceTree 中“未暂存区域”却不显示文件。如果你也有类似的问题&#xff0c;不防来看看吧。 我可能不知道什么时候动了下面的配置&#xff1a; 配置选择为“待定”&#xff0c;新增的未提交文件就显示出来了&…

Pycharm连接云算力远程服务器(AutoDL)训练深度学习模型全过程

前言&#xff1a;在上一篇windows搭建深度学习环境中&#xff0c;我试图使用笔记本联想小新air14的mx350显卡训练一个图像检测的深度学习模型&#xff0c;但是训练时长大概需要几天时间远超我的预期&#xff0c;所以我便选择租用GPU进行训练&#xff0c;在对多家平台对比后找到…

在Arduino给自己的SSD1306 OLED显示定制Logo或者图片

我在使用Arduino上的SSD1306显示屏时&#xff0c;基本都用使用Adafruit的SSD1306库&#xff0c;但是Adafruit的开机logo实在没特色&#xff08;如下图&#xff09;&#xff0c;如果在开机时&#xff0c;让自己的项目上显示自己的定制logo&#xff0c;甚至是照片&#xff08;如果…

【蓝桥杯日记】复盘篇三——循环结构

前言 本篇内容是对循环结构进行复盘的&#xff0c;循环可谓是在基础阶段特别重要的东西&#xff0c;是三大结构&#xff08;顺序结构、选择结构、循环结构&#xff09;中最重要的结构之一。 目录 &#x1f351;1.找最小值 分析&#xff1a; 知识点&#xff1a; 代码如下 &…

Multi ElasticSearch Head插件基本操作

Multi ElasticSearch Head插件安装好之后我们可以进行一些基本的操作。 1、复合查询 因为ES提供了一些Restful风格的接口&#xff0c;可以让任何语言去调用&#xff0c;因此我们可以将之前的请求地址粘贴到Multi ElasticSearch Head插件里面&#xff0c;选择GET请求方式&#x…

【机器学习】监督学习算法之:线性回归

线性回归 1、引言2、线性回归2.1 定义2.2 基本原理2.3 公式2.4 实现2.5 代码示例 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c;最近机器学习的文章写的不少啊。 小鱼&#xff1a;你还挺细心的哦。 小屌丝&#xff1a;那必须的&#xff0c;我要学习&#xff0c;我要成长…

【靶场实战】Pikachu靶场XSS跨站脚本关卡详解

Nx01 系统介绍 Pikachu是一个带有漏洞的Web应用系统&#xff0c;在这里包含了常见的web安全漏洞。 如果你是一个Web渗透测试学习人员且正发愁没有合适的靶场进行练习&#xff0c;那么Pikachu可能正合你意。 Nx02 XSS跨站脚本概述 Cross-Site Scripting 简称为“CSS”&#xff…

【大厂AI课学习笔记】1.3 人工智能产业发展(2)

&#xff08;注&#xff1a;腾讯AI课学习笔记。&#xff09; 1.3.1 需求侧 转型需求&#xff1a;人口红利转化为创新红利。 场景丰富&#xff1a;超大规模且多样的应用场景。主要是我们的场景大&#xff0c;数据资源丰富。 抗疫加速&#xff1a;疫情常态化&#xff0c;催生新…

微信小程序(二十七)列表渲染改变量名

注释很详细&#xff0c;直接上代码 上一篇 新增内容&#xff1a; 1.改变默认循环单元item变量名 2.改变默认循环下标index变量名 基础模板有问题可以先看上一篇 源码&#xff1a; index.wxml <view class"students"><view class"item"><te…

MFC串行化的应用实例

之前写过一篇MFC串行化的博文;下面看一个具体例子; 新建一个单文档应用程序;在最后一步,把View类的基类改为CFormView; 然后在资源面板编辑自己的字段; 然后到doc类的头文件添加对应变量, public:CString name;int age;CString sex;CString dept;CString zhiwu;CStrin…

Python+Selenium+Unittest 之selenium15--等待时间

在正常的自动化过程中&#xff0c;如果整篇代码中没有加等待时间的话&#xff0c;有时候可能页面跳转或者还没开始点击就执行到下一个流程了&#xff0c;这时候因为页面没有加载完毕&#xff0c;所以有可能会导致找不到对应的元素而报错&#xff0c;因此我们需要在整个代码流程…

C++语法学习

一、字符串 1.字符与整数的联系--ASCII表 0~9 :48~57 A~Z:65~90 a~z:97~122 字符与数字之间转换: 1.1字符转数字&#xff1a; 字符转数字&#xff1a; char c A;cout << c-A << endl; //输出0cout << (int)c << endl; //输出…

go并发编程-runtime、Channel与Goroutine

1. runtime包 1.1.1. runtime.Gosched() 让出CPU时间片&#xff0c;重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤&#xff0c;但是你妈让你去相亲,两种情况第一就是你相亲速度非常快&#xff0c;见面就黄不耽误你继续烧烤&#xff0c;第二种情况就是你相亲速度…

日志报错:Unexpected EOF read on the socket

记一次关于网关的问题及修复问题。 项目提测后&#xff0c;修改时web端页面出现502&#xff0c;查看后台服务日志发现&#xff1a; org.springframework.web.multipart.MultipartException: Failed to parse multipart servlet request; nested exception is java.io.IOExcept…

鸿蒙harmony--TypeScript基础语法

把青春献给身后那座辉煌的都市&#xff0c;为了这个美梦我们付出着代价 目录 一&#xff0c;基础类型 二&#xff0c;数组 三&#xff0c;any 四&#xff0c;变量的类型注释 五&#xff0c;函数 5.1 参数类型注解 5.2 返回类型注解 5.3 匿名函数 六&#xff0c;对象类型 可选属…

整数对最小和 - 华为OD统一考试

OD统一考试&#xff08;C卷&#xff09; 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定两个整数数组array1、array2&#xff0c;数组元素按升序排列。 假设从array1、array2中分别取出一个元素可构成一对元素&#xff0c;现在需要取出k对元素&am…

【Java的基本数据类型及其占用内存大小和默认值】

Java的基本数据类型及其占用内存大小和默认值 Java的基本数据类型包括以下几种&#xff1a; byte&#xff08;字节型&#xff09;&#xff1a;占用1个字节&#xff0c;取值范围是-128到127&#xff0c;默认值为0。short&#xff08;短整型&#xff09;&#xff1a;占用2个字节&…

【LeetCode每日一题】1109. 航班预订统计1094. 拼车 (差分数组)

差分数组 差分数组的主要适用场景是频繁对原始数组的某个区间的元素进行增减。 一、基本概念&#xff1a; 差分数组的定义如下&#xff1a; 假设原始数组为arr&#xff0c;差分数组为diff&#xff0c;其中diff[i] arr[i] - arr[i-1]&#xff08;0 < i < n&#xff0…

Ubuntu-22.04上ToDest设置开机不弹出图形界面

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、开始操作1.设置图形端 总结 前言 有时候远程成为开发必不可少的工具&#xff0c;目前国内有很多相关的软件&#xff0c;比较有名的是向日葵、ToDesk、Rust…

美团面试:Sentinel底层滑动时间窗限流算法怎么实现的?

尼恩说在前面 在40岁老架构师 尼恩的读者交流群(50)中&#xff0c;最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、希音、百度、网易、美团的面试资格&#xff0c;遇到很多很重要的面试题&#xff1a; 问题1&#xff1a;Sentinel高可用熔断降级&#xff0c;是如何…