当前位置: 首页 > news >正文

Android笔记(三十七):封装一个RecyclerView Item曝光工具——用于埋点上报

背景

项目中首页列表页需要统计每个item的曝光情况,给产品运营提供数据报表分析用户行为,于是封装了一个通用的列表Item曝光工具,方便曝光埋点上报

源码分析

  • 核心就是监听RecyclerView的滚动,在滚动状态为SCROLL_STATE_IDLE的时候开始计算哪些item是可见的
private fun calculateVisibleItemInternal() {if (!isRecording) {return}val lastRange = currVisibleRangeval currRange = findItemVisibleRange()val newVisibleItemPosList = createCurVisiblePosList(lastRange, currRange)visibleItemCheckTasks.forEach {it.updateVisibleRange(currRange)}if (newVisibleItemPosList.isNotEmpty()) {VisibleCheckTimerTask(newVisibleItemPosList, this, threshold).also {visibleItemCheckTasks.add(it)}.execute()}currVisibleRange = currRange}
  • 根据LayoutManager找出当前可见item的范围,剔除掉显示不到80%的item
private fun findItemVisibleRange(): IntRange {return when (val lm = currRecyclerView?.layoutManager) {is GridLayoutManager -> {val first = lm.findFirstVisibleItemPosition()val last = lm.findLastVisibleItemPosition()return fixCurRealVisibleRange(first, last)}is LinearLayoutManager -> {val first = lm.findFirstVisibleItemPosition()val last = lm.findLastVisibleItemPosition()return fixCurRealVisibleRange(first, last)}is StaggeredGridLayoutManager -> {val firstItems = IntArray(lm.spanCount)lm.findFirstVisibleItemPositions(firstItems)val lastItems = IntArray(lm.spanCount)lm.findLastVisibleItemPositions(lastItems)val first = when (RecyclerView.NO_POSITION) {firstItems[0] -> {firstItems[lm.spanCount - 1]}firstItems[lm.spanCount - 1] -> {firstItems[0]}else -> {min(firstItems[0], firstItems[lm.spanCount - 1])}}val last = when (RecyclerView.NO_POSITION) {lastItems[0] -> {lastItems[lm.spanCount - 1]}lastItems[lm.spanCount - 1] -> {lastItems[0]}else -> {max(lastItems[0], lastItems[lm.spanCount - 1])}}return fixCurRealVisibleRange(first, last)}else -> {IntRange.EMPTY}}}
  • 对可见的item进行分组,形成一个位置列表,上一次和这一次的分开组队
private fun createCurVisiblePosList(lastRange: IntRange, currRange: IntRange): List<Int> {val result = mutableListOf<Int>()currRange.forEach { pos ->if (pos !in lastRange) {result.add(pos)}}return result}
  • 将分组好的列表装进一个定时的task,在延迟一个阈值的时间后执行onVisibleCheck
class VisibleCheckTimerTask(input: List<Int>,private val callback: VisibleCheckCallback,private val delay: Long
) : Runnable {private val visibleList = mutableListOf<Int>()private var isExecuted = falseinit {visibleList.addAll(input)}fun updateVisibleRange(keyRange: IntRange) {val iterator = visibleList.iterator()while (iterator.hasNext()) {val entry = iterator.next()if (entry !in keyRange) {iterator.remove()}}}override fun run() {callback.onVisibleCheck( this, visibleList)}fun execute() {if (isExecuted) {return}mHandler.postDelayed(this, delay)isExecuted = true}fun cancel() {mHandler.removeCallbacks(this)}interface VisibleCheckCallback {fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>)}
}
  • 达到阈值后,再获取一遍可见item的范围,对于仍然可见的item回调onItemShow
override fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>) {val visibleRange = findItemVisibleRange()visibleList.forEach {if (it in visibleRange) {notifyItemShow(it)}}visibleItemCheckTasks.remove(task)}

完整源码

val mHandler = Handler(Looper.getMainLooper())class ListItemExposeUtil(private val threshold: Long = 100): RecyclerView.OnScrollListener(), VisibleCheckTimerTask.VisibleCheckCallback {private var currRecyclerView: RecyclerView? = nullprivate var isRecording = falseprivate var currVisibleRange: IntRange = IntRange.EMPTYprivate val visibleItemCheckTasks = mutableListOf<VisibleCheckTimerTask>()private val itemShowListeners = mutableListOf<OnItemShowListener>()fun attachTo(recyclerView: RecyclerView) {recyclerView.addOnScrollListener(this)currRecyclerView = recyclerView}fun start() {isRecording = truecurrRecyclerView?.post {calculateVisibleItemInternal()}}fun stop() {visibleItemCheckTasks.forEach {it.cancel()}visibleItemCheckTasks.clear()currVisibleRange = IntRange.EMPTYisRecording = false}fun detach() {if (isRecording) {stop()}itemShowListeners.clear()currRecyclerView?.removeOnScrollListener(this)currRecyclerView = null}override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {if (newState == RecyclerView.SCROLL_STATE_IDLE) {calculateVisibleItemInternal()}}private fun calculateVisibleItemInternal() {if (!isRecording) {return}val lastRange = currVisibleRangeval currRange = findItemVisibleRange()val newVisibleItemPosList = createCurVisiblePosList(lastRange, currRange)visibleItemCheckTasks.forEach {it.updateVisibleRange(currRange)}if (newVisibleItemPosList.isNotEmpty()) {VisibleCheckTimerTask(newVisibleItemPosList, this, threshold).also {visibleItemCheckTasks.add(it)}.execute()}currVisibleRange = currRange}private fun findItemVisibleRange(): IntRange {return when (val lm = currRecyclerView?.layoutManager) {is GridLayoutManager -> {val first = lm.findFirstVisibleItemPosition()val last = lm.findLastVisibleItemPosition()return fixCurRealVisibleRange(first, last)}is LinearLayoutManager -> {val first = lm.findFirstVisibleItemPosition()val last = lm.findLastVisibleItemPosition()return fixCurRealVisibleRange(first, last)}is StaggeredGridLayoutManager -> {val firstItems = IntArray(lm.spanCount)lm.findFirstVisibleItemPositions(firstItems)val lastItems = IntArray(lm.spanCount)lm.findLastVisibleItemPositions(lastItems)val first = when (RecyclerView.NO_POSITION) {firstItems[0] -> {firstItems[lm.spanCount - 1]}firstItems[lm.spanCount - 1] -> {firstItems[0]}else -> {min(firstItems[0], firstItems[lm.spanCount - 1])}}val last = when (RecyclerView.NO_POSITION) {lastItems[0] -> {lastItems[lm.spanCount - 1]}lastItems[lm.spanCount - 1] -> {lastItems[0]}else -> {max(lastItems[0], lastItems[lm.spanCount - 1])}}return fixCurRealVisibleRange(first, last)}else -> {IntRange.EMPTY}}}/*** 检查该item是否真实可见* view区域80%显示出来就算*/private fun checkItemCurrRealVisible(pos: Int): Boolean {val holder = currRecyclerView?.findViewHolderForAdapterPosition(pos)return if (holder == null) {false} else {val rect = Rect()holder.itemView.getGlobalVisibleRect(rect)if (holder.itemView.width > 0 && holder.itemView.height > 0) {return (rect.width() * rect.height() / (holder.itemView.width * holder.itemView.height).toFloat()) > 0.8f} else {return false}}}/*** 双指针寻找真实可见的item的范围(有一些item没显示完整剔除)*/private fun fixCurRealVisibleRange(first: Int, last: Int): IntRange {return if (first >= 0 && last >= 0) {var realFirst = firstwhile (!checkItemCurrRealVisible(realFirst) && realFirst <= last) {realFirst++}var realLast = lastwhile (!checkItemCurrRealVisible(realLast) && realLast >= realFirst) {realLast--}if (realFirst <= realLast) {realFirst..realLast} else {IntRange.EMPTY}} else {IntRange.EMPTY}}/*** 创建当前可见的item位置列表*/private fun createCurVisiblePosList(lastRange: IntRange, currRange: IntRange): List<Int> {val result = mutableListOf<Int>()currRange.forEach { pos ->if (pos !in lastRange) {result.add(pos)}}return result}/*** 达到阈值后,再获取一遍可见item的范围,对于仍然可见的item回调onItemShow*/override fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>) {val visibleRange = findItemVisibleRange()visibleList.forEach {if (it in visibleRange) {notifyItemShow(it)}}visibleItemCheckTasks.remove(task)}private fun notifyItemShow(pos: Int) {itemShowListeners.forEach {it.onItemShow(pos)}}fun addItemShowListener(listener: OnItemShowListener) {itemShowListeners.add(listener)}
}interface OnItemShowListener {fun onItemShow(pos: Int)
}class VisibleCheckTimerTask(input: List<Int>,private val callback: VisibleCheckCallback,private val delay: Long) : Runnable {private val visibleList = mutableListOf<Int>()private var isExecuted = falseinit {visibleList.addAll(input)}fun updateVisibleRange(keyRange: IntRange) {val iterator = visibleList.iterator()while (iterator.hasNext()) {val entry = iterator.next()if (entry !in keyRange) {iterator.remove()}}}override fun run() {callback.onVisibleCheck(this, visibleList)}fun execute() {if (isExecuted) {return}mHandler.postDelayed(this, delay)isExecuted = true}fun cancel() {mHandler.removeCallbacks(this)}interface VisibleCheckCallback {fun onVisibleCheck(task: VisibleCheckTimerTask, visibleList: List<Int>)}
}
  • 测试代码
    在这里插入图片描述
  • 运行结果
    在这里插入图片描述
    在这里插入图片描述

相关文章:

Android笔记(三十七):封装一个RecyclerView Item曝光工具——用于埋点上报

背景 项目中首页列表页需要统计每个item的曝光情况&#xff0c;给产品运营提供数据报表分析用户行为&#xff0c;于是封装了一个通用的列表Item曝光工具&#xff0c;方便曝光埋点上报 源码分析 核心就是监听RecyclerView的滚动&#xff0c;在滚动状态为SCROLL_STATE_IDLE的时…...

【Linux】内核模版加载modprobe | lsmod

modprobe modprobe 是一个用于加载和卸载 Linux 内核模块的命令。它不仅能够加载单个模块&#xff0c;还能处理模块之间的依赖关系&#xff0c;确保所有依赖的模块都被正确加载。以下是一些关于 modprobe 命令的基本用法和常见选项的详细介绍。 基本语法 modprobe [option…...

Android从Drawable资源Id直接生成Bitmap,Kotlin

Android从Drawable资源Id直接生成Bitmap,Kotlin val t1 System.currentTimeMillis()val bmp getBmpFromDrawId(this, R.mipmap.ic_launcher_round)Log.d("fly", "1 ${bmp?.byteCount} h${bmp?.height} w${bmp?.width} cost time${System.currentTimeMillis…...

蓝桥杯——数组

1、移动数组元素 package day3;import java.util.Arrays;public class Demo1 {public static void main(String[] args) {int[] arr {1,2,3,4,5,6};int k 2;int[] arr_new f(arr,k);for (int i : arr_new) {System.out.print(i",");}//或System.out.println();St…...

在Flutter中,禁止侧滑的方法

在Flutter中&#xff0c;如果你想禁用侧滑返回功能&#xff0c;你可以使用WillPopScope小部件&#xff0c;并在onWillPop回调中返回false来阻止用户通过侧滑返回到上一个页面。 class DisableSwipePop extends StatelessWidget {overrideWidget build(BuildContext context) {…...

黑盒测试案例设计方法的使用(1)

黑盒测试用例的设计是确保软件质量的关键步骤之一。 一、等价类划分法 定义&#xff1a;把所有可能的输入数据&#xff0c;即程序的输入域划分成若干部分&#xff08;子集&#xff09;&#xff0c;然后从每一个子集中选取少数具有代表性的数据作为测试用例。 步骤&#xff1a…...

第二十一章 TCP 客户端 服务器通信 - 客户端OPEN命令

文章目录 第二十一章 TCP 客户端 服务器通信 - 客户端OPEN命令客户端OPEN命令 第二十一章 TCP 客户端 服务器通信 - 客户端OPEN命令 客户端OPEN命令 客户端OPEN命令与服务器端OPEN命令只有一个方面的不同&#xff1a;第一个设备参数必须指定要连接的主机。要指定主机&#xf…...

pycharm报错:no module named cv2.cv2

1、问题概述&#xff1f; 在项目中报错no module named cv2.cv2&#xff0c;就会导致import cv2 as cv无法使用。 需要安装opencv-python,一个开源的计算机视觉库。 2、解决办法&#xff1f; 【第一步&#xff1a;如果当前环境中已经安装&#xff0c;先卸载】 有时候会出现…...

Android音视频直播低延迟探究之:WLAN低延迟模式

Android WLAN低延迟模式 Android WLAN低延迟模式是 Android 10 引入的一种功能&#xff0c;允许对延迟敏感的应用将 Wi-Fi 配置为低延迟模式&#xff0c;以减少网络延迟&#xff0c;启动条件如下&#xff1a; Wi-Fi 已启用且设备可以访问互联网。应用已创建并获得 Wi-Fi 锁&a…...

docker 部署freeswitch(非编译方式)

一&#xff1a;安装部署 1.拉取镜像 参考&#xff1a;https://hub.docker.com/r/safarov/freeswitch docker pull safarov/freeswitch 2.启动镜像 docker run --nethost --name freeswitch \-e SOUND_RATES8000:16000 \-e SOUND_TYPESmusic:en-us-callie \-v /home/xx/f…...

OpenHarmony的公共事件

OpenHarmony的公共事件 公共事件简介 CES&#xff08;Common Event Service&#xff0c;公共事件服务&#xff09;为应用程序提供订阅、发布、退订公共事件的能力。 公共事件分类 公共事件从系统角度可分为&#xff1a;系统公共事件和自定义公共事件。 系统公共事件&#…...

深度学习transformer

Transformer可是深度学习领域的一个大热门呢&#xff01;它是一个基于自注意力的序列到序列模型&#xff0c;最初由Vaswani等人在2017年提出&#xff0c;主要用于解决自然语言处理&#xff08;NLP&#xff09;领域的任务&#xff0c;比如机器翻译、文本生成这些。它厉害的地方在…...

低成本出租屋5G CPE解决方案:ZX7981PG/ZX7981PM WIFI6千兆高速网络

刚搬进新租的房子&#xff0c;没有网络&#xff0c;开个热点&#xff1f;续航不太行。随身WIFI&#xff1f;大多是百兆级网络。找人拉宽带&#xff1f;太麻烦&#xff0c;退租的时候也不能带着走。5G CPE倒是个不错的选择&#xff0c;插入SIM卡就能直接连接5G网络&#xff0c;千…...

【黑马点评debug日记】redis登录跳转不成功

登录后一直跳转登录界面&#xff1b; debug: 网络日志报401&#xff0c; 说明前端获取的token为空&#xff1b; 查看应用程序&#xff0c; 发现没有token存储信息 前端网页增加 sessionStorage.setItem("token", data); 记得刷新网页 成功存储token...

C#自定义特性-SQL

语法 原则 自定义特性必须继承自System.Attribute类&#xff1b; AttributeUsage属性来指定特性的使用范围和是否允许重复等&#xff1b; 在特性类中定义属性&#xff0c;这些属性将用于存储特性值。 示例 using System;// 定义一个自定义特性类 [Attribute…...

协方差矩阵及其计算方法

协方差矩阵&#xff08;Covariance Matrix&#xff09;是一个描述多维数据特征之间相互关系的矩阵&#xff0c;广泛应用于统计学和机器学习中。它用于表示各个特征之间的协方差&#xff0c;是分析多维数据分布和特征依赖性的重要工具。 什么是协方差矩阵&#xff1f; 协方差矩…...

【OH】openHarmony开发环境搭建(基于windows子系统WSL)

前言 本文主要介绍基于windows子系统WSL搭建openHarmony开发环境。 WSL与Vmware虚拟机的区别&#xff0c;可以查看WSL与虚拟机的区别 更详细的安装配置过程可参考微软官网&#xff1a; ​安装 WSL 前提 以下基于windows 111专业版进行配置&#xff0c;windows 10应该也是可以…...

Visual Studio Code 端口转发功能详解

Visual Studio Code 端口转发功能详解 引言 Visual Studio Code&#xff08;简称 VS Code&#xff09;是一个功能强大的源代码编辑器&#xff0c;它支持多种编程语言的语法高亮、智能代码补全、自定义快捷键、代码重构等特性。除了这些基本功能外&#xff0c;VS Code 还提供了…...

Android Framework AMS(14)ContentProvider分析-1(CP组件应用及开机启动注册流程解读)

该系列文章总纲链接&#xff1a;专题总纲目录 Android Framework 总纲 本章关键点总结 & 说明&#xff1a; 说明&#xff1a;本章节主要解读ContentProvider组件的基本知识。关注思维导图中左上侧部分即可。 有了前面activity组件分析、service组件分析、广播组件分析的基…...

Three.js PBR材质

本文将详细介绍Three.js中的PBR&#xff08;Physically Based Rendering&#xff09;材质&#xff0c;包括PBR的基本概念、适用场景、PBR材质的构建以及一些高级应用技巧。 1. PBR&#xff08;Physically Based Rendering&#xff09;基本概念 PBR&#xff0c;即Physically B…...

k8s业务程序联调工具-KtConnect

概述 原理 工具作用是建立了一个从本地到集群的单向VPN&#xff0c;根据VPN原理&#xff0c;打通两个内网必然需要借助一个公共中继节点&#xff0c;ktconnect工具巧妙的利用k8s原生的portforward能力&#xff0c;简化了建立连接的过程&#xff0c;apiserver间接起到了中继节…...

IoT/HCIP实验-3/LiteOS操作系统内核实验(任务、内存、信号量、CMSIS..)

文章目录 概述HelloWorld 工程C/C配置编译器主配置Makefile脚本烧录器主配置运行结果程序调用栈 任务管理实验实验结果osal 系统适配层osal_task_create 其他实验实验源码内存管理实验互斥锁实验信号量实验 CMISIS接口实验还是得JlINKCMSIS 简介LiteOS->CMSIS任务间消息交互…...

什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南

文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/55aefaea8a9f477e86d065227851fe3d.pn…...

python执行测试用例,allure报乱码且未成功生成报告

allure执行测试用例时显示乱码&#xff1a;‘allure’ &#xfffd;&#xfffd;&#xfffd;&#xfffd;&#xfffd;ڲ&#xfffd;&#xfffd;&#xfffd;&#xfffd;ⲿ&#xfffd;&#xfffd;&#xfffd;Ҳ&#xfffd;&#xfffd;&#xfffd;ǿ&#xfffd;&am…...

初学 pytest 记录

安装 pip install pytest用例可以是函数也可以是类中的方法 def test_func():print()class TestAdd: # def __init__(self): 在 pytest 中不可以使用__init__方法 # self.cc 12345 pytest.mark.api def test_str(self):res add(1, 2)assert res 12def test_int(self):r…...

Go 语言并发编程基础:无缓冲与有缓冲通道

在上一章节中&#xff0c;我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道&#xff0c;它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好&#xff0…...

CSS | transition 和 transform的用处和区别

省流总结&#xff1a; transform用于变换/变形&#xff0c;transition是动画控制器 transform 用来对元素进行变形&#xff0c;常见的操作如下&#xff0c;它是立即生效的样式变形属性。 旋转 rotate(角度deg)、平移 translateX(像素px)、缩放 scale(倍数)、倾斜 skewX(角度…...

三分算法与DeepSeek辅助证明是单峰函数

前置 单峰函数有唯一的最大值&#xff0c;最大值左侧的数值严格单调递增&#xff0c;最大值右侧的数值严格单调递减。 单谷函数有唯一的最小值&#xff0c;最小值左侧的数值严格单调递减&#xff0c;最小值右侧的数值严格单调递增。 三分的本质 三分和二分一样都是通过不断缩…...

Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)

引言 工欲善其事&#xff0c;必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后&#xff0c;我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集&#xff0c;就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...

ui框架-文件列表展示

ui框架-文件列表展示 介绍 UI框架的文件列表展示组件&#xff0c;可以展示文件夹&#xff0c;支持列表展示和图标展示模式。组件提供了丰富的功能和可配置选项&#xff0c;适用于文件管理、文件上传等场景。 功能特性 支持列表模式和网格模式的切换展示支持文件和文件夹的层…...