一、前情提要
- 我当前需要开发一个TV应用,但是之前处理过的焦点问题的很少,现在空下来了,对过往的工作做一个总结分享。
- 在手机APP开发中常用的 RecycleView 在 TV 中开发时,无法解决大量的焦点问题,所以使用leanback进行列表数据展示,以此来解决焦点问题。本文主要记录 leanback 库的基础用法以及一些技巧分享
二、leanback 库 GridView 的基础使用
1. 引入 leanback 库
implementation 'com.android.support:leanback-v17:28.0.0'
2. xml中引入容器
<androidx.leanback.widget.VerticalGridView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:focusOutEnd="true"
tools:listitem="@layout/item_choose_zip" />
<declare-styleable name="lbBaseGridView">
<attr format="boolean" name="focusOutFront"/>
<attr format="boolean" name="focusOutEnd"/>
<attr format="boolean" name="focusOutSideStart"/>
<attr format="boolean" name="focusOutSideEnd"/>
<attr name="android:horizontalSpacing"/>
<attr name="android:verticalSpacing"/>
<attr name="android:gravity"/>
</declare-styleable>
3. kotlin/java 中填充数据
- 其实至此、GridView 和 RecycleView 的使用是没有区别的,但是在填充数据时,GridView 中使用了不一样的方式。
- GridView 的 adapter-ViewHolder 概念被淡化,开发中不再需要过多的关注这一步,已被封装为
ItemBridgeAdapter(继承自RecycleView.Adapter)
和ObjectAdapter
,而 ObjectAdapter
的初始化需要一个新的 玩意儿:Presenter - 这个
Presenter
不是MVP结构中的Presenter,但是他们的功能以及职责是相似的,用于承载数据,并将数据封装到视图中。这里以一个例子来说明使用方法。
<TextView
android:id="@+id/gvItem"
android:layout_width="match_parent"
android:layout_height="@dimen/px100"/>
class TestPresenter: Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder = DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.item_test,
parent,
false
)
override fun onBindViewHolder(viewHolder: ViewHolder?, item: Any?) {
if(viewHolder is MViewHolder) {
if (item is String) {
viewHolder.bindData(item)
}
}
}
override fun onUnbindViewHolder(viewHolder: ViewHolder?) {
}
inner class MViewHolder(mBinding: TestItemBinding) : ViewHolder(mBinding.root) {
override fun bindData(data: String) {
mBinding.gvItem.setText(data)
}
}
}
val mAdapter = ArrayObjectAdapter(MPresenter())
val mAdapter = ArrayObjectAdapter(object : PresenterSelector() {
override fun getPresenter(item: Any?): Presenter {
if (item is String) {
return MPresenter()
} else {
...
}
}
})
val itemBridgeAdapter = ItemBridgeAdapter(mAdapter)
binding.chooseZipGridView.adapter = itemBridgeAdapter
val testData = arrayOf("111", "222", "333")
mAdapter.clear()
mAdapter.addAll(testData)
val bridgeAdapter = ItemBridgeAdapter(mAdapter)
bridgeAdapter.setAdapterListener(object : ItemBridgeAdapter.AdapterListener() {
override fun onAddPresenter(presenter: Presenter?, type: Int) {
}
override fun onCreate(viewHolder: ItemBridgeAdapter.ViewHolder?) {
}
override fun onBind(viewHolder: ItemBridgeAdapter.ViewHolder?) {
}
override fun onBind(viewHolder: ItemBridgeAdapter.ViewHolder?, payloads: List<*>?) {
onBind(viewHolder)
}
override fun onUnbind(viewHolder: ItemBridgeAdapter.ViewHolder?) {
}
override fun onAttachedToWindow(viewHolder: ItemBridgeAdapter.ViewHolder?) {
}
override fun onDetachedFromWindow(viewHolder: ItemBridgeAdapter.ViewHolder?) {
}
})
三、优化结构,简化使用,提高效率
leanback
库的 presenter
没有泛型的概念,虽然会有更多的发展方向,但是对我而言,我是更需要泛型的存在的。- 如果你也同样需要像
RecycleView
中一样,需要明确知道自己的数据类型,而不是全靠类型判断的话,可以参考本节内容,否则直接看下一节就好
1. 创建基类
import android.view.View
import android.view.ViewGroup
import androidx.leanback.widget.Presenter
abstract class BaseGridPresenter<D, B: BaseGridViewHolder<*, D>>: Presenter() {
override fun onCreateViewHolder(parent: ViewGroup): ViewHolder = getViewHolder(parent)
abstract fun getViewHolder(parent: ViewGroup): B
override fun onBindViewHolder(viewHolder: ViewHolder?, item: Any?) {
castViewHolder(viewHolder)?.bindData(castItem(item) ?: return)
}
abstract fun castViewHolder(viewHolder: ViewHolder?): B?
abstract fun castItem(item: Any?): D?
override fun onUnbindViewHolder(viewHolder: ViewHolder?) {
}
}
import androidx.leanback.widget.Presenter.ViewHolder
import androidx.viewbinding.ViewBinding
abstract class BaseGridViewHolder<B: ViewBinding, D>(val mBinding: B) : ViewHolder(mBinding.root) {
abstract fun bindData(data: D)
}
2. 创建范本
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.databinding.DataBindingUtil
import com.loostone.ui.base.leanback.BaseGridPresenter
import com.loostone.ui.base.leanback.BaseGridViewHolder
import com.loostone.ui.extension.castTarget
import com.lscm.lskaraoke.R
class $className$: BaseGridPresenter<$D$, $className$.MViewHolder>() {
override fun getViewHolder(parent: ViewGroup) = MViewHolder(
DataBindingUtil.inflate(
LayoutInflater.from(parent.context),
R.layout.$layout$,
parent,
false
)
)
override fun castViewHolder(viewHolder: ViewHolder?) = viewHolder.castTarget<MViewHolder>()
override fun castItem(item: Any?) = item.castTarget<$D$>()
class MViewHolder(mBinding: $B$) :
BaseGridViewHolder<$B$, $D$>(mBinding) {
override fun bindData(data: $D$) {
}
}
}
- 配置范本默认值
- 其中,上文中的 castTarget 方法是扩展,方法如下:
inline fun <reified T> Any?.castTarget(): T? {
return if (this is T) this else null
}
3. 使用范本快速创建 Presenter
- 现在你已经配置了范本,在你需要创建一个新的presenter时,只需要新建一个类,然后在类中输入 presenter,即可完成一个 Presenter 的创建
- 填写剩余的三个变量,即可完成这个新的 Presenter 的创建
四、坑点吐槽
- 在前文中有提到,给 ObjectAdapter 设置数据有两种方法:
- clear + addAll
- setData
- 事实上 setData 不仅可以少一行调用,还可以使用 DiffCallback,这个回调可以支持比对 item 数据,并结合 ItemAnimation 实现很多酷炫的动画效果,而使用方法也很简单:
mAdapter.setItems(dataList, object : DiffCallback<Any>() {
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
return true
}
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
return true
}
override fun getChangePayload(oldItem: Any, newItem: Any): Any {
return "state change"
}
})
binding.grideView.itemAnimator = SimpleItemAnimator()
- 注意!在数据变化不频繁时,此方法是可用的,且效果很好,可以很好的展示数据,处理焦点,以及动画
- 但是如果数据频繁变化,将有可能触发 RecycleView 的内部错误
RecycleView$Recycler.unscrpView on a null object reference
- 这是GridView 的内部处理逻辑问题,具体错误原因这里就不过多赘述了(篇幅有些长了)解决方法很简单,改为 clear + setData 的方式就可以了
五、结尾
- 如果我的文章有帮到你,请给我一个点赞收藏,这对我真的很重要~
- 如果你有任何问题,欢迎在评论区提问交流~