仿京东拼多多商品分类页-(RecyclerView悬浮头部实现、xml绘制ItemDecoration)

文章目录

  • 前言
  • 效果图
  • 思路
    • 方式一:通过xml布局来实现
    • 方式二:通过ItemDecoration方式来实现
  • 实现步骤
    • 1、数据项格式
    • 2、左侧列表适配器
    • 3、右侧列表适配器
    • 4、头部及悬浮头部绘制
      • 4.1头部偏移高度为要绘制xml布局的高度--getItemOffsets()
      • 4.2 绘制固定头部--onDraw()
      • 4.3 绘制悬浮头部-onDrawOver()
  • 总结
  • 参考文章

前言

做过的功能一定要总结,因为,过段时间你就忘记了哈哈哈
最近在做功能分类页功能,我看了下,这不和我之前做的美团购物车功能差不多么,然后就再看了遍之前写的文章,并看了下底下的评论,不得不说,当时实现的方式确实复杂,搞得我都有点懵,所以就打算优化下当时实现的方式。

效果图

先上张最后实现的效果图吧
在这里插入图片描述

思路

有两种方式

方式一:通过xml布局来实现

  • 右侧每个标题加下面的分组列表为一个ItemView,直接在该ItemView的xml布局内绘制好即可
  • 悬浮头部是直接在右侧整个RecyclerView上方和他重合,绘制一个固定的头部布局即可

看下图即可明白该如何来实现,主要的内容是在xml来直接设置好头部及悬浮头部位置布局

在这里插入图片描述

具体的实现可以参考Android 仿京东、拼多多商品分类页这篇文章

优点:

  • 悬浮头部和itemView的头部均可点击
  • 实现便捷

缺点:

  • 每个ItemView的头部样式都一样,不可以来动态的更改

方式二:通过ItemDecoration方式来实现

固定头部通过onDraw()方法来绘制,悬浮头部通过onDrawOver()方法绘制。

这种方法在Android购物车效果实现(RecyclerView悬浮头部实现)中使用过,原理差不多,但是

当时写的比较复杂,主要麻烦在两点:

1、数据项格式太复杂,之前实现的方式是将数据进行整合后,将右侧所有的子项形成一个集合,然后用一个RecyclerView来展示,这样导致左右联动,右侧滑动找左侧父id时,很麻烦。

2、绘制悬浮头部和各组的标题头时,是在onDraw()onDrawOver()中来绘制的,对于简单的TextView还可以,但是对于一些复杂的头部的话绘制就比较复杂,尤其是不太擅长的小白那就更别说了。

改善点

1、使用源数据的分组结构,左右两侧的数据均使用同一集合,右侧列表的ItemView由RecyclerView组成,这样实现了右侧的数据分组,而不再是将数据分开后再重新分组。这样做可以使左右联动更方便,左右联动只需各自将相同位置的ItemView项展示出来即可。

2、组标题和悬浮头部的绘制使用xml加载布局并在onDraw()onDrawOver()中绘制,可以实现复杂头部简单加载

难点:

  • 如何使用xml布局来连续绘制到Canvas里
  • onDrawOver()中如何绘制实现悬浮头部

优点

  • 可以动态给每个ItemView都设置不一样的头部布局
  • 切换头部布局和悬浮头部很方便,解耦,直接替换就好

缺点:

  • 悬浮头部和子项头部都不能点击

实现步骤

这里主要介绍下使用ItemDecoration的方式来绘制分组头部布局的实现方法。

1、数据项格式

这里数据使用Android 仿京东、拼多多商品分类页内提供的数据,格式如下

在这里插入图片描述

2、左侧列表适配器

增加点击事件,当点击position位置时,让右侧recyclerView的position项滑动到顶部即可

leftAdapter.setLeftClickListener(object : LeftAdapter.LeftClickListener {override fun onItemClick(position: Int) {var layoutManager = binding.rightRcy.layoutManager as LinearLayoutManager//将position该位置的itemView移动到第一项layoutManager.scrollToPositionWithOffset(position, 0)}
})

3、右侧列表适配器

增加滑动监听,实现两个功能:

  • 当滑动时,实时获取右侧第一个可见项所在的位置position,同时将左侧RecyclerView的position项选中
  • 当滑动到底部且无法下滑时,将左侧RecyclerView的最后一项选中
  • 后续可以增加:如果左侧选中项位置太低,将其滑动到上方来的操作
binding.rightRcy.addOnScrollListener(object :RecyclerView.OnScrollListener(){
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {super.onScrollStateChanged(recyclerView, newState)
}override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {super.onScrolled(recyclerView, dx, dy)//无法下滑,移动到最后时,将左侧列表的最后一项设置为选中if (!recyclerView.canScrollVertically(1)) {leftAdapter.setSelectedNum(dataList.size-1)}//右侧列表可以滑动else {val rightLayoutManager = binding.rightRcy.layoutManager as LinearLayoutManagerval position = rightLayoutManager.findFirstVisibleItemPosition()leftAdapter.setSelectedNum(position)}
}
})

4、头部及悬浮头部绘制

4.1头部偏移高度为要绘制xml布局的高度–getItemOffsets()

override fun getItemOffsets(outRect: Rect,view: View,parent: RecyclerView,state: RecyclerView.State,
) {super.getItemOffsets(outRect, view, parent, state)if (headTitleView == null) {headTitleView =LayoutInflater.from(parent.context).inflate(R.layout.head_itemview, null, false)val width = parent.layoutManager?.width?:0headTitleView?.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED))}headTitleView?.let {//距离ItemView的上方偏移topHeight高度outRect.top = it.measuredHeight}
}

这里我们首先需要加载headTitleView布局,然后获取该布局的高度,最后通过outRect.top来偏移该布局的高度,后面我们在onDraw()onDrawOver()方法里分别绘制头部和悬浮头部。

注意:

  • View的宽高属性在measure()方法调用之前都是默认值,不反映实际情况。

    measure()方法是用来测量View的大小的,它会根据父容器传递的限制条件(例如这里的width和height参数)来确定View的实际宽高。

    所以在获取View的宽高之前,需要先调用measure()方法,否则得到的只是默认值,不符合实际需要,调用它之后才能保证后续的宽高数据是准确的。

  • 这里使用layoutManager来获取recyclerview的宽度,因为在此处直接调用parent.width parent.measuredWidth方法获取到的宽度均为0

    • getItemOffsets在RecyclerView完成布局和测量前调用,这时measuredWidth还没准备好,所以获取到的宽度为0
    • layoutManager可以获取到RecyclerView的宽高限制条件spec,知道RecyclerView的宽高限制,所以只能通过layoutManager.width获取宽度,measuredWidth无效
  • 这里的头部布局如下,建议在最外层套一层

    <?xml version="1.0" encoding="utf-8"?>
    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="wrap_content"><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="50dp"android:background="@drawable/sp_headtitle"android:layout_gravity="center_horizontal"><ImageViewandroid:layout_width="129dp"android:layout_height="match_parent"android:layout_alignParentRight="true"android:src="@mipmap/ic_bg"android:scaleType="fitXY"/><TextViewandroid:id="@+id/tvTitle"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:text="头部标题"android:textColor="@color/black"android:textSize="18sp" /></RelativeLayout>
    </FrameLayout>
    

    如果按如下方式写,否则会出现下面的情况

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="50dp"android:layout_gravity="center_horizontal"android:background="@drawable/sp_headtitle"><ImageViewandroid:layout_width="129dp"android:layout_height="match_parent"android:layout_alignParentRight="true"android:scaleType="fitXY"android:src="@mipmap/ic_bg" /><TextViewandroid:id="@+id/tvTitle"android:layout_width="match_parent"android:layout_height="match_parent"android:gravity="center"android:text="头部标题"android:textColor="@color/black"android:textSize="18sp" />
    </RelativeLayout>

    在这里插入图片描述

  • 宽高设置

    val width = parent.layoutManager?.width?:0
    headTitleView?.measure(View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
    )
    

    宽度:
    因为我们头部布局的父容器为match_parent,且我们想绘制的宽度为占满右侧RecyclerView的宽度,所以这里View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)View.MeasureSpec.EXACTLY代表精确模式,将其设定为我们获取到的RecyclerView的宽度即可。

    其实使用View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.AT_MOST)也可以,代表子View宽度不确定,但是最大为我们测量的RecyclerView的width即可。

    高度:

    因为我们加载headTitleView后,需要通过measure()方法测量后才可用,所以此时我们并不知道它的具体高度,所以不能用EXACTLY或AT_MOST模式,所以使用UNSPECIFIED,代表父容器不对子View有限制,子View要多大给多大

4.2 绘制固定头部–onDraw()

/*** 绘制头部*/
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {super.onDraw(c, parent, state)val childCount = parent.childCountfor (i in 0 until childCount) {val child = parent.getChildAt(i)val bottom = child.topheadTitleView?.let {val top = bottom - it.measuredHeightval itemView = parent.getChildAt(i)val position = parent.getChildAdapterPosition(itemView)//获取该位置的标题名称val groupTitleName = titleDataList[position].toUpperCase()//设置标题内容it.findViewById<TextView>(R.id.tvTitle).text = groupTitleName// 保存 Canvas 的状态c.save()// 平移 Canvas,使 View 绘制在正确位置c.translate(0f, top.toFloat())it.layout(0, top, parent.measuredWidth, bottom)it.draw(c)c.restore()}}
}

具体的头部绘制的位置,可参考前两篇文章Android购物车效果实现(RecyclerView悬浮头部实现)

自定义ItemDecoration分割线的高度、颜色、偏移,看完这个你就懂了

这里主要讲下注意事项

  • 设置title的名字要在draw()方法之前,不然你都绘制了,还在那设置名字没有意义

  • 因为右侧每个ItemView都是一组数据,该ItemView布局由一个RecyclerView构成,所以需要给每个ItemView都绘制头部布局,getItemOffsets() 是针对每一个 ItemView,而 onDraw()方法却是针对 RecyclerView 本身,所以在onDraw()方法中需要遍历屏幕上可见的ItemView来循环绘制。

  • 这里在绘制前分别调用了translate()layout()方法:
    刚开始是直接调用 headTitleView.draw(canvas),但发现并没有绘制出来,这是因为我们没有将Canvas平移到指定位置,直接绘制的话,头部View会默认绘制在Canvas的(0,0)坐标点,而我们期望它绘制在ItemView的顶部适当位置。通过translate平移和layout重新布局,可以重用同一个头部View来绘制不同Item的头部,避免重复创建View。

  • 这里在绘制前和绘制后分别调用了c.save()c.restore()方法:

    保存Canvas状态可以防止绘制操作对Canvas产生影响,绘制完成后恢复状态可以保证不污染Canvas。

4.3 绘制悬浮头部-onDrawOver()

 override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {super.onDrawOver(c, parent, state)val itemView = parent.getChildAt(0)val position = parent.getChildAdapterPosition(itemView)var titleName = titleDataList[position].toUpperCase()val left = 0val right = parent.measuredWidth//默认的指定高度var height = headTitleView?.measuredHeight ?: 0//当前ItemView的底部var bottom = itemView.bottomif (bottom<height){height=bottom}headTitleView?.let {it.findViewById<TextView>(R.id.tvTitle).text = titleNamec.save()// 平移 Canvas,使 View 绘制在正确位置c.translate(0f, (height-it.measuredHeight).toFloat())it.layout(left, height-it.measuredHeight, right, height)it.draw(c)c.restore()}}

通过不断改变绘制的顶部和底部位置来实现被顶出的动画效果,这里不再详细阐述,具体可看Android购物车效果实现(RecyclerView悬浮头部实现)的第4小节
具体的绘制和onDraw()方法中的绘制流程一致。

总结

其实主要还是ItemDecoration相关的内容,相比较Android购物车效果实现(RecyclerView悬浮头部实现)的内容,不同点在于优化了数据项的分组使用和头部绘制使用xml两个地方,所以说做功能前还是要先考虑考虑数据该如何使用,不然会增加很多工作量。

如果本文对你有帮助,请别忘记三连,如果有不恰当的地方也请提出来,下篇文章见。

参考文章

MeasureSpec讲解

DividerItemDecoration.java

Android 仿京东、拼多多商品分类页

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode刷题详解——岛屿数量

1. 题目链接&#xff1a;200. 岛屿数量 2. 题目描述&#xff1a; 给你一个由 1&#xff08;陆地&#xff09;和 0&#xff08;水&#xff09;组成的的二维网格&#xff0c;请你计算网格中岛屿的数量。 岛屿总是被水包围&#xff0c;并且每座岛屿只能由水平方向和/或竖直方向上…

RabbitMQ之延迟队列(万字总结,手把手教你学习延迟队列)

文章目录 一、延迟队列概念二、延迟队列使用场景三、RabbitMQ 中的 TTL1、队列设置 TTL2、消息设置 TTL3、两者的区别 四、整合 springboot1、添加依赖2、修改配置文件3、添加 Swagger 配置类 五、队列 TTL1、代码架构图2、配置文件类代码3、消息生产者代码4、消息消费者代码 六…

Java GUI小程序之图片浏览器

以下是一个简单的图片浏览器示例代码&#xff0c;它包含了图片放大缩小、拖拽、上一张/下一张查看等功能。你可以根据它进行扩展&#xff0c;提高用户体验。 import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.e…

Linux系统编程——进程中vfork函数

函数原型 pid_t vfork(void);//pid_t是无符号整型 所需头文件 #include <sys/types.h> #include <unistd.h> 功能 vfork() 函数和 fork() 函数一样都是在已有的进程中创建一个新的进程&#xff0c;但它们创建的子进程是有区别的。 返回值 成功子进程中返回 …

如何使用内网穿透实现远程公网访问windows node.js的服务端

使用Nodejs搭建简单的web网页并实现公网访问 前言 Node.js是建立在谷歌Chrome的JavaScript引擎(V8引擎)的Web应用程序框架。 Node.js自带运行时环境可在Javascript脚本的基础上可以解释和执行(这类似于JVM的Java字节码)。这个运行时允许在浏览器以外的任何机器上执行JavaScri…

Zookeeper 命令使用和数据说明

文章目录 一、概述二、命令使用2.1 登录 ZooKeeper2.2 ls 命令&#xff0c;查看目录树&#xff08;节点&#xff09;2.3 create 命令&#xff0c;创建节点2.4 delete 命令&#xff0c;删除节点2.5 set 命令&#xff0c;设置节点数据2.6 get 命令&#xff0c;获取节点数据 三、数…

在 Electron上安装better-sqlite3出错

错误问题 一直卡npm install --global windows-build-tools --vs2015 这一步 解决 安装&#xff1a;pnpm install better-sqlite3 --save安装命令 pnpm i -D electron-rebuild 手动运行&#xff1a;node_modules/.bin/electron-rebuild -f -w better-sqlite3 我直接在packa…

一文了解VR全景拍摄设备如何选择,全景图片如何处理

引言&#xff1a; 在如今的数字化时代&#xff0c;虚拟现实&#xff08;VR&#xff09;技术不仅为我们的生活增添了许多乐趣&#xff0c;也为摄影领域带来了新的摄影方式&#xff0c;那么VR全景拍摄如何选择设备&#xff0c;全景图片又怎样处理呢&#xff1f; 一. VR全景拍摄设…

创建数据透视表:根据表中一列作为分类的依据统计每个类别下不同子项数量cross_tab()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 创建数据透视表&#xff1a; 根据表中一列作为分类的依据 统计每个类别下不同子项数量 cross_tab() [太阳]选择题 关于以下代码的说法中正确的是? import pandas as pd data{A:[a1,a2,a1,a2,a…

给VSCode插上一双AI的翅膀

#AI编程助手哪家好&#xff1f;DevChat“真”好用# 文章目录 前言一、安装DevChat1.1、访问地址1.2、注册1.3、在VSCode里安装DevChat插件1.3.1、未安装状态1.3.2、已安装状态 二、设置Access Key2.1. 点击左下角管理&#xff08;“齿轮”图标&#xff09;—命令面板&#xff…

vscode远程linux安装codelldb

在windows上使用vscode通过ssh远程连接linux进行c调试时&#xff0c;在线安装codelldb-x86_64-linux.vsix扩展插件失败&#xff0c;原因是linux服务器上的网络问题&#xff0c;所以需要进行手动安装。 首先在windows上下载&#xff1a; codelldb-x86_64-linux.vsix&#xff1b;…

打开word文档报错,提示HRESULT 0x80004005 位置: 部分: /word/comments.xml,行: 0,列: 0

某用户遇到这样一个奇怪的问题&#xff0c;就是回复完word的批注后&#xff0c;保存文档再打开就会报错&#xff0c;提示很抱歉&#xff0c;无法打开XXX&#xff0c;因为内容有问题。&#xff0c;详细信息提示HRESULT 0x80004005 位置: 部分: /word/comments.xml,行: 0,列: 0 c…

iOS性能优化

了解屏幕成像的原理。 有一个电子枪然后在很多横轴方向上 发射电子&#xff0c;不同横轴的电子枪根据显示器中的硬件时钟产生一系列的定时信号&#xff0c;以此来让电子以不同的时间发射出去 这些电子一瞬间的运动形成了一帧动画。 CPU优化&#xff1a; 1.文本计算优化 如果一…

docker 安装xxl-job

1.拉取镜像 docker pull xuxueli/xxl-job-admin:2.4.0 2.docker镜像创建并运行 docker run -e PARAMS"--spring.datasource.urljdbc:mysql://xxxxx:3306/xxl_job?useUnicodetrue&characterEncodingUTF-8&autoReconnecttrue&serverTimezoneAsia/Shanghai&…

C# 将PDF文档转换为Word文档

一.开发框架&#xff1a; .NetCore6.0 工具&#xff1a;Visual Studio 2022 二.思路&#xff1a; 1.使用SHA256Hash标识文档转换记录&#xff0c;数据库已经存在对应散列值&#xff0c;则直接返还已经转换过的文档 2.数据库没有对应散列值记录的话&#xff0c;则保存上传PDF…

深入Android S(12.0) 探索 Android Framework 之 SystemServer 进程启动详解

深入学习 Android Framework 第三&#xff1a;深入Android S(12.0) 探索 Android Framework 之 SystemServer 进程启动详解 文章目录 深入学习 Android Framework前言一、Android 系统的启动流程1. 流程图2. 启动流程概述 二、源码详解1. 时序图2. 源代码1、ZygoteInit # main…

Java14新增特性

前言 前面的文章&#xff0c;我们对Java9、Java10、Java11、Java12 、Java13的特性进行了介绍&#xff0c;对应的文章如下 Java9新增特性 Java10新增特性 Java11新增特性 Java12新增特性 Java13新增特性 今天我们来一起看一下Java14这个版本的一些重要信息 版本介绍 Java 14…

21 Linux 自带的LED驱动

一、Linux 自带 LED 驱动使能 其实 Linux 内核自带 LED 抢夺那个&#xff0c;但在此之前需要配置 Linux 驱动来使能 LED 驱动。 输入以下命令&#xff1a; cd linux/atk-mpl/linux/my_linux/linux-5.4.31 make menuconfig 根据以下路径找到 LED 驱动&#xff1a; → Device D…

Python OpenCV 通过trackbar调整图像亮度对比度颜色

上一篇文章通过设置固定值的方式来调整图像&#xff0c;这篇文章通过trackbar来动态调整参数&#xff0c;从而实时展现图像处理结果&#xff0c;得到想要的图像处理参数。 1. 创建trackbar import cv2 import numpy as npdef nothing(x):passcv2.namedWindow(image) # 创建5个…

【原创】java+swing+mysql个人日记管理系统设计与实现

摘要&#xff1a; 个人日记管理系统是一个可以记录、管理、存储和检索个人日记的应用程序。这个系统允许用户创建和管理多个日记帐户&#xff0c;每个帐户都可以有多个日记条目。用户可以随时添加、编辑或删除日记条目&#xff0c;并可以将这些条目按照主题或其他标准进行分类…