仿京东拼多多商品分类页-(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.mzph.cn/news/143701.shtml

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

相关文章

MAT工具定位分析Java堆内存泄漏问题方法

原创/朱季谦 一、MAT概述与安装 MAT&#xff0c;全称Memory Analysis Tools&#xff0c;是一款分析Java堆内存的工具&#xff0c;可以快速定位到堆内泄漏问题。该工具提供了两种使用方式&#xff0c;一种是插件版&#xff0c;可以安装到Eclipse使用&#xff0c;另一种是独立版…

【python 生成器 面试必备】yield关键字,协程必知必会系列文章--自己控制程序调度,体验做上帝的感觉 1

python生成器系列文章目录 第一章 yield — Python (Part I) 文章目录 python生成器系列文章目录前言1. Generator Function 生成器函数2.并发和并行&#xff0c;抢占式和协作式2.Let’s implement Producer/Consumer pattern using subroutine: 生成器的状态 generator’s st…

ESP32网络开发实例-BME280传感器数据保存到InfluxDB时序数据库

BME280传感器数据保存到InfluxDB时序数据库 文章目录 BME280传感器数据保存到InfluxDB时序数据库1、BM280和InfluxDB介绍2、软件准备3、硬件准备4、代码实现在本文中,将详细介绍如何将BME280传感器数据上传到InfluxDB中,方便后期数据处理。 1、BM280和InfluxDB介绍 InfluxDB…

tracert命令

前言&#xff1a;今天在阅读“Web性能权威指南”这本书的时候&#xff0c;发现 tracert 这个命令挺有意思的&#xff0c;在分析网络性能瓶颈的时候也能使用的到&#xff0c;在此就小记一笔以备后用。 1&#xff1a;作用 tracert 是一个简单的网络诊断工具&#xff0c;可以列出…

如何在Windows 10中进行屏幕截图

本文介绍如何在Windows 10中捕获屏幕截图&#xff0c;包括使用键盘组合、使用Snipping Tool、Snipp&Sketch Tool或Windows游戏栏。 使用打印屏幕在Windows 10中捕获屏幕截图 在Windows 10中捕获屏幕截图的最简单方法是按下键盘上的PrtScWindows键盘组合。你将看到屏幕短暂…

python自动化第一篇—— 带图文的execl的自动化合并

简述 最近接到一个需求&#xff0c;需要为公司里的一个部门提供一个文件上传自动化合并的系统&#xff0c;以供用户稽核&#xff0c;谈到自动化&#xff0c;肯定是选择python&#xff0c;毕竟python的轮子多。比较了市面上几个用得多的python库&#xff0c;我最终选择了xlwings…

将按键放到输入框内:

如何将将Button放到输入框内&#xff1f; 效果图&#xff1a; 步骤如下&#xff1a; button 外围用template 包裹一层 <template #suffix v-if"row.WorkerRole TPM"> <el-inputtype"text"v-model"row.JobNumber"placeholder"…

云原生下GIS服务规划与设计

作者&#xff1a;lisong 目录 背景云原生环境下GIS服务的相关概念GIS服务在云原生环境下的规划调度策略GIS服务在云原生环境下的调度手段GIS服务在云原生环境下的服务规划调度实践 背景 作为云原生GIS系统管理人员&#xff0c;在面对新建的云GIS系统时&#xff0c;通常需要应对…

使用 PYTORCH 进行图像风格迁移

一、介绍 本教程介绍如何实现 由 Leon A. Gatys、Alexander S. Ecker 和 Matthias Bethge 开发的神经风格算法。神经风格或神经传输允许您拍摄图像并以新的艺术风格再现它。该算法采用三幅图像&#xff0c;即输入图像、内容图像和风格图像&#xff0c;并将输入更改为类似于内容…

Python框架篇(1):FastApi-快速入门

1.介绍 前言: 不管学什么语言&#xff0c;都应该至少掌握一个框架&#xff0c;方面我们后续&#xff0c;进行服务部署、服务对外支持等; 1.1 官网介绍 下面是来自FastAPI官网的介绍: FastAPI 是一个用于构建 API 的现代、快速&#xff08;高性能&#xff09;的 web 框架&#…

为忙碌的软件工程师精心准备的编码面试准备材料,超过 100,000 人受益!

这是一个针对技术面试准备的手册。它收集了大量的面试问题和答案&#xff0c;涵盖了算法、系统设计、前端等主题&#xff0c;并且还在不断更新和完善中。 这个项目是“Tech Interview Handbook”&#xff0c;解决了求职者在技术面试中遇到的各种难题&#xff0c;帮助他们更好地…

C++ Qt 学习(六):Qt http 编程

1. http 基础 HTTP 基础教程C Web 框架 drogonoatpp 2. C Qt 用户登录、注册功能实现 login_register.h #pragma once#include <QtWidgets/QDialog> #include "ui_login_register.h" #include <QNetworkReply>class login_register : public QDialog…

开源网安受邀参加网络空间安全合作与发展论坛,为软件开发安全建设献计献策

​11月10日&#xff0c;在广西南宁举办的“2023网络空间安全合作与发展论坛”圆满结束。论坛在中国兵工学会的指导下&#xff0c;以“凝聚网络空间安全学术智慧&#xff0c;赋能数字经济时代四链融合”为主题&#xff0c;邀请了多位专家及企业代表共探讨网络安全发展与数字经济…

《网络协议》05. 网络通信安全 · 密码技术

title: 《网络协议》05. 网络通信安全 密码技术 date: 2022-09-10 15:16:15 updated: 2023-11-12 07:03:52 categories: 学习记录&#xff1a;网络协议 excerpt: 网络通信安全&#xff08;ARP 欺骗&#xff0c;DoS & DDoS&#xff0c;SYN 洪水攻击&#xff0c;LAND 攻击&a…

MVC使用的设计模式

MVC使用的设计模式 一、背景 MVC模式是"Model-View-Controller"的缩写&#xff0c;中文翻译为"模式-视图-控制器"。MVC应用程序总是由这三个部分组成。Event(事件)导致Controller改变Model或View&#xff0c;或者同时改变两者。只要Controller改变了Model…

5个用于地理空间数据分析的Python包

大家好&#xff0c;地理空间数据分析在城市规划、环境研究、农业和交通等行业中至关重要。不断增长的需求导致Python包在各种地理数据分析要求上的使用增加&#xff0c;例如分析气候模式、研究城市发展或跟踪疾病传播等&#xff0c;评估和选择具有快速处理、修改和可视化能力的…

IDEA安装配置SceneBuilder

1、下载 SceneBuilder 地址&#xff1a; Scene Builder - Gluon ​​​​​​​ Scene Builder | JavaFX中文官方网站 选择符合自己系统版本&#xff0c;JAVA版本的下载 2、安装SceneBuilder 执行下载的 SceneBuilder 安装程序&#xff0c;并按照安装向导的指示进行…

高防IP可以抵御哪些恶意攻击

高防IP协议可以隐藏用户的站点&#xff0c;使得攻击者无法发现恶意攻击的目标网络资源&#xff0c;从而提高了源站的安全性。能够有效抵御常见的恶意攻击类型ICMPFlood、UDPFlood、 TCPFlood、SYNFlood、ACKFlood等&#xff0c;帮助游戏、金 融、电子商务、互联网、政企等行业抵…

实际使用Elasticdump工具对Elasticsearch集群进行数据备份和数据还原

文/朱季谦 目录一、Elasticdump工具介绍二、Elasticdump工具安装三、Elasticdump工具使用 最近在开发当中做了一些涉及到Elasticsearch映射结构及数据导出导入的工作&#xff0c;怕以后会把这过程忘记&#xff0c;可谓好记性不如烂笔头&#xff0c;故而记录成一篇博文。 玩El…

钉钉统计部门个人请假次数go

前言 最近小组需要统计部门各种请假次数&#xff0c;写了一个方法&#xff0c;第一次实战中用到递归函数&#xff0c;简单记录一下。 效果展示 这些数据不需要返回json&#xff0c;这里这样是为了方便测试。可以通过这些数据完成其它的操作。 功能实现 钉钉服务端调试工具A…