Android ViewPager使用预加载机制导致出现页面穿透问题
缘由
在应用中使用ViewPager,并且设置预加载页面。结果出现了一些异常的现象。
我们有4个页面,分别是4个Fragment,暂且称为FragmentA、FragmentB、FragmentC、FragmentD,ViewPager在MainActivity中,切换时,FragmentB会将FragmentA覆盖,这个时候,在FragmentB中点击某一处,如果该处的FragmentA也有控件,会出现异常的情况,事件会响应到FragmentA中的控件中去,也就是事件穿透了。
原因分析:
这个问题在使用 ViewPager 和 PageTransformer 时确实比较常见。根本原因在于 ViewPager 的工作机制以及 PageTransformer 如何影响页面视图。
- ViewPager 预加载机制:
ViewPager为了实现平滑的滑动效果,默认会预加载当前页面左右两侧的页面(数量由setOffscreenPageLimit(int limit)控制,默认为1)。这意味着即使某个页面在视觉上已经部分或完全移出屏幕,它的视图 (View) 仍然存在于ViewPager的视图层级 (View Hierarchy) 中,并且可能仍然是活动的。 - PageTransformer 的作用:
PageTransformer主要通过修改页面的绘制属性(如translationX,translationY,scaleX,scaleY,alpha,rotation等)来实现切换动画。它改变的是页面的 视觉 位置和外观,但通常 不改变 页面视图在视图层级中的存在状态或其接收触摸事件的能力。 - 事件分发机制: 当你在屏幕上点击时,Android 的触摸事件会从父视图(这里是
ViewPager)分发到子视图。即使某个页面(比如前一个页面)通过PageTransformer被平移到了当前页面的后面或者屏幕外,如果它的视图边界(在进行变换之前的原始边界,或者变换后的边界仍然与点击位置有重叠,并且它没有被设置为不可交互)仍然能够响应触摸事件,那么点击事件就可能被它拦截并处理。尤其是当PageTransformer只是平移 (translation) 而没有处理视图的 Z 轴顺序 (elevation) 或可点击状态时,这个问题更容易发生。默认情况下,后添加的页面(索引更大的页面)会绘制在先添加的页面之上,但事件分发可能不会严格遵守这个视觉顺序,特别是对于经过变换的视图。
总结来说: 前一个页面虽然在视觉上被移开了(或者被当前页面遮挡了),但它的 View 实例仍然存在于内存和视图树中,并且默认情况下是可交互的。当你点击的位置恰好也落在了这个“隐藏”页面的可交互控件的原始或变换后的区域内时,事件可能就会被它错误地接收。
解决方案:
最有效的解决办法是在你的 PageTransformer 的 transformPage(View page, float position) 方法中,根据页面的 position 来动态地管理页面的可交互状态。
position的含义:0: 当前完全可见的页面。(-1, 0): 左侧相邻的页面,正在移入或移出屏幕。(0, 1): 右侧相邻的页面,正在移入或移出屏幕。<= -1: 完全移出屏幕左侧的页面。>= 1: 完全移出屏幕右侧的页面。
你需要确保只有当前页面(或者你希望可以交互的过渡页面)是可点击的,而那些视觉上处于非活动状态(尤其是被当前页面遮挡的页面)应该被禁用交互。
具体实现方法:
在你的 PageTransformer 的 transformPage 方法中添加逻辑:
方法一:直接设置 clickable 或 enabled (简单但不一定完全解决子视图问题)
// Java
import androidx.viewpager.widget.ViewPager;
import android.view.View;public class MyPageTransformer implements ViewPager.PageTransformer {@Overridepublic void transformPage(View page, float position) {// ... 你原有的变换逻辑 (translation, scale, alpha etc.)// 关键:根据 position 决定页面是否可以交互// 通常我们只希望当前页面 (-1 < position < 1 范围内的页面,特别是 position 接近 0 的) 可以交互// 对于完全移出屏幕或在后面的页面 (position <= -1 或 position >= 1,尤其是 position < 0 时) 禁用交互if (position < -1f || position > 1f) {// 完全不可见page.setAlpha(0f); // 视觉上隐藏 (可选)page.setEnabled(false);page.setClickable(false);} else {// 在屏幕内或正在过渡page.setEnabled(true);page.setClickable(true);// 这里可以根据 position 的具体值调整 alpha 或其他视觉效果if (position == 0) {// 当前页面,确保完全可见和可交互page.setAlpha(1f);} else {// 过渡页面,可以根据需要设置透明度等// 例如,让远离中心的页面更透明float scaleFactor = Math.max(0.85f, 1 - Math.abs(position));page.setScaleX(scaleFactor);page.setScaleY(scaleFactor);page.setAlpha(1 - Math.abs(position)); // 渐变效果}}// 可能需要递归禁用子视图,如果 setEnabled(false) 对父视图不够的话// setChildrenClickable(page, position > -1f && position < 1f);}// 辅助方法,递归设置子视图的可点击性 (如果需要)private void setChildrenClickable(View view, boolean clickable) {if (view instanceof ViewGroup) {ViewGroup viewGroup = (ViewGroup) view;for (int i = 0; i < viewGroup.getChildCount(); i++) {setChildrenClickable(viewGroup.getChildAt(i), clickable);}}view.setClickable(clickable);}
}
// Kotlin
import androidx.viewpager.widget.ViewPager
import android.view.View
import android.view.ViewGroupclass MyPageTransformer : ViewPager.PageTransformer {override fun transformPage(page: View, position: Float) {// ... 你原有的变换逻辑 (translation, scale, alpha etc.)// 关键:根据 position 决定页面是否可以交互val isVisible = position >= -1f && position <= 1fpage.isEnabled = isVisiblepage.isClickable = isVisible // 设置根视图的可点击性if (!isVisible) {page.alpha = 0f // 完全不可见时隐藏 (可选)} else {// 在屏幕内或正在过渡// 可以根据 position 调整 alpha 等视觉效果if (position == 0f) {// 当前页面page.alpha = 1f} else {// 过渡页面val scaleFactor = maxOf(0.85f, 1 - kotlin.math.abs(position))page.scaleX = scaleFactorpage.scaleY = scaleFactorpage.alpha = 1 - kotlin.math.abs(position) // 渐变效果}}// 如果 page.isEnabled = false 不足以阻止子视图响应点击,// 你可能需要递归地禁用子视图。// setChildrenClickable(page, isVisible)}// 辅助方法,递归设置子视图的可点击性 (如果需要)private fun setChildrenClickable(view: View, clickable: Boolean) {view.isClickable = clickableif (view is ViewGroup) {for (i in 0 until view.childCount) {setChildrenClickable(view.getChildAt(i), clickable)}}}
}
方法二:使用 Elevation (API 21+)
如果你的应用最低支持 API 21 (Lollipop),你可以利用 elevation 属性。通常,Z 轴更高的视图会接收触摸事件。确保当前页面的 elevation 最高。
// Java (API 21+)
import androidx.viewpager.widget.ViewPager;
import android.view.View;
import android.os.Build;public class MyPageTransformer implements ViewPager.PageTransformer {private static final float MAX_ELEVATION = 8f; // dp, or just a relative value@Overridepublic void transformPage(View page, float position) {// ... 你原有的变换逻辑 ...if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {// 让当前页面及其邻近页面的 Elevation 根据 position 变化// 当前页面 (position=0) Elevation 最高// 越远的页面 Elevation 越低float absPos = Math.abs(position);float elevation = (1 - absPos) * MAX_ELEVATION;page.setElevation(elevation);}// 你仍然可能需要结合方法一中的 setClickable/setEnabled// 特别是对于完全移出屏幕的页面 (absPos >= 1)if (position < -1f || position > 1f) {page.setAlpha(0f);page.setEnabled(false);page.setClickable(false);} else {page.setEnabled(true);page.setClickable(true);// 根据需要设置 alpha 等page.setAlpha(1 - Math.abs(position)); // 示例}}
}
// Kotlin (API 21+)
import androidx.viewpager.widget.ViewPager
import android.view.View
import android.os.Buildclass MyPageTransformer : ViewPager.PageTransformer {companion object {private const val MAX_ELEVATION = 8f // dp, or just a relative value}override fun transformPage(page: View, position: Float) {// ... 你原有的变换逻辑 ...if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {val absPos = kotlin.math.abs(position)// 越靠近中间 (position=0), elevation 越高page.elevation = (1 - absPos) * MAX_ELEVATION}// 结合管理 clickable/enabled 状态val isVisible = position >= -1f && position <= 1fpage.isEnabled = isVisiblepage.isClickable = isVisibleif (!isVisible) {page.alpha = 0f} else {page.alpha = 1 - kotlin.math.abs(position) // 示例}}
}
总结建议:
- 首选方法一,在
transformPage中根据position明确设置page.setEnabled(false)和/或page.setClickable(false)对于那些不应响应交互的页面(特别是position < 0且视觉上在后面的页面)。如果页面根布局设置clickable=false不足以阻止其子控件响应事件,你可能需要递归地禁用子控件的可点击状态,或者直接将整个页面的setEnabled(false)。 - 如果你的
minSdkVersion>= 21,可以结合方法二使用elevation来辅助控制 Z 轴顺序,这有助于系统更正确地进行事件分发。 - 测试:仔细测试你的
PageTransformer在各种滑动状态下的表现,确保只有期望的页面可以响应点击。
通过在 PageTransformer 中主动管理页面的交互状态,能够解决这个点击穿透的问题。
相关文章:
Android ViewPager使用预加载机制导致出现页面穿透问题
缘由 在应用中使用ViewPager,并且设置预加载页面。结果出现了一些异常的现象。 我们有4个页面,分别是4个Fragment,暂且称为FragmentA、FragmentB、FragmentC、FragmentD,ViewPager在MainActivity中,切换时&#x…...
【今日三题】添加字符(暴力枚举) / 数组变换(位运算) / 装箱问题(01背包)
⭐️个人主页:小羊 ⭐️所属专栏:每日两三题 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 添加字符(暴力枚举)数组变换(位运算)装箱问题(01背包) 添加字符(暴力枚举) 添加字符 当在A的开头或结尾添加字符直到和B长度…...
【AIoT】智能硬件GPIO通信详解(二)
前言 上一篇我们深入解析了智能硬件GPIO通信原理(传送门:【AIoT】智能硬件GPIO通信详解(一))。接下来,我们将结合无人售货机控制场景,通过具体案例进一步剖析物联网底层通信机制的实际应用。 在智能零售领域,无人售货机通过AI技术升级为智能柜,其设备控制的底层通信…...
Python(18)Python中JSON的妙用:详解序列化与反序列化原理及实战案例
目录 一、背景:为什么Python需要JSON?二、核心技术解析:序列化与反序列化2.1 核心概念2.2 类型映射对照表 三、Python操作JSON的四大核心方法3.1 基础方法库3.2 方法详解1. json.dumps()2. json.loads()3. json.dump()4. json.load() 四、实战…...
【Python进阶】字典:高效键值存储的十大核心应用
目录 前言:技术背景与价值当前技术痛点解决方案概述目标读者说明 一、技术原理剖析核心概念图解核心作用讲解关键技术模块技术选型对比 二、实战演示环境配置要求核心代码实现(10个案例)案例1:基础操作案例2:字典推导式…...
MATLAB脚本实现了一个三自由度的通用航空运载器(CAV-H)的轨迹仿真,主要用于模拟升力体在不同飞行阶段(初始滑翔段、滑翔段、下压段)的运动轨迹
%升力体:通用航空运载器CAV-H %读取数据1 升力系数 alpha = [10 15 20]; Ma = [3.5 5 8 10 15 20 23]; alpha1 = 10:0.1:20; Ma1 = 3.5:0.1:23; [Ma1, alpha1] = meshgrid(Ma1, alpha1); CL = readmatrix(simulation.xlsx, Sheet, Sheet1, Range, B2:H4); CL1 = interp2(…...
多角度分析Vue3 nextTick() 函数
nextTick() 是 Vue 3 中的一个核心函数,它的作用是延迟执行某些操作,直到下一次 DOM 更新循环结束之后再执行。这个函数常用于在 Vue 更新 DOM 后立即获取更新后的 DOM 状态,或者在组件渲染完成后执行某些操作。 官方的解释是,当…...
Linux——消息队列
目录 一、消息队列的定义 二、相关函数 2.1 msgget 函数 2.2 msgsnd 函数 2.3 msgrcv 函数 2.4 msgctl 函数 三、消息队列的操作 3.1 创建消息队列 3.2 获取消息队列并发送消息 3.3 从消息队列接收消息recv 四、 删除消息队列 4.1 ipcrm 4.2 msgctl函数 一、消息…...
领慧立芯LHE7909可兼容替代TI的ADS1299
LHE7909是一款由领慧立芯(Legendsemi)推出的24位高精度Δ-Σ模数转换器(ADC),主要面向医疗电子和生物电势测量应用,如脑电图(EEG)、心电图(ECG)等设备。以下是…...
在PyTorch中,使用不同模型的参数进行模型预热
在PyTorch中,使用不同模型的参数进行模型预热(Warmstarting)是一种常见的迁移学习和加速训练的策略。以下是结合多个参考资料总结的实现方法和注意事项: 1. 核心机制:load_state_dict()与strict参数 • 部分参数加载&…...
conda 创建、激活、退出、删除环境命令
参考博客:Anaconda创建环境、删除环境、激活环境、退出环境 使用起来觉得有些不方便可以改进,故写此文。 1. 创建环境 使用 -y 跳过确认 conda create -n 你的环境名 -y 也可以直接选择特定版本 python 安装,以 3.10 为例: co…...
Redis核心数据类型在实际项目中的典型应用场景解析
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 Redis作为高性能的键值存储系统,在现代软件开发中扮演着重要角色。其多样化的数据结构为开发者提供了灵活的解决方案,本文将通过真实项…...
MongoDB简单用法
图片中 MongoDB Compass 中显示了默认的三个数据库: adminconfiglocal 如果在 .env 文件中配置的是: MONGODB_URImongodb://admin:passwordlocalhost:27017/ MONGODB_NAMERAGSAAS💡 一、为什么 Compass 里没有 RAGSAAS 数据库?…...
如何学习嵌入式
写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难,但我还是想去做! 本文写于:2025.04.16 请各位前辈能否给我提点建议,或者学习路线指导一下 STM32单片机学习总…...
【AI】IDEA 集成 AI 工具的背景与意义
一、IDEA 集成 AI 工具的背景与意义 随着人工智能技术的迅猛发展,尤其是大语言模型的不断演进,软件开发行业也迎来了智能化变革的浪潮。对于开发者而言,日常工作中面临着诸多挑战,如代码编写的重复性劳动、复杂逻辑的实现、代码质…...
uniapp-商城-26-vuex 使用流程
为了能在所有的页面都实现状态管理,我们按照前面讲的页面进行状态获取,然后再进行页面设置和布局,那就是重复工作,vuex 就会解决这样的问题,如同类、高度提炼的接口来帮助我们实现这些重复工作的管理。避免一直在造一样的轮子。 https://vuex.vuejs.org/zh/#%E4%BB%80%E4…...
UDP概念特点+编程流程
UDP概念编程流程 目录 一、UDP基本概念 1.1 概念 1.2 特点 1.2.1 无连接性: 1.2.2 不可靠性 1.2.3 面向报文 二、UDP编程流程 2.1 客户端 cli.c 2.2 服务端ser.c 一、UDP基本概念 1.1 概念 UDP 即用户数据报协议(User Datagram Protocol &…...
celery rabbitmq 配置 broker和backend
在使用Celery和RabbitMQ作为消息代理和结果后端时,你需要正确配置Celery以便它们可以有效地通信。以下是如何配置Celery以使用RabbitMQ作为broker(消息代理)和backend(结果后端)的步骤: 安装必要的库 首先…...
vue+electron ipc+sql相关开发(三)
在 Electron 中使用 IPC(Inter-Process Communication)与 SQLite 数据库进行通信是一个常见的模式,特别是在需要将数据库操作从渲染进程(Vue.js)移到主进程(Electron)的情况下。这样可以更好地管理数据库连接和提高安全性。下一篇介绍结合axios写成通用接口形式,虽然没…...
[特殊字符] PostgreSQL MCP 开发指南
简介 🚀 PostgreSQL MCP 是一个基于 FastMCP 框架的 PostgreSQL 数据库交互服务。它提供了一套简单易用的工具函数,让你能够通过 API 方式与 PostgreSQL 数据库进行交互。 功能特点 ✨ 🔄 数据库连接管理与重试机制🔍 执行 SQL…...
GD32裸机程序-SFUD接口文件记录
SFUD gitee地址 SFUD spi初始化 /********************************************************************************* file : bsp_spi.c* author : shchl* brief : None* version : 1.0* attention : None* date : 25-…...
Flutter项目之设置页
目录: 1、实现效果图2、实现流程2.1、引入依赖2.2、封装弹窗工具类2.3、设置页2.4、路由中注册设置页面 1、实现效果图 2、实现流程 2.1、引入依赖 2.2、封装弹窗工具类 import package:fluttertoast/fluttertoast.dart;class CommontToast {static showToast(Str…...
【Pandas】pandas DataFrame tail
Pandas2.2 DataFrame Indexing, iteration 方法描述DataFrame.head([n])用于返回 DataFrame 的前几行DataFrame.at快速访问和修改 DataFrame 中单个值的方法DataFrame.iat快速访问和修改 DataFrame 中单个值的方法DataFrame.loc用于基于标签(行标签和列标签&#…...
通过GO后端项目实践理解DDD架构
最近在工作过程中重构的项目要求使用DDD架构,在网上查询资料发现教程五花八门,并且大部分内容都是长篇的概念讲解,晦涩难懂,笔者看了一些github上入门的使用DDD的GO项目,并结合自己开发中的经验,谈谈自己对…...
精益数据分析(2/126):解锁数据驱动的商业成功密码
精益数据分析(2/126):解锁数据驱动的商业成功密码 大家好!在如今这个数据爆炸的时代,数据就像一座蕴含无限宝藏的矿山,等待着我们去挖掘和利用。最近我在深入研读《精益数据分析》这本书,收获了…...
天线静电防护:NRESDTLC5V0D8B
一. 物联网天线的使用环境 1.1 联网天线广泛应用于智能家居领域,比如智能门锁、智能摄像头等设备中,通过天线实现设备与家庭网络的连接,用户可以远程控制和监控家居设备。以智能摄像头为例,它通过天线将拍摄的画面实时传输到用户…...
【Linux 并发与竞争】
【Linux 并发与竞争】 Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对共享资源的保护,也就是要处理对共享…...
实用类题目
1. 密码强度检测 题目描述:生活中,为保证账户安全,密码需要有一定强度。编写一个方法,接收一个字符串作为密码,判断其是否符合以下强度要求:长度至少为 8 位,包含至少一个大写字母、一个小写字…...
STM32F103C8T6-基于FreeRTOS系统实现步进电机控制
引言 上一篇文章讲述了如何使用蓝牙连接stm32进行数据收发控制步进电机,这篇在之前的基础上通过移植操作系统(FreeRTOS或者其他的也可以,原理操作都类似)实现步进电机控制。 上篇博客指路:STM32蓝牙连接Android实现云…...
macOS安装java
一、下载 官网Java Downloads | Oracle 安装载java8,下载对应的JDK Java Downloads | Oracle 二、双击安装 安装 完成 三、查看安装位置 打开终端窗口,执行命令: /usr/libexec/java_home -V /Library/Java/JavaVirtualMachines/jdk-1.8.jdk/Content…...
