4. 电视端卡片视图与内容行数据绑定
为什么需要 Presenter 与 ArrayObjectAdapter
BrowseFragment 本身只负责“行”的容器与焦点导航,行内卡片长什么样、数据从哪来,需要由 Presenter 和 ArrayObjectAdapter 协作完成。可以把它们理解为一条数据装配链:ArrayObjectAdapter 持有数据对象列表,Presenter 决定每个对象如何渲染成卡片视图,ListRow 再把适配器包装成 BrowseFragment 能识别的一行。
定义卡片数据模型
先定义一个简单的 Movie 类,承载卡片需要的三个字段:标题、副标题和缩略图资源 ID。使用 Parcelable 不是必须的,但便于后续详情页传递。
public class Movie {
private final String title;
private final String subtitle;
private final int thumbnailResId;
public Movie(String title, String subtitle, int thumbnailResId) {
this.title = title;
this.subtitle = subtitle;
this.thumbnailResId = thumbnailResId;
}
public String getTitle() { return title; }
public String getSubtitle() { return subtitle; }
public int getThumbnailResId() { return thumbnailResId; }
}
实现 CardPresenter
CardPresenter 继承 Presenter,负责创建卡片视图并绑定数据。关键点:onCreateViewHolder 返回一个 ImageCardView,onBindViewHolder 把 Movie 的字段写入视图。
public class CardPresenter extends Presenter {
private Context context;
public CardPresenter(Context context) {
this.context = context;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent) {
ImageCardView cardView = new ImageCardView(context);
cardView.setFocusable(true);
cardView.setFocusableInTouchMode(true);
cardView.setMainImageDimensions(313, 176);
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.getSubtitle());
cardView.setMainImage(context.getDrawable(movie.getThumbnailResId()));
}
@Override
public void onUnbindViewHolder(ViewHolder viewHolder) {
ImageCardView cardView = (ImageCardView) viewHolder.view;
cardView.setMainImage(null);
}
}
在 BrowseFragment 中绑定内容行
在 onCreate 或 onActivityCreated 中构建适配器并设置行。注意:setAdapter 必须在 BrowseFragment 的视图创建完成后调用。
public class MainFragment extends BrowseFragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setupUI();
loadRows();
}
private void setupUI() {
setTitle("电视应用");
setHeadersState(HEADERS_ENABLED);
setBrandColor(getResources().getColor(R.color.brand));
}
private void loadRows() {
// 第一行:电影
ArrayObjectAdapter movieAdapter = new ArrayObjectAdapter(new CardPresenter(getActivity()));
movieAdapter.add(new Movie("星际穿越", "科幻 · 2014", R.drawable.poster_interstellar));
movieAdapter.add(new Movie("盗梦空间", "悬疑 · 2010", R.drawable.poster_inception));
movieAdapter.add(new Movie("银翼杀手2049", "科幻 · 2017", R.drawable.poster_blade_runner));
ListRow movieRow = new ListRow(new HeaderItem(0, "热门电影"), movieAdapter);
// 第二行:电视剧
ArrayObjectAdapter tvAdapter = new ArrayObjectAdapter(new CardPresenter(getActivity()));
tvAdapter.add(new Movie("怪奇物语", "美剧 · 2016", R.drawable.poster_stranger));
tvAdapter.add(new Movie("切尔诺贝利", "迷你剧 · 2019", R.drawable.poster_chernobyl));
ListRow tvRow = new ListRow(new HeaderItem(1, "热门剧集"), tvAdapter);
// 组装到 BrowseFragment
ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
rowsAdapter.add(movieRow);
rowsAdapter.add(tvRow);
setAdapter(rowsAdapter);
}
}
如果卡片显示为空白或标题不出现,优先检查三点:
CardPresenter 是否重写了 onBindViewHolder;ArrayObjectAdapter 构造时传入的 Presenter 是否与数据类型匹配;setAdapter 是否在视图创建后调用。验证卡片显示效果
运行应用后,用遥控器上下键切换行、左右键切换卡片,观察以下现象:
- 每行左侧显示行标题(“热门电影”“热门剧集”)。
- 卡片上显示缩略图、标题和副标题,且焦点卡片有放大或高亮效果。
- 切换行时,行标题和卡片内容同步更新,无错位或空白卡片。
若缩略图不显示,检查 R.drawable 资源是否存在且尺寸适合电视屏幕(建议 313×176 或 16:9 比例)。若标题文字被截断,可调整 ImageCardView 的宽度或减少标题字数。