Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android TV: Building Apps with Google’s Leanback Library

1cf799036b5d9439e9ed823c9b0c15cb?s=47 Joe Birch
August 02, 2016

Android TV: Building Apps with Google’s Leanback Library

In this class, we'll look at how we can create Android TV apps with the help of Google's leanback library. After a brief introduction to the TV platform and an open-source Vine TV app, we'll move straight into how you can begin building applications for yourself using the leanback library, following best practices along the way. We'll take a look at the different components that are provided by the framework and how you can craft custom components of your own to enhance your application's UX. Seeing as Android TV applications are completely testable, we'll also take a brief look at how this can be done to ensure your app functions as expected!

1cf799036b5d9439e9ed823c9b0c15cb?s=128

Joe Birch

August 02, 2016
Tweet

Transcript

  1. Android TV: Building apps with Google’s Leanback Library

  2. Joe Birch Android Engineer @Buffer @hitherejoe / hitherejoe.com

  3. What is Android TV?

  4. None
  5. Build on Material

  6. Build on Material Casual Consumption

  7. Build on Material Casual Consumption Cinematic Experience

  8. Build on Material Casual Consumption Cinematic Experience Simplicity

  9. Navigation Getting around

  10. D-Pad controls

  11. Focus based Navigation

  12. None
  13. Setting up Getting your project ready

  14. <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>
  15. github.com/hitherejoe/vineyard github.com/hitherejoe/bourbon

  16. BrowseFragment Display browsable content to the user

  17. None
  18. <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"/>

  19. None
  20. setBrandColor(ContextCompat.getColor(this, R.color.fastlane_background));

  21. Color color = ContextCompat.getColor(this, R.color.accent); setSearchAffordanceColor(color);

  22. Drawable badge = ContextCompat.getDrawable( this, R.drawable.banner_shadow); setBadgeDrawable(badge);

  23. setHeadersState(HEADERS_ENABLED);

  24. setHeadersState(HEADERS_HIDDEN);

  25. setHeadersState(HEADERS_DISABLED);

  26. None
  27. None
  28. Browse Fragment Header Item Presenter Header Item List Row Array

    Object Adapter Post Adapter
  29. Browse Fragment Header Item Presenter Header Item List Row Array

    Object Adapter Post Adapter
  30. 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 } }
  31. 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 } }
  32. <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>
  33. 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 } }
  34. @Override public 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()); }
  35. 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 } }
  36. setHeaderPresenterSelector(new PresenterSelector() { @Override public Presenter getPresenter(Object o) { return

    new IconHeaderItemPresenter(); } });
  37. Browse Fragment Header Item Presenter Header Item List Row Array

    Object Adapter Array Object Adapter
  38. Header Item List Row Array Object Adapter

  39. ArrayObjectAdapter rowAdapter = new ArrayObjectAdapter(this); rowAdapter.add(…); HeaderItem header = new

    HeaderItem(headerPosition, tag); mRowsAdapter.add(new ListRow(header, rowAdapter));
  40. Browse Fragment Array Object Adapter

  41. setOnItemViewClickedListener(mOnItemViewClickedListener); setOnItemViewSelectedListener(mOnItemViewSelectedListener);

  42. @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row

    row) { // Do stuff with clicked item object }
  43. @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row

    row) { // Do stuff with selected item object }
  44. None
  45. None
  46. BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity());

  47. BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow());

  48. BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); backgroundManager.setBitmap(resource);

  49. BackgroundManager backgroundManager = BackgroundManager.getInstance(getActivity()); backgroundManager.attach(getActivity().getWindow()); backgroundManager.setBitmap(resource); // Don’t forget to

    release!! backgroundManager.release();
  50. SearchFragment Allow users to search for content

  51. None
  52. @Override public boolean onQueryTextChange(String newQuery) @Override public boolean onQueryTextSubmit(String query)

  53. Post Results (Array Object Adapter) Search Results (Array Object Adapter)

    Row Adapter (Array Object Adapter) Focused item triggers Post search
  54. None
  55. VerticalGridFragment Display a grid of browsable content to the user

  56. None
  57. VerticalGridPresenter gridPresenter = new VerticalGridPresenter(); gridPresenter.setNumberOfColumns(NUM_COLUMNS); setGridPresenter(gridPresenter);

  58. PlaybackActivity Display media content on screen

  59. None
  60. mSession = new MediaSession(this, getString(R.string.app_name); mSession.setCallback(new MediaSessionCallback()); mSession.setActive(true); setMediaController(new MediaController(this,

    mSession.getSessionToken());
  61. PlaybackOverlayFragment Display playback controls to the user

  62. None
  63. None
  64. 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) { } }
  65. ArrayObjectAdapter (Row Adapter) ArrayObjectAdapter (Related Posts) ArrayObjectAdapter (Primary Actions) ArrayObjectAdapter

    (Secondary Actions) PlayBackControlsRow Meta Data
  66. ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector(); mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); mSecondaryActionsAdapter

    = new ArrayObjectAdapter(presenterSelector); mPlaybackControlsRow .setPrimaryActionsAdapter(mPrimaryActionsAdapter); mPlaybackControlsRow .setSecondaryActionsAdapter(mPrimaryActionsAdapter);
  67. public class Action { private Drawable mIcons; private CharSequence mLabel1;

    private CharSequence mLabel2; private ArrayList mKeyCodes; … }
  68. 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);
  69. 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); } } });
  70. 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);

  71. Post item = (Post) mPlaybackControlsRow.getItem(); item.description = description; item.username =

    username; mPlaybackControlsRow.setTotalTime((int) duration); mPlaybackControlsRow.setImageDrawable(resource); mPlaybackControlsRow.setCurrentTime(currentTime); mPlaybackControlsRow.setBufferedProgress(bufferedT
  72. Post item = (Post) mPlaybackControlsRow.getItem(); item.description = description; item.username =

    username; mPlaybackControlsRow.setTotalTime((int) duration); mPlaybackControlsRow.setImageDrawable(resource); mPlaybackControlsRow.setCurrentTime(currentTime); mPlaybackControlsRow.setBufferedProgress(bufferedTime);
  73. ArrayObjectAdapter (Adapter Rows) ArrayObjectAdapter (Related Posts) ArrayObjectAdapter (Primary Actions) ArrayObjectAdapter

    (Secondary Actions) PlayBackControlsRow Meta Data
  74. GuidedStepFragment Display a set of selectable options to the user

  75. None
  76. None
  77. @Override public GuidanceStylist.Guidance onCreateGuidance(Bundle savedInstanceState) { String title = getString(…);

    String description = getString(…); Drawable icon = getActivity().getDrawable(…); return new GuidanceStylist.Guidance( title, description, "", icon); }
  78. @Override public 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); }
  79. ErrorFragment Display an error message to the user (Because things

    don’t always go as planned)
  80. None
  81. ErrorFragment errorFragment = new ErrorFragment(); errorFragment.setTitle(…); errorFragment.setMessage(…); errorFragment.setButtonText(…); errorFragment.setButtonClickListener(…);

  82. Custom Views Because your app doesn’t have to look boring

  83. Tag Card Tag Card View Base Card View Text View

    Image View
  84. 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); }
  85. Icon Card Icon Card View Base Card View Text View

    Image View Text View
  86. 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); }
  87. Loading Card Loading Card View Base Card View Progress Bar

  88. Loading Card LoadingCardView cardView = new LoadingCardView(parent.getContext()); IconCardView cardView =

    (IconCardView) viewHolder.view; cardView.setIsLoading(true);
  89. Live Card

  90. Live Card Live Card View Base Card View Preview Card

    View Looping Video View Progress Bar Image View View (Transparent Overlay) Video View
  91. Live Card LiveCardView 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()); }
  92. Leanback Cards https://github.com/hitherejoe/LeanbackCards

  93. None
  94. Google Guidelines

  95. None
  96. Testing.

  97. 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!
  98. Android N(utella?)

  99. Picture-in-Picture

  100. None
  101. <activity android:name=“.ui.video.VideoActivity” android:resizeableActivity="true" android:supportsPictureInPicture="true" android:configChanges= “screenSize|smallestScreenSize|screenLayout|orientation" />

  102. @Override public void onActionClicked(Action action) { if (action.getId() == R.id.lb_control_picture_in_picture)

    { getActivity().enterPictureInPicture(); return; } }
  103. @Override public void onPictureInPictureChanged(boolean inPictureInPicture) { if (inPictureInPicture) { //

    Hide the controls in picture-in-picture mode. } else { // Restore the playback UI based on the playback status. } }
  104. @Override public void onPause() { if (mInPictureInPicture) { // Continue

    playback } // If paused but not in PIP, pause playback if necessary }
  105. TV Recording

  106. Sharing Code

  107. None
  108. None
  109. None
  110. None
  111. github.com/hitherejoe/bourbon

  112. What’s next?

  113. The future of TV

  114. None
  115. None
  116. None
  117. Resources Official Android TV Documentation github.com/hitherejoe/vineyard Google Plus Android TV

    Community github.com/hitherejoe/AndroidTvBoilerplate github.com/hitherejoe/leanbackcards medium.com/@hitherejoe