Square Cycler – RecyclerView API
前言
不用adapter和ViewHolder也可以使用RecycleView,你知道吗?
Cycler,是Square的开源项目,目的是以简洁的方式使用配置Android RecycleView。将Adapter 和ViewHoler,DiffUtil,ItemTouchHelper.Callback等封装起来。使用时,不用创建adapter,viewHoler,方便快捷。Update类维护数据源,且在内部刷新数据时,会去计算数据源的差异,调用不同的adapter 的刷新数据的方法。
首先配上官方使用demo
demo1:
override fun config(recyclerView: RecyclerView) {
// 返回的是Recycler
cycler = Recycler.adopt(recyclerView) {
//根据StandardRowSpec会创建ViewHolder
//Item ,Discount,GrandTotal 都是数据Bean
//ItemView,ConstraintLayout ,TextView 是数据bean 对应的ItemView
row<Item, ItemView> {
create { context ->
view = ItemView(context, showDragHandle = false)
bind { index, item ->
view.show(item, index % 2 == 0)
}
}
}
row<Discount, ConstraintLayout> {
//自定义的匹配规则
forItemsWhere { it.isStarred }
create(R.layout.starred_discount) {
val amount = view.findViewById<TextView>(R.id.amount)
bind { discount -> amount.text = discount.amount.format() }
}
}
row<Discount, ConstraintLayout> {
create(R.layout.discount) {
val amount = view.findViewById<TextView>(R.id.amount)
bind { discount -> amount.text = discount.amount.format() }
}
}
extraItem<GrandTotal, TextView> {
create { context ->
view = TextView(context)
view.setTextAppearance(R.style.TextAppearance_Bold)
bind { total ->
view.text = "Grand total: ${total.total.format()}"
}
}
}
}
update()
}
// 更新数据
private fun update() {
val total = data
.sumByFloat { it.amount }
.coerceAtLeast(0f)
cycler.update {
// toDataSource 生成DataSource<T>,复制给Update的data成员变量
data = this@SimplePage.data.toDataSource()
extraItem = if (showTotal) GrandTotal(total) else null
}
}
}
这样就使用了一个简单的Recycleview,三个不同类型的ItemView和一个扩展的ItemView。
demo2:
cycler = Recycler.adopt(recyclerView) {
row<Item, ItemView> {
create { context ->
view = ItemView(context, showDragHandle = true)
//设定ItemView拖动的View
dragHandle(view.dragHandle)
bind { index, item ->
view.show(item, index % 2 == 0)
}
}
}
enableMutations {
// 上下拖动的开关
dragAndDropEnabled = true
//左右移除的开关
swipeToRemoveEnabled = true
onMove { from, to ->
Toast.makeText(
recyclerView.context,
"Moved index $from to index $to",
Toast.LENGTH_SHORT
).show()
}
//左右移除的回调函数
onSwiped { it ->
Toast.makeText(
recyclerView.context,
"Swiped ${it.name} away",
Toast.LENGTH_SHORT
).show()
}
}
}
update()
这样简单的几句,就创建了Recyclview并且可以左右移除,和上下拖拽
Recycler类
内部维护adapter和Config等
创建Recycler
创建Recycler有两种方式:create 和 adopt 都是Recycler的静态方法,返回值是 Recycler
-
如果项目中不存在RecycleView
使用create()方法,创建RecycleView,并且Recycler和RecycleView相关联@param context 上下文环境 @param id 为RecyclewView指定id @param layoutProvider 为RecyclewView指定布局管理器 @param block Recycle的一些配置函数 inline fun <I : Any> create( context: Context, id: Int = View.NO_ID, noinline layoutProvider: (Context) -> LayoutManager = { LinearLayoutManager(it) }, crossinline block: Config<I>.() -> Unit ): Recycler<I> { val view = RecyclerView(context) view.id = id return adopt(view, layoutProvider, block) }
-
如果项目中存在RecycleView
使用adopt() 将Recycler和RecycleView相关联。并且create最后也调用的是adopt()@param view RecycleView @param layoutProvider 为RecyclewView指定布局管理器 @param block Recycle的一些配置函数 inline fun <I : Any> adopt( view: RecyclerView, noinline layoutProvider: ((Context) -> LayoutManager)? = null, crossinline block: Config<I>.() -> Unit ): Recycler<I> { layoutProvider?.invoke(view.context) .let { view.layoutManager = it } requireNotNull(view.layoutManager) { "RecyclerView needs a layoutManager assigned. " + "Assign one to the view, or pass a layoutProvider argument." } return Config<I>() .apply(block) .setUp(view) }
注意: 如果在adopt 中不去设置LayoutManager会崩溃。而我们在以前用RecycleView,忘记设置LayoutManager,为空白。
Config 配置块
config 配置块将具有一些常规配置,例如数据比较器,每种itemView类型的定义。以及用来将数据bean和itemview 相关联.(核心)
class Config<I : Any> @PublishedApi internal constructor() {
// rowSpecs 储存在config中创建的StandardRowSpec,后面会根据这个生成ViewHolder
@PublishedApi internal val rowSpecs = mutableListOf<RowSpec<I, *, *>>()
// extraItemSpecs 格外的ItemView
@PublishedApi internal val extraItemSpecs = mutableListOf<RowSpec<Any, *, *>>()
internal val extensionSpecs = mutableListOf<ExtensionSpec<I>>()
internal var hasStableIdProvider = false
// 提供一个id比较函数
internal var stableIdProvider: StableIdProvider<I> = {
throw java.lang.IllegalStateException("stableIdProvider not set.")
}
// 提供内容比较器。stableIdProvider和contentComparator都定义,将会生成一个默认的比较器
internal var contentComparator: ContentComparator<I>? = null
/**
* 更新数据必须在创建ViewRootImpl的线程中(通常是UI线程)
*/
var mainDispatcher: CoroutineDispatcher = Dispatchers.Main
/**
* 设定比较数据的线程,默认是IO线程
*
*/
var backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO
/**
* 提供stableIdProvider和contentComparator就会生成itemComparator比较器,用于比较数据
*
*/
var itemComparator: ItemComparator<I>? = null
/**
* 提供stableIdProvider 自定义,为每个item数据提供一个识别号(一般是id)
*/
fun stableId(block: StableIdProvider<I>) {
stableIdProvider = block
hasStableIdProvider = true
}
/**
* 提供 内容比较 lambda
* stableId 和compareItemsContent 必须都调用,才能生成比较器
*/
fun compareItemsContent(block: ContentComparator<I>) {
contentComparator = block
}
/**
*
* 生成StandardRowSpec ,并且添加到rowSpecs
* 生成ItemView (函数重载)
*/
inline fun <reified S : I, reified V : View> row(
crossinline specBlock: StandardRowSpec<I, S, V>.() -> Unit
) {
row(
StandardRowSpec<I, S, V> { it is S }.apply {
specBlock()
}
)
}
/**
* 生成ItemView (函数重载)
*/
inline fun <reified S : I, reified V : View> row(
noinline creatorBlock: (CreatorContext) -> V,
noinline bindBlock: (Int, S, V) -> Unit
) {
// Converts the bindBlock in a BinderRowSpec block.
row<S, V>(creatorBlock) { -> bind(bindBlock) }
}
/**
* 生成ItemView (函数重载)
*/
inline fun <reified S : I, reified V : View> row(
noinline creatorBlock: (CreatorContext) -> V,
crossinline specBlock: BinderRowSpec<I, S, V>.() -> Unit
) {
row(
BinderRowSpec<I, S, V>(
typeMatchBlock = { it is S },
creatorBlock = creatorBlock
).apply { specBlock() }
)
}
/**
* 定义格外项
*/
inline fun <reified S : Any, reified V : View> extraItem(
crossinline specBlock: StandardRowSpec<Any, S, V>.() -> Unit
) {
extraItem(
StandardRowSpec<Any, S, V> { it is S }.apply {
specBlock()
}
)
}
/**
* 生成比较器
* 提供stableIdProvider和contentComparator 则生成,否则为null
*/
internal fun createItemComparator(): ItemComparator<I>? {
return itemComparator ?: stableIdProvider.let { ids ->
contentComparator?.let { contentComp ->
DefaultItemComparator(ids, contentComp)
}
}
}
fun row(rowSpec: RowSpec<I, *, *>) {
rowSpecs.add(rowSpec)
}
@PublishedApi internal fun extraItem(rowSpec: RowSpec<Any, *, *>) {
extraItemSpecs.add(rowSpec)
}
/**
* 添加回调函数,(比如:左右移除ItemView,上下移动ItemView)
* 不能直接调用
*/
fun extension(spec: ExtensionSpec<I>) {
extensionSpecs.add(spec)
}
/**
*
* 生成Recycler
*/
@PublishedApi internal fun setUp(view: RecyclerView): Recycler<I> {
return Recycler(view, this)
}
}
用例代码如下:
val block: Recycler.Config<BaseItem>.() -> Unit = {
//生成ItemView ,将ItemView和数据源进行绑定
row<Item, ItemView> {
create { context ->
view = ItemView(context, showDragHandle = false)
bind { index, item ->
view.show(item, index % 2 == 0)
}
}
}
//定义不同的 ItemView
row<Discount, ConstraintLayout> {
//生成ItemView
create(R.layout.discount) {
val amount = view.findViewById<TextView>(R.id.amount)
// 数据进行绑定
bind { discount -> amount.text = discount.amount.format() }
}
}
//定义不同的 ItemView
row<Discount, ConstraintLayout> {
// 自定义的匹配规则
forItemsWhere { true }
create(R.layout.starred_discount) {
val amount = view.findViewById<TextView>(R.id.amount)
bind { discount -> amount.text = discount.amount.format() }
}
}
// 格外的ItemView 只可以定义一个
extraItem<GrandTotal, TextView> {
create { context ->
view = TextView(context)
view.setTextAppearance(R.style.TextAppearance_Bold)
bind { total ->
view.text = "Grand total: ${total.total.format()}"
}
}
}
}
Config.row()中会生成StandardRowSpec,并且调用rowSpecs.add()
StandardRowSpec.create() 生成Creator(内部维护View 和bind 函数);
Item ,Discount,GrandTotal 都是数据Bean
ItemView,ConstraintLayout ,TextView 是数据bean 对应的ItemView
StandardRowSpec extends RowSpec ,内部有ViewHolder.我们在config内部定义的行。
在Adapter的onCreateViewHolder 会去调用StandardRowSpec.onCreateViewHolder
生成一个Viewholder。每一个行都会对应生成一个Viewholder。
数据更新
Update 类
数据更新的类
维护两个数据 一个是data ,ItemView 的数据源
另一个是extraItem ,扩展ItemView 的数据源
toDataSource() 是一个扩展函数,生成DataSource<T> DataSoure 用来判断数据源是否有效
示例用法:
// 将数据赋值给Update 类
cycler.update {
data = this@SimplePage.data.toDataSource()
extraItem = GrandTotal
}
RecyclerDiff 类
/**
* 实现自Android 的DiffUtil.Callback()
*/
class DataSourceDiff<T>(
private val helper: ItemComparator<T>,
private val oldList: DataSource<T>,
private val newList: DataSource<T>
) : DiffUtil.Callback() {
// 得到就旧数据大小
override fun getOldListSize(): Int = oldList.size
// 得到新数据大小
override fun getNewListSize(): Int = newList.size
// 新老数据集Item是否是同一个 (一般比较的id)
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
helper.areSameIdentity(oldList[oldItemPosition], newList[newItemPosition])
//比较同一个item 的内容是否相同(上面方法返回true 时,会调用此方法)
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
helper.areSameContent(oldList[oldItemPosition], newList[newItemPosition])
//Object就是表示Item改变了哪些内容(上面方法返回false 时,会调用此方法)
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int) = null
}
/**
* 如果我们定义了 idsProvider & contentComparator 函数,Adapter在init时候就会去创建这个Item比较器
*
*/
internal class DefaultItemComparator<I>(
val idsProvider: StableIdProvider<I>,
val contentComparator: ContentComparator<I>
) : ItemComparator<I> {
override fun areSameIdentity(
oldItem: I,
newItem: I
): Boolean {
return idsProvider(oldItem) == idsProvider(newItem)
}
override fun areSameContent(
oldItem: I,
newItem: I
): Boolean {
return contentComparator(oldItem, newItem)
}
}
- currentUpdate = new Update()
- 调用更新上的函数
- 异步计算 currentData 和 CurrentUpdate 差异 (利用DiffUtil 传入数据,根据比较结果的不同,调用不同的更新函数)
ItemView的回调函数
为itemView设置上下拖动,左右移除的回调函数
示例用法:
/**
*
* 在Config 中进行配置 使用比较方便
*/
enableMutations {
// 拖拽开关
dragAndDropEnabled = true
// 左右移除开关
swipeToRemoveEnabled = true
// 长按开关 if(没有设置draghandle ,打开这个开关,长按上下拖动才有效。反之,如果设置了dranghangle ,则不需要打开这个开关,点击设置的view ,就可以上下拖动)
longPressDragEnabled = true
// 上下拖拽 回调的函数
onDragDrop{originalIndex, dropIndex ->
Toast.makeText(
recyclerView.context,
"onDragDrop ${originalIndex} away",
Toast.LENGTH_SHORT
).show()
}
// 左右移除 回调的函数
onSwiped { it ->
Toast.makeText(
recyclerView.context,
"Swiped ${it.name} away",
Toast.LENGTH_SHORT
).show()
}
MutationExtension 中成员变量 MyItemTouchCallback
MyItemTouchCallback 继承 ItemTouchHelper.Callback,当onMove,onSwiped 回调MyItemTouchCallback.函数,在MyItemTouchCallback中调用MutationExtension中定义的函数
Java调用
Recycler 底层是kotlin写的
调用时用kotlin比较方便,用Java涉及到大量的Fuction 函数( lambda),代码不优雅美观,缺乏可读性。
// 第一个行的定义
StandardRowSpec<BaseItem, BaseItem.Discount, ItemView> rowSpec1 = new StandardRowSpec<BaseItem, BaseItem.Discount, ItemView>(baseItem -> true);
rowSpec1.forItemsWhere(discount -> !discount.isStarred());
rowSpec1.create(R.layout.discount, creator -> {
TextView textView = ((StandardRowSpec.Creator) creator).getView().findViewById(R.id.amount);
((StandardRowSpec.Creator) creator).bind(o -> {
if (o != null && o instanceof BaseItem.Discount) {
textView.setText(String.valueOf(((BaseItem.Discount) o).getAmount()));
}
return null;
});
return null;
});
// 第二个行的定义
StandardRowSpec<BaseItem, BaseItem.Discount, ConstraintLayout> rowSpec2 = new StandardRowSpec<BaseItem, BaseItem.Discount, ConstraintLayout>(baseItem ->
true
);
rowSpec2.forItemsWhere(discount -> discount.isStarred());
rowSpec2.create(R.layout.starred_discount, creator -> {
if (creator instanceof StandardRowSpec.Creator) {
TextView textView = creator.getView().findViewById(R.id.amount);
((StandardRowSpec.Creator) creator).bind(o -> {
if (o instanceof BaseItem.Discount) {
textView.setText(String.valueOf(((BaseItem.Discount) o).getAmount()));
}
return null;
});
}
return null;
});
Recycler<BaseItem> recycler = Recycler.Companion.adopt(recyclerView,
context -> new LinearLayoutManager(context, RecyclerView.VERTICAL, false),
config -> {
config.row(rowSpec1);
config.row(rowSpec2);
return null;
});
recycler.update(update -> {
if (update instanceof Update) {
((Update) update).setData(new DataSource() {
@Override
public boolean isEmpty() {
return list.isEmpty();
}
@Override
public Object get(int i) {
return list.get(i);
}
@Override
public int getSize() {
return list.size();
}
});
}
return null;
}
);