This post demonstrates how to add (Android) leanback controls to ExoPlayer to control playback. I couldn’t find any tutorials outlining the implementation details of leanback controls. The amount of classes and their dependencies resulted in a sluggish implementation.
Here’s an outline of what’s covered in this post:
- Adding the leanback library dependency
- Adding a view to house the leanback controls
- Customizing the leanback controls
- Initializing the classes required
- Displaying the controls
Classes Used
Here’s a list of the classes used in this post:
- PlaybackOverlayFragment
- PlaybackControlsRowPresenter
- ControlsButtonPresenterSelector
- PlaybackControlsRow
- ClassPresenterSelector
- ArrayObjectAdapter
Add the leanback dependency:
Add the leanback library by adding it to your modules build.gradle
. At the time of this writing I used 24.1.1
because I have an minSdk
of 16. Use of this library may require the v4 support library depending on your applications setup.
compile "com.android.support:leanback-v17:24.1.1" |
Add a new view to hold the leanback controls:
This view should be a sibling of the same parent view of your ExoPlayer view. In this example the video_player
layout has a width
and height
of match_parent
.
1 2 3 4 5 6 7 8 9 10 11 12 | <merge xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <include android:id="@id/video_player" layout="@layout/my_video_player" /> <FrameLayout android:id="@+id/my_playback_overlay_fragment_view" android:layout_width="match_parent" android:layout_height="match_parent" /> </merge> |
Create a subclass of PlaybackOverlayFragment
Add a setupLeanbackControls()
to the onCreate()
method to hold the code below.
1 2 3 4 5 6 7 8 | public class MyPlaybackOverlayFragment extends PlaybackOverlayFragment { @Override public void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupLeanbackControls(); } } |
Create a PlaybackControlsRowPresenter:
The PlaybackControlsRowPresenter
is used to display a series of playback control buttons. If you don’t want your secondary actions (beyond rewind, play/pause, and fast forward) to be hidden behind a “more actions” button use setSecondaryActionsHidden(false)
.
1 2 | PlaybackControlsRowPresenter playbackControlsRowPresenter = new PlaybackControlsRowPresenter(); playbackControlsRowPresenter.setSecondaryActionsHidden(false); |
Create a ControlButtonPresenterSelector:
The ControlButtonPresenterSelector
is used to display primary and secondary controls for PlaybackControlsRow
(see below). Add the ControlButtonPresenterSelector
to a ArrayObjectAdapter
(see below).
1 2 | final ControlButtonPresenterSelector controlButtonPresenterSelector = new ControlButtonPresenterSelector(); ArrayObjectAdapter primaryActionsAdapter = new ArrayObjectAdapter(controlButtonPresenterSelector); |
Create primary leanback actions:
These represent the buttons which will appear on the leanback controls when shown. Add the desired primary controls to the primaryActionsAdapter
created above.
1 2 3 4 5 6 7 | playPauseAction = new PlayPauseAction(getActivity()); rewindAction = new RewindAction(getActivity()); fastForwardAction = new FastForwardAction(getActivity()); primaryActionsAdapter.add(rewindAction); primaryActionsAdapter.add(playPauseAction); primaryActionsAdapter.add(fastForwardAction); |
Create a ClassPresenterSelector:
The ClassPresenterSelector
selects a Presenter
based on the item’s Java class.
1 2 | final ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector(); classPresenterSelector.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); |
Finally, create a PlaybackControlsRow:
A Row
of playback controls to be displayed by a PlaybackControlsRowPresenter
. This row consists of some optional item detail, a series of primary actions, and an optional series of secondary actions.
1 2 3 4 5 | PlaybackControlsRow playbackControlsRow = new PlaybackControlsRow(); ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(classPresenterSelector); rowsAdapter.add(playbackControlsRow); playbackControlsRow.setPrimaryActionsAdapter(primaryActionsAdapter); |
Set the PlaybackOverlayFragment adapter:
This sets the list of rows for the fragment. This is a method of PlaybackOverlayFragment
.
1 | setAdapter(rowsAdapter); |
All your MyPlaybackOverlayFragment.setupLeanbackControls() method should look something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | private void setupLeanbackControls() { PlaybackControlsRowPresenter playbackControlsRowPresenter = new PlaybackControlsRowPresenter(); playbackControlsRowPresenter.setSecondaryActionsHidden(false); final ControlButtonPresenterSelector controlButtonPresenterSelector = new ControlButtonPresenterSelector(); ArrayObjectAdapter primaryActionsAdapter = new ArrayObjectAdapter(controlButtonPresenterSelector); playPauseAction = new PlayPauseAction(getActivity()); rewindAction = new RewindAction(getActivity()); fastForwardAction = new FastForwardAction(getActivity()); primaryActionsAdapter.add(rewindAction); primaryActionsAdapter.add(playPauseAction); primaryActionsAdapter.add(fastForwardAction); final ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector(); classPresenterSelector.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); PlaybackControlsRow playbackControlsRow = new PlaybackControlsRow(); ArrayObjectAdapter rowsAdapter = new ArrayObjectAdapter(classPresenterSelector); rowsAdapter.add(playbackControlsRow); playbackControlsRow.setPrimaryActionsAdapter(primaryActionsAdapter); setAdapter(rowsAdapter); } |
Implement Action Listeners
You can set a OnActionClickedListener
to the actions created / added in the example above (playPauseAction
, rewindAction
and fastForwardAction
).
1 2 3 4 5 6 7 8 9 10 11 12 13 | playbackControlsRowPresenter.setOnActionClickedListener(action -> { if (!isVisible()) return; if (action == playPauseAction) { setPlayWhenReady(); } else if (action == fastForwardAction) { seek(DEFAULT_SEEK_LENGTH); } else if (action == rewindAction) { seek(-DEFAULT_SEEK_LENGTH); } else if (action == closedCaptioningAction) { setClosedCaptions(); } }); |
Populate the view we added earlier:
Now the controls are ready, we can add the video when starting ExoPlayer playback. I’ve omitted the details of ExoPlayer here for simplicity.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public class TVPlayerActivity extends Activity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setupExoPlayer(); // Configure ExoPlayer, start video playback, etc. setupMyPlaybackOverlayFragment(); // Initialize the leanback controls } private void setupMyPlaybackOverlayFragment() { final MyPlaybackOverlayFragment myPlaybackOverlayFragment = new MyPlaybackOverlayFragment(); final FragmentManager fragmentManager = getFragmentManager(); fragmentManager.beginTransaction() .replace(R.id.my_playback_overlay_fragment_view, myPlaybackOverlayFragment, null) .commit(); } } |
Temporarily Disable TV Screen Saver
Many Android TV devices have a built in screen saver that displays itself on top of video playback. To work around this I added a flag to the window during onCreate()
by invoking.
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); |
And then removed it during onStop()
by invoking:
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); |
Summary
The PlaybackOverlayFragment
handles remote control input to show and hide (tickle()
) the leanback controls. It somewhat simplifies implementing TV style controls for an existing player / application.