[Update 2015.12.14]: revise & add DetailsOverviewRowPresenter
sample implementation, see bottom of this post.
Contents
Aim of this chapter – DetailsActivity implementation
Implementing
setOnItemViewClickedListener
–onItemClicked
callback function inMainFragment
After implementation, we can click cards to go to next action. We show content detail information for each Movie item in this chapter.DetailsActivity
,VideoDetailsFragment
, andDetailsDescriptionPresenter
DetailsActivity
is invoked by clicking card inMainActivity
. It showsVideoDetailsFragment
, which is a layout to show card content.
Implementing click listener in MainFragment
To register a next action when user clicked certain card, we can use setOnItemViewClickedListener
method defined in BrowseFragment
class (MainFragment
class is a sub class of BrowseFragment
).
Sample implementation is as follows. This is almost same with setOnItemViewSelectedListener
introduced in previous chapter.
@Override public void onActivityCreated(Bundle savedInstanceState) { ... setupEventListeners(); picassoBackgroundManager = new PicassoBackgroundManager(getActivity()); } private void setupEventListeners() { setOnItemViewSelectedListener(new ItemViewSelectedListener()); setOnItemViewClickedListener(new ItemViewClickedListener()); } private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { // each time the item is clicked, code inside here will be executed. } } }
DetailsActivity & VideoDetailsFragment – Explanation
Below is a picture from AOSP sample applicaton.
In the Android sample application, we have 2 rows in VideoDetailsFragment
. First row is DetailsOverviewRow
and second row is ListRow
(which is already explained in MainFragment
). DetailsOverviewRow
shows the content details including picture in the lect, description and some actions are set in the left-bottom.
We can prepare our own Presenter to specify the design layout of DetailsOverviewRow
. There are 2 pre-implemented presenters we can use in Leanback support library.
DetailsOverviewRowPresenter
: Shown in the above picture, but it is already deprecated in leanback library version 22.2.0.FullWidthDetailsOverviewRowPresenter
: Instead ofDetailsOverviewRowPresenter
, use this presenter is recommended in the AOSP document.
In the following, I will try to introduce this new FullWidthDetailsOverviewRowPresenter
(*updated 2015.12.4: you can also check DetailsOverviewRowPresenter
, see bottom of this post). It will specify the design layout of DetailsOverviewRow
, which is usually used in the first row of your DetailsFragment
to show item details information.
FullWidthDetailsOverviewRowPresenter
is consisting of 3 parts, namely
- Logo view – customizable (option), by implementing
DetailsOverViewLogoPresenter
- Action list view
- Detailed description view – customizable (MUST), implement subclass of
AbstractDetailsDescriptionPresenter
We define “DetailsDescriptionPresenter
” which extends AbstractDetailsDescriptionPresenter
defined in Leanback libarary. AbstractDetailsDescriptionPresenter
decides the design layout of descrption view.
DetailsActivity & VideoDetailsFragment – Implementation
We proceed to create DetailsActivity
for showing the UI of content details. The design is specified in VideoDetailsFragment
, which is a subclass of DetailsFragment.
Creating DetailsActivity
& VideoDetailsFragment
is done in the same way with MainActivity
& MainFragment
introduced in chapter 1.
DetailsActivity
New → Activity → BlankActivity
Activity Name: DetailsActivity
Layout Name: activity_details
Title: DetailsActivity
Menu Resource Name: menu_details
Hierarchical Parent: blank
VideoDetailsFragment
New -> Java Class -> Name: VideoDetailsFragment
First, modify activity_details.xml
as follows so that it only displays VideoDetailsFragment
.
<?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/details_fragment" android:name="com.corochann.androidtvapptutorial.ui.VideoDetailsFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".DetailsActivity" tools:deviceIds="tv" />
Second modify VideoDetailsFragment
. We will make this VideoDetailsFragment
as a sub-class of DetailsFragment
. DetailsFragment
class is in leanback support library to create UI for content details. In the VideoDetailsFragment
, declared private member mFwdorPresenter
is the instance of FullWidthDetailsOverviewRowPresenter
.
Note that AsyncTask
is for executing some tasks in background thread (“doInBackground
“) followed by executing some tasks in UI thread (“onPostExecute
“). Here, we load a picture image in background, and updating UI in UI thread. You can find information of AsyncTask
below.
package com.corochann.androidtvapptutorial; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.os.AsyncTask; import android.os.Bundle; import android.support.v17.leanback.app.DetailsFragment; import android.support.v17.leanback.widget.Action; import android.support.v17.leanback.widget.ArrayObjectAdapter; import android.support.v17.leanback.widget.ClassPresenterSelector; import android.support.v17.leanback.widget.DetailsOverviewRow; import android.support.v17.leanback.widget.FullWidthDetailsOverviewRowPresenter; import android.support.v17.leanback.widget.HeaderItem; import android.support.v17.leanback.widget.ListRow; import android.support.v17.leanback.widget.ListRowPresenter; import android.support.v17.leanback.widget.SparseArrayObjectAdapter; import android.util.Log; import com.squareup.picasso.Picasso; import java.io.IOException; /** * Created by corochann on 6/7/2015. */ public class VideoDetailsFragment extends DetailsFragment { private static final String TAG = VideoDetailsFragment.class.getSimpleName(); private static final int DETAIL_THUMB_WIDTH = 274; private static final int DETAIL_THUMB_HEIGHT = 274; private static final String MOVIE = "Movie"; private CustomFullWidthDetailsOverviewRowPresenter mFwdorPresenter; private PicassoBackgroundManager mPicassoBackgroundManager; private Movie mSelectedMovie; private DetailsRowBuilderTask mDetailsRowBuilderTask; @Override public void onCreate(Bundle savedInstanceState) { Log.i(TAG, "onCreate"); super.onCreate(savedInstanceState); mFwdorPresenter = new CustomFullWidthDetailsOverviewRowPresenter(new DetailsDescriptionPresenter()); mPicassoBackgroundManager = new PicassoBackgroundManager(getActivity()); mSelectedMovie = (Movie)getActivity().getIntent().getSerializableExtra(MOVIE); mDetailsRowBuilderTask = (DetailsRowBuilderTask) new DetailsRowBuilderTask().execute(mSelectedMovie); mPicassoBackgroundManager.updateBackgroundWithDelay(mSelectedMovie.getCardImageUrl());; } @Override public void onStop() { mDetailsRowBuilderTask.cancel(true); super.onStop(); } private class DetailsRowBuilderTask extends AsyncTask<Movie, Integer, DetailsOverviewRow> { @Override protected DetailsOverviewRow doInBackground(Movie... params) { DetailsOverviewRow row = new DetailsOverviewRow(mSelectedMovie); try { Bitmap poster = Picasso.with(getActivity()) .load(mSelectedMovie.getCardImageUrl()) .resize(Utils.convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_WIDTH), Utils.convertDpToPixel(getActivity().getApplicationContext(), DETAIL_THUMB_HEIGHT)) .centerCrop() .get(); row.setImageBitmap(getActivity(), poster); } catch (IOException e) { Log.w(TAG, e.toString()); } return row; } @Override protected void onPostExecute(DetailsOverviewRow row) { /* 1st row: DetailsOverviewRow */ SparseArrayObjectAdapter sparseArrayObjectAdapter = new SparseArrayObjectAdapter(); for (int i = 0; i<10; i++){ sparseArrayObjectAdapter.set(i, new Action(i, "label1", "label2")); } row.setActionsAdapter(sparseArrayObjectAdapter); /* 2nd row: ListRow */ ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); for(int i = 0; i < 10; i++){ Movie movie = new Movie(); if(i%3 == 0) { movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg"); } else if (i%3 == 1) { movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02630.jpg"); } else { movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02529.jpg"); } movie.setTitle("title" + i); movie.setStudio("studio" + i); listRowAdapter.add(movie); } HeaderItem headerItem = new HeaderItem(0, "Related Videos"); ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector(); mFwdorPresenter.setInitialState(FullWidthDetailsOverviewRowPresenter.STATE_SMALL); Log.e(TAG, "mFwdorPresenter.getInitialState: " +mFwdorPresenter.getInitialState()); classPresenterSelector.addClassPresenter(DetailsOverviewRow.class, mFwdorPresenter); classPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); ArrayObjectAdapter adapter = new ArrayObjectAdapter(classPresenterSelector); /* 1st row */ adapter.add(row); /* 2nd row */ adapter.add(new ListRow(headerItem, listRowAdapter)); /* 3rd row */ //adapter.add(new ListRow(headerItem, listRowAdapter)); setAdapter(adapter); } } }
Note that constructor of adapter is defferent between MainFragment
and VideoDetailsFragment
. We are only using ListRow
– ListRowPresenter
in the MainFragment
. In that case we can instantiate adapter by setting Presenter
itself like
adapter = new ArrayObjectAdapter(new ListRowPresenter());
However, we are using DetailsOverviewRow
– FullWidthDetailsOverviewRowPresenter
& ListRow – ListRowPresenter
in VideoDetails
Fragment. ClassPresenterSelector
defines this correspondence, and we can use it in the argument of constructor of adapter.
ClassPresenterSelector classPresenterSelector = new ClassPresenterSelector(); classPresenterSelector.addClassPresenter(DetailsOverviewRow.class, mFwdorPresenter); classPresenterSelector.addClassPresenter(ListRow.class, new ListRowPresenter()); adapter = new ArrayObjectAdapter(classPresenterSelector);
Next, add description member in Movie
class, you can implement Getter and Setter method by [Alt]+[Insert] in Android studio. Also, make Movie
Serializable
so that we can pass this object through intent. Because we pass Movie
object through intent from MainActivity
to DetailsActivity
.
* See also: Fast, easy Parcelable implementation with Android studio Parcelable plugin for better performance implementation.
public class Movie implements Serializable { private static final String TAG = Movie.class.getSimpleName(); static final long serialVersionUID = 727566175075960653L; private long id; private String title; private String studio; private String description; private String cardImageUrl; ... public String getDescription() { return description; } public void setDescription(String description) { this.description = description; } ... }
Copy DetailsDescriptionPresenter
from AOSP sample source code as follows. Again, DetailsDescriptionPresenter
extends AbstractDetailsDescriptionPresenter
, which decides the design layout of descrption view.
/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package com.corochann.androidtvapptutorial; import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter; public class DetailsDescriptionPresenter extends AbstractDetailsDescriptionPresenter { @Override protected void onBindDescription(ViewHolder viewHolder, Object item) { Movie movie = (Movie) item; if (movie != null) { viewHolder.getTitle().setText(movie.getTitle()); viewHolder.getSubtitle().setText(movie.getStudio()); viewHolder.getBody().setText(movie.getDescription()); } } }
Finally, modify MainFragment
to send intent to launch DetailsActivity
.
private void setupEventListeners() { setOnItemViewSelectedListener(new ItemViewSelectedListener()); setOnItemViewClickedListener(new ItemViewClickedListener()); } private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { // each time the item is clicked, code inside here will be executed. if (item instanceof Movie) { Movie movie = (Movie) item; Log.d(TAG, "Item: " + item.toString()); Intent intent = new Intent(getActivity(), DetailsActivity.class); intent.putExtra(DetailsActivity.MOVIE, movie); getActivity().startActivity(intent); } } }
Build and Run!
Now you can see the content details by clicking card.
Initial Display, Logo is on the left, background is on the top half, and description is on the bottom half.
When user press “down” key, description view will take full screen.
When user press “down” key again, next row (ListRow
in this example) will appear.
Source code is on github.
——- Update: 2015.12.14 Add DetailsOverviewRowPresenter
implementation —————-
Build and Run 2!
Please check updated source code on github if you are interested in (already deprecated) DetailsOverviewRowPresenter
implementation. Why I updated this code? Because it seems still some developer prefers DetailsOverviewRowPresenter
design, even if it was deprecated!