27 September 2023

Android TV
Development: Using the Leanback Library

Published on 27 September 2023

Android TV Development: Using the Leanback Library

This in-depth analysis of BrowseFragment's capabilities for Android TV development is based on the example of an online cinema application.


We will explore the functionalities of BrowseFragment, which, according to Google's concept, serves as the main screen of the application. In our case, it was an application for an online cinema under a large media holding.


In this article, I will cover:


· BrowseFragment and its core components: titleView, HeadersFragment, and RowsFragment.


· Configuring the titleView container and creating custom options.


· The fundamental setup of HeadersFragment and RowsFragment.


BrowseFragment and Its Components


BrowseFragment is a fragment designed for creating screens with lists of elements and titles. 


Let's delve into each of these elements:


TitleView


**TitleView is a container with various elements. It serves the purpose of branding the TextView and ImageView within the application and includes the provision of the Search OrbView search button by default. To ensure the visibility of the search button, you need to set a listener. This can be accomplished by calling setOnSearchClickedListener {//your code} within the fragment.


There are several methods to customize the color scheme of the button:


• By setting searchAffordanceColor = context.getColorRes (R.color.search_opaque). With this approach, only the color of the circle will be altered.


• By setting searchAffordanceColors = SearchOrbView.Colors (context.getColor(R.color.search_opaque), context.getColor(R.color.search_opaque_bright), context.getColor(R.color.search_opaque_icon)). The SearchOrbView.Colors has an overloaded constructor that offers more flexible configuration options.


Colors(@ColorInt int color) —      sets the color of the circle.

Colors(@ColorInt int color,      @ColorInt int brightColor) — sets the color of the circle and the      animation color of the circle.

Colors(@ColorInt int color,      @ColorInt int brightColor, @ColorInt int iconColor) — sets the color of      the circle, the animation color of the circle, and the icon color. The      animation of the circle pertains to the flickering effect.

Within titleView, you can set either a text title with title = "Finch" or a logo with:


badgeDrawable = ContextCompat.getDrawable(context, R.drawable.app_icon_your_company).


Additionally, only one of these elements can be installed at a time, and the logo always takes precedence.


These elements are highly customizable, and it is advisable to adhere to Google's recommendations. If needed, you can create a custom titleView. For example, if you wish to replace the search button with a text field, you can follow these steps: [...]**


Design a layout for the new TitleView:


<merge xmlns:android="http://schemas.android.com/apk/res/android"


android:layout_width="match_parent"


android:layout_height="wrap_content">


<TextView


android:id="@+id/vTitle"


android:layout_width="wrap_content"


android:layout_height="wrap_content"


android:layout_alignParentStart="true"


android:layout_marginStart="40dp"


android:layout_marginEnd="24dp"


android:textAllCaps="true"


android:textSize="20sp"


android:textColor="@color/search_opaque"


android:visibility="visible" />


</merge>


Create a View that implements the TitleViewAdapter.Provider and then create the TitleViewAdapter itself:


class CustomTitleView @JvmOverloads constructor(


context: Context,


attrs: AttributeSet? = null,


@AttrRes


defStyleAttr: Int = 0


) : FrameLayout(context, attrs, defStyleAttr), TitleViewAdapter.Provider {


private valtitleViewAdapter = object : TitleViewAdapter() {


override fun getSearchAffordanceView(): View? {


return null


}


override fun setTitle(titleText: CharSequence?) {


this@CustomTitleView.setTitle(titleText)


}


}


init {


View.inflate(context, R.layout.view_custom_title, this)


}


private fun setTitle(title: CharSequence?) {


vTitle.text = title


}


override fun getTitleViewAdapter(): TitleViewAdapter {


return titleViewAdapter


}


}


Create another layout that contains only one element — CustomTitleView:


<fm.finch.tv_test_project.extensions.browse.customTitle.CustomTitleView xmlns:android="http://schemas.android.com/apk/res/android"


android:id="@+id/browse_title_group"


android:layout_width="match_parent"


android:layout_height="wrap_content"


android:padding="16dp">


</fm.finch.tv_test_project.extensions.browse.customTitle.CustomTitleView>


Then create a style for the activity:


<style name="BrowseCustomTitleTheme" parent="Leanback.CustomTitle" />


<style name="Leanback.CustomTitle" parent="@style/Theme.Leanback.Browse">


<item name="browseTitleViewLayout">@layout/title_view</item>


<item name="browseRowsMarginTop">60dp</item>


</style>


And set it as the theme of our activity in the manifest:


<activity android:name=".MainActivity"


android:theme="@style/BrowseCustomTitleTheme">


<intent-filter>


<action android:name="android.intent.action.MAIN" />


<category android:name="android.intent.category.LEANBACK_LAUNCHER" />


</intent-filter>


</activity>


HeadersFragment & RowsFragment


BrowseSupportFragment is a fragment designed to render the elements of its adapter (ObjectAdapter) as rows in a vertical list. Visually, this fragment is divided into two parts: HeadersFragment, which displays a list of titles on the left side of the screen, and RowsFragment, which acts as a container for content on the right side of the screen. These fragments work together, and BrowseFragment delegates the rendering of its adapter elements to them.


All elements provided to the BrowseFragment's adapter must be subclasses of Row since this object carries information about both the header and content to display.


Header Fragment is intended for rendering and interacting with the HeaderItem, which is a part of the Row passed to the BrowseFragment's adapter.


RowsFragment is designed for rendering and interacting with the elements of its ObjectAdapter. It's not a BrowseFragment's adapter, and it doesn't belong to the Row passed to the BrowseFragment. Instead, it receives objects that describe the content.


I will illustrate this with an example. Let's create a Presenter that will pass text to a TextView:


class TextPresenter : Presenter() {


override fun onCreateViewHolder(parent: ViewGroup): ViewHolder {


val view = TextView(parent.context)


view.layoutParams = ViewGroup.LayoutParams(300, 200)


view.isFocusable = true


view.isFocusableInTouchMode = true


view.setBackgroundColor(


ContextCompat.getColor(


parent.context,


android.R.color.background_light


)


)


view.gravity = Gravity.CENTER


view.setTextColor(Color.BLACK)


return Presenter.ViewHolder(view)


}


override fun onBindViewHolder(viewHolder: ViewHolder, item: Any) {


val textView = viewHolder.view as TextView


val str = item as String


textView.text = str


}


override fun onUnbindViewHolder(viewHolder: ViewHolder) {


val textView = viewHolder.view as TextView


textView.text = ""


}


}


Then we will create all the necessary adapters, populate them with data, and set up the BrowseFragment to display the data:


class MainFragment : BrowseSupportFragment() {


override fun onActivityCreated(savedInstanceState: Bundle?) {


super.onActivityCreated(savedInstanceState)


initView(savedInstanceState)


}


private fun initView(savedInstanceState: Bundle?) {


headersState = HEADERS_HIDDEN


title = "Finch"


val browseAdapter = ArrayObjectAdapter(ListRowPresenter())


val rowsAdapter = ArrayObjectAdapter(TextPresenter())


rowsAdapter.add("Element 1")


rowsAdapter.add("Element 2")


rowsAdapter.add("Element 3")


val firstHeader = HeaderItem("Heading 1")


val secondHeader = HeaderItem("Heading 2")


val thirdHeader = HeaderItem("Heading 3")


browseAdapter.add(ListRow(firstHeader, rowsAdapter))


browseAdapter.add(ListRow(secondHeader, rowsAdapter))


browseAdapter.add(ListRow(thirdHeader, rowsAdapter))


adapter = browseAdapter


}


}


We will assign a Presenter to each adapter to render the elements:


For the BrowseFragment, we utilize the standard implementation of ListRowPresenter. This Presenter can work with List Rows and visualizes them using a Horizontal Grid View placed in a ListRowView.


For the RowsFragment adapter, we use our custom TextPresenter() implementation since we pass String and TextPresenter() elements to it, and it knows how to display them.


Now we have a list attached to each title in the left half of the screen and content in the right half of the screen.


Let's delve deeper into the HeadersFragment.


By default, the HeadersFragment is immediately visible to the user. However, you can alter this behavior using the setHeadersState(int) method. You should access it during the onActivityCreated() call and pass one of the following states:


HEADERS_ENABLED: The fragment is      visible to the user.

HEADERS_HIDDEN: The fragment is      collapsed.

HEADERS_DISABLED: The fragment      is completely hidden from the screen.

BrowseFragment handles onBackPressed() by default. If the HeadersFragment is minimized, pressing the "Back" button on the remote control or joystick will expand it and make it visible. This behavior can be disabled by passing false to the setHeadersTransitionOnBackEnabled() method.


To manually control the HeadersFragment state, you can use the startHeadersTransition(boolean) method. Depending on the input parameter, the header fragment will either be shown (if true) or collapsed (if false).


If you need to listen to the beginning and end of the animation of the HeadersFragment transition from the collapsed state to the active state and vice versa, you can set a listener using setBrowseTransitionListener(BrowseSupportFragment.BrowseTransitionListener).


There are several other Row implementations that specifically affect the Header Fragment:


DividerRow: Adds a divider      between headers.

SectionRow: Introduces a subtitle.

Let's make the code changes as follows:


browseAdapter.add(ListRow(firstHeader, rowsAdapter))


browseAdapter.add(DividerRow())


browseAdapter.add(ListRow(secondHeader, rowsAdapter))


browseAdapter.add(DividerRow())


browseAdapter.add(ListRow(thirdHeader, rowsAdapter))


browseAdapter.add(SectionRow("Subtitle 1"))


What happened: Separators have been added between the elements in HeadersFragment, and the last element now has a subtitle.


Customization of Header Fragment


Suppose we want to modify the appearance of the headers and include icons. To achieve this, we must create a unique header item with a custom Presenter and also establish a PresenterSelector, which functions as a Presenter for each list item. Let's begin with the header element:


class IconHeaderItem(


name: String,


id: Long = -1,


val iconResId: Int = NO_ICON


) : HeaderItem(id, name) {


companion object {


val NO_ICON = -1


}


}


We have extended the standard HeaderItem to include the capability of setting icons from resources. Now, let's create the layout for the new title:


<TextView xmlns:android="http://schemas.android.com/apk/res/android"


android:focusable="true"


android:layout_width="wrap_content"


android:layout_height="wrap_content"


android:gravity="center"


android:drawablePadding="16dp"


android:textSize="18sp"/>


Let's create our own presenter capable of rendering Icon Header Items:


class IconRowHeaderPresenter : RowHeaderPresenter() {


override fun onCreateViewHolder(viewGroup: ViewGroup): ViewHolder {


val view = LayoutInflater


.from(viewGroup.context)


.inflate(R.layout.item_icon_header, viewGroup, false)


val viewHolder = ViewHolder(view)


setSelectLevel(viewHolder, 0f)


return viewHolder


}


override fun onBindViewHolder(viewHolder: Presenter.ViewHolder, data: Any) {


val row = data as? Row


val iconHeaderItem = row?.headerItem as? IconHeaderItem


iconHeaderItem?.let {


val tv = viewHolder


.view


.textView


if (iconHeaderItem.iconResId != IconHeaderItem.NO_ICON) {


tv.setCompoundDrawablesWithIntrinsicBounds(iconHeaderItem.iconResId, 0, 0, 0)


}


tv.text = iconHeaderItem.name


}


}


override fun onUnbindViewHolder(viewHolder: Presenter.ViewHolder) {


val tv = viewHolder


.view


.textView


tv.text = ""


tv.setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0)


}


}


Let's set up a new PresenterSelector for headers. To do this, use the setHeaderPresenterSelector method in BrowseFragment:


setHeaderPresenterSelector(object : PresenterSelector() {


val presenter = IconRowHeaderPresenter()


val defaultPresenterSelector = headersSupportFragment.presenterSelector


override fun getPresenter(data: Any): Presenter {


return if ((data as? Row)?.headerItem is IconHeaderItem)


presenter


else


defaultPresenterSelector.getPresenter(data)


}


})


Now let's focus on the Row header. If it's an IconHeaderItem, we'll return a Presenter that can handle it – in this case, the IconRowHeaderPresenter. Replace the headers with the new ones:


val firstHeader = IconHeaderItem("Heading 1", iconResId = R.drawable.ic_header_item)


val secondHeader = IconHeaderItem("Heading 2", iconResId = R.drawable.ic_header_item)


val thirdHeader = IconHeaderItem("Heading 3", iconResId = R.drawable.ic_header_item)


In the same manner, you can customize the subtitles in SectionRow or the separators in DividerRow.


Conclusion


We have explored the capabilities of BrowseFragment, which is part of the Leanback library. However, Leanback is not confined to this template. Its components are extensive enough to create a complete Android TV application while adhering to all the guidelines.

Recommended Tutorials

Stay updated with our latest articles, industry insights, and expert tips to keep your business informed and inspired.