Presentation is loading. Please wait.

Presentation is loading. Please wait.

Android TV UI設計 建國科技大學 資管系 饒瑞佶 2016/12 V1 2017/10 V2.

Similar presentations


Presentation on theme: "Android TV UI設計 建國科技大學 資管系 饒瑞佶 2016/12 V1 2017/10 V2."— Presentation transcript:

1 Android TV UI設計 建國科技大學 資管系 饒瑞佶 2016/12 V1 2017/10 V2

2 Leanback Library

3 Android TV UI設計思維 Android TV的介面設計與傳統的手機與平板介面設計不同
舉例來說我們需要設計只用 ↑↓→← 方向鍵來操作的介面,而不是使用觸碰方式  使用Fragment方式設計

4 建立新專案for TV

5 選擇No Activity 從完全空的了解使用Android TV Activity專案需要設定或改變什麼?

6 Add Empty Activity

7 不要勾選使用AppCompct

8 Add Fragment for MainActivity

9 修改MainFragment extends BrowseFragment

10 implement method-onActivityCreated
public class MainFragment extends BrowseFragment { private static final String TAG = MainFragment.class.getSimpleName(); @Override public void onActivityCreated(Bundle savedInstanceState) { Log.i(TAG, "onActivityCreated"); super.onActivityCreated(savedInstanceState); } }

11 修改activity_main.xml <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android=" xmlns:app=" xmlns:tools=" android:name="tw.edu.ctu.emtcmoretv.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" tools:deviceIds="tv" tools:ignore="MergeRootFrame" />

12 <?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android=" xmlns:app=" xmlns:tools=" android:name="tw.edu.ctu.rcjao.androidtv.androidtvdemo.MainFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity" tools:deviceIds="tv" tools:ignore="MergeRootFrame" />

13 Build and Run it HeaderFragment (header) part 兩者來自於BrowseFragment類別
RowsFragment (contents) part 兩者來自於BrowseFragment類別

14 Adding setupUIElements() to MainFragment.java
@Override public void onActivityCreated(Bundle savedInstanceState) { Log.i(TAG, “onActivityCreated”); super.onActivityCreated(savedInstanceState); // 設定UI內容 setupUIElements(); } private void setupUIElements() { // 如果要LOGO圖片 // setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner)); // 如果要使用文字title setTitle(“Hello Android TV"); // Badge, when set, takes precedent // set fastLane (or headers) background color setBrandColor(getResources().getColor(R.color.fastlane_background)); // set search icon color setSearchAffordanceColor(getResources().getColor(R.color.search_opaque)); }

15 @Override public void Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // 設定UI內容 setupUIElements(); } private void setupUIElements() { // 如果要LOGO圖片 //setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner)); // 如果要使用文字title setTitle("Hello Android TV"); // set fastLane (or headers) background color setBrandColor(getResources().getColor(R.color.fastlane_background)); // set search icon color setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));

16 colors.xml Right click res/values and choose
New  Values Resource file File name: colors.xml  OK

17 colors.xml <?xml version="1.0" encoding="utf-8"?> <resources> <color name="fastlane_background">#0096e6</color> <color name="search_opaque">#ffaa3f</color> </resources> <?xml version="1.0" encoding="utf-8"?> <resources> <color name="fastlane_background">#0096e6</color> <color name="search_opaque">#ffaa3f</color> </resources>

18

19 修改AndroidManifest.xml You may notice that application icon will not be shown in your Leanback Launcher (Android TV home launcher app) until now. We need some declaration in our app to be considered as Android TV app.

20 add <!-- TV app need to declare touchscreen not required --> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <!-- true: your app runs on only TV false: your app runs on phone and TV --> <uses-feature android:name="android.software.leanback" android:required="true" />

21 加入icon圖示

22 To show activity icon in Leanback launcher
You can show activity icon in Leanback launcher by declaring intent-filter <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> 改成

23 Activity icon will appear in Leanback Launcher

24 To show application icon in Leanback launcher
這個與前頁的activity icon 不同 activity icon and application icon are different.

25 application icon will appear in your Downloaded apps list

26 Build and Run

27 ListRow  ListRow is represented by blue circle. And a blue square is a RowsAdapter, which is as set of blue circle.

28 Construction of each ListRow

29 To summarize ArrayObjectAdapter (RowsAdapter) ← A set of ListRow ListRow = HeaderItem + ArrayObjectAdapter (RowAdapter) ArrayObjectAdapter (RowAdapter) ← A set of Object (CardInfo/Item)

30 The design of card is determined by class Presenter
Presenter defines how to show/present cardInfo. Presenter class itself is an abstract class, so you need to extend this class for your app’s suitable UI design. When you extend Presenter, you need to override at least below 3 methods.  onCreateViewHolder(Viewgroup parent) onBindViewHolder(ViewHolder viewHolder, Object cardInfo/item) onUnbindViewHolder(ViewHolder viewHolder)

31 Implement HeadersFragment & RowsFragment
 HeaderFragment (header) part RowsFragment (contents) part

32 Implement HeadersFragment & RowsFragment
The layout of view is defined in the onCreateViewHolder() Argument of onBindViewHolder(), we can access viewHolder created by onCreateViewHolder and also Object (CardInfo/item), which stores card information (In this example, just a String).

33 修改MainFragment.java 建立Presenter
private static final int GRID_ITEM_WIDTH = 300; private static final int GRID_ITEM_HEIGHT = 200;

34 private class GridItemPresenter extends Presenter { @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { // 產生一個textview TextView view = new TextView(parent.getContext()); view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT)); view.setFocusable(true); view.setFocusableInTouchMode(true); view.setBackgroundColor(getResources().getColor(R.color.default_background)); view.setTextColor(Color.WHITE); view.setGravity(Gravity.CENTER); return new ViewHolder(view); } @Override public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText((String) item); } @Override public void onUnbindViewHolder(ViewHolder viewHolder) { } }

35 private class GridItemPresenter extends Presenter {
@Override public ViewHolder onCreateViewHolder(ViewGroup parent) { // 產生一個textview TextView view = new TextView(parent.getContext()); view.setLayoutParams(new ViewGroup.LayoutParams(GRID_ITEM_WIDTH, GRID_ITEM_HEIGHT)); view.setFocusable(true); view.setFocusableInTouchMode(true); view.setBackgroundColor(getResources().getColor(R.color.default_background)); view.setTextColor(Color.WHITE); view.setGravity(Gravity.CENTER); return new ViewHolder(view); } public void onBindViewHolder(ViewHolder viewHolder, Object item) { ((TextView) viewHolder.view).setText((String) item); public void onUnbindViewHolder(ViewHolder viewHolder) {

36 修改res/values/color.xml <?xml version="1.0" encoding="utf-8"?>
<resources> <color name="fastlane_background">#0096e6</color> <color name="search_opaque">#ffaa3f</color> <color name="default_background">#3d3d3d</color> </resources>

37 set RowsAdapter at the start time of Activity
in onActivityCreated() in MainFragment

38 private ArrayObjectAdapter mRowsAdapter;
private void loadRows() { mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); /* GridItemPresenter */ HeaderItem gridItemPresenterHeader = new HeaderItem(0, "GridItemPresenter"); GridItemPresenter mGridPresenter = new GridItemPresenter(); ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter); gridRowAdapter.add("ITEM 1"); gridRowAdapter.add("ITEM 2"); gridRowAdapter.add("ITEM 3"); mRowsAdapter.add(new ListRow(gridItemPresenterHeader, gridRowAdapter)); /* set */ setAdapter(mRowsAdapter); }

39 private void loadRows() {
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); /* GridItemPresenter */ HeaderItem gridItemPresenterHeader = new HeaderItem(0, "GridItemPresenter"); GridItemPresenter mGridPresenter = new GridItemPresenter(); ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter); gridRowAdapter.add("ITEM 1"); gridRowAdapter.add("ITEM 2"); gridRowAdapter.add("ITEM 3"); mRowsAdapter.add(new ListRow(gridItemPresenterHeader, gridRowAdapter)); /* set */ setAdapter(mRowsAdapter); }

40 To summarize GridItemPresenter Presenter: GridItemPresenter
ViewHolder’s view: TextView CardInfo/Item: String

41 Build and Run

42 another type of Presenter
Presenter: CardPresenter ViewHolder’s view: ImageCardView CardInfo/Item: Movie class

43 ImageCardView ImageCardView class is provided from Android SDK, and it provides a card design layout with main image, title text and content text. ImageCardView is a subclass of BaseCardView,

44 Implement CardPresenter
New → class → CardPresenter(使用ImageCardView) New → class → Movie(定義資料結構) Movie class defines the CardInfo/Item which CardPresenter will present using ImageCardView New → class → Utils(取用Movie套用CardPresenter)

45 public class Movie { private long id; private String title; private String studio; public Movie() { } public long getId() { return id; } public void setId(long id) { this.id = id; } public String getTitle() { return title; }

46 public void setTitle(String title) { this
public void setTitle(String title) { this.title = title; } public String getStudio() { return studio; } public void setStudio(String studio) { this.studio = studio; } @Override public String toString() { return "Movie{" "id=" + id ", title='" + title + '\'' '}'; } }

47 public class Movie { private long id; private String title; private String studio; public Movie() { } public long getId() { return id; public void setId(long id) { this.id = id; public String getTitle() { return title; public void setTitle(String title) { this.title = title; public String getStudio() { return studio; public void setStudio(String studio) { this.studio = studio; @Override public String toString() { return "Movie{" + "id=" + id + ", title='" + title + '\'' + '}';

48 CardPresenter CardPresenter owns ViewHolder extended from parent’s Presenter.ViewHolder. This ViewHolder holds ImageCardView which is used to present UI for the Movieitem.

49 public class CardPresenter extends Presenter {
private static final String TAG = CardPresenter.class.getSimpleName(); private static Context mContext; private static int CARD_WIDTH = 313; private static int CARD_HEIGHT = 176; static class ViewHolder extends Presenter.ViewHolder { private Movie mMovie; private ImageCardView mCardView; private Drawable mDefaultCardImage; public ViewHolder(View view) { super(view); mCardView = (ImageCardView) view; mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie); } public void setMovie(Movie m) { mMovie = m; public Movie getMovie() { return mMovie; public ImageCardView getCardView() { return mCardView; public Drawable getDefaultCardImage() { return mDefaultCardImage;

50 @Override public ViewHolder onCreateViewHolder(ViewGroup parent) { Log.d(TAG, "onCreateViewHolder"); mContext = parent.getContext(); ImageCardView cardView = new ImageCardView(mContext); cardView.setFocusable(true); cardView.setFocusableInTouchMode(true); cardView.setBackgroundColor(mContext.getResources().getColor(R.color.fastlane_background)); return new ViewHolder(cardView); } public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { Movie movie = (Movie) item; ((ViewHolder) viewHolder).setMovie(movie); Log.d(TAG, "onBindViewHolder"); ((ViewHolder) viewHolder).mCardView.setTitleText(movie.getTitle()); ((ViewHolder) viewHolder).mCardView.setContentText(movie.getStudio()); ((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT); ((ViewHolder) viewHolder).mCardView.setMainImage(((ViewHolder) viewHolder).getDefaultCardImage());

51 @Override public void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { Log.d(TAG, "onUnbindViewHolder"); }

52 修改loadRows() in MainFragment.java
private void loadRows() { mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); /* GridItemPresenter */ HeaderItem gridItemPresenterHeader = new HeaderItem(0, "GridItemPresenter"); GridItemPresenter mGridPresenter = new GridItemPresenter(); ArrayObjectAdapter gridRowAdapter = new ArrayObjectAdapter(mGridPresenter); gridRowAdapter.add("ITEM 1"); gridRowAdapter.add("ITEM 2"); gridRowAdapter.add("ITEM 3"); mRowsAdapter.add(new ListRow(gridItemPresenterHeader, gridRowAdapter)); // CardPresenter HeaderItem cardPresenterHeader = new HeaderItem(1, "CardPresenter"); CardPresenter cardPresenter = new CardPresenter(); ArrayObjectAdapter cardRowAdapter = new ArrayObjectAdapter(cardPresenter); for(int i=0; i<10; i++) { Movie movie = new Movie(); movie.setTitle("title" + i); movie.setStudio("studio" + i); cardRowAdapter.add(movie); } mRowsAdapter.add(new ListRow(cardPresenterHeader, cardRowAdapter)); // /* set */ setAdapter(mRowsAdapter);

53 Build and Run

54 Updating main image after downloading picture from web using Picasso library
 we want to use picasso library, which can be included by adding a following line in app/build.gradle file dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:recyclerview-v7:22.2.0' compile 'com.android.support:leanback-v17:22.2.0' compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.squareup.picasso:picasso:2.3.2' }

55 add cardImageUrl member to Movie class
private String cardImageUrl; public String getCardImageUrl() { return cardImageUrl; } public void setCardImageUrl(String cardImageUrl) { this.cardImageUrl = cardImageUrl; public URI getCardImageURI() { try { return new URI(getCardImageUrl()); } catch (URISyntaxException e) { return null;

56 定義一個新類別PicassoImageCardViewTarget
public static class PicassoImageCardViewTarget implements Target { private ImageCardView mImageCardView; public PicassoImageCardViewTarget(ImageCardView imageCardView) { mImageCardView = imageCardView; } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { Drawable bitmapDrawable = new BitmapDrawable(mContext.getResources(), bitmap); mImageCardView.setMainImage(bitmapDrawable); public void onBitmapFailed(Drawable drawable) { mImageCardView.setMainImage(drawable); public void onPrepareLoad(Drawable drawable) { // Do nothing, default_background manager has its own transitions

57 Target interface allows us to implement 3 listener functions.
onBitmapLoaded   –  Callback when an image has been successfully loaded. onBitmapFailed      – Callback when an image has been successfully loaded. linked with error() onPrepareLoad      – Callback invoked right before your request is submitted. linked with placeholder()

58 CardPresenter takes care of updating image using picasso
CardPresenter takes care of updating image using picasso. This is done by implementing updateCardViewImage function. 加入 private PicassoImageCardViewTarget mImageCardViewTarget;

59 加入 mImageCardViewTarget=new PicassoImageCardViewTarget(mCardView);

60 加入 沒網路會使用預設圖片 At the last line of updateCardViewImage it calls into(mImageCardViewTarget) method to load the image to imageview protected void updateCardViewImage(URI uri) { Picasso.with(mContext) .load(uri.toString()) .resize(Utils.convertDpToPixel(mContext, CARD_WIDTH), Utils.convertDpToPixel(mContext, CARD_HEIGHT)) .placeholder(mDefaultCardImage) .error(mDefaultCardImage) .into(mImageCardViewTarget); }

61 加入Utils類別(新檔案) public class Utils { /*
* Making sure public utility methods remain static */ private Utils() { } /** * Returns the screen/display size public static Point getDisplaySize(Context context) { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); Point size = new Point(); display.getSize(size); return size; * Shows a (long) toast public static void showToast(Context context, String msg) { Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); * Shows a (long) toast. public static void showToast(Context context, int resourceId) { Toast.makeText(context, context.getString(resourceId), Toast.LENGTH_LONG).show(); public static int convertDpToPixel(Context ctx, int dp) { float density = ctx.getResources().getDisplayMetrics().density; return Math.round((float) dp * density); * Formats time in milliseconds to hh:mm:ss string format. public static String formatMillis(int millis) { String result = ""; int hr = millis / ; millis %= ; int min = millis / 60000; millis %= 60000; int sec = millis / 1000; if (hr > 0) { result += hr + ":"; if (min >= 0) { if (min > 9) { result += min + ":"; } else { result += "0" + min + ":"; if (sec > 9) { result += sec; result += "0" + sec; return result;

62 加入與註解 ((ViewHolder) viewHolder).updateCardViewImage(movie.getCardImageURI()); //((ViewHolder) viewHolder).mCardView.setMainImage(((ViewHolder) viewHolder).getDefaultCardImage());

63 specify cardImageUrl from MainFragment
for(int i=0; i<10; i++) { Movie movie = new Movie(); // 從URL取得照片 movie.setCardImageUrl(" movie.setTitle("title" + i); movie.setStudio("studio" + i); cardRowAdapter.add(movie); } 加入

64 Internet permission <uses-permission android:name="android.permission.INTERNET" />

65 Build and Run

66 分成選擇OnItemViewSelected 與 點選OnItemViewClicked兩類
開始加入事件 分成選擇OnItemViewSelected 點選OnItemViewClicked兩類

67 選擇OnItemViewSelected
when the user move the cursor and change the selection of item use setOnItemViewSelectedListener(OnItemViewSelectedListener listener) function for this purpose

68 修改MainFragment.java 加入 setupEventListeners(); // 設定選項事件

69 以下配合底圖切換來操作上面的onItemSelected事件
private void setupEventListeners() { setOnItemViewSelectedListener(new ItemViewSelectedListener()); } private final class ItemViewSelectedListener implements OnItemViewSelectedListener { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { // each time the item is selected, code inside here will be executed. 以下配合底圖切換來操作上面的onItemSelected事件

70 background image 加入SimpleBackgroundManager.java
Right click package name → New → class → SimpleBackgroundManager

71 public class SimpleBackgroundManager {
private static final String TAG = SimpleBackgroundManager.class.getSimpleName(); private final int DEFAULT_BACKGROUND_RES_ID = R.drawable.default_background; private static Drawable mDefaultBackground; private Activity mActivity; // 主要的背景管理class private BackgroundManager mBackgroundManager; public SimpleBackgroundManager(Activity activity) { mActivity = activity; mDefaultBackground = activity.getDrawable(DEFAULT_BACKGROUND_RES_ID); mBackgroundManager = BackgroundManager.getInstance(activity); mBackgroundManager.attach(activity.getWindow()); activity.getWindowManager().getDefaultDisplay().getMetrics(new DisplayMetrics()); } public void updateBackground(Drawable drawable) { mBackgroundManager.setDrawable(drawable); public void clearBackground() { mBackgroundManager.setDrawable(mDefaultBackground);

72 修改MainFragment.java 加入
private static SimpleBackgroundManager simpleBackgroundManager = null; 加入 simpleBackgroundManager = new SimpleBackgroundManager(getActivity());

73

74 加入 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { // each time the item is selected, code inside here will be executed. if (item instanceof String) { // GridItemPresenter row simpleBackgroundManager.clearBackground(); } else if (item instanceof Movie) { // CardPresenter row simpleBackgroundManager.updateBackground(getActivity().getDrawable(R.drawable.movie)); }

75 Build and run 選擇CardPresenter時底圖改變,切回GridItemPresenter時又恢復

76 改善SimpleBackgroundManager
使用PicassoBackgroundManager  implement TimerTask to wait certain period of time from updating background image getting the background image from web by using Picasso Picasso library is “A powerful image downloading and caching library for Android“. We will use it for more easier image resource handling.

77 建立PicassoBackgroundManager.java public class PicassoBackgroundManager { private static final String TAG = PicassoBackgroundManager.class.getSimpleName(); private static int BACKGROUND_UPDATE_DELAY = 500; private final int DEFAULT_BACKGROUND_RES_ID = R.drawable.default_background; private static Drawable mDefaultBackground; // Handler attached with main thread private final Handler mHandler = new Handler(Looper.getMainLooper()); private Activity mActivity; private BackgroundManager mBackgroundManager = null; private DisplayMetrics mMetrics; private URI mBackgroundURI; private PicassoBackgroundManagerTarget mBackgroundTarget; Timer mBackgroundTimer; // null when no UpdateBackgroundTask is running.

78 // 建構子 public PicassoBackgroundManager (Activity activity) { mActivity = activity; mDefaultBackground = activity.getDrawable(DEFAULT_BACKGROUND_RES_ID); mBackgroundManager = BackgroundManager.getInstance(activity); mBackgroundManager.attach(activity.getWindow()); mBackgroundTarget = new PicassoBackgroundManagerTarget(mBackgroundManager); mMetrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(mMetrics); } /** * if UpdateBackgroundTask is already running, cancel this task and start new task. */ private void startBackgroundTimer() { if (mBackgroundTimer != null) { mBackgroundTimer.cancel(); mBackgroundTimer = new Timer(); /* set delay time to reduce too much background image loading process */ mBackgroundTimer.schedule(new UpdateBackgroundTask(), BACKGROUND_UPDATE_DELAY);

79 private class UpdateBackgroundTask extends TimerTask {
@Override public void run() { /* Here is TimerTask thread, not UI thread */ mHandler.post(new Runnable() { /* Here is main (UI) thread */ if (mBackgroundURI != null) { updateBackground(mBackgroundURI); } }); public void updateBackgroundWithDelay(String url) { try { URI uri = new URI(url); updateBackgroundWithDelay(uri); } catch (URISyntaxException e) { /* skip updating background */ Log.e(TAG, e.toString());

80 /** * updateBackground with delay * delay time is measured in other Timer task thread. uri */ public void updateBackgroundWithDelay(URI uri) { mBackgroundURI = uri; startBackgroundTimer(); } private void updateBackground(URI uri) { try { Picasso.with(mActivity) .load(uri.toString()) .resize(mMetrics.widthPixels, mMetrics.heightPixels) .centerCrop() .error(mDefaultBackground) .into(mBackgroundTarget); } catch (Exception e) { Log.e(TAG, e.toString());

81 public class PicassoBackgroundManagerTarget implements Target {
BackgroundManager mBackgroundManager; public PicassoBackgroundManagerTarget(BackgroundManager backgroundManager) { this.mBackgroundManager = backgroundManager; } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { this.mBackgroundManager.setBitmap(bitmap); public void onBitmapFailed(Drawable errorDrawable) { this.mBackgroundManager.setDrawable(drawable); public void onPrepareLoad(Drawable placeHolderDrawable) {

82 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; PicassoBackgroundManagerTarget that = (PicassoBackgroundManagerTarget) o; if (!mBackgroundManager.equals(that.mBackgroundManager)) } public int hashCode() { return mBackgroundManager.hashCode();

83 改用PicassoBackgroundManager
replace from SimpleBackgroundManager to PicassoBackgroundManager in MainFragment.java private static PicassoBackgroundManager picassoBackgroundManager = null;

84 加入與取代 picassoBackgroundManager = new PicassoBackgroundManager(getActivity());

85 private final class ItemViewSelectedListener implements OnItemViewSelectedListener {
@Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { // each time the item is selected, code inside here will be executed. /* 簡易版底圖更新 if (item instanceof String) { // GridItemPresenter row simpleBackgroundManager.clearBackground(); } else if (item instanceof Movie) { // CardPresenter row simpleBackgroundManager.updateBackground(getActivity().getDrawable(R.drawable.movie)); } */ // 加強版底圖更新 if (item instanceof String) { // GridItemPresenter picassoBackgroundManager.updateBackgroundWithDelay(" } else if (item instanceof Movie) { // CardPresenter picassoBackgroundManager.updateBackgroundWithDelay(((Movie) item).getCardImageUrl()); 加入與取代 預設載入的圖片

86 加入與取代 多顯示幾個項目 if(i%3 == 0) {
movie.setCardImageUrl(" } else if (i%3 == 1) { movie.setCardImageUrl(" } else { movie.setCardImageUrl(" }

87 Build and run

88 設定選項點擊事件 setOnItemViewClickedListener – onItemClicked callback function in MainFragment

89 Implementing click listener in MainFragment
加入 加入 setOnItemViewClickedListener(new ItemViewClickedListener());

90 以下配合DetailsActivity & VideoDetailsFragment
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 介紹上面的ItemViewClickedListener方法

91 DetailsActivity & VideoDetailsFragment
顯示詳細資料

92 VideoDetailsFragment
2 rows in VideoDetailsFragment. First row is DetailsOverviewRow second row is ListRow (which is already explained in MainFragment).  DetailsOverviewRow shows the content details including picture in the left, description and some actions are set in the left-bottom.

93 FullWidthDetailsOverviewRowPresenter is consisting of 3 parts
Logo view – customizable (option), by implementing DetailsOverViewLogoPresenter Action list view Detailed description view – customizable (MUST), implement subclass of AbstractDetailsDescriptionPresenter

94

95 建立Activity DetailsActivity
New → Activity → BlankActivity Activity Name: DetailsActivity

96 DetailsActivity.java public class DetailsActivity extends Activity {
public static final String MOVIE = "Movie"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_details); } public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_details, menu); return true; public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return super.onOptionsItemSelected(item);

97 加入menu_details.xml <menu xmlns:android=" xmlns:tools=" tools:context="com.corochann.androidtvapptutorial.DetailsActivity"> <item android:orderInCategory="100" android:showAsAction="never" /> </menu>

98 修改strings.xml 加入 <resources>
<string name="app_name">AndroidTVDemo</string> <string name="title_activity_main">MainActivity</string> <string name="hello_world">Hello world!</string> <string name="action_settings">Settings</string> <string name="title_activity_details">DetailsActivity</string> </resources> 加入

99 建立VideoDetailsFragment.java New -> Java Class -> Name: VideoDetailsFragment 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());; }

100 @Override public void onStop() { mDetailsRowBuilderTask.cancel(true); super.onStop(); } private class DetailsRowBuilderTask extends AsyncTask<Movie, Integer, DetailsOverviewRow> { 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;

101 @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(" } else if (i%3 == 1) { movie.setCardImageUrl(" } else { movie.setCardImageUrl(" 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());

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

103 建立CustomFullWidthDetailsOverviewRowPresenter
public class CustomFullWidthDetailsOverviewRowPresenter extends FullWidthDetailsOverviewRowPresenter { private static final String TAG = CustomFullWidthDetailsOverviewRowPresenter.class.getSimpleName(); CustomFullWidthDetailsOverviewRowPresenter(Presenter presenter) { super(presenter); } @Override protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { super.onBindRowViewHolder(holder, item); this.setState((ViewHolder) holder, FullWidthDetailsOverviewRowPresenter.STATE_SMALL);

104 建立DetailsDescriptionPresenter
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()); }

105 修改MainFragment.java 加入

106 修改MainFragment.java 加入
String description = "Lorem ipsum dolor sit amet, qui mundi vivendum cu. Mazim dicant possit te his. Quo solet dicant prodesset eu, pri deseruisse concludaturque ea, saepe maiorum sea et. Impetus discere sed at. Vim eu novum erant integre, te tale voluptatibus est. Facer labores te mel.\n" + "\n" + "Dictas denique qualisque mea id, cu mei verear fabellas. Mel no autem nusquam, viderer oblique te mei. At minimum corpora consulatu vim. Cibo nominavi vis no, in verterem vulputate eos, essent iriure cu vel. Ius ferri expetendis ad, omnes aeterno nominati id his, eum debitis lobortis comprehensam id.\n" + "Illud dicit nostrud sit no. Eu quod nostro pro. Ut gubergren mnesarchum has, nostro detracto scriptorem et quo, no illud phaedrum recteque sea. Ad his summo probatus recusabo. Qui amet tale viris et, ei his quodsi torquatos adipiscing. Laudem malorum no eum, accusam mandamus sit ex, est ut tractatos dissentiet. Dictas feugiat usu et, an his cibo appareat placerat, eu quis dignissim qui.\n" + "Euripidis neglegentur eu per, denique singulis vel cu, malis dolore ne duo. Cum no iracundia persecuti expetendis. Vim alii dolore malorum at, veniam perfecto salutandi cu nec, vix ad nonumes consulatu scripserit. At sit nonumy dolores aliquando, eu nam sumo legere. Eu maiorum adipisci torquatos his, vidit appareat eos no.\n" + "Solet laboramus no quo, cu aperiam inermis vix. Eum animal graecis id, ne quodsi abhorreant sit. Tale persequeris te qui. Labitur invenire explicari in vix." + "Lorem ipsum dolor sit amet, qui mundi vivendum cu. Mazim dicant possit te his. Quo solet dicant prodesset eu, pri deseruisse concludaturque ea, saepe maiorum sea et. Impetus discere sed at. Vim eu novum erant integre, te tale voluptatibus est. Facer labores te mel.\n" + "Solet laboramus no quo, cu aperiam inermis vix. Eum animal graecis id, ne quodsi abhorreant sit. Tale persequeris te qui. Labitur invenire explicari in vix."; movie.setDescription(description);

107 修改Movie.java 加入 private String description;
public class Movie implements Serializable private String description; public String getDescription() { return description; } public void setDescription(String description) { this.description = description;

108 修改modify activity_details.xml
只顯示VideoDetailsFragment <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android=" xmlns:tools=" android:name="tw.edu.ctu.emtcmoretv.VideoDetailsFragment" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".DetailsActivity" tools:deviceIds="tv" />

109 修改MainFragment 加入 send intent to launch DetailsActivity
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); }

110 判斷GridItem項目被click if (item instanceof String) { // 來自GridItemPresenter if (((String) item).trim().replace(“ ”, “”).equals(“選項文字")) { // 顯示GridFragment排版畫面 Intent intent = new Intent(getActivity(), VerticalGridActivity.class); getActivity().startActivity(intent); }

111 RUN

112 handling video contents

113 Creating PlaybackOverlayActivity & PlaybackOverlayFragment
New → Activity → BlankActivity Activity Name: PlaybackOverlayActivity Layout Name: activity_playback_overlay

114 activity_playback_overlay.xml It is constructed in 2 layer – VideoView in the back and PlaybackOverlayFragment in the front. VideoView is the view which we will play video contents PlaybackOverlayFragment will show the UI for controlling video.

115 activity_playback_overlay.xml 依據package name修改
<FrameLayout xmlns:android=" android:layout_width="match_parent" android:layout_height="match_parent"> <VideoView android:layout_height="match_parent" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:layout_centerInParent="true" android:layout_gravity="center"></VideoView> <fragment android:name="com.corochann.androidtvapptutorial.PlaybackOverlayFragment" android:layout_height="match_parent" /> </FrameLayout> 依據package name修改

116 建立PlaybackOverlayFragment.java New -> Java Class -> Name: PlaybackOverlayFragment PlaybackOverlayFragment is subclass of android.support.v17.leanback.app.PlaybackOverlayFragment which provides us component to make video control UI.

117

118 PlaybackControlsRowPresenter
PrimaryActionsAdapter      – It owns icons for main row SecondaryActionsAdapter – It owns icons for sub rowNote that PlaybackControlsRow class provides us many useful default icons for video control. We only need to instantiate its inner class. PlaybackControlsRowPresenter DescriptionPresenter – It is Presenter for displaying item details on the top of PrimaryActions bar. 

119

120 PlaybackOverlayFragment.java public class PlaybackOverlayFragment extends android.support.v17.leanback.app.PlaybackOverlayFragment { private static final String TAG = PlaybackOverlayFragment.class.getSimpleName(); private Movie mSelectedMovie; private PlaybackControlsRow mPlaybackControlsRow; private ArrayObjectAdapter mPrimaryActionsAdapter; private ArrayObjectAdapter mSecondaryActionsAdapter; private PlaybackControlsRow.PlayPauseAction mPlayPauseAction; private PlaybackControlsRow.RepeatAction mRepeatAction; private PlaybackControlsRow.ThumbsUpAction mThumbsUpAction; private PlaybackControlsRow.ThumbsDownAction mThumbsDownAction; private PlaybackControlsRow.ShuffleAction mShuffleAction; private PlaybackControlsRow.SkipNextAction mSkipNextAction; private PlaybackControlsRow.SkipPreviousAction mSkipPreviousAction; private PlaybackControlsRow.FastForwardAction mFastForwardAction; private PlaybackControlsRow.RewindAction mRewindAction; private PlaybackControlsRow.HighQualityAction mHighQualityAction; private PlaybackControlsRow.ClosedCaptioningAction mClosedCaptioningAction; private PlaybackControlsRow.MoreActions mMoreActions;

121 @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSelectedMovie = (Movie) getActivity().getIntent().getSerializableExtra(DetailsActivity.MOVIE); setBackgroundType(PlaybackOverlayFragment.BG_LIGHT); setFadingEnabled(true); setUpRows(); } private ArrayObjectAdapter mRowsAdapter; private void setUpRows() { ClassPresenterSelector ps = new ClassPresenterSelector(); PlaybackControlsRowPresenter playbackControlsRowPresenter; playbackControlsRowPresenter = new PlaybackControlsRowPresenter(new DetailsDescriptionPresenter()); ps.addClassPresenter(PlaybackControlsRow.class, playbackControlsRowPresenter); ps.addClassPresenter(ListRow.class, new ListRowPresenter()); mRowsAdapter = new ArrayObjectAdapter(ps); /* * Add PlaybackControlsRow to mRowsAdapter, which makes video control UI. * PlaybackControlsRow is supposed to be first Row of mRowsAdapter. */ addPlaybackControlsRow(); /* add ListRow to second row of mRowsAdapter */ addOtherRows(); setAdapter(mRowsAdapter);

122 private void addPlaybackControlsRow() {
mPlaybackControlsRow = new PlaybackControlsRow(mSelectedMovie); mRowsAdapter.add(mPlaybackControlsRow); ControlButtonPresenterSelector presenterSelector = new ControlButtonPresenterSelector(); mPrimaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); mSecondaryActionsAdapter = new ArrayObjectAdapter(presenterSelector); mPlaybackControlsRow.setPrimaryActionsAdapter(mPrimaryActionsAdapter); mPlaybackControlsRow.setSecondaryActionsAdapter(mSecondaryActionsAdapter); Activity activity = getActivity(); mPlayPauseAction = new PlaybackControlsRow.PlayPauseAction(activity); mRepeatAction = new PlaybackControlsRow.RepeatAction(activity); mThumbsUpAction = new PlaybackControlsRow.ThumbsUpAction(activity); mThumbsDownAction = new PlaybackControlsRow.ThumbsDownAction(activity); mShuffleAction = new PlaybackControlsRow.ShuffleAction(activity); mSkipNextAction = new PlaybackControlsRow.SkipNextAction(activity); mSkipPreviousAction = new PlaybackControlsRow.SkipPreviousAction(activity); mFastForwardAction = new PlaybackControlsRow.FastForwardAction(activity); mRewindAction = new PlaybackControlsRow.RewindAction(activity); mHighQualityAction = new PlaybackControlsRow.HighQualityAction(activity); mClosedCaptioningAction = new PlaybackControlsRow.ClosedCaptioningAction(activity); mMoreActions = new PlaybackControlsRow.MoreActions(activity); /* PrimaryAction setting */ mPrimaryActionsAdapter.add(mSkipPreviousAction); mPrimaryActionsAdapter.add(mRewindAction); mPrimaryActionsAdapter.add(mPlayPauseAction); mPrimaryActionsAdapter.add(mFastForwardAction); mPrimaryActionsAdapter.add(mSkipNextAction); /* SecondaryAction setting */ mSecondaryActionsAdapter.add(mThumbsUpAction); mSecondaryActionsAdapter.add(mThumbsDownAction); mSecondaryActionsAdapter.add(mRepeatAction); mSecondaryActionsAdapter.add(mShuffleAction); mSecondaryActionsAdapter.add(mHighQualityAction); mSecondaryActionsAdapter.add(mClosedCaptioningAction); mSecondaryActionsAdapter.add(mMoreActions); }

123 private void addOtherRows() {
ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); Movie movie = new Movie(); movie.setTitle("Title"); movie.setStudio("studio"); movie.setDescription("description"); movie.setCardImageUrl(" listRowAdapter.add(movie); HeaderItem header = new HeaderItem(0, "OtherRows"); mRowsAdapter.add(new ListRow(header, listRowAdapter)); }

124 修改VideoDetailsFragment.java 修改 加入
private static final int ACTION_PLAY_VIDEO = 0; 修改 加入

125 sparseArrayObjectAdapter
sparseArrayObjectAdapter.set(0, new Action(ACTION_PLAY_VIDEO, "Play Video")); sparseArrayObjectAdapter.set(1, new Action(1, "Action 2", "label")); sparseArrayObjectAdapter.set(2, new Action(2, "Action 3", "label")); // for (int i = 0; i<10; i++){ // sparseArrayObjectAdapter.set(i, new Action(i, "label1", "label2")); // } row.setActionsAdapter(sparseArrayObjectAdapter); mFwdorPresenter.setOnActionClickedListener(new OnActionClickedListener() { @Override public void onActionClicked(Action action) { if (action.getId() == ACTION_PLAY_VIDEO) { Intent intent = new Intent(getActivity(), PlaybackOverlayActivity.class); intent.putExtra("Movie", mSelectedMovie); intent.putExtra("shouldStart", true); startActivity(intent); } });

126 launch PlaybackOverlayAcitivity from DetailsActivity
PlaybackOverlayActivity need to have VideoView field variable “mVideoView” to control video

127 PlaybackOverlayActivity
public class PlaybackOverlayActivity extends Activity { private static final String TAG = PlaybackOverlayActivity.class.getSimpleName(); private VideoView mVideoView; private LeanbackPlaybackState mPlaybackState = LeanbackPlaybackState.IDLE; private int mPosition = 0; private long mStartTimeMillis; private long mDuration = -1; /* * List of various states that we can be in */ public enum LeanbackPlaybackState { PLAYING, PAUSED, IDLE } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_playback_overlay); loadViews();

128 @Override public void onDestroy() { super.onDestroy(); stopPlayback(); mVideoView.suspend(); mVideoView.setVideoURI(null); } private void loadViews() { mVideoView = (VideoView) findViewById(R.id.videoView); mVideoView.setFocusable(false); mVideoView.setFocusableInTouchMode(false); Movie movie = (Movie) getIntent().getSerializableExtra(DetailsActivity.MOVIE); setVideoPath(movie.getVideoUrl()); public void setVideoPath(String videoUrl) { setPosition(0); mVideoView.setVideoPath(videoUrl); mVideoView.start(); //自動播放 mStartTimeMillis = 0; mDuration = Utils.getDuration(videoUrl);

129 private void stopPlayback() {
if (mVideoView != null) { mVideoView.stopPlayback(); } private void setPosition(int position) { if (position > mDuration) { mPosition = (int) mDuration; } else if (position < 0) { mPosition = 0; mStartTimeMillis = System.currentTimeMillis(); } else { mPosition = position; Log.d(TAG, "position set to " + mPosition); public int getPosition() { return mPosition; public void setPlaybackState(LeanbackPlaybackState playbackState) { this.mPlaybackState = playbackState;

130 @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.menu_playback_overlay, menu); return true; } public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return super.onOptionsItemSelected(item);

131 public void playPause(boolean doPlay) {
if (mPlaybackState == LeanbackPlaybackState.IDLE) { /* Callbacks for mVideoView */ setupCallbacks(); } if (doPlay && mPlaybackState != LeanbackPlaybackState.PLAYING) { mPlaybackState = LeanbackPlaybackState.PLAYING; if (mPosition > 0) { mVideoView.seekTo(mPosition); mVideoView.start(); mStartTimeMillis = System.currentTimeMillis(); } else { mPlaybackState = LeanbackPlaybackState.PAUSED; int timeElapsedSinceStart = (int) (System.currentTimeMillis() - mStartTimeMillis); setPosition(mPosition + timeElapsedSinceStart); mVideoView.pause(); public void fastForward() { if (mDuration != -1) { // Fast forward 10 seconds. setPosition(mVideoView.getCurrentPosition() + (10 * 1000));

132 public void rewind() { // rewind 10 seconds setPosition(mVideoView.getCurrentPosition() - (10 * 1000)); mVideoView.seekTo(mPosition); } private void setupCallbacks() { mVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { mVideoView.stopPlayback(); mPlaybackState = LeanbackPlaybackState.IDLE; return false; }); mVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { public void onPrepared(MediaPlayer mp) { if (mPlaybackState == LeanbackPlaybackState.PLAYING) { mVideoView.start(); mVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { public void onCompletion(MediaPlayer mp) {

133 修改Move.java private String videoUrl; public String getVideoUrl() {
return videoUrl; } public void setVideoUrl(String videoUrl) { this.videoUrl = videoUrl;

134 修改Utils.java 加入 public static long getDuration(String videoUrl) {
MediaMetadataRetriever mmr = new MediaMetadataRetriever(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) { mmr.setDataSource(videoUrl, new HashMap<String, String>()); } else { mmr.setDataSource(videoUrl); } return Long.parseLong(mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION));

135 建立res/menu/menu_playback_overlay.xml <menu xmlns:android=" xmlns:tools=" tools:context="com.corochann.androidtvapptutorial.PlaybackOverlayActivity"> <item android:orderInCategory="100" android:showAsAction="never" /> </menu>

136 修改MainFragment 加入影片的URL
movie.setVideoUrl("

137 RESULT

138 如果要播放YouTube 使用Intent直接透過YouTuBe App播放 需要使用YouTubeAndroidPlayerApi.jar
Intent intent = YouTubeIntents.createPlayVideoIntentWithOptions(getActivity(), "Sjx-T7_CGQA", true, false); startActivity(intent);

139 Search

140 Android search Android TV uses the Android search interface to retrieve content data from installed apps and deliver search results to the user. Your app's content data can be included with these results, to give the user instant access to the content in your app. Your app must provide Android TV with the data fields from which it generates suggested search results as the user enters characters in the search dialog. To do that, your app must implement a Content Provider that serves up the suggestions along with a searchable.xmlconfiguration file that describes the content provider and other vital information for Android TV. 

141 OK, Google: Search Inside My Apps!

142 how all you need is a small addition to your AndroidManifest.xml in order to connect the Google Now SEARCH_ACTION with your searchable activity <activity android:name=".SearchableActivity"> <intent-filter> <action android:name="com.google.android.gms.actions.SEARCH_ACTION"/> <category android:name="android.intent.category.DEFAULT"/> </intent-filter> </activity>

143

144 MainFragment .java 加入 // Existence of this method make In-app search icon visible setOnSearchClickedListener(new View.OnClickListener() { @Override public void onClick(View view) { Intent intent = new Intent(getActivity(), SearchActivity.class); startActivity(intent); } });

145 MainFragment .java 加入 // set search icon color
setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));

146 SearchActivity.java public class SearchActivity extends Activity { private static final String TAG = SearchActivity.class.getSimpleName(); private SearchFragment mSearchFragment; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); mSearchFragment = (SearchFragment) getFragmentManager().findFragmentById(R.id.search_fragment); } @Override public boolean onSearchRequested() { //if (mSearchFragment.hasResults()) { startActivity(new Intent(this, SearchActivity.class)); //} else { //mSearchFragment.startRecognition(); //} return true; } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present getMenuInflater().inflate(R.menu.menu_search, menu); return true; }

147 @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml int id = item.getItemId(); //noinspection SimplifiableIfStatement if (id == R.id.action_settings) { return true; } return super.onOptionsItemSelected(item); } }

148 Activity_search.xml <?xml version="1.0" encoding="utf-8"?> <fragment xmlns:android=" android:name="tw.edu.ctu.emtcmoretv.ui.SearchFragment" android:layout_width="match_parent" android:layout_height="match_parent" />

149 Menu_search.xml <menu xmlns:android=" xmlns:tools=" tools:context="com.corochann.androidtvapptutorial.SearchActivity"> <item android:orderInCategory="100" android:showAsAction="never" android:ti

150 SearchFragment.java public class SearchFragment extends android.support.v17.leanback.app.SearchFragment implements android.support.v17.leanback.app.SearchFragment.SearchResultProvider { private static final String TAG = SearchFragment.class.getSimpleName(); private static final int REQUEST_SPEECH = 0x ; private ArrayObjectAdapter mRowsAdapter; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); setSearchResultProvider(this); //setOnItemViewClickedListener(new ItemViewClickedListener()); if (!Utils.hasPermission(getActivity(), Manifest.permission.RECORD_AUDIO)) { Log.v(TAG, "no permission RECORD_AUDIO"); // SpeechRecognitionCallback is not required and if not provided recognition will be handled // using internal speech recognizer, in which case you must have RECORD_AUDIO permission setSpeechRecognitionCallback(new SpeechRecognitionCallback() { @Override public void recognizeSpeech() { Log.v(TAG, "recognizeSpeech"); try { startActivityForResult(getRecognizerIntent(), REQUEST_SPEECH); } catch (ActivityNotFoundException e) { Log.e(TAG, "Cannot find activity for speech recognizer", e); } } }); } }

151 public boolean hasResults() { return mRowsAdapter
public boolean hasResults() { return mRowsAdapter.size() > 0; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.v(TAG, "onActivityResult requestCode=" + requestCode " resultCode=" + resultCode " data=" + data); switch (requestCode) { case REQUEST_SPEECH: switch (resultCode) { case Activity.RESULT_OK: setSearchQuery(data, true); break; } } } @Override public ObjectAdapter getResultsAdapter() { Log.d(TAG, "getResultsAdapter"); Log.d(TAG, mRowsAdapter.toString()); // It should return search result here, // but static Movie Item list will be returned here now for practice. ArrayList<Movie> mItems = MovieProvider.getMovieItems(); ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); listRowAdapter.addAll(0, mItems); HeaderItem header = new HeaderItem("Search results"); mRowsAdapter.add(new ListRow(header, listRowAdapter)); return mRowsAdapter; }

152 @Override public boolean onQueryTextChange(String newQuery){ Log
@Override public boolean onQueryTextChange(String newQuery){ Log.i(TAG, String.format("Search Query Text Change %s", newQuery)); //loadQuery(newQuery); return true; } @Override public boolean onQueryTextSubmit(String query) { Log.i(TAG, String.format("Search Query Text Submit %s", query)); //loadQuery(query); return true; } }

153 修改Utils.java 加入 public static boolean hasPermission(final Context context, final String permission) { return PackageManager.PERMISSION_GRANTED == context.getPackageManager().checkPermission( permission, context.getPackageName()); }

154 修改MainActivity.java 加入
@Override public boolean onSearchRequested() { startActivity(new Intent(this, SearchActivity.class)); return true; }

155 改成app內搜尋 修改SearchFragment.java
private ArrayList<Movie> mItems = MovieProvider.getMovieItems(); private String mQuery; 加入下頁code

156 private void loadRows() { // offload processing from the UI thread new AsyncTask<String, Void, ListRow>() { private final String query = mQuery; @Override protected void onPreExecute() { mRowsAdapter.clear(); } @Override protected ListRow doInBackground(String... params) { final List<Movie> result = new ArrayList<>(); for (Movie movie : mItems) { // Main logic of search is here // Just check that "query" is contained in Title or Description or not. (NOTE: excluded studio information here) if (movie.getTitle().toLowerCase(Locale.ENGLISH) contains(query.toLowerCase(Locale.ENGLISH)) || movie.getDescription().toLowerCase(Locale.ENGLISH) contains(query.toLowerCase(Locale.ENGLISH))) { result.add(movie); } } ArrayObjectAdapter listRowAdapter = new ArrayObjectAdapter(new CardPresenter()); listRowAdapter.addAll(0, result); HeaderItem header = new HeaderItem("Search Results"); return new ListRow(header, listRowAdapter); } @Override protected void onPostExecute(ListRow listRow) { mRowsAdapter.add(listRow); } }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); }

157 註解掉原有的code

158 加入

159 加入 private static final long SEARCH_DELAY_MS = 1000L; private final Handler mHandler = new Handler(); private final Runnable mDelayedLoad = new Runnable() { @Override public void run() { loadRows(); } }; private void loadQueryWithDelay(String query, long delay) { mHandler.removeCallbacks(mDelayedLoad); if (!TextUtils.isEmpty(query) && !query.equals("nil")) { mQuery = query; mHandler.postDelayed(mDelayedLoad, delay); } }

160 修改 @Override public boolean onQueryTextChange(String newQuery){ Log.i(TAG, String.format("Search Query Text Change %s", newQuery)); //loadQuery(newQuery); loadQueryWithDelay(newQuery, SEARCH_DELAY_MS); return true; } @Override public boolean onQueryTextSubmit(String query) { Log.i(TAG, String.format("Search Query Text Submit %s", query)); //loadQuery(query); //mQuery = query; //loadRows(); loadQueryWithDelay(query, 0); return true; }

161 修改文字出現空白 @Override public boolean onQueryTextChange(String newQuery) { Log.i("AAA", String.format("Search Query Text Change %s", newQuery.trim().replace(" ", ""))); //loadQuery(newQuery); // 將搜尋字串中的空白拿掉 loadQueryWithDelay(newQuery.trim().replace(" ", ""), SEARCH_DELAY_MS); return true; } @Override public boolean onQueryTextSubmit(String query) { Log.i("AAA", String.format("Search Query Text Submit %s", query.trim().replace(" ", ""))); //loadQuery(query); //mQuery = query; //loadRows(); // 將搜尋字串中的空白拿掉 loadQueryWithDelay(query.trim().replace(" ", ""), 0); return true; }

162 加入點選事件 // 搜尋後的結果點選事件處理 private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie) { // 來自CardPresenter 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); } else if (item instanceof String) { // 來自GridItemPresenter Toast.makeText(getActivity(), ((String) item), Toast.LENGTH_SHORT).show(); } } }

163 呼叫點選事件 在onCreate中加入 // 設定搜尋後的結果點選事件 setOnItemViewClickedListener(new ItemViewClickedListener());

164 VerticalGridFragment
垂直排列

165 建立VerticalGridActivity
用以讓Fragment依附

166 layout/activity_vertical_grid.xml <RelativeLayout xmlns:android=" xmlns:tools=" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="tw.edu.ctu.emtcmoretv.VerticalGridActivity"> <fragment xmlns:android=" android:name="tw.edu.ctu.emtcmoretv.ui.VerticalGridFragment" android:layout_width="match_parent" android:layout_height="match_parent" /> </RelativeLayout>

167 建立VerticalGridFragment.java 主要事件 setGridPresenter (必要) setAdapter (必要)
public class VerticalGridFragment extends android.support.v17.leanback.app.VerticalGridFragment { private static final String TAG = VerticalGridFragment.class.getSimpleName(); @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); } } 主要事件 setGridPresenter (必要) setAdapter (必要) setTitle setOnItemViewSelectedListener setOnItemViewClickedListener setSelectedPosition

168 setGridPresenter 在這裡加入內容
@Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); // 設定右上角標題 setTitle("VerticalGridFragment"); // 設定Fragment的內容 setupFragment(); } private void setupFragment() { VerticalGridPresenter gridPresenter = new VerticalGridPresenter(); gridPresenter.setNumberOfColumns(NUM_COLUMNS); setGridPresenter(gridPresenter); mAdapter = new ArrayObjectAdapter(new CardPresenter()); setAdapter(mAdapter); } 在這裡加入內容

169 run

170 加入內容 private ArrayObjectAdapter mAdapter; private void setupFragment() { VerticalGridPresenter gridPresenter = new VerticalGridPresenter(); gridPresenter.setNumberOfColumns(NUM_COLUMNS); setGridPresenter(gridPresenter); mAdapter = new ArrayObjectAdapter(new CardPresenter()); // 加入內容 CardPresenter cardPresenter = new CardPresenter(); ArrayObjectAdapter cardRowAdapter = new ArrayObjectAdapter(cardPresenter); ArrayList<Movie> mItems = MovieProvider.getMovieItems(); for (Movie movie : mItems) { mAdapter.add(movie); } // 設定顯示內容 setAdapter(mAdapter); }

171 加入事件 @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, “onCreate”); super.onCreate(savedInstanceState); // 設定右上角標題 setTitle(“VerticalGridFragment”); //setBadgeDrawable(getResources().getDrawable(R.drawable.app_icon_your_company)); // 設定Fragment的內容 setupFragment(); // 設定事件 setupEventListeners(); }

172 private void setupEventListeners() { setOnItemViewClickedListener(new ItemViewClickedListener()); setOnItemViewSelectedListener(new ItemViewSelectedListener()); } private final class ItemViewClickedListener implements OnItemViewClickedListener { @Override public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { if (item instanceof Movie) { Movie movie = (Movie) item; Intent intent = new Intent(getActivity(), DetailsActivity.class); intent.putExtra(DetailsActivity.MOVIE, movie); getActivity().startActivity(intent); } } } 點選後跳到詳細頁面

173 private PicassoBackgroundManager picassoBackgroundManager; @Override public void onCreate(Bundle savedInstanceState) { Log.d(TAG, "onCreate"); super.onCreate(savedInstanceState); picassoBackgroundManager = new PicassoBackgroundManager(getActivity()); // 設定右上角標題 setTitle("VerticalGridFragment"); //setBadgeDrawable(getResources().getDrawable(R.drawable.app_icon_your_company)); // 設定Fragment的內容 setupFragment(); setupEventListeners(); //設定預設被選擇的項目. setSelectedPosition(1); } private final class ItemViewSelectedListener implements OnItemViewSelectedListener { @Override public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row) { picassoBackgroundManager.updateBackgroundWithDelay(((Movie) item).getCardImageUrl()); } } 上下左右切換項目改底圖

174 直接啟動VerticalGridActivity就可以執行

175

176

177

178

179

180

181

182

183 Error handle

184 加入 cardView.setCardType(BaseCardView.CARD_TYPE_INFO_UNDER);
cardView.setInfoVisibility(BaseCardView.CARD_REGION_VISIBLE_ALWAYS); cardView.setFocusable(true); cardView.setFocusableInTouchMode(true); cardView.setBackgroundColor(mContext.getResources().getColor(R.color.fastlane_background));

185 修改PlaybackOverlayFragment.java 加入

186 /* onClick */ playbackControlsRowPresenter.setOnActionClickedListener(new OnActionClickedListener() { public void onActionClicked(Action action) { if (action.getId() == mPlayPauseAction.getId()) { /* PlayPause action */ togglePlayback(mPlayPauseAction.getIndex() == PlaybackControlsRow.PlayPauseAction.PLAY); } else if (action.getId() == mSkipNextAction.getId()) { /* SkipNext action */ next(mCurrentPlaybackState == PlaybackState.STATE_PLAYING); } else if (action.getId() == mSkipPreviousAction.getId()) { /* SkipPrevious action */ prev(mCurrentPlaybackState == PlaybackState.STATE_PLAYING); } else if (action.getId() == mFastForwardAction.getId()) { /* FastForward action */ fastForward(); } else if (action.getId() == mRewindAction.getId()) { /* Rewind action */ rewind(); } if (action instanceof PlaybackControlsRow.MultiAction) { /* Following action is subclass of MultiAction * - PlayPauseAction * - FastForwardAction * - RewindAction * - ThumbsAction * - RepeatAction * - ShuffleAction * - HighQualityAction * - ClosedCaptioningAction */ notifyChanged(action); /* Change icon */ if (action instanceof PlaybackControlsRow.ThumbsUpAction || action instanceof PlaybackControlsRow.ThumbsDownAction || action instanceof PlaybackControlsRow.RepeatAction || action instanceof PlaybackControlsRow.ShuffleAction || action instanceof PlaybackControlsRow.HighQualityAction || action instanceof PlaybackControlsRow.ClosedCaptioningAction) { ((PlaybackControlsRow.MultiAction) action).nextIndex(); });


Download ppt "Android TV UI設計 建國科技大學 資管系 饒瑞佶 2016/12 V1 2017/10 V2."

Similar presentations


Ads by Google