lightbulb_outline Please take our October 2018 developer survey. Start survey

Use Leanback transport controls

The Leanback library has new playback controls that provide an improved user experience. For video apps, the transport controls suppport video scrubbing with the forward/backward controls. While scrubbing the display shows thumbnails to help navigate through the video.

The library includes abstract classes as well as out-of-the-box implementations that provide more granular control for developers. Using the pre-built implementations you can quickly build a feature-rich app without much coding. If you need more customization, you can extend any of the library's pre-built components.

Controls and player

The leanback library separates the UI with the transport controls from the player that plays back video. This is accomplished with two components: a playback fragment for displaying the transport controls (and optionally the video) and a player adapter to encapsulate a media player.

Playback fragment

Your app's UI Activity should use a PlaybackFragment or a VideoFragment. Both contain the leanback transport controls:

You can customize a fragment's ObjectAdapter to enhance the UI. For example, use setAdapter() to add a "related videos" row.

PlayerAdapter

PlayerAdapter is an abstract class that controls the underlying media player. Developers can chose the pre-built MediaPlayerAdapter implementation, or write their own implementation of this class.

Glueing the pieces together

You must use some "control glue" to connect the playback fragment to the player. The leanback library provides two kinds of glue:

leanback transport control glue

If you want your app to support video scrubbing you must use PlaybackTransportControlGlue.

You also need to specify a "glue host" that binds the glue to the playback fragment, draws the transport controls in the UI and maintains their state, and passes transport control events back to the glue. The host must match the playback fragment type. Use PlaybackFragmentGlueHost with a PlaybackFragment, and VideoFragmentGlueHost with a VideoFragment.

Here's an illustration showing how the pieces of a leanback transport control fit together:

leanback transport control glue

The code that glues your app together should be inside the PlaybackFragment or VideoFragment that defines the UI.

In the following example, the app constructs an instance ofPlaybackTransportControlGlue, naming it playerGlue, and connects its VideoFragment to a newly-created MediaPlayerAdapter. Since this is a VideoFragment the setup code calls setHost() to attach a VideoFragmentGlueHost to playerGlue. The code is included inside the class that extends the VideoFragment.

Kotlin

    class MyVideoFragment : VideoFragment() {

    fun onCreate(savedInstanceState: Bundle) {
        super.onCreate(savedInstanceState)
        val playerGlue = PlaybackTransportControlGlue(getActivity(),
            MediaPlayerAdapter(getActivity()))
        playerGlue.setHost(VideoFragmentGlueHost(this))
        playerGlue.addPlayerCallback(object : PlaybackGlue.PlayerCallback() {
            fun onPreparedStateChanged(glue: PlaybackGlue) {
                if (glue.isPrepared()) {
                    playerGlue.setSeekProvider(MySeekProvider())
                    playerGlue.play()
                }
            }
        })
        playerGlue.setSubtitle("Leanback artist")
        playerGlue.setTitle("Leanback team at work")
        val uriPath = "android.resource://com.example.android.leanback/raw/video"
        playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath))
    }
}
</pre>

Java

<pre class="prettyprint lang-java"> public class MyVideoFragment extends VideoFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final PlaybackTransportControlGlue<MediaPlayerAdapter> playerGlue = new PlaybackTransportControlGlue(getActivity(), new MediaPlayerAdapter(getActivity())); playerGlue.setHost(new VideoFragmentGlueHost(this)); playerGlue.addPlayerCallback(new PlaybackGlue.PlayerCallback() { @Override public void onPreparedStateChanged(PlaybackGlue glue) { if (glue.isPrepared()) { playerGlue.setSeekProvider(new MySeekProvider()); playerGlue.play(); } } }); playerGlue.setSubtitle("Leanback artist"); playerGlue.setTitle("Leanback team at work"); String uriPath = "android.resource://com.example.android.leanback/raw/video"; playerGlue.getPlayerAdapter().setDataSource(Uri.parse(uriPath)); } } </pre>
</div>

Note that the setup code also defines a PlayerAdapter.Callback to handle events from the media player.

Customizing the UI glue

You can customize the PlaybackBannerControlGlue and PlaybackTrabsportControlGlue. to change the PlaybackControlsRow.

Customizing the title and description

To customize the title and description at the top of the playback controls, override onCreateRowPresenter():

Kotlin

override fun onCreateRowPresenter(): PlaybackRowPresenter {
    return super.onCreateRowPresenter().apply {
        (this as? PlaybackTransportRowPresenter)
                ?.setDescriptionPresenter(MyCustomDescriptionPresenter())
    }
}

Java

@Override
protected PlaybackRowPresenter onCreateRowPresenter() {
  PlaybackTransportRowPresenter presenter = (PlaybackTransportRowPresenter) super.onCreateRowPresenter();
  presenter.setDescriptionPresenter(new MyCustomDescriptionPresenter());
  return presenter;
}

Adding controls

The control glue displays controls for actions in a PlaybackControlsRow.

The actions in the PlaybackControlsRow are assigned to two groups: primary actions and secondary actions. Controls for the primary group appear above the seek bar and controls for the secondary group appear below the seek bar. Initially, there is only a single primary action for the play/pause button, and no secondary actions.

You can add actions the primary and secondary groups by overriding onCreatePrimaryActions() and onCreateSecondaryActions().

Kotlin

private lateinit var mRepeatAction: PlaybackControlsRow.RepeatAction
private lateinit var mPipAction: PlaybackControlsRow.PictureInPictureAction
private lateinit var mThumbsUpAction: PlaybackControlsRow.ThumbsUpAction
private lateinit var mThumbsDownAction: PlaybackControlsRow.ThumbsDownAction
private lateinit var mSkipPreviousAction: PlaybackControlsRow.SkipPreviousAction
private lateinit var mSkipNextAction: PlaybackControlsRow.SkipNextAction
private lateinit var mFastForwardAction: PlaybackControlsRow.FastForwardAction
private lateinit var mRewindAction: PlaybackControlsRow.RewindAction

override fun onCreatePrimaryActions(primaryActionsAdapter: ArrayObjectAdapter) {
    // Order matters, super.onCreatePrimaryActions() will create the play / pause action.
    // Will display as follows:
    // play/pause, previous, rewind, fast forward, next
    //   > /||      |<        <<        >>         >|
    super.onCreatePrimaryActions(primaryActionsAdapter)
    primaryActionsAdapter.apply {
        add(mSkipPreviousAction)
        add(mRewindAction)
        add(mFastForwardAction)
        add(mSkipNextAction)
    }
}

override fun onCreateSecondaryActions(adapter: ArrayObjectAdapter?) {
    super.onCreateSecondaryActions(adapter)
    adapter?.apply {
        add(mThumbsDownAction)
        add(mThumbsUpAction)
    }
}

Java

private PlaybackControlsRow.RepeatAction mRepeatAction;
private PlaybackControlsRow.PictureInPictureAction mPipAction;
private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction;
private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction;
private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction;
private PlaybackControlsRow.SkipNextAction mSkipNextAction;
private PlaybackControlsRow.FastForwardAction mFastForwardAction;
private PlaybackControlsRow.RewindAction mRewindAction;

@Override
protected void onCreatePrimaryActions(ArrayObjectAdapter primaryActionsAdapter) {
    // Order matters, super.onCreatePrimaryActions() will create the play / pause action.
    // Will display as follows:
    // play/pause, previous, rewind, fast forward, next
    //   > /||      |<        <<        >>         >|
    super.onCreatePrimaryActions(primaryActionsAdapter);
    primaryActionsAdapter.add(mSkipPreviousAction);
    primaryActionsAdapter.add(mRewindAction);
    primaryActionsAdapter.add(mFastForwardAction);
    primaryActionsAdapter.add(mSkipNextAction);
}

@Override
protected void onCreateSecondaryActions(ArrayObjectAdapter adapter) {
    super.onCreateSecondaryActions(adapter);
    adapter.add(mThumbsDownAction);
    adapter.add(mThumbsUpAction);
}

You must override onActionClicked() to handle the new actions.

Kotlin

override fun onActionClicked(action: Action) {
    when(action) {
        mRewindAction -> {
            // Handle Rewind
        }
        mFastForwardAction -> {
            // Handle FastForward
        }
        mThumbsDownAction -> {
            // Handle ThumbsDown
        }
        mThumbsUpAction -> {
            // Handle ThumbsUp
        }
        else ->
            // The superclass handles play/pause and delegates next/previous actions to abstract methods,
            // so those two methods should be overridden rather than handling the actions here.
            super.onActionClicked(action)
    }
}

override fun next() {
    // Skip to next item in playlist.
}

override fun previous() {
    // Skip to previous item in playlist.
}

Java

@Override
public void onActionClicked(Action action) {
    if (action == mRewindAction) {
        // Handle Rewind
    } else if (action == mFastForwardAction ) {
        // Handle FastForward
    } else if (action == mThumbsDownAction) {
        // Handle ThumbsDown
    } else if (action == mThumbsUpAction) {
        // Handle ThumbsUp
    } else {
        // The superclass handles play/pause and delegates next/previous actions to abstract methods,
        // so those two methods should be overridden rather than handling the actions here.
        super.onActionClicked(action);
    }
}

@Override
public void next() {
    // Skip to next item in playlist.
}

@Override
public void previous() {
    // Skip to previous item in playlist.
}

In special cases, you might want to implement your own PlaybackTransportRowPresenter to render custom controls and respond to seek actions using the PlaybackSeekUi.

Video scrubbing

If your app uses a VideoFragment and you want to support video scrubbing.

scrubbing

You need to provide an implementation of PlaybackSeekDataProvider. This component provides the video thumbnails used when scrolling. You must implement your own provider by extending PlaybackSeekDataProvider. See the example in the leanback-showcase app.