Android TV:Building apps with Google’s Leanback Library
What is Android TV?
Build on Material
Build on Material Casual Consumption
Build on Material Casual Consumption
Cinematic Experience
Build on Material Casual Consumption
Cinematic Experience Simplicity
NavigationGetting around
D-Pad controls
Focus based Navigation
Setting upGetting your project ready
<uses-feature android:name="android.hardware.microphone" android:required="false"/>
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.software.leanback" android:required="true"/>
<activity android:name=“com.hitherejoe.vineyard.ui.main.LeanbackActivity” android:label="@string/app_name" android:theme="@style/Theme.Leanback">
<intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LEANBACK_LAUNCHER"/> </intent-filter>
</activity>
github.com/hitherejoe/vineyard
github.com/hitherejoe/bourbon
BrowseFragmentDisplay browsable content to the user
<fragment xmlns:android="http://schemas.android.com/apk/res/android" android:name=“com.hitherejoe.vineyard.ui.fragment.BrowseFragment” android:id="@+id/main_browse_fragment" android:layout_width="match_parent" android:layout_height="match_parent"/>
setBrandColor(ContextCompat.getColor(this, R.color.fastlane_background));
Color color = ContextCompat.getColor(this, R.color.accent);setSearchAffordanceColor(color);
Drawable badge = ContextCompat.getDrawable( this, R.drawable.banner_shadow);setBadgeDrawable(badge);
setHeadersState(HEADERS_ENABLED);
setHeadersState(HEADERS_HIDDEN);
setHeadersState(HEADERS_DISABLED);
Browse Fragment
Header Item Presenter
Header Item
List RowArray Object Adapter
Post Adapter
Browse Fragment
Header Item Presenter
Header Item
List RowArray Object Adapter
Post Adapter
public class IconHeaderItemPresenter extends RowHeaderPresenter {
@Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { // inflate layout }
@Override public void onBindViewHolder(Presenter.ViewHolder viewHolder,
Object o) { // set text, icons etc } @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { // free resources }
}
public class IconHeaderItemPresenter extends RowHeaderPresenter {
@Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { // inflate layout }
@Override public void onBindViewHolder(Presenter.ViewHolder viewHolder,
Object o) { // set text, icons etc }
@Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { // free resources }
}
<android.support.v17.leanback.widget.NonOverlappingLinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height=“match_parent" android:orientation="horizontal">
<ImageView android:id="@+id/header_icon" android:layout_width="32dp" android:layout_height="32dp"/>
<TextView android:id="@+id/header_label" android:layout_marginLeft="6dp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:textColor="@color/white" android:textSize=“@dimen/header_text”/>
</android.support.v17.leanback.widget.NonOverlappingLinearLayout>
public class IconHeaderItemPresenter extends RowHeaderPresenter {
@Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { // inflate layout }
@Override public void onBindViewHolder(Presenter.ViewHolder viewHolder,
Object o) { // set text, icons etc }
@Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { // free resources }
}
@Overridepublic void onBindViewHolder(Presenter.ViewHolder viewHolder, Object o) { HeaderItem headerItem = ((ListRow) o).getHeaderItem();
setIconDrawable(headerItem.getName(), viewholder.iconImage);
TextView label = viewHolder.headerText; label.setText(headerItem.getName());}
public class IconHeaderItemPresenter extends RowHeaderPresenter {
@Override public ViewHolder onCreateViewHolder(ViewGroup viewGroup) { // inflate layout }
@Override public void onBindViewHolder(Presenter.ViewHolder viewHolder,
Object o) { // set text, icons etc } @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { // release bitmaps if used }
}
setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return new IconHeaderItemPresenter(); }});
Browse Fragment
Header Item Presenter
Header Item
List RowArray Object Adapter
Array Object Adapter
Header Item
List Row
Array Object Adapter
ArrayObjectAdapter rowAdapter = new ArrayObjectAdapter(this);rowAdapter.add(…);
HeaderItem header = new HeaderItem(headerPosition, tag);
mRowsAdapter.add(new ListRow(header, rowAdapter));
Browse Fragment Array Object Adapter
setOnItemViewClickedListener(mOnItemViewClickedListener);
setOnItemViewSelectedListener(mOnItemViewSelectedListener);
@Overridepublic void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
// Do stuff with clicked item object
}
@Overridepublic void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) {
// Do stuff with selected item object
}
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
backgroundManager.setBitmap(resource);
BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());
backgroundManager.attach(getActivity().getWindow());
backgroundManager.setBitmap(resource);
// Don’t forget to release!!backgroundManager.release();
SearchFragmentAllow users to search for content
@Overridepublic boolean onQueryTextChange(String newQuery)
@Overridepublic boolean onQueryTextSubmit(String query)
Post Results (Array Object Adapter)
Search Results (Array Object Adapter)
Row Adapter (Array Object Adapter)
Focused item triggers Post search
VerticalGridFragmentDisplay a grid of browsable content to the user
VerticalGridPresenter gridPresenter = new VerticalGridPresenter();
gridPresenter.setNumberOfColumns(NUM_COLUMNS);setGridPresenter(gridPresenter);
PlaybackActivityDisplay media content on screen
mSession = new MediaSession(this, getString(R.string.app_name);mSession.setCallback(new MediaSessionCallback());mSession.setActive(true);
setMediaController(new MediaController(this, mSession.getSessionToken());
PlaybackOverlayFragmentDisplay playback controls to the user
mMediaController = getActivity().getMediaController();mMediaController.registerCallback(mMediaControllerCallback);
private class MediaControllerCallback extends MediaController.Callback {
@Override public void onPlaybackStateChanged(@NonNull PlaybackState state) { }
@Override public void onMetadataChanged(@NonNull MediaMetadata metadata) { }
}
ArrayObjectAdapter (Row Adapter)
ArrayObjectAdapter (Related Posts)
ArrayObjectAdapter (Primary Actions)
ArrayObjectAdapter (Secondary Actions)
PlayBackControlsRow
Meta Data
ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector();
mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector);
mPlaybackControlsRow .setPrimaryActionsAdapter(mPrimaryActionsAdapter);mPlaybackControlsRow .setSecondaryActionsAdapter(mPrimaryActionsAdapter);
public class Action { private Drawable mIcons;
private CharSequence mLabel1;private CharSequence mLabel2;private ArrayList mKeyCodes;…
}
mPlayPauseAction = new PlayPauseAction(getActivity());mRepeatAction = new RepeatAction(getActivity());mSkipNextAction = new SkipNextAction(getActivity());mSkipPreviousAction = new SkipPreviousAction(getActivity());
mPrimaryActionsAdapter.add(mPlayPauseAction);mPrimaryActionsAdapter.add(mSkipNextAction);mPrimaryActionsAdapter.add(mSkipPreviousAction);
mSecondaryActionsAdapter.add(mRepeatAction);
playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() { public void onActionClicked(Action action) { if (action.getId() == mPlayPauseAction.getId()) { togglePlayback(mPlayPauseAction.getIndex() == PlayPauseAction.PLAY); } else if (action.getId() == mSkipNextAction.getId()) { next(true); } else if (action.getId() == mSkipPreviousAction.getId()) { prev(true); } else if (action.getId() == mRepeatAction.getId()) { loopVideos(); } if (action instanceof PlaybackControlsRow.MultiAction) { notifyChanged(action); } } });
mMediaController.getTransportControls().play();mMediaController.getTransportControls().pause;mMediaController.getTransportControls().skipToNext();mMediaController.getTransportControls().skipToPrevious();mMediaController.getTransportControls().fastForward;mMediaController.getTransportControls().rewind();
mMediaController.getTransportControls().sendCustomAction(CUSTOM_ACTION_AUTO_LOOP, null);
Post item = (Post) mPlaybackControlsRow.getItem();item.description = description;item.username = username;
mPlaybackControlsRow.setTotalTime((int) duration);mPlaybackControlsRow.setImageDrawable(resource);
mPlaybackControlsRow.setCurrentTime(currentTime);mPlaybackControlsRow.setBufferedProgress(bufferedT
Post item = (Post) mPlaybackControlsRow.getItem();item.description = description;item.username = username;
mPlaybackControlsRow.setTotalTime((int) duration);mPlaybackControlsRow.setImageDrawable(resource);
mPlaybackControlsRow.setCurrentTime(currentTime);mPlaybackControlsRow.setBufferedProgress(bufferedTime);
ArrayObjectAdapter (Adapter Rows)
ArrayObjectAdapter (Related Posts)
ArrayObjectAdapter (Primary Actions)
ArrayObjectAdapter (Secondary Actions)PlayBackControlsRow
Meta Data
GuidedStepFragmentDisplay a set of selectable options to the user
@Overridepublic GuidanceStylist.Guidance
onCreateGuidance(Bundle savedInstanceState) {
String title = getString(…); String description = getString(…); Drawable icon = getActivity().getDrawable(…);
return new GuidanceStylist.Guidance( title, description, "", icon);
}
@Overridepublic void onCreateActions( @NonNull List<GuidedAction> actions, Bundle savedInstanceState) {
GuidedAction guidedAction = new GuidedAction.Builder() .id(…) .title(…) .description(…) .checkSetId(OPTION_CHECK_SET_ID) .build(); guidedAction.setChecked(isChecked); actions.add(guidedAction);}
ErrorFragmentDisplay an error message to the user
(Because things don’t always go as planned)
ErrorFragment errorFragment = new ErrorFragment();
errorFragment.setTitle(…);errorFragment.setMessage(…);errorFragment.setButtonText(…);errorFragment.setButtonClickListener(…);
Custom ViewsBecause your app doesn’t have to look boring
Tag Card
Tag Card View
Base Card View
Text View
Image View
Tag Card
TagCardView cardView = new TagCardView(parent.getContext());
Tag post = (Tag) item;TagCardView cardView = (TagCardView) viewHolder.view;
if (post.tag != null) { cardView.setCardText(post.tag); cardView.setCardIcon(R.drawable.ic_tag);}
Icon Card
Icon Card View
Base Card View
Text View
Image View
Text View
Icon Card
IconCardView cardView = new IconCardView(parent.getContext());
Option option = (Option) item;IconCardView cardView = (IconCardView) viewHolder.view;
if (option.tag != null) { cardView.setCardIcon(R.drawable.ic_loop); cardView.setTitleText(option.title); cardView.setValueText(option.title);}
Loading Card
Loading Card View
Base Card View
Progress Bar
Loading Card
LoadingCardView cardView = new LoadingCardView(parent.getContext());
IconCardView cardView = (IconCardView) viewHolder.view;cardView.setIsLoading(true);
Live Card
Live Card
Live Card View
Base Card View
Preview Card View
Looping Video View
Progress Bar
Image View
View (Transparent Overlay)
Video View
Live CardLiveCardView cardView = new LiveCardView(parent.getContext());
Post post = (Post) item;LiveCardView cardView = (LiveCardView) viewHolder.view;
if (post.videoUrl != null) { cardView.setTitleText(post.description); cardView.setContentText(post.username); cardView.setVideoUrl(post.videoUrl);
Glide.with(cardView.getContext()) .load(post.thumbnailUrl) .centerCrop() .error(mDefaultCardImage) .into(cardView.getMainImageView());}
Leanback Cards
https://github.com/hitherejoe/LeanbackCards
Google Guidelines
Testing.
onView(withId(R.id.title_orb)) .perform(click());
onView(withId(R.id.browse_headers)) .perform(RecyclerViewActions .actionOnItemAtPosition(i, click()));
onView(withItemText(post.description, R.id.browse_container_dock)) .perform(click());
Dig deep and remember, everything has IDs!
Android N(utella?)
Picture-in-Picture
<activity android:name=“.ui.video.VideoActivity” android:resizeableActivity="true" android:supportsPictureInPicture="true" android:configChanges= “screenSize|smallestScreenSize|screenLayout|orientation" />
@Overridepublic void onActionClicked(Action action) { if (action.getId() == R.id.lb_control_picture_in_picture) { getActivity().enterPictureInPicture(); return; }}
@Overridepublic void onPictureInPictureChanged(boolean inPictureInPicture) { if (inPictureInPicture) { // Hide the controls in picture-in-picture mode. } else { // Restore the playback UI based on the playback status. }}
@Overridepublic void onPause() { if (mInPictureInPicture) { // Continue playback } // If paused but not in PIP, pause playback if necessary}
TV Recording
Sharing Code
What’s next?
The future of TV
Resources
Official Android TV Documentation
github.com/hitherejoe/vineyard
Google Plus Android TV Community
github.com/hitherejoe/AndroidTvBoilerplategithub.com/hitherejoe/leanbackcardsmedium.com/@hitherejoe