一、前情提要
- 我当前需要开发一个TV应用,但是之前处理过的焦点问题的很少,现在空下来了,对过往的工作做一个总结分享。
- 在手机APP开发中常用的 RecycleView 在 TV 中开发时,无法解决大量的焦点问题,所以使用leanback进行列表数据展示,以此来解决焦点问题。本文主要记录 leanback 库的基础用法以及一些技巧分享
二、leanback 库 GridView 的基础使用
1. 引入 leanback 库
implementation 'com.android.support:leanback-v17:28.0.0'
2. xml中引入容器
<androidx.leanback.widget.VerticalGridViewandroid: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,但是他们的功能以及职责是相似的,用于承载数据,并将数据封装到视图中。这里以一个例子来说明使用方法。
<TextViewandroid: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 = itemBridgeAdapterval 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.Presenterabstract class BaseGridPresenter<D, B: BaseGridViewHolder<*, D>>: Presenter() {override fun onCreateViewHolder(parent: ViewGroup): ViewHolder = getViewHolder(parent)abstract fun getViewHolder(parent: ViewGroup): Boverride 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.ViewBindingabstract 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.Rclass $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 的方式就可以了
五、结尾
- 如果我的文章有帮到你,请给我一个点赞收藏,这对我真的很重要~
- 如果你有任何问题,欢迎在评论区提问交流~