9、列表与网格布局:VerticalGridFragment与HorizontalGridView的使用
做TV应用开发,列表和网格是躲不开的核心组件。你想想看,电视屏幕上最常出现的是什么?无非就是一行行的视频卡片、一页页的应用列表。今天我们就来聊聊Android TV上两个最实用的布局组件——VerticalGridFragment和HorizontalGridView。
9.1 为什么TV上要用专门的布局组件?
刚接触TV开发时,我犯过一个低级错误——直接把手机上的RecyclerView搬过来用。结果呢?遥控器焦点乱跳,界面卡顿,用户根本没法操作。电视和手机的操作逻辑完全不同:
- 焦点导航:电视靠遥控器方向键,不是触摸
- 可视距离:用户坐在2-3米外,卡片要够大
- 性能要求:TV设备配置普遍偏低,渲染要轻量
Google专门为TV设计了Leanback库,里面的VerticalGridFragment和HorizontalGridView就是来解决这些问题的。
核心结论:TV上的列表和网格,必须用Leanback组件。自己手写RecyclerView适配器?那是给自己挖坑。
9.2 VerticalGridFragment:电视上的网格布局
这个组件说白了就是一个垂直滚动的网格。它继承自BaseFragment,内部封装了VerticalGridView。我个人习惯用它来展示视频分类、应用列表这类场景。
9.2.1 基本用法
先看一个最简单的例子。假设我们要展示一个电影分类网格:
public class MovieGridFragment extends VerticalGridFragment {
private static final int NUM_COLUMNS = 5;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置列数
setGridStyle(NUM_COLUMNS,
getResources().getDimensionPixelSize(R.dimen.grid_item_width),
getResources().getDimensionPixelSize(R.dimen.grid_item_height));
// 创建适配器
ArrayObjectAdapter adapter = new ArrayObjectAdapter(new CardPresenter());
// 填充数据
for (int i = 0; i < 20; i++) {
Movie movie = new Movie("电影 " + i, "描述...");
adapter.add(movie);
}
setAdapter(adapter);
}
}
嗯,这里要注意——setGridStyle方法里的列数不是随便填的。我曾经在项目中把列数设成8,结果卡片挤在一起,遥控器焦点都看不清。电视上一般3到6列比较合适,具体取决于屏幕尺寸。
9.2.2 CardPresenter:卡片渲染器
网格里的每个卡片怎么显示?靠CardPresenter。它类似于RecyclerView的ViewHolder,但更专注于TV场景:
public class CardPresenter extends Presenter {
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
// 创建卡片布局
ImageCardView cardView = new ImageCardView(parent.getContext());
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
// 设置卡片尺寸
cardView.setMainImageDimensions(
parent.getResources().getDimensionPixelSize(R.dimen.card_width),
parent.getResources().getDimensionPixelSize(R.dimen.card_height));
return new ViewHolder(cardView);
}
@Override
public void onBindViewHolder(ViewHolder viewHolder, Object item) {
Movie movie = (Movie) item;
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setTitleText(movie.getTitle());
cardView.setContentText(movie.getDescription());
// 加载图片...
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
// 清理资源
}
}
我的经验:ImageCardView自带焦点效果,但默认的阴影有点丑。我一般会重写setFocusState方法,自定义焦点放大动画。记住,电视上焦点反馈一定要明显,用户隔着3米远呢。
9.3 HorizontalGridView:横向滚动列表
如果说VerticalGridFragment是电视上的"首页",那HorizontalGridView就是"详情页"的标配。它横向滚动,常用于展示推荐列表、相关视频等。
9.3.1 在Fragment中使用
我通常把它放在一个普通的Fragment里,配合其他布局使用:
public class DetailFragment extends Fragment {
private HorizontalGridView horizontalGridView;
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
View root = inflater.inflate(R.layout.fragment_detail, container, false);
horizontalGridView = root.findViewById(R.id.horizontal_grid);
// 设置布局参数
horizontalGridView.setNumRows(1); // 单行
horizontalGridView.setRowHeight(
getResources().getDimensionPixelSize(R.dimen.row_height));
horizontalGridView.setItemMargin(
getResources().getDimensionPixelSize(R.dimen.item_margin));
// 设置适配器
ArrayObjectAdapter adapter = new ArrayObjectAdapter(new CardPresenter());
// 填充数据...
horizontalGridView.setAdapter(adapter);
return root;
}
}
布局文件里这样写:
<androidx.leanback.widget.HorizontalGridView
android:id="@+id/horizontal_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true" />
注意:HorizontalGridView默认会抢占焦点。如果你页面上还有其他可聚焦的控件,记得用android:descendantFocusability="afterDescendants"控制焦点顺序。我曾经因为这个被测试提了bug——遥控器按方向键,焦点死活跳不到上面的标题栏。
9.3.2 多行横向网格
有时候单行不够用,比如展示"更多推荐"时,可以设置多行:
horizontalGridView.setNumRows(2); // 两行
horizontalGridView.setRowHeight(200);
horizontalGridView.setItemMargin(8);
这样就会形成一个2行×N列的网格,横向滚动。说白了就是电视版的"瀑布流"。
9.4 焦点管理:电视交互的灵魂
电视上没有触摸,全靠焦点。我见过太多新手在焦点管理上翻车。这里分享几个关键点:
9.4.1 默认焦点行为
VerticalGridFragment和HorizontalGridView都内置了焦点管理。默认情况下:
- 进入页面时,第一个可聚焦的item获得焦点
- 按方向键,焦点在item之间移动
- 按确定键,触发item的点击事件
但有时候默认行为不够用。比如,我希望进入页面时焦点落在第三个item上:
// 在VerticalGridFragment中
setSelectedPosition(2); // 索引从0开始
9.4.2 自定义焦点搜索
我曾经遇到一个需求:横向列表滚动到末尾时,焦点要跳到下面的按钮上。默认的焦点搜索算法搞不定,需要重写:
horizontalGridView.setFocusSearchListener(new HorizontalGridView.FocusSearchListener() {
@Override
public View focusSearch(View focused, int direction) {
if (direction == View.FOCUS_RIGHT && isLastItem(focused)) {
return nextButton; // 跳到下一个控件
}
return null; // 使用默认算法
}
});
避坑指南:我曾经在焦点搜索里返回了错误的View,导致遥控器按下去焦点直接消失。调试了半天才发现是返回了一个不可聚焦的View。记住,focusSearch返回的View必须isFocusable() == true。
9.5 性能优化:电视设备很脆弱
电视的硬件配置普遍不如手机。我踩过的坑包括:
- 图片加载:用Glide时记得设置
override(),别加载原图 - ViewHolder复用:
CardPresenter默认会复用,但如果你在onBindViewHolder里做了耗时操作,滚动就会卡 - 数据量控制:一次加载50条以内,多了用分页
// 图片加载优化示例
Glide.with(cardView.getContext())
.load(movie.getImageUrl())
.override(300, 200) // 限制尺寸
.format(DecodeFormat.PREFER_RGB_565) // 减少内存
.into(cardView.getMainImageView());
9.6 知识体系总览
下面这张图总结了本章的核心内容,你可以对照着回顾:
9.7 总结
做TV开发,VerticalGridFragment和HorizontalGridView是绕不开的两个组件。记住几个要点:
- 网格用
VerticalGridFragment,横向列表用HorizontalGridView - 卡片渲染用
CardPresenter,别自己写ViewHolder - 焦点管理要细心,遥控器操作和触摸完全不同
- 性能优化从图片加载和复用做起
我在项目中见过太多人把TV开发想得太简单,结果在焦点和性能上栽跟头。嗯,希望今天的内容能帮你少走弯路。
最后说一句:如果你在真机上测试,记得用遥控器操作,别用鼠标点。鼠标点出来的效果和遥控器完全是两码事——我吃过这个亏。