KBeanie

Android TV — Understanding BrowseSupportFragment — Part 1

If you have already started with Android TV development, you would know about BrowseSupportFragment. For the absolute beginners, to get started quickly with Android TV development, BrowseSupportFragment is the first thing that you would come across. Here’s a snippet from the documentation,

A fragment for creating Leanback browse screens. It is composed of a RowsSupportFragment and a HeadersSupportFragment.

It’s a bit too technical I would say. Frankly, I didn’t understand much on the first read. And since, this is the starting point anyway, I wanted to understand what exactly is this special fragment doing?

Visually, the fragment does this….

Yes, this fragment does a lot of things. Let’ see a video.

All the scrolling, navigation between the sidebar and the content pane on the right, animations, etc, are happening inside the BrowseSupportFragment. Well, actually, there are other classes involved, but everything starts from this really special fragment.

When you create an Android TV project, it gives you this implementation by default, so that you can get started with your app. I wanted to customise the HeaderSupportFragment, but it wasn’t very clear where to start from. So, I had to dig a little deeper.

It contains 2 fragments, HeadersSupportFragment and RowsSuportFragment.

2 Fragments — BrowseSupportFragment

Ok. Now it’s quite clear how to proceed. If you want to customise the left sidebar, you need to deal with HeadersSupportFragment. Let’s dig a little deeper how that can be done.

HeadersSupportFragment consists of list row headers. You need to support 3 types of row items.

  • Divider Row — Renders a divider
  • Row — Renders a focusable item that highlights the related data on the right side
  • Section Row — Renders a section view which helps organise the options into various sections
Divider, Rows and Sections
Types of items in the HeadersSupportFragment

The leanback library already provides default implementations for these item types. If you want to customise the visual and the behaviour, you will need to roll out your own.

Default implementations

Now, let’s see how we can roll out our own Presenter for the “Rows”, i.e. category items like Category Zero, Category One etc.

Presenters are closely related to the concept of RecyclerView.Adapter. They are used to generate views and bind objects/data to them. The implementation would also be very much similar to RecyclerView.Adapter and the ViewHolder patterns that you must be already aware of.

class QTVRowViewHolder(view: View) : RowHeaderPresenter.ViewHolder(view) {
val tvTitle: TextView
init {
tvTitle = view.findViewById(R.id.tvTitle)
}
}

First, I have created a custom view holder that is a sub-class of the RowHeaderPresenter.ViewHolder. For now, this only consist of a simple TextView.

class QTVRowPresenter : Presenter() {
override fun onCreateViewHolder(parent: ViewGroup?): QTVRowViewHolder {
val root: View = LayoutInflater.from(parent!!.context)
.inflate(R.layout.presenter_row, parent, false)

val viewHolder = QTVRowViewHolder(root)
return viewHolder
}

override fun onBindViewHolder(viewHolder: ViewHolder?, item: Any?) {
val headerItem = if (item == null) null else (item as Row).headerItem
val vh = viewHolder as QTVRowViewHolder
vh.tvTitle.text = headerItem?.name
}

override fun onUnbindViewHolder(viewHolder: ViewHolder?) {
val vh = viewHolder as QTVRowViewHolder
vh.tvTitle.text = null
}
}

Now, using that view holder, I have created a custom Presenter. You can see familiar methods like onCreateViewHolder and onBindViewHolder.

In the onBindViewHolder method, you will receive the view holder and the item. Currently, by default, the type of this “item” is a HeaderItem, which has an “id: Long” and “name: String” properties. For this example, I have bound the name of item to the TextView that I have in the “R.layout.presenter_row”. Later, we will see how we can customise the HeaderItem to add other data like icons, descriptions etc.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:id="@+id/tvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>

Now, that our custom QTVRowPresenter is ready, let’s see how we can replace the default RowPresenter with our own. For this, we need to look into the BrowseSupportFragment again. Since, it already has a default HeadersSupportFragment implementation, we have 2 approaches here.

  • Roll out our own implementation of HeadersSupportFragment
  • Modify the default HeadersSupportFragment

With the first approach, we will need to do a bit more work. But with the second approach, for this scenario, we will need to do very minimal changes. Here’s the piece of code that you need to add in BrowseSupportFragment.

private fun setupUIElements() {
title = getString(R.string.browse_title)
// over title
headersState = BrowseSupportFragment.HEADERS_ENABLED
isHeadersTransitionOnBackEnabled = true

// set fastLane (or headers) background color
brandColor = ContextCompat.getColor(context!!, R.color.color_primary)
// set search icon color
searchAffordanceColor = ContextCompat.getColor(context!!, R.color.search_opaque)

// Lines of code to be added
val sHeaderPresenter: PresenterSelector = ClassPresenterSelector()
.addClassPresenter(DividerRow::class.java, DividerPresenter())
.addClassPresenter(
SectionRow::class.java,
RowHeaderPresenter()
)
.addClassPresenter(Row::class.java, QTVRowPresenter())

setHeaderPresenterSelector(sHeaderPresenter)
}

HeadersSupportFragment has a method to set your own Presenter implementations. The setHeaderPresenterSelector takes in a ClassPresenterSelector object, which is actually a collection of 3 classes/presenters:

  • DividerPresenter — For dividers
  • RowHeaderPresenter — For sections
  • RowPresenter — For data items

Here, as you can see, I have added my newly created QTVRowPresenter.

That’s it. If you run the app now, you wouldn’t see the sections or the dividers though. The default project that Android Studio creates for you doesn’t add any data for dividers and sections. Let’s see how you can add data for them.

private fun loadRows() {
val list = MovieList.list

val rowsAdapter = ArrayObjectAdapter(ListRowPresenter())
val cardPresenter = CardPresenter()

// Add a section
rowsAdapter.add(SectionRow(100, "Section 1"))
// Add a divider
rowsAdapter.add(DividerRow())

for (i in 0 until NUM_ROWS) {
if (i != 0) {
Collections.shuffle(list)
}
val listRowAdapter = ArrayObjectAdapter(cardPresenter)
for (j in 0 until NUM_COLS) {
listRowAdapter.add(list[j % 5])
}
val header = HeaderItem(i.toLong(), MovieList.MOVIE_CATEGORY[i])
rowsAdapter.add(ListRow(header, listRowAdapter))
}

// One more divider
rowsAdapter.add(DividerRow())
// One more section
rowsAdapter.add(SectionRow(100, "Section 2"))

val gridHeader = HeaderItem(NUM_ROWS.toLong(), "PREFERENCES")

val mGridPresenter = GridItemPresenter()
val gridRowAdapter = ArrayObjectAdapter(mGridPresenter)
gridRowAdapter.add(resources.getString(R.string.grid_view))
gridRowAdapter.add(getString(R.string.error_fragment))
gridRowAdapter.add(resources.getString(R.string.personal_settings))
rowsAdapter.add(ListRow(gridHeader, gridRowAdapter))

adapter = rowsAdapter
}

Once you run this now, you can see this result.

In subsequent posts, I will be covering more about the BrowseSupportFragment. Stay tuned!!!

Leave a Reply

Your email address will not be published. Required fields are marked *