Litho的使用–下拉刷新上拉加载
上篇介绍了Litho的基本使用,本文看下日常操作RecyclerView的嵌套使用以及下拉刷新上拉加载。
准备工作
先模拟一下网络请求以及使用的bean类
@Event
class RvListModel {
//kotlin默认成员变量注解后,反射修饰符为private
//@JvmField注解过后的对象,编译后修饰符变为默认修饰符public
@JvmField
var list: MutableList<RvItemBean>? = null
}
这里的@Event注解后面会用到,注入列表数据的。其中RvItemBean:
class RvItemBean {
var title: String? = null
var content: String? = null
var clickStr: String? = null
var isSubRv: Boolean = false//item是否是子RecyclerView
var iconRes: Int = 0
var imageUrl: String? = null
}
bean类很简单,下面看下模拟的网络请求工具类:
class DataService {
private val random = Random()
private var dataModelEventHandler: EventHandler<RvListModel>? = null
fun registerLoadingEvent(dataModelEventHandler: EventHandler<RvListModel>) {
this.dataModelEventHandler = dataModelEventHandler
}
fun unregisterLoadingEvent() {
this.dataModelEventHandler = null
}
/**
* 加载数据
*/
fun fetch(start: Int, count: Int) {
//延迟2s模拟刷新操作
Handler().postDelayed({
val rvListModel = getData(start, count)
//分发消息,更新界面,会走到onDataLoaded
dataModelEventHandler!!.dispatchEvent(rvListModel)
}, 2000)
}
/**
* 刷新操作
*/
fun reFetch(start: Int, count: Int) {
Handler().postDelayed({
val rvListModel = getData(start, count)
//分发消息,更新界面,会走到onDataLoaded
dataModelEventHandler!!.dispatchEvent(rvListModel)
}, 2000)
}
fun getData(start: Int, count: Int): RvListModel {
val rvListModel = RvListModel()
var list = mutableListOf<RvListModel>()
rvListModel.list = ArrayList()
for (i in start until start + count) {
val rvItemBean = getRvItemBean(i)
rvListModel.list!!.add(rvItemBean)
}
return rvListModel
}
private fun getRvItemBean(i: Int): RvItemBean {
val rvItemBean = RvItemBean()
rvItemBean.title = "我是标题${(random.nextInt(10) + i)}"
rvItemBean.content = "我是内容$i"
rvItemBean.iconRes = R.mipmap.ic_launcher
rvItemBean.imageUrl = mImgUrls[0]
rvItemBean.clickStr = "click$i"
rvItemBean.isSubRv = true
if (random.nextInt(10) % 3 == 0) {
rvItemBean.imageUrl = mImgUrls[random.nextInt(mImgUrls.size)]
rvItemBean.isSubRv = false
}
return rvItemBean
}
/**
* 网络图片资源,便于随机取图片
*/
private val mImgUrls = arrayOf(
"http://pic37.nipic.com/20140113/8800276_184927469000_2.png",
"http://k.zol-img.com.cn/sjbbs/7692/a7691515_s.jpg",
"http://pic9.nipic.com/20100923/2531170_140325352643_2.jpg",
"http://pic25.nipic.com/20121205/10197997_003647426000_2.jpg",
"http://img1.imgtn.bdimg.com/it/u=1483033257,4287748004&fm=26&gp=0.jpg",
"http://img2.imgtn.bdimg.com/it/u=667158997,739004683&fm=26&gp=0.jpg",
"http://img4.imgtn.bdimg.com/it/u=852943962,2632646500&fm=26&gp=0.jpg",
"http://img3.imgtn.bdimg.com/it/u=1451961535,3314663922&fm=26&gp=0.jpg",
"http://img1.imgtn.bdimg.com/it/u=1533275126,1287779573&fm=26&gp=0.jpg",
"http://img3.imgtn.bdimg.com/it/u=2132929001,1562758156&fm=26&gp=0.jpg",
"http://img3.imgtn.bdimg.com/it/u=1239957118,2147373077&fm=26&gp=0.jpg",
"http://img5.imgtn.bdimg.com/it/u=3558462636,3096335901&fm=26&gp=0.jpg",
"http://img1.imgtn.bdimg.com/it/u=3893146502,314297687&fm=26&gp=0.jpg",
"http://img2.imgtn.bdimg.com/it/u=1409224092,1124266154&fm=26&gp=0.jpg"
)
}
已经做了简单的注释,一看就懂。
RecyclerView的使用
做了准备工作,直接开干,先造一个item的Spec
@LayoutSpec
object SubRvItemSpec {
@OnCreateLayout
fun onCreateLayout(c: ComponentContext, @Prop imgRes: Int?): Component {
return card(c){
paddingDip(YogaEdge.ALL, 16f)
backgroundColor(Color.WHITE)
content(image(c){
drawableRes(imgRes ?: R.mipmap.ic_launcher)
})
clickHandler(ImageViewComponent.onClick(c, "click native"))
}.build()
}
@OnEvent(ClickEvent::class)
fun onClick(c: ComponentContext, @FromEvent view: View, @Param someProp: String) {
Toast.makeText(c.applicationContext, "click:$someProp", Toast.LENGTH_SHORT).show()
}
}
Litho中的RecyclerView需要一个Section组件生成列表,如下:
@GroupSectionSpec
object SubRvSectionSpec {
@OnCreateChildren
fun onCreateChildren(c: SectionContext, @Prop list: List<Int>): Children {
val builder = Children.create()
for (i in list.indices) {
builder.child(
SingleComponentSection.create(c)
.key(i.toString())
.component(
SubRvItem.create(c)
.imgRes(list[i])
.build()
)
)
}
return builder.build()
}
}
编译后,会生成SubRvItem和SubRvSection,Litho使用RecyclerView是通过RecyclerCollectionComponent构建的:
recyclerCollectionComponent(c){
heightDip(100f)
recyclerConfiguration(ListRecyclerConfiguration.create().orientation(LinearLayoutManager.HORIZONTAL).reverseLayout(false).build())
section(SubRvSection.create(SectionContext(c))
.list(RvItemSpec.getImageArray(bean))
.build())
disablePTR(true)
}.build()
其中disablePTR(true),设置为true,不要下拉刷新。这里就不再试了,最后运行一下嵌套的RecyclerView看下效果。
嵌套的RecyclerView
首先看item的spec:
@LayoutSpec
object RvItemSpec {
@OnCreateLayout
fun onCreateLayout(c: ComponentContext, @Prop bean: RvItemBean): Component {
return column(c){
paddingDip(YogaEdge.ALL, 16f)
backgroundColor(Color.WHITE)
child(text(c){
marginDip(YogaEdge.TOP, 8f)
marginDip(YogaEdge.LEFT, 16f)
marginDip(YogaEdge.RIGHT, 16f)
marginDip(YogaEdge.BOTTOM, 8f)
text(bean.title)
textSizeSp(40f)
})
child(text(c){
marginDip(YogaEdge.LEFT, 16f)
marginDip(YogaEdge.RIGHT, 16f)
marginDip(YogaEdge.BOTTOM, 8f)
text(bean.content)
textSizeSp(20f)
})
child(if (bean.isSubRv) getSubRv(c, bean) else getImgUrlComponent(c, bean))
clickHandler(ImageViewComponent.onClick(c, bean.clickStr))
}
}
/**
* 图片组件
*/
private fun getImgUrlComponent(c: ComponentContext, bean: RvItemBean): GlideImage {
return GlideImage.create(c)
.heightDip(200f)
.imageUrl(bean.imageUrl?:"")
.build()
}
/**
* 嵌套的子RecyclerView组件
*/
private fun getSubRv(c: ComponentContext, bean: RvItemBean): Component {
return recyclerCollectionComponent(c){
heightDip(100f)
recyclerConfiguration(ListRecyclerConfiguration.create().orientation(LinearLayoutManager.HORIZONTAL).reverseLayout(false).build())
section(SubRvSection.create(SectionContext(c))
.list(RvItemSpec.getImageArray(bean))
.build())
disablePTR(true)
}.build()
}
/**
* 获取子RecyclerView的图片列表,图片都用默认图片,这里只做个数的随机
*/
private fun getImageArray(bean: RvItemBean): List<Int> {
val images = ArrayList<Int>()
for (i in 0 until Random().nextInt(5) + 1) {
images.add(bean.iconRes)
}
return images
}
@OnEvent(ClickEvent::class)
fun onClick(c: ComponentContext, @FromEvent view: View, @Param someProp: String) {
Toast.makeText(c.applicationContext, "click:$someProp", Toast.LENGTH_SHORT).show()
}
}
主要是根据bean类的isSubRv字段判断是普通图片组件还是子RecyclerView组件。下面看下SectionSpec,这个是重头戏:
@GroupSectionSpec
object RvSectionSpec {
private const val TAG = "RvSectionSpec"
/**
* 初始化数据
* StateValue的变量名要与@State注解的变量名保持一致
*/
@OnCreateInitialState
fun createInitialState(c: SectionContext, list: StateValue<MutableList<RvItemBean>>, start: StateValue<Int>, count: StateValue<Int>, isFetching: StateValue<Boolean>) {
start.set(0)
count.set(15)
list.set(DataService().getData(0, 15).list)
isFetching.set(false)
}
/**
* 布局
*/
@OnCreateChildren
fun onCreateChildren(c: SectionContext, @State list: MutableList<RvItemBean>): Children {
val builder = Children.create()
for (i in list.indices) {
builder.child(
SingleComponentSection.create(c)
.key(i.toString())
.component(
RvItem.create(c)
.bean(list[i])
.build()
)
)
}
//上拉加载的进度条
builder.child(
SingleComponentSection.create(c)
.component(ProgressLayout.create(c))
.build()
)
return builder.build()
}
/**
* 创建请求服务
*/
@OnCreateService
fun onCreateService(c: SectionContext, @State list: MutableList<RvItemBean>, @State start: Int, @State count: Int): DataService {
return DataService()
}
/**
* 绑定请求服务
*/
@OnBindService
fun onBindService(c: SectionContext, service: DataService) {
service.registerLoadingEvent(RvSection.onDataLoaded(c))
}
/**
* 解绑请求服务
*/
@OnUnbindService
fun onUnbindService(c: SectionContext, service: DataService) {
service.unregisterLoadingEvent()
}
/**
* 接受到获取数据的消息
*/
@OnEvent(RvListModel::class)
fun onDataLoaded(c: SectionContext, @FromEvent list: MutableList<RvItemBean>) {
//更新数据,看源码可以看到走到了updateData设置数据,
// 然后调用了SectionTree的updateState,利用CalculateChangeSetRunnable,走到了calculateNewChangeSet,
// 调用createNewTreeAndApplyStateUpdates,里面会调用nextRoot.createChildren更新视图。这里也体现了Litho的异步measure、layout
RvSection.updateData(c, list)
//设置已经获取数据
RvSection.setFetching(c, false)
//发送刷新已经完成消息,即隐藏刷新进度条
SectionLifecycle.dispatchLoadingEvent(c, false, LoadingEvent.LoadingState.SUCCEEDED, null)
}
/**
* 更新数据
*/
@OnUpdateState
fun updateData(list: StateValue<MutableList<RvItemBean>>, start: StateValue<Int>, @Param newList: MutableList<RvItemBean>) {
if (start.get() == 0) {
list.set(newList)
} else {
val listContain =mutableListOf<RvItemBean>()
listContain.addAll(list.get()!!)
listContain.addAll(newList)
list.set(listContain)
}
}
/**
* 下拉刷新
*/
@OnRefresh
fun onRefresh(c: SectionContext, service: DataService, @State list: MutableList<RvItemBean>, @State start: Int, @State count: Int) {
RvSection.updateStartParam(c, 0)
service.reFetch(0, 15)
}
/**
* 设置是否刷新,并保存到State中的isFetching属性
*/
@OnUpdateState
fun setFetching(isFetching: StateValue<Boolean>, @Param fetch: Boolean) {
isFetching.set(fetch)
}
/**
* 更新
*/
@OnUpdateState
fun updateStartParam(start: StateValue<Int>, @Param newStart: Int) {
start.set(newStart)
}
/**
* 滑动监听,可以用来判断上拉加载
*/
@OnViewportChanged
fun onViewportChanged(c: SectionContext, firstVisiblePosition: Int, lastVisiblePosition: Int, firstFullyVisibleIndex: Int, lastFullyVisibleIndex: Int, totalCount: Int, service: DataService, @State list: MutableList<RvItemBean>, @State start: Int, @State count: Int, @State isFetching: Boolean) {
Log.e(TAG, "firstVisiblePosition=$firstVisiblePosition;lastVisiblePosition=$lastVisiblePosition;firstFullyVisibleIndex=$firstFullyVisibleIndex;lastFullyVisibleIndex=$lastFullyVisibleIndex;totalCount=$totalCount;list.size()=${list.size};start=$start count=$count isFetching=$isFetching")
//滑动到最后一个位置的时候
if (totalCount == list.size && !isFetching) {
//上拉加载更多的判断
RvSection.setFetching(c, true)
RvSection.updateStartParam(c, list.size)
service.fetch(list.size, count)
}
}
}
关键代码加了注释,下面再稍微说一下:
- @OnCreateInitialState注解的方法是初始数据的方法,可以看到从DataService获取数据。值得注意的一点:StateValue的变量名要与@State注解的变量名保持一致。
- @OnCreateChildren注解不用说了,创建布局,值得注意的是布局最下方追加了上拉加载的进度条。
- @OnCreateService,@OnBindService,@OnUnbindService注入管理服务的方法。
- @OnRefresh这是下拉刷新的关键代码,可以看出,就是在这里进行请求获取数据的。同时看下DataService的刷新方法reFetch里在请求成功调用了EventHandler.dispatchEvent(),分发消息,更新界面,会走到SectionSpec的onDataLoaded方法。
- 可以看到onDataLoaded的注解@OnEvent(RvListModel::class),而RvListModel类同时也有@Event注解,相对应。onDataLoaded方法中的注释已经相当详细了,更新完数据后,会再走到onCreateChildren更新视图。
- 上拉加载:主要是由@OnViewportChanged注解的方法实现,这个方法可以进行滑动监听,判断上拉至最后一个item,就加载数据,之后跟刷新比较类似,就是折腾一下数据。
使用起来就相当简单了:
/**
* 下拉刷新上拉加载的RecyclerView复合组件
* @param context
* @return
*/
private fun getRvComponent(context: ComponentContext): Component {
return RecyclerCollectionComponent.create(context)
.section(RvSection.create(SectionContext(context)).build())
.build()
}
下面看下效果:
附上demo地址:LithoDemo