使用 leanback 库 GridView 管理AnroidTV的焦点

news2024/11/27 1:38:23

一、前情提要

  • 我当前需要开发一个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. 如果你有任何问题,欢迎在评论区提问交流~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1952349.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

OSPF LSA 格式及字段详解

在 AS 内的每台设备&#xff0c;根据设备的路由器类型产生一种或多种 LSA。 LSA 的集合形成了 LSDB&#xff08;Link-state Database&#xff09;。 OSPF 中对路由信息的描述都是封装在 LSA 中发布出去的。 常用的 LSA 包括&#xff1a; Router-LSANetwork-LSASummary-LSA&…

postgres启动错误

说明&#xff1a;记录一次在Linux上启动postgres数据错误&#xff1b; 问题&#xff1a;安装好postgres数据库后&#xff0c;我使用systemctl启动数据库&#xff0c;报下面的错误 ● postgresql-15.service - PostgreSQL 15 database serverLoaded: loaded (/usr/lib/systemd…

力扣225 | 两个队列实现栈的思想解析 | 详细图解 Java代码实现

做题链接 目录 前言&#xff1a; 一、思想详解 1.push(x) 2.pop() 弹出栈顶元素 3.top() 获取栈顶元素&#xff08;但不弹出&#xff09; 4.empty() 二、编程实战 1.Java前置知识 2.代码实现&#xff1a; 前言&#xff1a; 栈是一种后进先出数据结构。队列是一种先进…

学习大数据DAY22 Linux 基 本 指 令 3与 在 Linux 系 统 中 配 置MySQL 和 Oracle

目录 网络配置类 ps 显示系统执行的进程 kill systemctl 服务管理 配置静态 ip 常见错误---虚拟机重启网卡失败或者网卡丢失 mysql 操作 上机练习 6---安装 mysql---参考《mysql 安装》文档 解锁 scott 重启后的步骤 上机练习 7---安装 oracle---参考《oracle 安装》…

redis:清除缓存的最简单命令示例

清除redis缓存命令(执行命令列表见截图) 1.打开cmd窗口&#xff0c;并cd进入redis所在目录 2.登录redis redis-cli 3.查询指定队列当前的记录数 llen 队列名称 4.清除指定队列所有记录 ltrim 队列名称 1 0 5.再次查询&#xff0c;确认队列的记录数是否已清除

使用API Monitor探测C++程序在调用HtmlHelp接口打开.chm文件时传入了哪些参数

目录 1、API Monitor介绍 2、为何要使用API Monitor工具&#xff1f; 2、HtmlHelp函数在API列表函数中找不到&#xff0c;将所在模块作为外部Extern DLL模块添加到API Monitor中 3、开启对Beyond Compare工具软件的实时监测 4、在Beyond Compare软件中打开chm帮助文档&…

MiniExcel:.NET中处理Excel的高效方案

在.NET开发环境中&#xff0c;处理Excel文件是一项常见的任务&#xff0c;无论是数据导入、导出还是报表生成。传统的解决方案可能存在性能瓶颈或功能限制。MiniExcel作为一个现代、高效的库&#xff0c;为.NET开发者提供了一个强大的工具来简化Excel操作。本文将介绍MiniExcel…

【数据结构】排序算法——Lesson2

Hi~&#xff01;这里是奋斗的小羊&#xff0c;很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~~ &#x1f4a5;&#x1f4a5;个人主页&#xff1a;奋斗的小羊 &#x1f4a5;&#x1f4a5;所属专栏&#xff1a;数据结构 &#x1f680;本系列文章为个人学…

Scrapy 爬取旅游景点相关数据(四)

本节内容主要为&#xff1a; &#xff08;1&#xff09;创建数据库 &#xff08;2&#xff09;创建数据库表 &#xff08;3&#xff09;爬取数据进MYSQL库 1 新建数据库 使用MYSQL数据库存储数据&#xff0c;创建一个新的数据库 create database scrapy_demo;2 新建数据表 CR…

2024年【非高危行业生产经营单位主要负责人解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 非高危行业生产经营单位主要负责人及安全管理人员安全生产知识和管理能力考试报名是安全生产模拟考试一点通生成的&#xff0c;非高危行业生产经营单位主要负责人及安全管理人员安全生产知识和管理能力证模拟考试题库…

【Redis宕机啦!】Redis数据恢复策略:RDB vs AOF vs RDB+AOF

文章目录 Redis宕机了&#xff0c;如何恢复数据为什么要做持久化持久化策略RDBredis.conf中配置RDBCopy-On-Write, COW快照的频率如何把握优缺点 AOFAOF日志内容redis.conf中配置AOF写回策略AOF日志重写AOF重写会阻塞吗优缺点 RDB和AOF混合方式总结 Redis宕机了&#xff0c;如何…

C语言图书信息管理系统

题目&#xff1a;图书信息管理系统 内容及主要功能描述&#xff1a; 该系统用于管理图书信息&#xff0c;包括图书的增加、删除、查找、修改、浏览、按出版社统计图书数量等功能。具体功能包括&#xff1a; 增加图书&#xff1a;输入图书信息并添加到系统中。删除图书&#x…

golang设置远程调试

1. 目标机器构建安装dlv https://github.com/go-delve/delve go build之后将编译号的dlv命令路径添加到PATH里 2. 目标机器下载源代码并且运行dlv dlv debug --headless --listen:2345 --api-version2 --accept-multiclient 3.本机添加go remote 4. 设置断点即可

JAVA简介与开发环境配置(基础介绍 一)

目录 Java 简介 主要特性 发展历史 Java开发工具 Java 开发环境配置 window系统安装java 下载JDK 配置环境变量 通过控制台测试JDK是否安装成功 Linux&#xff0c;UNIX&#xff0c;Solaris&#xff0c;FreeBSD环境变量设置 流行JAVA开发工具 使用 Eclipse 运行第一…

C++程序的UI界面闪烁问题的解决办法总结

Windows C++程序复杂的UI界面要使用多种绘图技术(使用GDI、GDI+、ddraw、D3D等绘图),并要贴图去美化,在窗口移动或者改变大小的时候可能会出现闪烁。下面罗列一下UI界面产生闪烁的几种可能的原因,并给出相应的解决办法。 1、原因一 如果熟悉显卡原理的话,调用GDI函数向屏…

Visual Studio2022在屏幕缩放后界面问题的解决方法

Visual Studio2022在屏幕缩放后界面问题的解决方法 最近帮客户修改一个几年前用C#开发的WinForm程序&#xff0c;遇到个奇怪问题&#xff0c;记录一下解决方法。 事情是这样&#xff0c;年初时换了台2K高分屏的开发笔记本&#xff0c;终于淘汰了那台不堪重负的用了五年的Think…

leetcode-98. 验证二叉搜索树

题目描述 给你一个二叉树的根节点 root &#xff0c;判断其是否是一个有效的二叉搜索树。 有效 二叉搜索树定义如下&#xff1a; 节点的左 子树 只包含 小于 当前节点的数。节点的右子树只包含 大于 当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。 示例 1&…

mysql报错:Unknown collation: ‘utf8mb4_0900_ai_ci‘的原因及解决方法

参考博客&#xff1a;http://t.csdnimg.cn/NRzyk 报错场景描述 使用navicate在查询中运行sql语句时报错&#xff1a;Unknown collation: utf8mb4_0900_ai_ci 报错原因 生成转储文件的数据库版本为8.0&#xff0c;我本地数据库版本为5.6&#xff0c;高版本导入到低版本&…

国科大作业考试资料《人工智能原理与算法》2024新编-第十三次作业整理

1、假设我们从决策树生成了一个训练集&#xff0c;然后将决策树学习应用于该训练集。当训练集的大小趋于无穷时&#xff0c;学习算法将最终返回正确的决策树吗&#xff1f;为什么是或不是&#xff1f; 本次有两个参考&#xff1a; 参考一&#xff1a; 当训练集的大小趋于无穷…

Spring Bean - xml 配置文件创建对象

类型&#xff1a; 1、值类型 2、null &#xff08;标签&#xff09; 3、特殊符号 &#xff08;< -> < &#xff09; 4、CDATA <?xml version"1.0" encoding"UTF-8"?> <beans xmlns"http://www.springframework.org/schema/bea…