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

news2024/12/25 9:08:33

文章目录

  • 前言
  • 效果图
  • 思路
    • 方式一:通过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 LinearLayoutManager
        val 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?:0

        headTitleView?.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">
        <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:background="@drawable/sp_headtitle"
            android:layout_gravity="center_horizontal">
            <ImageView
                android:layout_width="129dp"
                android:layout_height="match_parent"
                android:layout_alignParentRight="true"
                android:src="@mipmap/ic_bg"
                android:scaleType="fitXY"/>
            <TextView
                android: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">
    
        <ImageView
            android:layout_width="129dp"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:scaleType="fitXY"
            android:src="@mipmap/ic_bg" />
    
        <TextView
            android: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.childCount
    for (i in 0 until childCount) {
        val child = parent.getChildAt(i)
        val bottom = child.top

        headTitleView?.let {
            val top = bottom - it.measuredHeight
            val 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 = 0
        val right = parent.measuredWidth
        //默认的指定高度
        var height = headTitleView?.measuredHeight ?: 0
        //当前ItemView的底部
        var bottom = itemView.bottom
        if (bottom<height){
            height=bottom
        }
        headTitleView?.let {
            it.findViewById<TextView>(R.id.tvTitle).text = titleName
            c.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.coloradmin.cn/o/1203101.html

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

相关文章

Node.js 框架 star 星数量排名——NestJs跃居第二

文章目录 什么是NodeJs?什么是NodeJs框架?图表数据框架排名 什么是NodeJs? Node.js是一个基于Chrome V8引擎的JavaScript运行环境&#xff0c;它使得我们可以在服务器端使用JavaScript开发高效、可扩展的应用程序。作为一个快速、轻量级的平台&#xff0c;Node.js在Web开发领…

CTFhub-RCE-php://input

我们需要使用php://input来构造发送的指令 查看phpinfo&#xff0c;找到一下字段 证明是可以使用php://input 1. 使用Burpsuite抓包并转至Repeater 2. 构造包 方法&#xff1a;POST 目标&#xff1a;/?filephp://input Body&#xff1a;<?php system("ls /"…

元宇宙时代,数字员工正成为企业服务的黄金担当!

未来&#xff0c;你的同事可能不是“人” 自2021年“元宇宙”爆火之后&#xff0c;作为连接现实世界和元宇宙的媒介之一&#xff0c;虚拟人开始大量跑步入场。伴随着虚拟数字人相关技术包括CG、语音识别、图像识别、动捕等的共同成熟&#xff0c;让数字虚拟产业在今年渐入佳境…

(免费领源码)Springboot宠物医院管理系统的设计与实现84724-计算机毕业设计项目选题推荐

摘 要 现如今生活质量提高&#xff0c;人们追求精神健康&#xff0c;与家中宠物朝夕相处&#xff0c;感情深厚&#xff0c;宠物渐渐成了我们身边的朋友。因而宠物生病了&#xff0c;需要去看病&#xff0c;自古医院救死扶伤&#xff0c;生命无贵贱&#xff0c;无论人类还是动物…

一文详解进销存管理系统!

一、什么是进销存管理系统&#xff1f; 进销存软件是一种针对制造业企业设计的管理软件系统&#xff0c;旨在协调和优化企业的生产、采购、销售以及库存管理等方面的活动。该系统的主要目标是提高企业的生产效率、降低库存成本、优化供应链&#xff0c;并增强企业的整体运营效…

如何使用Cpolar+Tipask,在ubuntu系统上搭建一个私人问答网站

文章目录 前言2.Tipask网站搭建2.1 Tipask网站下载和安装2.2 Tipask网页测试2.3 cpolar的安装和注册 3. 本地网页发布3.1 Cpolar临时数据隧道3.2 Cpolar稳定隧道&#xff08;云端设置&#xff09;3.3 Cpolar稳定隧道&#xff08;本地设置&#xff09; 4. 公网访问测试5. 结语 前…

Nat. Med. | 成年人的城市生活环境对心理健康的影响

今天为大家介绍的是来自Jiayuan Xu和Gunter Schumann团队的一篇论文。城市居民暴露于许多可能相互结合和相互作用的环境因素&#xff0c;这些因素可能影响心理健康。目前尚未有工作尝试建模城市生活的复杂实际暴露与大脑和心理健康之间的关系&#xff0c;以及这如何受遗传因素调…

每日互动(个推)全新推出AITA智选人群工具,助力品牌营销升级

11月9日&#xff0c;在2023年世界互联网大会“新产品新技术特色场景发布活动”上&#xff0c;数据智能服务商每日互动&#xff08;个推&#xff09;全新打造的AITA智选人群工具首次正式对外发布。作为每日互动在品牌营销领域的大模型应用最新成果&#xff0c;AITA智选人群工具将…

【学习笔记】 - GIT的基本操作,IDEA接入GIT以及上传hub

用github蛮多&#xff0c;但git没怎么用&#xff0c;看着视频对着写点笔记以及操作 一、GIT文件的三种状态和模式 已提交(committed) 已提交表示数据已经安全的保存在本地数据库中。 已修改(modified) 已修改表示修改了文件&#xff0c;但还没保存到数据库中。…

速锐得HJ1239车载终端TBOX柴油商用车远程排放管理工况模型应用

其实排放模型&#xff0c;并不是生涩难懂的问题&#xff0c;首先我们准备好一台TBOX&#xff0c;比如无论是海康、华为、速锐得、博世、联电、LG、西门子都可以做到&#xff0c;在满足TBOX具备4G物联网2路CAN支持远程升级控车&#xff0c;支持国四国五国六车型&#xff0c;带定…

Kafka简单汇总

Kafka的结构图 多个Parttion共同组成这个topic的所有消息。每个consumer都属于一个consumer group&#xff0c;每条消息只能被consumer group中的一个Consumer消费&#xff0c; 但可以被多个consumer group消费。即组间数据是共享的&#xff0c;组内数据是竞争的。二、消费模型…

【学习辅助】Axure手机时间管理APP原型,告别手机控番茄任务模板

作品概况 页面数量&#xff1a;共 30 页 兼容软件&#xff1a;Axure RP 9/10&#xff0c;不支持低版本 应用领域&#xff1a;时间管理、系统工具 作品申明&#xff1a;页面内容仅用于功能演示&#xff0c;无实际功能 作品特色 本品为「手机时间管理」APP原型&#xff0c;…

【华为OD题库-015】报文重排序-Java

题目 对报文进行重传和重排序是常用的可靠性机制&#xff0c;重传缓冲区内有一定数量的子报文&#xff0c;每个子报文在原始报文中的顺序已知&#xff0c;现在需要恢复出原始报文。 输入描述 输入第一行为N,表示子报文的个数&#xff0c;0<N < 1000。 输入第二行为N个子报…

深入理解Kafka3.6.0的核心概念,搭建与使用

Kafka是最初由Linkedin公司开发&#xff0c;是一个分布式、支持分区的&#xff08;partition&#xff09;、多副本的&#xff08;replica&#xff09;&#xff0c;基于zookeeper协调的分布式消息系统&#xff0c;它的最大的特性就是可以实时的处理大量数据以满足各种需求场景&a…

卫星位置解算

武大GPS原理及应用 1.导航电文

探秘 Vue 数据绑定:为何 data 必须是函数而非对象?

&#x1f3ac; 江城开朗的豌豆&#xff1a;个人主页 &#x1f525; 个人专栏 :《 VUE 》 《 javaScript 》 &#x1f4dd; 个人网站 :《 江城开朗的豌豆&#x1fadb; 》 ⛺️ 生活的理想&#xff0c;就是为了理想的生活 ! 目录 ⭐ 专栏简介 &#x1f4d8; 文章引言 一、实…

削峰填谷:居民小区电动汽车有序充电策略研究

摘 要&#xff1a;针对电动汽车在居民小区无序充电对电网系统产生严重隐患及充电间时过长问题&#xff0c;提出一种采用延迟充电的电动汽车有序充电控制策略&#xff0c;并在分析国内外电动汽车有序充电的研究现状后&#xff0c;设计了居民小区电动汽车有序充电策略的总体框架。…

双十一大促已过,虾皮、Lazada年底如何通过测评补单打造搜索排名

双十一大促已过&#xff0c;有人欢喜有人忧&#xff0c;不管怎么样&#xff0c;年底的这波旺季还是要好好把握的。 如何提升虾皮搜索排名 1、标题关键词匹配度 Shopee、Lazada的排名规则主要是根据用户搜索时输入的关键字和卖家的商品标题、描述等是否相匹配来进行排名&…

独立站商品信息是怎么获取的呢

独立站商品信息的获取主要通过以下几种方式&#xff1a; 人工收集&#xff1a;卖家可以通过在各个电商平台、网站等渠道进行手动搜索和收集商品信息&#xff0c;包括商品名称、价格、描述、图片等&#xff0c;然后将其导入到自己的独立站中。使用采集工具&#xff1a;目前市面…

解决 Django 开发中的环境配置问题:Windows 系统下的实战指南20231113

简介&#xff1a; 在本文中&#xff0c;我想分享一下我最近在 Windows 环境下进行 Django 开发时遇到的一系列环境配置问题&#xff0c;以及我是如何一步步解决这些问题的。我的目标是为那些可能遇到类似困难的 Django 开发者提供一些指导和帮助。 问题描述&#xff1a; 最近…