如何在Android TV上基于Leanback框架实现类似设置界面的单列垂直列表
我之前也碰到过一模一样的问题——普通RecyclerView在TV端的焦点交互体验确实拉胯,而VerticalSupportFragment的全屏宽度bug也挺闹心。这里给你两个靠谱的实现方案,都是基于Leanback框架,完美适配TV端的焦点、导航和动画:
方案一:用RowsSupportFragment + 自定义Presenter(最推荐)
RowsSupportFragment是Leanback专门为垂直列表场景设计的核心组件,内置了TV端必备的焦点缩放、边缘滚动、D-pad导航逻辑,而且完全可以解决宽度铺满的问题。
步骤1:添加Leanback依赖
首先确保你的build.gradle里引入了Leanback库:
dependencies { implementation "androidx.leanback:leanback:1.0.0" }
步骤2:创建数据模型类
先定义每个设置项的数据结构:
data class SettingsItem( val title: String, val iconResId: Int, val description: String? = null )
步骤3:自定义ListItemPresenter
这是核心部分,用来控制每个列表项的布局、焦点状态变化(比如选中时的缩放、背景高亮):
class SettingsItemPresenter : RowPresenter() { override fun createRowViewHolder(parent: ViewGroup): RowPresenter.ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_settings, parent, false) // 强制布局宽度铺满屏幕 view.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) return ViewHolder(view) } override fun onBindRowViewHolder(holder: RowPresenter.ViewHolder, item: Any) { val settingsItem = item as SettingsItem val viewHolder = holder as ViewHolder viewHolder.titleTv.text = settingsItem.title settingsItem.description?.let { viewHolder.descTv.text = it viewHolder.descTv.visibility = View.VISIBLE } ?: run { viewHolder.descTv.visibility = View.GONE } viewHolder.iconIv.setImageResource(settingsItem.iconResId) } override fun onSelectChanged(holder: RowPresenter.ViewHolder, selected: Boolean) { super.onSelectChanged(holder, selected) // 处理焦点选中/取消的动画效果 val scale = if (selected) 1.05f else 1.0f holder.view.animate() .scaleX(scale) .scaleY(scale) .setDuration(150) .start() // 切换背景色区分焦点状态 holder.view.setBackgroundColor( ContextCompat.getColor(holder.view.context, if (selected) R.color.focused_bg else R.color.normal_bg) ) } inner class ViewHolder(view: View) : RowPresenter.ViewHolder(view) { val iconIv: ImageView = view.findViewById(R.id.icon_iv) val titleTv: TextView = view.findViewById(R.id.title_tv) val descTv: TextView = view.findViewById(R.id.desc_tv) } }
对应的item_settings.xml布局(确保根布局宽度为match_parent):
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="16dp" android:gravity="center_vertical" android:orientation="horizontal"> <ImageView android:id="@+id/icon_iv" android:layout_width="48dp" android:layout_height="48dp" android:marginEnd="16dp"/> <LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical"> <TextView android:id="@+id/title_tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="20sp" android:textColor="@android:color/white"/> <TextView android:id="@+id/desc_tv" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="14sp" android:textColor="@android:color/darker_gray" android:visibility="gone"/> </LinearLayout> </LinearLayout>
步骤4:在Fragment中使用RowsSupportFragment
class SettingsFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val rowsFragment = RowsSupportFragment() // 设置自定义Presenter val adapter = ArrayObjectAdapter(SettingsItemPresenter()) // 添加测试数据 adapter.addAll(0, listOf( SettingsItem("网络设置", R.drawable.ic_network, "Wi-Fi、蓝牙、移动网络"), SettingsItem("显示设置", R.drawable.ic_display, "分辨率、亮度、壁纸"), SettingsItem("声音设置", R.drawable.ic_sound, "音量、音效、音频输出"), SettingsItem("应用管理", R.drawable.ic_apps, "已安装应用、权限管理") )) rowsFragment.adapter = adapter // 设置点击事件 rowsFragment.onItemViewClickedListener = OnItemViewClickedListener { _, item, _, _ -> val settingsItem = item as SettingsItem // 处理点击逻辑,比如跳转到对应的设置页面 Toast.makeText(context, "点击了${settingsItem.title}", Toast.LENGTH_SHORT).show() } return rowsFragment.view } }
这样实现的列表不仅能铺满全屏宽度,还自带TV端特有的焦点缩放、流畅的D-pad导航体验,完全符合系统设置的交互风格。
方案二:用VerticalGridView(更灵活的轻量方案)
如果你不想用RowsFragment,Leanback的VerticalGridView是另一个选择——它是专门为TV优化的垂直滚动列表,比普通RecyclerView多了焦点管理、边缘滚动等TV专属特性。
步骤1:布局中添加VerticalGridView
<androidx.leanback.widget.VerticalGridView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/vertical_grid" android:layout_width="match_parent" android:layout_height="match_parent" android:numColumns="1"/> <!-- 设置为1列就是垂直单列 -->
步骤2:自定义Adapter和ViewHolder
class SettingsAdapter(private val items: List<SettingsItem>) : RecyclerView.Adapter<SettingsAdapter.ViewHolder>() { inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { val iconIv: ImageView = view.findViewById(R.id.icon_iv) val titleTv: TextView = view.findViewById(R.id.title_tv) val descTv: TextView = view.findViewById(R.id.desc_tv) } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { val view = LayoutInflater.from(parent.context) .inflate(R.layout.item_settings, parent, false) return ViewHolder(view) } override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = items[position] holder.titleTv.text = item.title item.description?.let { holder.descTv.text = it holder.descTv.visibility = View.VISIBLE } ?: run { holder.descTv.visibility = View.GONE } holder.iconIv.setImageResource(item.iconResId) // 处理焦点状态变化 holder.itemView.setOnFocusChangeListener { v, hasFocus -> val scale = if (hasFocus) 1.05f else 1.0f v.animate() .scaleX(scale) .scaleY(scale) .setDuration(150) .start() v.setBackgroundColor( ContextCompat.getColor(v.context, if (hasFocus) R.color.focused_bg else R.color.normal_bg) ) } } override fun getItemCount() = items.size }
步骤3:在Fragment中初始化VerticalGridView
class SettingsFragment : Fragment() { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val view = inflater.inflate(R.layout.fragment_settings, container, false) val verticalGrid = view.findViewById<VerticalGridView>(R.id.vertical_grid) // 设置布局管理器(VerticalGridView自带垂直布局,可调整间距) verticalGrid.layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false) // 设置自定义Adapter val adapter = SettingsAdapter(listOf( SettingsItem("网络设置", R.drawable.ic_network, "Wi-Fi、蓝牙、移动网络"), SettingsItem("显示设置", R.drawable.ic_display, "分辨率、亮度、壁纸"), SettingsItem("声音设置", R.drawable.ic_sound, "音量、音效、音频输出"), SettingsItem("应用管理", R.drawable.ic_apps, "已安装应用、权限管理") )) verticalGrid.adapter = adapter // 设置点击事件 verticalGrid.setOnItemClickListener { _, _, position, _ -> val item = adapter.items[position] Toast.makeText(context, "点击了${item.title}", Toast.LENGTH_SHORT).show() } return view } }
关于VerticalSupportFragment的宽度问题
如果你之前想用VerticalSupportFragment,其实可以通过修改它的默认布局来解决宽度问题:创建一个自定义的VerticalSupportFragment子类,重写onCreateView方法,加载自己的布局(确保根布局宽度为match_parent),不过相比之下RowsSupportFragment更成熟稳定,推荐优先使用方案一。
总的来说,这两个方案都能完美实现系统设置风格的垂直列表,并且适配TV端的所有交互特性,比普通RecyclerView省心太多。
内容的提问来源于stack exchange,提问作者Fitzgerald Afful

