
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开发技术交流 | 微信 |
|---|---|
![]() |
![]() |











