[Updated 2015.11.18]: Revise.
[Updated 2016.3.13]: Updated wrong source code.
Contents
Aim of this chapter
Implementing background image update feature. Application was boring with no background, and it becomes much nice if it has appropriate background.
Just setting background is easy, though, Android TV sample application explains how to dynamically change background linking with your current selection of contents.
Before explanation of background change, I start explanation of onItemSelected
callback function so that we get the event notification when the item is selected. Next, I will show simple background change implementation followed by better performance implementation using Picasso library.
setOnItemViewSelectedListener listener & onItemSelected callback
BrowseFragment
supports to set listener when the itemview is selected & clicked. Current target is to be get notified when the user move the cursor and change the selection of item.
we can use setOnItemViewSelectedListener(OnItemViewSelectedListener listener)
function for this purpose. In the argument, you can put the listener class which should implement OnItemViewSelectedListener
interface which is also provided by leanback library. Then, you can implement onItemSelected
callback function, which is the function called when an item is selected.
@Override public void onActivityCreated(Bundle savedInstanceState) { Log.i(TAG, "onActivityCreated"); super.onActivityCreated(savedInstanceState); setupUIElements(); loadRows(); setupEventListeners(); } 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. } }
We will proceed to implement background change function in the following. Here I arranged to create SimpleBackgroundManager
, and PicassoBackgroundManager
to handle background image (Android TV sample application is doing it inside MainFragment.java).
SimpleBackgroundManager
There is explanation in official developer’s site, see Update the Background for reference.
I wrote some test code below.
Right click package name → New → class → SimpleBackgroundManager
This SimpleBackgroundManager
keeps a member mBackgroundManager
of BackgroundManager
class, which handles the actual background change. This BackgroundManager
instance is a singleton instance which can be obtained via BackgroundManager.getInstance()
.
package com.corochann.androidtvapptutorial; import android.app.Activity; import android.content.ContentResolver; import android.content.Context; import android.graphics.drawable.Drawable; import android.net.Uri; import android.support.v17.leanback.app.BackgroundManager; import android.util.DisplayMetrics; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.URI; /** * Created by corochann on 3/7/2015. */ 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; 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); } }
At first, instance of BackgroundManager
is created in Constructor. It must be attached with Window before updating background, and these initialization is done in constructor.
updateBackground
method will change the background, and clearBackground
method will update the background to default image. (I have added the res/drawable/default_background.xml
and updated res/values/colors.xml
.)
Modification of MainFragment
is small.
public class MainFragment extends BrowseFragment { ... private static SimpleBackgroundManager simpleBackgroundManager = null; @Override public void onActivityCreated(Bundle savedInstanceState) { ... simpleBackgroundManager = new SimpleBackgroundManager(getActivity()); } 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. if (item instanceof String) { // GridItemPresenter row simpleBackgroundManager.clearBackground(); } else if (item instanceof Movie) { // CardPresenter row simpleBackgroundManager.updateBackground(getActivity().getDrawable(R.drawable.movie)); } } }
Build and run!
We can check background is updated depending on the selection of the row. Also the Background will be back to default when you go back to GridItemPresenter
row.
Source code is on github.
PicassoBackgroundManager
Let’s improve SimpleBackgroundManager
implementation. What we will improve is following.
- Delay updating background
In the previous implementation, main thread will always try to update background when user is moving their cursor, and changing select item. It is busy, and may cause bad performance. Below we will implementTimerTask
to wait certain period of time from updating background image. - Image handling by using Picasso library
Picasso library is “A powerful image downloading and caching library for Android“. We will use it for more easier image resource handling.
Create new class PicassoBackgroundManager
, implement as follows
package com.corochann.androidtvapptutorial; import android.app.Activity; import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Handler; import android.os.Looper; import android.support.v17.leanback.app.BackgroundManager; import android.util.DisplayMetrics; import android.util.Log; import com.squareup.picasso.Picasso; import com.squareup.picasso.Target; import java.net.URI; import java.net.URISyntaxException; import java.util.Timer; import java.util.TimerTask; /** * Created by corochann on 3/7/2015. */ 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. 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); } private class UpdateBackgroundTask extends TimerTask { @Override public void run() { /* Here is TimerTask thread, not UI thread */ mHandler.post(new Runnable() { @Override public void run() { /* 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()); } } /** * updateBackground with delay * delay time is measured in other Timer task thread. * @param 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()); } } /** * Copied from AOSP sample code. * Inner class * Picasso target for updating default_background images */ public class PicassoBackgroundManagerTarget implements Target { BackgroundManager mBackgroundManager; public PicassoBackgroundManagerTarget(BackgroundManager backgroundManager) { this.mBackgroundManager = backgroundManager; } @Override public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom loadedFrom) { this.mBackgroundManager.setBitmap(bitmap); } @Override public void onBitmapFailed(Drawable drawable) { this.mBackgroundManager.setDrawable(drawable); } @Override public void onPrepareLoad(Drawable drawable) { // Do nothing, default_background manager has its own transitions } @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)) return false; return true; } @Override public int hashCode() { return mBackgroundManager.hashCode(); } } }
Now, we will replace from SimpleBackgroundManager
to PicassoBackgroundManager
in MainFragment.java
public class MainFragment extends BrowseFragment { ... private static PicassoBackgroundManager picassoBackgroundManager = null; @Override public void onActivityCreated(Bundle savedInstanceState) { ... picassoBackgroundManager = new PicassoBackgroundManager(getActivity()); } 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) { if (item instanceof String) { // GridItemPresenter picassoBackgroundManager.updateBackgroundWithDelay("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/10/RIMG0656.jpg"); } else if (item instanceof Movie) { // CardPresenter picassoBackgroundManager.updateBackgroundWithDelay(((Movie) item).getCardImageUrl()); } } } ... private void loadRows() { mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter()); ... 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); cardRowAdapter.add(movie); }
Build and run!
We can check background is updated after 500 ms thanks to the timer task. And we are getting the background image from web by using Picasso.
Source code is on github.
Until now, we cannot click item/card. Next chapter, DetailsOverviewRowPresenter & FullWidthDetailsOverviewRowPresenter – Android TV application hands on tutorial 5, is to implement a onClickListener
and show content details by DetailFragment
.