LxAdapter
LxAdapter
轻量 、 面向业务 为主要目的,一方面希望可以快速、简单的的完成数据的适配工作,另一方面针对业务中经常出现的场景能提供统一、简单的解决方案。
LxAdapter 是我做通用适配器的第三次重构版本,尝试了很多种方案,这次摒弃了很多复杂的东西,回归简单,希望会越来越好;
com.zfy:lxadapter:2.0.11
LxDragSwipeComponent | LxSnapComponent | LxPicker | LxSpaceComponent | LxSelectComponent |
拖拽,侧滑 | SnapHelper效果 | 滚轮选择器 | 多类型等距间隔 | 选择器效果 |
LxAnimComponent | LxExpandable | LxFixedComponent | LxLoadMoreComponent | LxNesting |
动画 | 分组 | 悬停 | 加载更多 | 垂直嵌套水平滑动,记录位置 |
目录
- 联系我
- 特性
- 设计分析
- 内置的数据类型
- 基础
- 功能
- 功能:事件发布 ~ 将数据更新抽象成事件
- 功能:跨越多列(Span)~ 灵活布局
- 功能:间隔(Space)~ 多类型布局等距间隔
- 功能:加载更多(LoadMore)~ 赋能分页加载
- 功能:选择器(Selector)~ 面向选择器业务场景
- 功能:列表动画(Animator)
- 功能:悬挂效果(Fixed)
- 功能:拖拽和侧滑(Drag/Swipe)
- 功能:实现 ViewPager (Snap)
- 功能:实现分组列表 (Expandable) ~ 按组划分,展开收起
- 功能:实现 RecyclerView 嵌套 (Nesting) ~ 嵌套滑动,恢复滑动位置
- 功能:实现滚轮选择器效果 (Picker) ~ 多级级联滚动,数据异步获取
- 进阶
特性
- 使用
LxAdapter
构建单类型、多类型数据适配器; - 使用
LxItemBinder
完成每种类型的数据绑定和事件处理,支持自定义类型,可灵活扩展实现Header/Footer/Loading/Empty
等场景效果,支持单击事件、双击事件、长按事件;; - 使用
LxViewHolder
作为ViewHolder
进行数据绑定; - 使用
LxList
作为数据源,基于DiffUtil
并自动完成数据比对和更新; - 使用
LxSource
和LxQuery
搭配LxList
,简化数据列表增删改查; - 使用
LxComponent
完成分离、易于扩展的扩展功能,如果加载更多等; - 使用
TypeOpts
针对每种数据类型,进行细粒度的配置侧滑、拖拽、顶部悬停、跨越多列、动画等效果; - 使用
LxSpaceComponent
实现多类型数据等距间隔; - 使用
LxLoadMoreComponent
支持列表顶部、列表底部,预加载更多数据; - 使用
LxSelectorComponent
支持快速实现选择器效果,单选、多选、滑动选中等。 - 使用
LxFixedComponent
实现顶部悬停效果; - 使用
LxDragSwipeComponent
实现拖拽排序,侧滑删除效果; - 使用
LxAnimatorComponent
支持ItemAnimator
/BindAnimator
两种方式实现添加布局动画。 - 使用
LxSnapComponent
支持借助SnapHelper
快速实现ViewPager
效果; - 使用
LxExpandable
快速实现分组列表; - 使用
LxNesting
快速实现RecyclerView
的嵌套滑动,返回时自动复位; - 使用
LxPicker
快速实现滚轮选择器效果; - 使用
LxCache
实现缓存,优化绑定耗时问题; - 支持自动检测数据更新的线程,避免出现在子线程更新数据的情况;
- 支持发布订阅模式的事件抽离,更容易分离公共逻辑;
- 支持使用
payloads
实现有效更新; - 支持使用
condition
实现条件更新,按照指定条件更新数据,拒绝无脑刷新;
设计分析
- 数据源统一使用
LxList
,内部借助DiffUtil
实现数据的自动更新,当需要更改数据时,只需要使用它的内部方法即可; - 每种类型是完全分离的,
LxAdapter
作为一个适配器的容器,实际上使用LxItemBinder
来描述如何对该类型进行数据的绑定,事件的响应,以此来保证每种类型数据绑定的可复用性,以及类型之间的独立性; - 拖拽、侧滑、
Snap
使用、动画、选择器、加载更多,这些功能都分离出来,每个功能由单独的component
负责,这样职责更加分离,需要时注入指定的component
即可,也保证了良好的扩展性; - 将类型分为了 内容类型 和 扩展类型 两种,内容类型一般指的是业务数据类型,扩展类型一般是其他的类型,比如
Header/Footer
这种,需要注意的是每种类型、内容类型都需要是连续的。 - 区块的概念,整个列表被分为多个区块,可以按照区块去更新数据,这样在多种类型的列表中可以灵活的更新某种类型的数据,注意,内容类型归属于一个区块,成为内容区块,扩展类型,每种类型属于一个区块,区块里面的数据必须是连续的;
内置的数据类型
TypeOpts
他用来标记一种类型及其附加的相关属性,具体可以看下面的注释说明;
1 | public class TypeOpts { |
LxModel
LxAdapter
的数据类型是 LxModel
,业务类型需要被包装成 LxModel
才能被 LxAdapter
使用,获取其中真正的业务数据可以使用 model.unpack()
方法;
1 | public class LxModel implements Diffable<LxModel>, Typeable, Selectable, Idable, Copyable<LxModel> { |
LxContext
LxContext
是数据绑定过程中的上下文对象,承载了一些附加的数据,易于扩展;
1 | public class LxContext { |
基础:LxGlobal
设置图片加载全局控制:
1 | LxGlobal.setImgUrlLoader((view, url, extra) -> { |
设置全局事件处理,这部份详细的会在下面 事件发布 一节说明:
1 | public static final String CLEAR_ALL_DATA = "CLEAR_ALL_DATA"; |
基础:LxAdapter
一般适配器的使用会有单类型和多类型的区分,不过单类型也是多类型的一种,数据的绑定使用 LxItemBinder
来做,所以 LxAdapter
就只作为一个容器, 不再考虑单类型和多类型的问题;
1 | // 构造数据源 |
基础:数据源
- 数据类型分为两种,内容类型 和 扩展类型;
- 数据源被分为多个区块,内容类型同属于一个区块,扩展类型每种类型属于一个区块;
- 每个区块需要是连续的不能被分隔开;
如下用来理解,区块、类型之间的对应差别:
1 | 列表流开始 |
当声明类型时,同时也决定了两件事情:
- 这个类型是内容类型还是扩展类型
- 类型在列表中相对的排列顺序
1 | // 内容类型1,属于内容区块 |
如果只有一种类型,我们建议使用 LxList
,如果是多类型的需要使用 LxTypedList
,区别就在于在扩展类型的单独更新上;
1 | // 单类型建议使用 LxList |
如何更新数据?
1 | LxList list = new LxTypedList(); |
基础:LxItemBinder
LxAdapter
是完全面向类型的,每种类型的数据绑定会单独处理,这些由 LxItemBinder
负责,这样可以使所有类型绑定更容易复用:
1 | // 自增的数据类型,不需要自己去定义 1、2、3 |
也支持使用构建者模式快速创建新的类型绑定:
1 | TypeOpts opts = TypeOpts.make(R.layout.order_item); |
基础:LxList
LxList
作为 LxAdapter
的数据来源,内部基于 DiffUtil
实现,辅助完成数据的自动比对和更新,彻底告别 notify
更新数据的方式,继承关系如下:
1 | AbstractList -> DiffableList -> LxList -> LxTypedList |
获取区块数据,可以对指定区块发布更新:
1 | LxList list = new LxTypedList(); |
以下是 LxList
内置 增删改查 方法,基本能满足开发需求,另外也可以使用 snapshot
获取快照,然后自定义扩展操作:
1 | // 内部使用 DiffUtil 实现,同步更新 |
增:
1 | // 添加元素 |
删:
1 | // 清空列表 |
改:
1 | // 使用索引更新某一项 |
查:
1 | List<Student> students = list.find(data -> data.getItemType() == TYPE_STUDENT, LxModel::unpack); |
快照更新:
1 | // 获取列表快照, 删除第一个元素, 发布更新 |
数据更新
由于 LxList
列表是基于 LxModel
的,在实际使用过程中,会有些不方便,为了解决这个问题,引入 LxSource
和 LxQuery
来对数据做自动的包装和解包装:
1 | // 初始化测试数据 |
使用 LxSource
构建数据源:
1 | LxSource source = null; |
使用 LxQuery
更方便的完成数据的更新:
1 | // 数据更新辅助类 |
增:
1 | // 增加元素基于 LxSource 实现 |
删:
1 | // 按条件删除元素 |
改:
1 | int index = 10; |
查:
1 | // 按条件查找元素 |
基础:LxViewHolder
为了支持同时对多个控件进行一样的绑定操作,可以使用 Ids
来包含多个 id
:
1 | // 为多个 TextView 设置相同的文字 |
使用 ID R.id.item_view
来标记 holder
的 itemView
:
1 | holder.setClick(R.id.item_view, v -> { |
为了更优雅的绑定数据显示,扩展了 ViewHolder
的功能,现在支持如下绑定方法
1 | holder |
基础:点击事件
点击事件需要在 TypeOpts
设置,单击事件默认是开启的,双击、长按事件需要手动开启;
重写 onBindEvent
方法,根据 eventType
的不同,对不同事件进行处理;
1 | class StudentItemBind extends LxItemBinder<Student> { |
基础:扩展自定义类型
首先声明类型
1 | public static final int TYPE_TEACHER = Lx.contentTypeOf(); |
构建 LxAdapter
和平常一样使用,这里我们使用了 4 种类型:
1 | LxList list = new LxTypedList(); |
添加数据,它们被添加到一个数据源列表中:
1 | LxList list = new LxList(); |
从源数据中获取区块数据,对他们做独立的修改操作:
1 | // 数据源 |
我们发现,拿到每种类型的区块数据后,添加和更改每种特殊的类型,是非常方便的,没有针对性的去做 Header
Footer
这些固定的功能,其实它们只是数据的一种类型,可以按照自己的需要做任意的扩展,这样会灵活很多,其他的比如骨架屏、空载页、加载中效果都可以基于这个实现;
功能:事件发布
一般来说我们数据和视图是分离的,Adapter
的数据源一般会被 Presenter
等逻辑层持有,一般会有以下两个场景:
- 从
Presenter
层有一些数据变化的需要,需要Adapter
响应; - 某一些
Adapter
的响应可以被抽离出来,更好的复用;
因此需要事件发布机制,他在 数据(LxList) 和 视图(Adapter) 中间搭建了一条事件通道,借助它可以发布和响应事件;这有点类似于 EventBus
不过他不是注册在内存中的,是依赖于 LxList
的;
1 | // 事件 |
发布事件:
1 | // 数据源 |
事件也可以被抽象封装出来,作为一些公共的逻辑复用,例如框架内部内置了如下几个事件:
1 | // 设置加载更多开关 |
功能:跨越多列(Span)
当使用 GridLayoutManager
布局时,可能某种类型需要跨越多列,需要针对每种类型进行指定;
1 | static class StudentItemBind extends LxItemBinder<Student> { |
指定一个确定的 SpanSize
通常是不灵活的,因为我们不知道 RecyclerView
在使用时指定的列数 (spanCount),因此建议使用一个标记表示:
1 | Lx.SpanSize.NONE // 不设置,默认值 |
可能这些还不足以兼容到所有情况,可以设置 SpanSize
适配接口,自己来处理这些标记:
1 | // 跨越 1/5 |
功能:间隔(Space)
一般在业务开发中,我们希望布局周边带有一样的间隔,这样比较整齐,一般有两种方案:
- 使用
padding
来做,中间相接的地方就会变为间隔的两倍,不能均分,也可以动态设置左右不同padding
,但是相对耗时耗力; - 使用
ItemDecoration
来做,可以根据位置动态的设置,上下左右间距,但是因为多类型的存在,每种类型的spanSize
不同,很难一下处理好;
为此提供了 LxSpaceComponent
,用来为所有类型布局周边添加相等的间隔,并且在数据增删变动时,也能及时自动修改间距,用法如下:
1 | LxAdapter.of(mLxModels) |
功能:加载更多(LoadMore)
加载更多功能由 LxStartEdgeLoadMoreComponent
和 LxEndEdgeLoadMoreComponent
承担,可以选择性的使用它们;
1 | LxAdapter.of(list) |
功能:选择器(Selector)
主要用于在列表中实现选择器的需求,单选、多选、状态变化等业务场景;
这部分功能交给 LxSelectComponent
1 | LxAdapter.of(list) |
在 BindView
中描述当数据被选中时如何显示:
1 | class SelectItemBind extends LxItemBinder<NoNameData> { |
选中某项时通常只是更改一个标记,我们不希望把整个 BindView
方法执行一遍,这会带来性能的损耗,有时还会造成图片闪烁等问题,当选中被触发时,框架也会发出一个 条件更新 的事件,关于 条件更新 可以参考后面相关的文档,这里简单说一下用法:
1 | class SelectItemBind extends LxItemBinder<NoNameData> { |
滑动选中:使用 LxSlidingSelectLayout
包裹 RecyclerView
会自动和 LxSelectComponent
联动实现滑动选中功能;
1 | <com.zfy.adapter.decoration.LxSlidingSelectLayout |
功能:列表动画(Animator)
动画分为了两种:
- 一种是
BindAnimator
,在onBindViewHolder
里面执行; - 一种是
ItemAnimator
, 是RecyclerView
官方的支持方案;
这部分功能由 LxBindAnimatorComponent
和 LxItemAnimatorComponent
完成;
BindAnimator
内置了以下几种,还可以再自定义扩展:
- BindAlphaAnimator
- BindScaleAnimator
- BindSlideAnimator
1 | LxAdapter.of(list) |
也可以分类型指定动画,每种类型给予不同的动画效果
1 | class StudentItemBind extends LxItemBinder<Student> { |
ItemAnimator
这部分参考 wasabeef-recyclerview-animators 实现,它可以提供更多动画类型的实现。
1 | LxAdapter.of(list) |
功能:悬挂效果(Fixed)
针对每种类型悬挂效果,可以支持所有类型所有布局文件的顶部悬挂效果,需要使用 LxFixedComponent
实现,支持两种实现方式:
- 采用绘制的方式,优点是悬挂的视图有挤压效果,效率上也更好,但是因为是绘制的所以不支持点击事件,可以采用覆盖一层
View
来解决这个问题; - 采用生成
View
的方式,优点是实实在在的View
,点击事件什么的自然都支持,缺点是你需要提供一个容器,而且视图之间没有挤压的效果;
1 | LxAdapter.of(list) |
同时在 TypeOpts
中说明哪些类型需要支持悬挂
1 | class StudentItemBind extends LxItemBinder<Student> { |
功能:拖拽和侧滑(drag/swipe)
针对每种类型支持拖拽和侧滑功能,由 LxDragSwipeComponent
完成该功能;
- 关注配置项,配置项决定了该类型的响应行为;
- 支持长按、触摸触发相应的响应;
- 支持全局自动触发和手动触发两种方式;
首先定义拖拽、侧滑得一些配置参数:
1 | public static class DragSwipeOptions { |
然后使用 LxDragSwipeComponent
完成拖拽、侧滑功能:
1 | LxDragSwipeComponent.DragSwipeOptions options = new LxDragSwipeComponent.DragSwipeOptions(); |
最后在 TypeOpts
里面配置该类型是否支持侧滑和拖拽,这样可以灵活的控制每种类型数据的行为:
1 | class StudentItemBind extends LxItemBinder<Student> { |
手动触发:使用以上方法会为整个 item
设置拖拽和侧滑响应,你可以指定某个控件触发这些操作,为了避免冲突我们现在配置项中关闭自动触发逻辑:
1 | LxDragSwipeComponent.DragSwipeOptions options = new LxDragSwipeComponent.DragSwipeOptions(); |
然后在 onBindView
时,手动关联触发操作:
1 | class StudentItemBind extends LxItemBinder<Student> { |
功能:实现 ViewPager (Snap)
内部使用 SnapHelper
实现,很简单,只是要把他封装成 LxComponent
的形式,统一起来,由 LxSnapComponent
实现;
1 | LxAdapter.of(list) |
模拟 ViewPager
添加了 OnPageChangeListener
1 | LxAdapter.of(mLxModels) |
功能:实现分组列表(Expandable)
基于我们基本的设计架构是可以很轻松的实现分组列表效果的,但是这个场景用到的时候比较多,所以内置一些辅助类,用来更好、更简单的实现分组列表;
针对分组列表的场景设计了 LxExpandable
辅助类;
首先 组 的数据结构需要实现接口 LxExpandable.ExpandableGroup
:
1 | static class GroupData implements LxExpandable.ExpandableGroup<GroupData, ChildData> { |
然后 子 的数据结构需要实现接口 LxExpandable.ExpandableChild
:
1 | static class ChildData implements LxExpandable.ExpandableChild<GroupData, ChildData> { |
然后定义的 GroupItemBind
和 ChildItemBind
:
点击分组可以展开或者收起当前的分组子数据:
1 | static class GroupItemBind extends LxItemBinder<GroupData> { |
点击子数据,可以删除当前子数据:
1 | static class ChildItemBind extends LxItemBinder<ChildData> { |
生成 LxAdapter
:
1 | LxAdapter.of(mLxModels) |
我们模拟一些假数据:
1 | List<GroupData> groupDataList = new ArrayList<>(); |
是不是很简单啊,感觉上还是写了一些代码,没有一行代码实现xxx 的感觉,只是提供一个思路,如果类库内部接管太多业务逻辑其实是不友好的,可以看下 LxExpandable
的代码,其实就是对数据处理的一些封装,基于基本的设计思想很容易抽离出来;
功能:实现嵌套滑动(Nesting)
开发中有种比较常见的场景,垂直的列表中,嵌套横向滑动的列表:
- 横向滑动和纵向滑动事件不能冲突;
- 上下滑动时,不能因为加载横向的列表造成滑动的卡顿;
- 滑动过的横向列表,再回来时,要保持原先的滑动状态;
针对这种场景,设计了 LxNesting
辅助工具;
最外层列表的使用跟之前一样就不再赘述了,主要说一下横向列表如何使用 LxNesting
1 | class NestingItemBinder extends LxItemBinder<NoNameData> { |
功能:实现滚轮选择器效果(Picker)
使用 LxPicker
实现滚轮选择器效果,内部使用 LxPickerComponent
+ LxSnapComponent
实现;
当多个选择器级联时,第一个选择后接着就会触发第二个选择,达到递归触发的效果;
1 | // 配置 |
数据绑定很简单,可以自己实现
1 | static class AddressItemBinder extends LxItemBinder<AddressPickItemBean> { |
进阶:使用缓存优化绑定性能
当列表滑动时,onBindView
方法会被执行很多次,因此如果在 onBindView
中执行了耗时操作就会影响列表的流畅度;应该尽量避免在 bind
方法中避免计算等操作,一些不会变的数据我们可以将其缓存起来,这部分功能借助 LxCache
实现;
以下是一个简单的例子,使用 Id
作为唯一标识
- 注册
Mapper
用户计算数据结果; - 使用
cache.getString()
获取结果;
1 | public class StudentItemBinder extends LxItemBinder<Student> { |
如果数据发生了变化,需要清除缓存,清除后数据下次绑定时数据会重新计算:
1 | LxCache.remove(R.id.time_tv, model); |
进阶:使用 Extra 扩展数据
在 LxModel
中增加了 extra
他是一个 bundle
类型的数据,可以在不增加字段的情况下扩展一下临时用的数据;
1 | LxModel model; |
进阶:使用 Idable 优化 change
使用 DiffUtil
比对数据时,类库不知道它们是不是同一个对象,会使用一个自增的 ID
作为唯一标示,以此来触发 notifyDataSetChange
,所以当你更改列表中的一个数据时,只会执行一次绑定,这是内部做的优化;
这也意味着每次创建对象这个 ID
都将改变,也就是说学生A 和 学生A,并不是同一个学生,因为这关系到使用者具体的业务逻辑,不过你可以通过实现 Idable
接口来返回你自己的业务 ID
,当然这不是必须的。
1 | static class Student implements Idable { |
进阶:使用 Typeable 内置类型
如果你的数据对象只有一个类型,也可以使用数据类实现 Typeable
接口,在接口方法中返回类型,这样打包数据的时候就不需要指定类型了,内部会检测是否是 Typeable
子类,获取真正的类型;
1 | static class InnerTypeData implements Typeable { |
进阶:使用条件更新
- 场景1:我们的数据并没有改变,但是我们仍旧想触发数据的更新;
- 场景2:只想更新一个控件,比如下载进度条,这个更新比较频繁,但是不想做不必要的刷新;
基于以上两种应用场景,条件更新应运而生,你可以不改变数据,但是触发更新,并且可以指定条件,仅刷新一个控件的显示,类似 payloads 但是不需要计算有效载荷,只需要制定一个条件即可;
1 | public static final String KEY_NEW_CONTENT = "KEY_NEW_CONTENT"; |
进阶:使用有效载荷(payloads)更新
某些场景我们只更改了一部分数据,但是会触发 notifyDataSetChanged
重新执行整个条目的绑定,这样会造成性能的损耗,有时图片要重新加载,很不友好,因此我们需要 payploads
更新的方式;
payloads
可以被称为有效载荷,它记录了哪些数据是需要被更新的, 我们只更新需要的那部分就可以了,既然称为有效载荷那么他肯定是需要比对和计算的,为了实现它需要自定义这个比对规则,我们看下以下比对方法的简单介绍:
areItemsTheSame
当返回 true 的时候表示是相同的元素,调用
areContentsTheSame
,推荐使用id
比对
当返回 false 的时候表示是一个完全的新元素,此时会调用insert
和remove
方法来达到数据更新的目的
areContentsTheSame
用来比较两项内容是否相同,只有在
areItemsTheSame
返回true
时才会调用
返回true
表示内容完全相同不需要更新
返回false
表示虽然是同个元素但是内容改变了,此时会调用changed
方法来更新数据
getChangePayload
只有在
areItemsTheSame
返回true
时才会调用,areContentsTheSame
返回false
时调用
返回更新事件列表,会触发payload
更新
为了实现它,需要对数据对象进行一些更改:
- 实现
Diffable
接口,声明比对规则 - 实现
Copyable
接口,实现对象的拷贝,如果对象有嵌套,可能需要嵌套拷贝; - 实现
Parcelable
接口,作用同Copyable
,写起来简单,但是性能会差一些,二选一即可;
1 | class Student implements Diffable<Student>, Copyable<Student> { |
这样我们就通过比对拿到了 payloads
, 那我们如何使用这些有效载荷呢?
1 | class StudentItemBind extends LxItemBinder<Student> { |
联系我
Android开发技术交流 | 微信 |
---|---|