Android TV: Building apps with Google’s Leanback Library

Preview:

Citation preview

Android TV:Building apps with Google’s Leanback Library

Joe BirchAndroid Engineer @Buffer

@hitherejoe / hitherejoe.com

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

github.com/hitherejoe/bourbon

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