4. 电视端卡片视图与内容行数据绑定

电视端卡片视图与内容行数据绑定 学习卡片

为什么需要 Presenter 与 ArrayObjectAdapter

BrowseFragment 本身只负责“行”的容器与焦点导航,行内卡片长什么样、数据从哪来,需要由 Presenter 和 ArrayObjectAdapter 协作完成。可以把它们理解为一条数据装配链:ArrayObjectAdapter 持有数据对象列表,Presenter 决定每个对象如何渲染成卡片视图,ListRow 再把适配器包装成 BrowseFragment 能识别的一行。

卡片视图数据绑定链路 本地模拟数据 Movie(title, subtitle, thumbnailResId) ArrayObjectAdapter add(movie1) add(movie2) add(movie3) CardPresenter onCreateViewHolder onBindViewHolder setTitle/setContent 卡片视图 缩略图 标题/副标题 ListRow(header, adapter) 将适配器包装为 BrowseFragment 可识别的一行 关键约束: 1. Presenter 必须实现 onCreateViewHolder 和 onBindViewHolder,否则卡片无法渲染。 2. ArrayObjectAdapter 中的对象类型必须与 Presenter 泛型一致,否则绑定会失败。

定义卡片数据模型

先定义一个简单的 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 的宽度或减少标题字数。