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

Android TV: Building Apps with Google’s Leanback Library

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!

Joe Birch

August 02, 2016
Tweet

More Decks by Joe Birch

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. What is Android TV?

    View Slide

  4. View Slide

  5. Build on Material

    View Slide

  6. Build on Material Casual Consumption

    View Slide

  7. Build on Material Casual Consumption
    Cinematic Experience

    View Slide

  8. Build on Material Casual Consumption
    Cinematic Experience Simplicity

    View Slide

  9. Navigation
    Getting around

    View Slide

  10. D-Pad controls

    View Slide

  11. Focus based Navigation

    View Slide

  12. View Slide

  13. Setting up
    Getting your project ready

    View Slide

  14. android:name="android.hardware.microphone"
    android:required="false"/>
    android:name="android.hardware.touchscreen"
    android:required="false"/>
    android:name="android.software.leanback"
    android:required="true"/>
    android:name=“com.hitherejoe.vineyard.ui.main.LeanbackActivity”
    android:label="@string/app_name"
    android:theme="@style/Theme.Leanback">





    View Slide

  15. github.com/hitherejoe/vineyard
    github.com/hitherejoe/bourbon

    View Slide

  16. BrowseFragment
    Display browsable content to the user

    View Slide

  17. View Slide

  18. android:name=“com.hitherejoe.vineyard.ui.fragment.BrowseFragment”
    android:id="@+id/main_browse_fragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

    View Slide

  19. View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. setHeadersState(HEADERS_ENABLED);

    View Slide

  24. setHeadersState(HEADERS_HIDDEN);

    View Slide

  25. setHeadersState(HEADERS_DISABLED);

    View Slide

  26. View Slide

  27. View Slide

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

    View Slide

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

    View Slide

  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
    }
    }

    View Slide

  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
    }
    }

    View Slide

  32. xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height=“match_parent"
    android:orientation="horizontal">
    android:id="@+id/header_icon"
    android:layout_width="32dp"
    android:layout_height="32dp"/>
    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”/>

    View Slide

  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
    }
    }

    View Slide

  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());
    }

    View Slide

  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
    }
    }

    View Slide

  36. setHeaderPresenterSelector(new PresenterSelector() {
    @Override
    public Presenter getPresenter(Object o) {
    return new IconHeaderItemPresenter();
    }
    });

    View Slide

  37. Browse Fragment
    Header Item
    Presenter
    Header Item
    List Row
    Array Object
    Adapter
    Array Object
    Adapter

    View Slide

  38. Header Item
    List Row
    Array Object
    Adapter

    View Slide

  39. ArrayObjectAdapter rowAdapter = new ArrayObjectAdapter(this);
    rowAdapter.add(…);
    HeaderItem header = new HeaderItem(headerPosition, tag);
    mRowsAdapter.add(new ListRow(header, rowAdapter));

    View Slide

  40. Browse Fragment
    Array Object
    Adapter

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  44. View Slide

  45. View Slide

  46. BackgroundManager backgroundManager =
    BackgroundManager.getInstance(getActivity());

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. SearchFragment
    Allow users to search for content

    View Slide

  51. View Slide

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

    View Slide

  53. Post Results
    (Array Object Adapter)
    Search Results
    (Array Object Adapter)
    Row Adapter
    (Array Object Adapter)
    Focused item triggers Post search

    View Slide

  54. View Slide

  55. VerticalGridFragment
    Display a grid of browsable content to the user

    View Slide

  56. View Slide

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

    View Slide

  58. PlaybackActivity
    Display media content on screen

    View Slide

  59. View Slide

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

    View Slide

  61. PlaybackOverlayFragment
    Display playback controls to the user

    View Slide

  62. View Slide

  63. View Slide

  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) { }
    }

    View Slide

  65. ArrayObjectAdapter
    (Row Adapter)
    ArrayObjectAdapter
    (Related Posts)
    ArrayObjectAdapter
    (Primary Actions)
    ArrayObjectAdapter
    (Secondary Actions)
    PlayBackControlsRow
    Meta Data

    View Slide

  66. ControlButtonPresenterSelector presenterSelector =
    new ControlButtonPresenterSelector();
    mPrimaryActionsAdapter =
    new ArrayObjectAdapter(presenterSelector);
    mSecondaryActionsAdapter =
    new ArrayObjectAdapter(presenterSelector);
    mPlaybackControlsRow
    .setPrimaryActionsAdapter(mPrimaryActionsAdapter);
    mPlaybackControlsRow
    .setSecondaryActionsAdapter(mPrimaryActionsAdapter);

    View Slide

  67. public class Action {
    private Drawable mIcons;
    private CharSequence mLabel1;
    private CharSequence mLabel2;
    private ArrayList mKeyCodes;

    }

    View Slide

  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);

    View Slide

  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);
    }
    }
    });

    View Slide

  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);

    View Slide

  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

    View Slide

  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);

    View Slide

  73. ArrayObjectAdapter
    (Adapter Rows)
    ArrayObjectAdapter
    (Related Posts)
    ArrayObjectAdapter
    (Primary Actions)
    ArrayObjectAdapter
    (Secondary Actions)
    PlayBackControlsRow
    Meta Data

    View Slide

  74. GuidedStepFragment
    Display a set of selectable options to the user

    View Slide

  75. View Slide

  76. View Slide

  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);
    }

    View Slide

  78. @Override
    public void onCreateActions(
    @NonNull List actions,
    Bundle savedInstanceState) {
    GuidedAction guidedAction = new GuidedAction.Builder()
    .id(…)
    .title(…)
    .description(…)
    .checkSetId(OPTION_CHECK_SET_ID)
    .build();
    guidedAction.setChecked(isChecked);
    actions.add(guidedAction);
    }

    View Slide

  79. ErrorFragment
    Display an error message to the user
    (Because things don’t always go as planned)

    View Slide

  80. View Slide

  81. ErrorFragment errorFragment =
    new ErrorFragment();
    errorFragment.setTitle(…);
    errorFragment.setMessage(…);
    errorFragment.setButtonText(…);
    errorFragment.setButtonClickListener(…);

    View Slide

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

    View Slide

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

    View Slide

  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);
    }

    View Slide

  85. Icon Card
    Icon Card
    View
    Base Card
    View
    Text View
    Image View
    Text View

    View Slide

  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);
    }

    View Slide

  87. Loading Card
    Loading Card
    View
    Base Card
    View
    Progress Bar

    View Slide

  88. Loading Card
    LoadingCardView cardView = new LoadingCardView(parent.getContext());
    IconCardView cardView = (IconCardView) viewHolder.view;
    cardView.setIsLoading(true);

    View Slide

  89. Live Card

    View Slide

  90. Live Card
    Live Card
    View
    Base Card
    View
    Preview Card
    View
    Looping Video
    View
    Progress Bar
    Image View
    View
    (Transparent Overlay)
    Video View

    View Slide

  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());
    }

    View Slide

  92. Leanback Cards
    https://github.com/hitherejoe/LeanbackCards

    View Slide

  93. View Slide

  94. Google Guidelines

    View Slide

  95. View Slide

  96. Testing.

    View Slide

  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!

    View Slide

  98. Android N(utella?)

    View Slide

  99. Picture-in-Picture

    View Slide

  100. View Slide

  101. android:resizeableActivity="true"
    android:supportsPictureInPicture="true"
    android:configChanges=
    “screenSize|smallestScreenSize|screenLayout|orientation" />

    View Slide

  102. @Override
    public void onActionClicked(Action action) {
    if (action.getId() == R.id.lb_control_picture_in_picture) {
    getActivity().enterPictureInPicture();
    return;
    }
    }

    View Slide

  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.
    }
    }

    View Slide

  104. @Override
    public void onPause() {
    if (mInPictureInPicture) {
    // Continue playback
    }
    // If paused but not in PIP, pause playback if necessary
    }

    View Slide

  105. TV Recording

    View Slide

  106. Sharing Code

    View Slide

  107. View Slide

  108. View Slide

  109. View Slide

  110. View Slide

  111. github.com/hitherejoe/bourbon

    View Slide

  112. What’s next?

    View Slide

  113. The future of TV

    View Slide

  114. View Slide

  115. View Slide

  116. View Slide

  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

    View Slide