使用 leanback 库 GridView 管理AnroidTV的焦点

一、前情提要

  • 我当前需要开发一个TV应用,但是之前处理过的焦点问题的很少,现在空下来了,对过往的工作做一个总结分享。
  • 在手机APP开发中常用的 RecycleView 在 TV 中开发时,无法解决大量的焦点问题,所以使用leanback进行列表数据展示,以此来解决焦点问题。本文主要记录 leanback 库的基础用法以及一些技巧分享

二、leanback 库 GridView 的基础使用

1. 引入 leanback 库

// 引入 leanback 库
implementation 'com.android.support:leanback-v17:28.0.0'

2. xml中引入容器

<!--xml中使用GrideView,以Vertical竖向为例-->
<androidx.leanback.widget.VerticalGridView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:focusOutEnd="true"
    tools:listitem="@layout/item_choose_zip" />
<!--GrideView系列中的一些特殊属性值-->
<declare-styleable name="lbBaseGridView">
    <!--允许方向键在视图的前端(位置= 0处)进行导航,默认值为false  -->
    <attr format="boolean" name="focusOutFront"/>
    <!-- 允许DPAD键在视图末尾导航出去,默认值为false -->
    <attr format="boolean" name="focusOutEnd"/>
    <!-- 允许使用DPAD键导航到第一行之外,对于HorizontalGridView,它是顶部边缘,对于VerticalGridView,它是“开始”边缘。默认值为true。  -->
    <attr format="boolean" name="focusOutSideStart"/>
    <!-- 允许DPAD键导航出最后一行,对于HorizontalGridView,它是底部边缘,对于VerticalGridView,它是“结束”边缘。默认值为true。  -->
    <attr format="boolean" name="focusOutSideEnd"/>
    <!-- 定义两个项目之间的水平空间 -->
    <attr name="android:horizontalSpacing"/>
    <!-- 定义两个项目之间的垂直空间 -->
    <attr name="android:verticalSpacing"/>
    <!-- 定义子视图的gravity -->
    <attr name="android:gravity"/>
</declare-styleable>

3. kotlin/java 中填充数据

  • 其实至此、GridView 和 RecycleView 的使用是没有区别的,但是在填充数据时,GridView 中使用了不一样的方式。
  • GridView 的 adapter-ViewHolder 概念被淡化,开发中不再需要过多的关注这一步,已被封装为 ItemBridgeAdapter(继承自RecycleView.Adapter)ObjectAdapter,而 ObjectAdapter的初始化需要一个新的 玩意儿Presenter
  • 这个 Presenter不是MVP结构中的Presenter,但是他们的功能以及职责是相似的,用于承载数据,并将数据封装到视图中。这里以一个例子来说明使用方法。
<!--写一个 item 的 layout-->
<TextView
	android:id="@+id/gvItem"
	android:layout_width="match_parent"
	android:layout_height="@dimen/px100"/>
// 写一个 Presenter 用于接收数据并将数据显示到 item 的视图
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?) {
    	// 在Presenter中,没有泛型的概念,所以需要自行判断类型来传参。
    	// 事实上,在leanback 库这个模块的设计理念中,它支持你以多种形式来构建自己的 Presenter
    	// 1. 你可以将你的 Presenter 指定为独立的专为一种数据服务,比如当前我们固定以 String 类型接收显示数据
        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)
        }
    }
}
// 使用 Presenter 向 GridView 中填充数据

// 列表数据格式相同,即仅一种 Presenter时
val mAdapter = ArrayObjectAdapter(MPresenter())

// 如果你有多种数据混合,可以使用这种方式初始化 ObjectAdapter
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)
// 其实还有一个 setData( list, diff) 的方法,但是这个方法自身存在问题,所以不建议使用,后边会讲

// 在 leanback 的Presenter中, Presenter 失去了很多 RecycleView 的Adapter信息
// - 比如itemType、itemPosition 等
// - 事实上这些信息都在 ItemBridgeAdapter 中(正如前边所说,BridgeAdapter 继承自 RecycleView.Adapter)
// - 如果你需要使用这些信息,你需要使用别的方法
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. 创建基类

// 创建 Presenter 基类

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?) {
    }
}
// 创建 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. 创建范本

Live Templates

  • 范本内容如下
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 设置数据有两种方法:
  1. clear + addAll
  2. setData
  • 事实上 setData 不仅可以少一行调用,还可以使用 DiffCallback,这个回调可以支持比对 item 数据,并结合 ItemAnimation 实现很多酷炫的动画效果,而使用方法也很简单:
mAdapter.setItems(dataList, object : DiffCallback<Any>() {
    override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
    	// 判断是否为同一个 item,如果返回 true,则认为这两个是同一个 item
    	// 会将本视图移动到 newItem 在新的数据中所处的位置,并触发 move 动画
    	// 如果返回 false,则认为不是同一个 item
    	// 在本轮判断结束后:
    	// 1. 当前视图中显示的 所有未匹配到新数据的item将被移除,并触发 remove 动画
    	// 2. 所有未匹配到旧数据的新数据将会为其创建新的 item,并触发 create 动画
        return true
    }

    override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
    	// 判断数据是否有变化,仅在 areItemsTheSame 通过后才会判断
    	// 如果返回true,则认为有变化,并触发 change 动画
    	return true
    }

    override fun getChangePayload(oldItem: Any, newItem: Any): Any {
        // 负载标记 - 后续由 SimpleItemAnimator 调用判断是否需要触发动画
        return "state change"
    }
})

// 为 GridView 添加动画
binding.grideView.itemAnimator = SimpleItemAnimator()
// 如果你需要调整动画,进行翻转等,可继承 SimpleItemAnimator 重写里边的动画
  • 注意!在数据变化不频繁时,此方法是可用的,且效果很好,可以很好的展示数据,处理焦点,以及动画
  • 但是如果数据频繁变化,将有可能触发 RecycleView 的内部错误 RecycleView$Recycler.unscrpView on a null object reference
    在这里插入图片描述
  • 这是GridView 的内部处理逻辑问题,具体错误原因这里就不过多赘述了(篇幅有些长了)解决方法很简单,改为 clear + setData 的方式就可以了

五、结尾

  1. 如果我的文章有帮到你,请给我一个点赞收藏,这对我真的很重要~
  2. 如果你有任何问题,欢迎在评论区提问交流~
  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

俺不理解

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值