You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Android TV上基于Leanback框架实现类似设置界面的单列垂直列表

在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 08:17:32