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

Android TV Leanback BrowseFragment如何为Header行添加自定义图片

问题描述

代码如下:

public class PageAndListRowFragment extends BrowseFragment {
    private static final long HEADER_ID_1 = 1;
    private static final String HEADER_NAME_1 = "Page Fragment";
    private static final long HEADER_ID_2 = 2;
    private static final String HEADER_NAME_2 = "Rows Fragment";
    private static final long HEADER_ID_3 = 3;
    private static final String HEADER_NAME_3 = "Settings Fragment";
    private static final long HEADER_ID_4 = 4;
    private static final String HEADER_NAME_4 = "User agreement Fragment";
    private BackgroundManager mBackgroundManager;

    private ArrayObjectAdapter mRowsAdapter;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setupUi();
        loadData();
        mBackgroundManager = BackgroundManager.getInstance(getActivity());
        mBackgroundManager.attach(getActivity().getWindow());
        getMainFragmentRegistry().registerFragment(PageRow.class,
                new PageRowFragmentFactory(mBackgroundManager));
    }

    private void setupUi() {
        setHeadersState(HEADERS_ENABLED);
        setHeadersTransitionOnBackEnabled(true);
        setBrandColor(getResources().getColor(R.color.fastlane_background));
        setTitle("Title goes here");
        setOnSearchClickedListener(new View.OnClickListener() {

            @Override
            public void onClick(View view) {
                Toast.makeText(
                        getActivity(), getString(R.string.implement_search), Toast.LENGTH_SHORT)
                        .show();
            }
        });

        prepareEntranceTransition();

    }

    private void loadData() {
        mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
        setAdapter(mRowsAdapter);

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                createRows();
                startEntranceTransition();
            }
        }, 2000);
    }

    private void createRows() {
        HeaderItem headerItem1 = new HeaderItem(HEADER_ID_1, HEADER_NAME_1);
        PageRow pageRow1 = new PageRow(headerItem1);
        mRowsAdapter.add(pageRow1);

        HeaderItem headerItem2 = new HeaderItem(HEADER_ID_2, HEADER_NAME_2);
        PageRow pageRow2 = new PageRow(headerItem2);
        mRowsAdapter.add(pageRow2);

        HeaderItem headerItem3 = new HeaderItem(HEADER_ID_3, HEADER_NAME_3);
        PageRow pageRow3 = new PageRow(headerItem3);
        mRowsAdapter.add(pageRow3);

        HeaderItem headerItem4 = new HeaderItem(HEADER_ID_4, HEADER_NAME_4);
        PageRow pageRow4 = new PageRow(headerItem4);
        mRowsAdapter.add(pageRow4);
    }

    private static class PageRowFragmentFactory extends BrowseFragment.FragmentFactory {
        private final BackgroundManager mBackgroundManager;

        PageRowFragmentFactory(BackgroundManager backgroundManager) {
            this.mBackgroundManager = backgroundManager;
        }


        @Override
        public Fragment createFragment(Object rowObj) {
            Row row = (Row)rowObj;
            mBackgroundManager.setDrawable(null);
            if (row.getHeaderItem().getId() == HEADER_ID_1) {
                return new SampleFragmentA();
            } else if (row.getHeaderItem().getId() == HEADER_ID_2) {
                return new SampleFragmentB();
            } else if (row.getHeaderItem().getId() == HEADER_ID_3) {
                return new SettingsFragment();
            } else if (row.getHeaderItem().getId() == HEADER_ID_4) {
                return new WebViewFragment();
            }

            throw new IllegalArgumentException(String.format("Invalid row %s", rowObj));
        }
    }

    public static class PageFragmentAdapterImpl extends MainFragmentAdapter<SampleFragmentA> {

        public PageFragmentAdapterImpl(SampleFragmentA fragment) {
            super(fragment);
        }
    }

    /**
     * Simple page fragment implementation.
     */
    public static class SampleFragmentA extends GridFragment {
        private static final int COLUMNS = 4;
        private final int ZOOM_FACTOR = FocusHighlight.ZOOM_FACTOR_SMALL;
        private ArrayObjectAdapter mAdapter;

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setupAdapter();
            loadData();
            getMainFragmentAdapter().getFragmentHost().notifyDataReady(getMainFragmentAdapter());
        }


        private void setupAdapter() {
            VerticalGridPresenter presenter = new VerticalGridPresenter(ZOOM_FACTOR);
            presenter.setNumberOfColumns(COLUMNS);
            setGridPresenter(presenter);

            CardPresenterSelector cardPresenter = new CardPresenterSelector(getActivity());
            mAdapter = new ArrayObjectAdapter(cardPresenter);
            setAdapter(mAdapter);

            setOnItemViewClickedListener(new OnItemViewClickedListener() {
                @Override
                public void onItemClicked(
                        Presenter.ViewHolder itemViewHolder,
                        Object item,
                        RowPresenter.ViewHolder rowViewHolder,
                        Row row) {
                    Card card = (Card)item;
                    Toast.makeText(getActivity(),
                            "Clicked on "+card.getTitle(),
                            Toast.LENGTH_SHORT).show();
                }
            });
        }

        private void loadData() {
            String json = Utils.inputStreamToString(getResources().openRawResource(
                    R.raw.grid_example));
            CardRow cardRow = new Gson().fromJson(json, CardRow.class);
            mAdapter.addAll(0, cardRow.getCards());
        }
    }

    /**
     * Page fragment embeds a rows fragment.
     */
    public static class SampleFragmentB extends RowsFragment {
        private final ArrayObjectAdapter mRowsAdapter;

        public SampleFragmentB() {
            mRowsAdapter = new ArrayObjectAdapter(new ShadowRowPresenterSelector());

            setAdapter(mRowsAdapter);
            setOnItemViewClickedListener(new OnItemViewClickedListener() {
                @Override
                public void onItemClicked(
                        Presenter.ViewHolder itemViewHolder,
                        Object item,
                        RowPresenter.ViewHolder rowViewHolder,
                        Row row) {
                    Toast.makeText(getActivity(), "Implement click handler", Toast.LENGTH_SHORT)
                            .show();
                }
            });
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            createRows();
            getMainFragmentAdapter().getFragmentHost().notifyDataReady(getMainFragmentAdapter());
        }

        private void createRows() {
                String json = Utils.inputStreamToString(getResources().openRawResource(
                        R.raw.page_row_example));
                CardRow[] rows = new Gson().fromJson(json, CardRow[].class);
                for (CardRow row : rows) {
                    if (row.getType() == CardRow.TYPE_DEFAULT) {
                        mRowsAdapter.add(createCardRow(row));
                    }
                }
        }

        private Row createCardRow(CardRow cardRow) {
            PresenterSelector presenterSelector = new CardPresenterSelector(getActivity());
            ArrayObjectAdapter adapter = new ArrayObjectAdapter(presenterSelector);
            for (Card card : cardRow.getCards()) {
                adapter.add(card);
            }

            HeaderItem headerItem = new HeaderItem(cardRow.getTitle());
            return new CardListRow(headerItem, adapter, cardRow);
        }
    }

    public static class SettingsFragment extends RowsFragment {
        private final ArrayObjectAdapter mRowsAdapter;

        public SettingsFragment() {
            ListRowPresenter selector = new ListRowPresenter();
            selector.setNumRows(2);
            mRowsAdapter = new ArrayObjectAdapter(selector);
            setAdapter(mRowsAdapter);
        }

        @Override
        public void onAttach(Activity activity) {
            super.onAttach(activity);
            new Handler().postDelayed(new Runnable() {
                @Override
                public void run() {
                    loadData();
                }
            }, 200);
        }

        private void loadData() {
            if (isAdded()) {
                String json = Utils.inputStreamToString(getResources().openRawResource(
                        R.raw.icon_example));
                CardRow cardRow = new Gson().fromJson(json, CardRow.class);
                mRowsAdapter.add(createCardRow(cardRow));
                getMainFragmentAdapter().getFragmentHost().notifyDataReady(
                        getMainFragmentAdapter());
            }
        }

        private ListRow createCardRow(CardRow cardRow) {
            SettingsIconPresenter iconCardPresenter = new SettingsIconPresenter(getActivity());
            ArrayObjectAdapter adapter = new ArrayObjectAdapter(iconCardPresenter);
            for(Card card : cardRow.getCards()) {
                adapter.add(card);
            }

            HeaderItem headerItem = new HeaderItem(cardRow.getTitle());
            return new CardListRow(headerItem, adapter, cardRow);
        }
    }

    public static class WebViewFragment extends Fragment implements MainFragmentAdapterProvider {
        private MainFragmentAdapter mMainFragmentAdapter = new MainFragmentAdapter(this);
        private WebView mWebview;

        @Override
        public MainFragmentAdapter getMainFragmentAdapter() {
            return mMainFragmentAdapter;
        }

        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            getMainFragmentAdapter().getFragmentHost().showTitleView(false);
        }

        @Override
        public View onCreateView(
                LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            FrameLayout root = new FrameLayout(getActivity());
            FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(
                    FrameLayout.LayoutParams.MATCH_PARENT,
                    FrameLayout.LayoutParams.MATCH_PARENT);
            lp.setMarginStart(32);
            mWebview = new WebView(getActivity());
            mWebview.setWebViewClient(new WebViewClient());
            mWebview.getSettings().setJavaScriptEnabled(true);
            root.addView(mWebview, lp);
            return root;
        }

        @Override
        public void onResume() {
            super.onResume();
            mWebview.loadUrl("https://www.google.com/policies/terms");
            getMainFragmentAdapter().getFragmentHost().notifyDataReady(getMainFragmentAdapter());
        }
    }
}

使用上述代码,已成功实现带行的Header Item展示,当前UI效果如下图所示:
当前效果

开发中使用了leanback库的BrowseFragment组件。
目前需要对每个Header行的标题进行自定义,在标题区域添加图片,最终实现如下图所示的展示效果:
目标效果

需要提供在Header行项中添加图片的具体实现方法。

实现方案

Leanback默认的Header渲染使用纯文本的HeaderPresenter,要实现带图标的Header效果,核心是自定义Header的渲染逻辑,替换默认实现,具体步骤如下:

  • 自定义Header布局,同时容纳图标和标题
  • 扩展原生HeaderItem,增加图标资源字段
  • 实现自定义Presenter处理Header的视图绑定
  • 给BrowseFragment设置自定义的Header渲染器
  • 替换原有Header创建逻辑,传入图标资源

1. 创建自定义Header布局

新建布局文件res/layout/custom_header_view.xml,横向排列图标和标题:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingHorizontal="24dp"
    android:paddingVertical="12dp">

    <ImageView
        android:id="@+id/iv_header_icon"
        android:layout_width="24dp"
        android:layout_height="24dp"
        android:layout_marginEnd="12dp"
        android:contentDescription="@null" />

    <TextView
        android:id="@+id/tv_header_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@android:color/white"
        android:textSize="15sp"
        android:textStyle="bold" />

</LinearLayout>

2. 扩展HeaderItem支持图标

原生HeaderItem没有存储图标的字段,自定义子类扩展:

public class IconHeaderItem extends HeaderItem {
    private final int iconRes;

    public IconHeaderItem(long id, String name, int iconRes) {
        super(id, name);
        this.iconRes = iconRes;
    }

    public int getIconRes() {
        return iconRes;
    }
}

3. 实现自定义Header Presenter

继承Presenter实现自定义渲染逻辑,完成图标和标题的绑定:

public class IconHeaderPresenter extends Presenter {
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.custom_header_view, parent, false);
        return new ViewHolder(view);
    }

    @Override
    public void onBindViewHolder(ViewHolder viewHolder, Object item) {
        HeaderItem header = (HeaderItem) item;
        View itemView = viewHolder.view;
        TextView titleView = itemView.findViewById(R.id.tv_header_title);
        ImageView iconView = itemView.findViewById(R.id.iv_header_icon);

        titleView.setText(header.getName());
        if (header instanceof IconHeaderItem) {
            iconView.setVisibility(View.VISIBLE);
            iconView.setImageResource(((IconHeaderItem) header).getIconRes());
        } else {
            iconView.setVisibility(View.GONE);
        }
    }

    @Override
    public void onUnbindViewHolder(ViewHolder viewHolder) {
        ImageView iconView = viewHolder.view.findViewById(R.id.iv_header_icon);
        iconView.setImageDrawable(null);
    }
}

4. 替换BrowseFragment默认Header渲染器

在原有setupUi()方法中,添加Header Presenter的设置:

private void setupUi() {
    setHeadersState(HEADERS_ENABLED);
    setHeadersTransitionOnBackEnabled(true);
    setBrandColor(getResources().getColor(R.color.fastlane_background));
    // 新增:设置自定义带图标Header渲染器
    setHeaderPresenterSelector(new PresenterSelector() {
        @Override
        public Presenter getPresenter(Object item) {
            return new IconHeaderPresenter();
        }
    });
    setTitle("Title goes here");
    setOnSearchClickedListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            Toast.makeText(
                    getActivity(), getString(R.string.implement_search), Toast.LENGTH_SHORT)
                    .show();
        }
    });

    prepareEntranceTransition();
}

5. 修改Header创建逻辑

把原有createRows()方法中创建普通HeaderItem的代码,替换为自定义的IconHeaderItem,传入对应图标资源即可:

private void createRows() {
    // 将R.drawable下的对应资源替换为你自己的Header图标
    HeaderItem headerItem1 = new IconHeaderItem(HEADER_ID_1, HEADER_NAME_1, R.drawable.ic_page);
    PageRow pageRow1 = new PageRow(headerItem1);
    mRowsAdapter.add(pageRow1);

    HeaderItem headerItem2 = new IconHeaderItem(HEADER_ID_2, HEADER_NAME_2, R.drawable.ic_rows);
    PageRow pageRow2 = new PageRow(headerItem2);
    mRowsAdapter.add(pageRow2);

    HeaderItem headerItem3 = new IconHeaderItem(HEADER_ID_3, HEADER_NAME_3, R.drawable.ic_settings);
    PageRow pageRow3 = new PageRow(headerItem3);
    mRowsAdapter.add(pageRow3);

    HeaderItem headerItem4 = new IconHeaderItem(HEADER_ID_4, HEADER_NAME_4, R.drawable.ic_agreement);
    PageRow pageRow4 = new PageRow(headerItem4);
    mRowsAdapter.add(pageRow4);
}

补充说明

  • 如果需要调整Header选中态的样式(比如图标/文字变色、放大效果),可以给布局添加状态选择器,或者在onBindViewHolder中根据选中状态动态调整样式。
  • 如果内部列表行的Header也需要加图标,给对应ListRowPresenter调用setHeaderPresenter()方法传入上述自定义的IconHeaderPresenter即可,逻辑完全一致。

内容的提问来源于stack exchange,提问作者MARSH

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 11:03:15