自定义RecyclerView.LayoutManager实现类实现卡片层叠布局的列表效果

news/2024/3/29 13:15:25/文章来源:https://blog.csdn.net/itTalmud/article/details/130332860

一.前言

  • 先看效果(大佬们请忽略水印):
    在这里插入图片描述

  • 卡片层叠列表的实现效果已经发布成插件,集成地址:implementation ‘com.github.MrFishC:YcrCardLayoutHepler:v1.1’;

  • 先讲解如何快速实现,然后再来讲解插件[支持加载固定数量的数据,无限循环,加载更多]中的实现细节;

二.使用方式

  • 先看代码
import android.annotation.SuppressLint
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import cn.jack.library_arouter.manager.constants.RouterPathActivity
import com.alibaba.android.arouter.facade.annotation.Route
import com.jack.lib_base.base.view.BaseSimpleActivity
import com.jack.simple_recycleview.databinding.ActivitySimpleRecycleviewBinding
import com.jack.ycr_rv_cardlayout.ConfigManager
import com.jack.ycr_rv_cardlayout.CustomItemTouchHelperCallBackImp
import com.jack.ycr_rv_cardlayout.CustomLayoutManager
import com.jack.ycr_rv_cardlayout.OnItemSwipeListener
import java.util.*
@Route(path = RouterPathActivity.SimpleRv.PAGER_SIMPLE_RV)
class SimpleRecycleViewActivity :BaseSimpleActivity<ActivitySimpleRecycleviewBinding>(ActivitySimpleRecycleviewBinding::inflate) {override fun prepareData() {super.prepareData()val adapter = MyAdapter()mBinding.recyclerView.adapter = adapterval manager = ConfigManager()val callBackImp = CustomItemTouchHelperCallBackImp(adapter, list, manager)callBackImp.setOnSwipedListener(object : OnItemSwipeListener<Int> {override fun onItemSwiping(viewHolder: RecyclerView.ViewHolder,ratio: Float,direction: Int) {when (direction) {manager.SWIPING_LEFT -> {println("向左侧滑动")}manager.SWIPING_RIGHT -> {println("向右侧滑动")}else -> {println("向未知方向滑动")}}}override fun onItemSwiped(viewHolder: RecyclerView.ViewHolder, t: Int, direction: Int) {when (direction) {manager.SWIPED_UP -> {println("从上方滑出")}manager.SWIPED_DOWN -> {println("从下方滑出")}manager.SWIPED_LEFT -> {println("从左侧滑出")}manager.SWIPED_RIGHT -> {println("从右侧滑出")}else -> {println("从未知方向滑出")}}}@SuppressLint("NotifyDataSetChanged")override fun onSwipedAllItem() {println("卡片全部滑出")//根据实际业务来实现 加载更多mBinding.recyclerView.postDelayed({initData()Objects.requireNonNull(mBinding.recyclerView.adapter).notifyDataSetChanged()}, 1500L)}})val touchHelper = ItemTouchHelper(callBackImp)val cardLayoutManager = CustomLayoutManager(mBinding.recyclerView, touchHelper, manager)mBinding.recyclerView.layoutManager = cardLayoutManagertouchHelper.attachToRecyclerView(mBinding.recyclerView)initData()}private fun initData() {list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)list.add(R.drawable.icon_common_bg)}private val list: MutableList<Int> = ArrayList()private inner class MyAdapter : RecyclerView.Adapter<MyAdapter.MyViewHolder>() {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {val view =LayoutInflater.from(parent.context).inflate(R.layout.simple_rv_item, parent, false)return MyViewHolder(view)}override fun onBindViewHolder(holder: MyViewHolder, position: Int) {}override fun getItemCount(): Int {return list.size}inner class MyViewHolder(itemView: View?) : RecyclerView.ViewHolder(itemView!!)}
}
  • 插件集成之后,只需要按照prepareData方法中的代码做一下配置便可以实现卡片层叠的列表;

三.插件实现细节

1.ConfigManager

  • 这里是一些配置信息,列表item的缩放比例,显示列表的item数量,item支持滑出的方向,item层叠的方式等等;

2.CustomLayoutManager

  • 这个是层叠布局实现的关键,通过自定义RecyclerView.LayoutManager的实现类可以实现很多炫酷的UI,其核心在于onLayoutChildren方法,绘制RecycleView的子View:
override fun onLayoutChildren(recycler: Recycler, state: RecyclerView.State) {// 先移除所有viewremoveAllViews()// 在布局之前,将所有的子 View 先 Detach 掉,放入到 Scrap 缓存中detachAndScrapAttachedViews(recycler)val itemCount = itemCount// 当数据源个数大于最大显示数时if (itemCount > mCManager.DEFAULT_SHOW_ITEM) {// 把数据源倒着循环,这样,第0个数据就在屏幕最上面了            为什么倒序就可以让第0个数据在屏幕最上面  原理是什么 待研究for (position in mCManager.DEFAULT_SHOW_ITEM downTo 0) {//从缓冲池中获取到itemViewval view = recycler.getViewForPosition(position)// 将 Item View 加入到 RecyclerView 中addView(view)// 测量 Item ViewmeasureChildWithMargins(view, 0, 0)// getDecoratedMeasuredWidth(view) 可以得到 Item View 的宽度// 所以 widthSpace 就是除了 Item View 剩余的值val widthSpace = width - getDecoratedMeasuredWidth(view)// 同理val heightSpace = height - getDecoratedMeasuredHeight(view)// recyclerview布局:在这里默认布局是放在 RecyclerView 中心// layoutDecoratedWithMargins: 将child显示在RecyclerView上面,left,top,right,bottom规定了显示的区域layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,widthSpace / 2 + getDecoratedMeasuredWidth(view),heightSpace / 2 + getDecoratedMeasuredHeight(view))// 其实屏幕上有 mCManager.DEFAULT_SHOW_ITEM + 1 张卡片,但是我们把第 mCManager.DEFAULT_SHOW_ITEM 张和// 第 mCManager.DEFAULT_SHOW_ITEM + 1 张卡片重叠在一起,这样看上去就只有 mCManager.DEFAULT_SHOW_ITEM  张// 第CardConfig.DEFAULT_SHOW_ITEM + 1张卡片主要是为了保持动画的连贯性if (position == mCManager.DEFAULT_SHOW_ITEM) {view.scaleX = 1 - (position - 1) * mCManager.DEFAULT_SCALEview.scaleY = 1 - (position - 1) * mCManager.DEFAULT_SCALEwhen (mCManager.getStackDirection()) {//从下往上层叠mCManager.UP ->view.translationY =((position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()//从上往下层叠mCManager.DOWN ->view.translationY =(-(position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()else -> view.translationY =(-(position - 1) * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()}} else if (position > 0) {view.scaleX = 1 - position * mCManager.DEFAULT_SCALEview.scaleY = 1 - position * mCManager.DEFAULT_SCALEwhen (mCManager.getStackDirection()) {//从下往上层叠mCManager.UP ->view.translationY =(position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()//从上往下层叠mCManager.DOWN ->view.translationY =(-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()else -> view.translationY =(-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()}} else {//只有顶层的卡片才能滑动view.setOnTouchListener(mOnTouchListener)}}} else {// 当数据源个数小于或等于最大显示数时for (position in itemCount - 1 downTo 0) {val view = recycler.getViewForPosition(position)addView(view)measureChildWithMargins(view, 0, 0)val widthSpace = width - getDecoratedMeasuredWidth(view)val heightSpace = height - getDecoratedMeasuredHeight(view)// recyclerview 布局layoutDecoratedWithMargins(view, widthSpace / 2, heightSpace / 2,widthSpace / 2 + getDecoratedMeasuredWidth(view),heightSpace / 2 + getDecoratedMeasuredHeight(view))if (position > 0) {view.scaleX = 1 - position * mCManager.DEFAULT_SCALEview.scaleY = 1 - position * mCManager.DEFAULT_SCALEwhen (mCManager.getStackDirection()) {//从下往上层叠mCManager.UP ->view.translationY =(position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()//从上往下层叠mCManager.DOWN ->view.translationY =(-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()else -> view.translationY =(-position * view.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y).toFloat()}} else {view.setOnTouchListener(mOnTouchListener)}}}}

3.CustomItemTouchHelperCallBackImp

  • ItemTouchHelper.Callback的实现类,两个核心方法:
    • onChildDraw:这个方法被触发的条件之一是,item滑动的时候,在该方法内部对所有可见的item进行缩放,对最上层的item进行旋转角度的设置,如此,用户体验效果更佳;
override fun onChildDraw(c: Canvas, recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder,dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean) {super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive)val itemView = viewHolder.itemViewif (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {//滑动的比例var ratio: Float = dX / mCManager.getThreshold(recyclerView)// ratio 最大为 1 或 -1if (ratio > 1) {ratio = 1f} else if (ratio < -1) {ratio = -1f}//旋转的角度itemView.rotation = ratio * mCManager.DEFAULT_ROTATE_DEGREEval childCount = recyclerView.childCount//卡片滑动过程中   对view进行缩放处理  [这里的逻辑需要跟自定义的RecyclerView.LayoutManager实现类onLayoutChildren方法对应]        具体的缩放效果可以自行通过计算来尝试// 当数据源个数大于最大显示数时if (childCount > mCManager.DEFAULT_SHOW_ITEM) {//position:从1开始       for循环中定义position的初始值以及其边界,目的是为了让第一张不做处理for (position in 1 until childCount - 1) {val index = childCount - position - 1val view = recyclerView.getChildAt(position)//通过调用setScaleX()和setScaleY()方法,可以实现View的缩放view.scaleX =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEview.scaleY =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEwhen (mCManager.getStackDirection()) {//从下往上层叠mCManager.UP -> view.translationY =(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y//从上往下层叠mCManager.DOWN -> view.translationY =-(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Yelse -> view.translationY =-(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y}}} else {// 当数据源个数小于或等于最大显示数时      for循环中定义position的初始值以及其边界,目的是为了让最后一张不做处理for (position in 0 until childCount - 1) {val index = childCount - position - 1val view = recyclerView.getChildAt(position)view.scaleX =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEview.scaleY =1 - index * mCManager.DEFAULT_SCALE + abs(ratio) * mCManager.DEFAULT_SCALEwhen (mCManager.getStackDirection()) {//从下往上层叠mCManager.UP ->view.translationY =(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y//从上往下层叠mCManager.DOWN ->view.translationY =-(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Yelse -> view.translationY =-(index - abs(ratio)) * itemView.measuredHeight / mCManager.DEFAULT_TRANSLATE_Y}}}//由于增加了上下方向 这里 可以按需添加业务逻辑if (ratio != 0f) {if (mListener != null) {mListener!!.onItemSwiping(viewHolder,ratio,if (ratio < 0) mCManager.SWIPING_LEFT else mCManager.SWIPING_RIGHT)}} else {if (mListener != null) {mListener!!.onItemSwiping(viewHolder, ratio, mCManager.SWIPING_NONE)}}}}
  • onSwiped:当item滑动的时候触发,这个方法内部做设置回调,设置支持无限循环的情况下集合需要添加顶层被移除的item,同时需要调用适配器的notifyDataSetChanged方法,对所有item“一视同仁”;
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {// 移除onTouchListener,防止触摸 滑动之间冲突viewHolder.itemView.setOnTouchListener(null)val layoutPosition = viewHolder.layoutPositionval remove: T = mDataList.removeAt(layoutPosition)if (mCManager.isLoopCard()) {mDataList.add(remove)}//主动调用刷新,否则会出现只有顶层卡片才能滑动mAdapter.notifyDataSetChanged()//使用接口回调进行拓展1if (mListener != null) {when (direction) {ItemTouchHelper.UP -> mListener!!.onItemSwiped(viewHolder,remove,mCManager.SWIPED_UP)ItemTouchHelper.DOWN -> mListener!!.onItemSwiped(viewHolder,remove,mCManager.SWIPED_DOWN)ItemTouchHelper.LEFT -> mListener!!.onItemSwiped(viewHolder,remove,mCManager.SWIPED_LEFT)ItemTouchHelper.RIGHT -> mListener!!.onItemSwiped(viewHolder,remove,mCManager.SWIPED_RIGHT)else -> mListener!!.onItemSwiped(viewHolder, remove, mCManager.SWIPED_NONE)}}//使用接口回调进行拓展2// 当没有数据时回调 mListenerif (mAdapter.itemCount == 0 && mListener != null && !mCManager.isLoopCard()) {mListener!!.onSwipedAllItem()}}

四.总结

  • 核心在于两点,其一:自定义RecyclerView.LayoutManager实现类,重写onLayoutChildren方法,对子item进行“绘制”;其二:自定义ItemTouchHelper.Callback实现类,重写onChildDraw和onSwiped方法;快速实现只需要按照前言中的方式进行配置即可,若想要了解细节,可以看插件代码中的注释(写的还是比较详细);

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

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

相关文章

托福高频真词List05 // 附托福TPO阅读真题

目录 4月23日单词 生词 熟词 4月24日真题 4月23日单词 生词 sparsethinly distributedadj 稀疏的sparselythinlyadv 稀疏地congestion / kənˈdʒestʃən / overcrowdingn 拥挤continuallyregularlyadv 持续的eradicateeliminatev 消除facilitatemake easiereasev 使..…

《面试1v1》java泛型

我是 javapub&#xff0c;一名 Markdown 程序员从&#x1f468;‍&#x1f4bb;&#xff0c;八股文种子选手。 面试官&#xff1a;小伙子,说实话,泛型这个机制一开始我也是一头雾水,搞不太明白它到底要解决什么问题。你能不能不那么书呆子,给我普普通通地讲一讲泛型? 候选人…

如何测试信号源或者发射机的回波损耗

信用源或者发射机的return loss测试过程 1.用网分线缆的第一步就是看线的抖动情况&#xff0c;后面还是要多注意 经过一系列排查后&#xff0c;选用两个抖动比较小的线缆&#xff0c;然后开始测试另外一台仪器。 2.检查测试仪器的输出功率&#xff0c;见图1 打开信号源或者发射…

可以一学的代码优化小技巧:减少if-else冗余

前言 if-else 语句对于程序员来说&#xff0c;是非常非常熟悉的一个判断语句&#xff0c;我们在日常开发和学习中都经常看见它&#xff0c;if-else语句主要用于需要做出选择的地方进行判断&#xff0c;这里就不再赘述if-else语法和特点了。 ​ 我们在写代码&#xff08;如图下…

PC1 - 搭建项目

先看路由&#xff0c;可以查看功能模块划分。熟悉什么看什么 router文件夹下routerConfig.tsx 配置路由&#xff0c;创建模块文件&#xff08;写好内容模块&#xff09;&#xff0c;lazy可懒加载导入。App.tsx配置一级路由&#xff0c;配置二级路由出口 { path:/, element: …

【记录】FFmpeg|超大视频本地有损压缩,500MB变5MB(支持 Windows、Linux、macOS)

参考&#xff1a; 如何将一分钟长的1080p视频压缩至5MB以内&#xff1f;-知乎-滔滔清风近期HEVC扩展备用安装方法-B站-悲剧天下 总共三个步骤&#xff0c;安装FFmpeg、运行指令、打开视频。 亲测 500MB 变 5MB。 1 安装FFmpeg 对于不需要看教程可以自行完成安装的同学们&…

7. 堆的简单学习

7. 堆 7.1 堆的定义 堆是计算机科学中一类特殊的数据结构的统称&#xff0c;堆通常可以被看做是一棵完全二叉树的数组实现。 堆的特性&#xff1a; 它是完全二叉树&#xff0c;除了树的最后一层结点不需要是满的&#xff0c;其它的每一层从左到右都是满的&#xff0c;如果最…

使用python实现自动点击功能

猜你感兴趣 使用Pyqt5玩转ChatGpt内网文件共享服务快速搭建私有pip镜像源python设计模式-创建型模式docker搭建私有git服务器&#xff0c;项目备份和迁移redis持久化方案 被测点击界面 新建counter.html添加下面代码并保存,使用编辑器或浏览器打开 <!DOCTYPE html> &l…

23.4.21总结

正则表达式 正则表达式使用单个字符串来描述、匹配一系列匹配某个句法规则的字符串&#xff0c;通常被用来检索、替换那些符合某个模式&#xff08;规则&#xff09;的文本。 正则表达式是一种对字符串操作的一种逻辑公式&#xff0c;就是用事先定义好的一些特定字符、及这些…

深度学习 - 42.特征交叉与 SetNET、Bilinear Interaction 与 FiBiNet

目录 一.引言 二.摘要 - ABSTRACT 三.介绍 - INTRODUCTION 四.相关工作 - RELATED WORK 1.因式分解机及其变体 - Factorization Machine and Its relevant variants 2. 基于深度学习的点击率模型 - Deep Learning based CTR Models 3.SENET Module 五.FiBiNet Model 1…

【C++】哈希的应用:位图和布隆过滤器

目录 1. 位图1.1 位图的概念1.2 位图的结构1.3 位图的实现 2. 布隆过滤器2.1 概念2.2 结构2.3 布隆过滤器的实现 1. 位图 1.1 位图的概念 &#x1f4ad;位图&#xff08;bitset&#xff09;是一种基于哈希思想设计的数据结构&#xff0c;其功能主要用于判断数据是否已存在。适…

来使用分支语句和循环语句实现一个小游戏吧(猜数字游戏)

猜数字游戏 1.代码展示2.菜单设计3.主函数部分3.随机数设计 所属专栏&#xff1a;C语言 博主首页&#xff1a;初阳785 代码托管&#xff1a;chuyang785 感谢大家的支持&#xff0c;您的点赞和关注是对我最大的支持&#xff01;&#xff01;&#xff01; 博主也会更加的努力&am…

rtthread默认网卡的操作

设置网卡优先级 在 RT-Thread 操作系统中&#xff0c;可以通过修改网卡的优先级来设置默认网卡。优先级越高的网卡会被优先选择为默认网卡。 下面介绍一些设置默认网卡优先级的方法&#xff1a; 在 RT-Thread 的网络配置文件 rtconfig.h 中&#xff0c;可以通过修改 NETIF_P…

Jmeter5.1.1报错:java.net.BindException: Address already in use: connect

Jmeter5.1.1报错&#xff1a;java.net.BindException: Address already in use: connect 原因&#xff1a;从网上找到资料&#xff1a;端口占用 Windows提供给TCP/IP链接的端口为 1024-5000&#xff0c;并且要四分钟来循环回收它们&#xff0c;就导致我们在短时间内跑大量的请…

把ChatGPT训练成你的得力助手

在调教chatgpt时&#xff0c;我们大部分的时候都需要一个好的学术翻译官&#xff0c;但是在他成为学术翻译官之前我们有很多规定要说明&#xff0c;比如不用回答我的问题&#xff0c;不用计算公式等。我将以下命令要求集成&#xff0c;在使用的时候只需要你发给它这段话&#x…

如何评估小程序开发费用:从项目规模到技术需求

作为一种越来越受欢迎的移动应用&#xff0c;小程序的开发费用是许多企业和个人考虑的重要因素之一。但是&#xff0c;要确定小程序开发费用并不是一件容易的事情&#xff0c;因为它涉及到多个因素&#xff0c;从项目规模到技术需求。 项目规模 小程序开发的费用通常与项目规…

Linux部署人大金仓(Kingbase8)

陈老老老板&#x1f9b8; &#x1f468;‍&#x1f4bb;本文专栏&#xff1a;国产数据库-人大金仓&#xff08;kingbase8&#xff09;&#xff08;主要讲一些人大金仓数据库相关的内容&#xff09; &#x1f468;‍&#x1f4bb;本文简述&#xff1a;本文讲一下LInux上部署人大…

Vue+Echarts 项目演练(中)后台数据接口的创建

全局引用Echarts与axios 后台接口创建express路由 api接口数据创建 全局引用Echarts与axios vue3.0的挂载方式&#xff1a;使用Provide/Inject依赖注入&#xff0c;将替代vue2中在原型链上挂载一些属性在app.vue中使用provider来给后代们提供数据 <script> import { p…

经典数据结构之2-3树

2-3树定义 2-3树&#xff0c;是最简单的B-树&#xff0c;其中2、3主要体现在每个非叶子节点都有2个或3个子节点&#xff0c;B-树即是平衡树&#xff0c;平衡树是为了解决不平衡树查询效率问题&#xff0c;常见的二叉平衡书有AVL树&#xff0c;它虽然提高了查询效率&#xff0c…

深入JVM了解Java对象实例化过程

文章目录 一、对象创建方式二、对象产生步骤1、判断对象是否已经加载、链接、初始化2、为对象分配内存空间3、处理并发问题3.1 TLAB 4、初始化零值5、完善对象内存布局的信息6、调用对象的实例化方法 <init>7、总结 三、对象的内存布局1、对象头1.1 运行时元数据&#xf…