How to use Presenter and ViewHolder?
– Android TV application hands on tutorial 3

picasso-image-download

[Update 2015.11.18]: Revise.

Aim of this chapter

In previous chapter, we looked GridItemPresenter. Its relationship was following.

  • Presenter: GridItemPresenter
  • ViewHolder’s view: TextView
  • CardInfo/Item: String

This was easy example. In this chapter, we proceed to introduce another type of Presenter,

  • Presenter: CardPresenter
  • ViewHolder’s view: ImageCardView
  • CardInfo/Item: Movie class

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, so it is nice to look BaseCardView class. This is the explanation of BaseCardView,

android.support.v17.leanback.widget
public class BaseCardView
extends android.widget.FrameLayout
A card style layout that responds to certain state changes. It arranges its children in a vertical column, with different regions becoming visible at different times.
A BaseCardView will draw its children based on its type, the region visibilities of the child types, and the state of the widget. A child may be marked as belonging to one of three regions: main, info, or extra. The main region is always visible, while the info and extra regions can be set to display based on the activated or selected state of the View. The card states are set by calling setActivated and setSelected.

BaseCardView itself does not provide specific design layout. So when you want to utilize this, you can make subclass of BaseCardView which have specific design. ImageCardView is one of the class, and currently I could find only ImageCardView class as the subclass of BaseCardView provided by SDK.

In this chapter, we will add this ImageCardView to our code.

Implement CardPresenter, Movie class

I will start by placing necessary files at first. Rightclick on package,

  1. New → class → CardPresenter
  2. New → class → Movie 
  3. For the main image, we use movie.png.
    Copy res/drawable/movie.png from Android TV sample application.
  4. We will use Utility functions provided by Android TV sample application.
    Copy [package name]/Utils class from Android TV sample application to your source code.

First, Utils.java is just copying from AOSP, which will be below.

/*
 * 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.content.Context;
import android.graphics.Point;
import android.view.Display;
import android.view.WindowManager;
import android.widget.Toast;

/**
 * A collection of utility methods, all static.
 */
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 / 3600000;
        millis %= 3600000;
        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;
        } else {
            result += "0" + sec;
        }
        return result;
    }
}

Second, Movie class defines the CardInfo/Item which CardPresenter will present using ImageCardView. It should have the imformation of

  • main image
  • title text
  • content text (studio)

But for the first stage, I only put the information of “title” and “content (studio)”.

/*
 * 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.util.Log;

import java.net.URI;
import java.net.URISyntaxException;

/**
 *  Modified from AOSP sample source code, by corochann on 2/7/2015.
 *  Movie class represents video entity with title, description, image thumbs and video url.
 */
public class Movie {

    private static final String TAG = Movie.class.getSimpleName();

    static final long serialVersionUID = 727566175075960653L;
    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 + '\'' +
                '}';
    }
}

Last implementaiton is CardPresenter, it is a subclass of PresenterCardPresenter owns ViewHolder extended from parent’s Presenter.ViewHolder. This ViewHolder holds ImageCardView which is used to present UI for the Movie item. 

/*
 * 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.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;

/**
 * Modified from AOSP sample source code, by corochann on 2/7/2015.
 */
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;
        }

    }

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

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

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

    @Override
    public void onViewAttachedToWindow(Presenter.ViewHolder viewHolder) {
        // TO DO
    }

}

Preparation of data model = Movie and presenter = CardPresenter are done. We can show Movie item by putting the item to adapter.

private void loadRows() {
        mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
        ...

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

         ...
    }

Build and Run 1

cardpresenter2

cardpresenter3

CardPresenter header will appear in the second line, and ImageCardView shows the default card image. The title and content text will appear when you move from header to contents (when items are “onActivated”).

Source code is on github.

Updating main image after downloading picture from web using Picasso

Above example shows the default image in the ImageCardView which must be included together with your app (image is static). Sometimes, however, you want to use the image downloading from web so that your application can show updated information.

Picasso image loader library will help us to achieve this easily. Here are the references.

In the CardPresenter class, 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.squareup.picasso:picasso:2.3.2'
}

We will add cardImageUrl member to Movie class, which points a URL for the main image. 

    private String cardImageUrl;

    public String getCardImageUrl() {
        return cardImageUrl;
    }

    public void setCardImageUrl(String cardImageUrl) {
        this.cardImageUrl = cardImageUrl;
    }

As your tips, getter and setter can be automatically generated by Android studio. In the above modification, you just need to declare cardImageUrl member followed by [Alt]+[Insert] and generate getter and setter. See How to automatically generate getters and setters in Android Studio. We also implement a getImageURI function, to convert URL string to URI format.

    public URI getCardImageURI() {
        try {
            return new URI(getCardImageUrl());
        } catch (URISyntaxException e) {
            return null;
        }
    }

CardPresenter takes care of updating image using picasso. This is done by implementing updateCardViewImage function. Picasso makes a source code intuitive to understand for loading, transforming image.

    public ViewHolder(View view) {
        super(view);
        mCardView = (ImageCardView) view;
        mImageCardViewTarget = new PicassoImageCardViewTarget(mCardView);
        mDefaultCardImage = mContext.getResources().getDrawable(R.drawable.movie);
    }

       ...

    protected void updateCardViewImage(URI uri) {
        Picasso.with(mContext)
                .load(uri.toString())
                .resize(Utils.convertDpToPixel(mContext, CARD_WIDTH),
                        Utils.convertDpToPixel(mContext, CARD_HEIGHT))
                .error(mDefaultCardImage)
                .into(mImageCardViewTarget);
    }
}


...

@Override
public void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) {
    Movie movie = (Movie) item;
    ((ViewHolder) viewHolder).setMovie(movie);

    Log.d(TAG, "onBindViewHolder");
    if (movie.getCardImageUrl() != null) {
        ((ViewHolder) viewHolder).mCardView.setTitleText(movie.getTitle());
        ((ViewHolder) viewHolder).mCardView.setContentText(movie.getStudio());
        ((ViewHolder) viewHolder).mCardView.setMainImageDimensions(CARD_WIDTH, CARD_HEIGHT);
        ((ViewHolder) viewHolder).updateCardViewImage(movie.getCardImageURI());
        //((ViewHolder) viewHolder).mCardView.setMainImage(((ViewHolder) viewHolder).getDefaultCardImage());
    }
}

At the last line of updateCardViewImage it calls into(mImageCardViewTarget)  method to load the image to imageview. This target is implemented as follows.

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

        @Override
        public void onBitmapFailed(Drawable drawable) {
            mImageCardView.setMainImage(drawable);
        }

        @Override
        public void onPrepareLoad(Drawable drawable) {
            // Do nothing, default_background manager has its own transitions
        }
    }

Interface Target is defined in picasso library, it 

represents an arbitrary listener for image loading.

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

Remaining task is to specify cardImageUrl from MainFragment, which is done in 

    private void loadRows() {

        ...

        for(int i=0; i<10; i++) {
            Movie movie = new Movie();
            movie.setCardImageUrl("http://heimkehrend.raindrop.jp/kl-hacker/wp-content/uploads/2014/08/DSC02580.jpg");
            movie.setTitle("title" + i);
            movie.setStudio("studio" + i);
            cardRowAdapter.add(movie);
        }
         ...
    }

At last, you need to add permission to use Internet in AndroidManifest.xml before building.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.corochann.androidtvapptutorial" >

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

    ...

Build and Run 2

picasso-image-download

Now main image is downloaded from the internet. 

Source code is on github.

Customizing ImageCardView, BaseCardView

We can change the design type, and the animation behavior of the card. To begin with, I recommend to refer BaseCardView explanation in source code provided by Android SDK,

A BaseCardView will draw its children based on its type, the region visibilities of the child types, and the state of the widget. A child may be marked as belonging to one of three regions: maininfo, or extra. The main region is always visible, while the info and extra regions can be set to display based on the activated or selected state of the View. The card states are set by calling setActivated and setSelected.

In BaseCardView class, you can check the options available to change the design.

  1. public void setCardType(int type)
  2. public void setInfoVisibility(int visibility)
  3. public void setExtraVisibility(int visibility)

setCardType(int type)

You can use following card type as argument

  • CARD_TYPE_MAIN_ONLY
  • CARD_TYPE_INFO_OVER
  • CARD_TYPE_INFO_UNDER
  • CARD_TYPE_INFO_UNDER_WITH_EXTRA

The example with ImageCardView

CARD_TYPE_MAIN_ONLY
CARD_TYPE_MAIN_ONLY
CARD_TYPE_INFO_OVER
CARD_TYPE_INFO_OVER
CARD_TYPE_INFO_UNDER
CARD_TYPE_INFO_UNDER, CARD_TYPE_INFO_UNDER_WITH_EXTRA

You can check the layout of ImageCardView in SDK folder, \sdk\extras\android\support\v17\leanback\res\layout\lb_image_card_view.xml.

ImageCardView has imageView as main region, and title and content text are in info region. The extra region is not set, therefore the behavior is same between CARD_TYPE_INFO_UNDER and CARD_TYPE_INFO_UNDER_WITH_EXTRA.

setInfoVisibility(int visibility), setExtraVisibility(int visibility)

You can use following card type as argument

  • CARD_REGION_VISIBLE_ALWAYS
    – the region (title & content text area) will always appear.
  • CARD_REGION_VISIBLE_ACTIVATED
    – the region will not appear when user is selecting header.
       the region will appear when user move to RowsFragment.
  • CARD_REGION_VISIBLE_SELECTED
    – the region will not appear when this card/item is not selected.
       the region will appear only when the card/item is selected.

The more detail explanation of these options can be referred from SDK source code.

Here I changed the setting by modifying onCreateViewHolder in CardPresenter class, 

    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent) {
        Log.d(TAG, "onCreateViewHolder");
        mContext = parent.getContext();

        ImageCardView cardView = new ImageCardView(mContext);
        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));
        return new ViewHolder(cardView);
    }

Source code is on github.

Next chapter, PicassoBackgroundManager – Android TV application hands on tutorial 4, is implementing background image update feature.

Construction of BrowseFragment
– Android TV application hands on tutorial 2

GridItemPresenter1

[Update 2015.11.17: revise]

Construction of BrowseFragment

In this chapter, we will implement a combination of header and selectable objects (so called “cards”). But before going to real implementation, it’s good to understand the construction of BrowseFragment. You can also read the source code in the sdk (android/support/v17/leanback/app/) by yourself. 

Let’s start explaination by using Android TV sample application. When you launch application, the contents are aligned in a grid structure. Each header title on the left have a each contents row, and this header – contents row relationship is one to one. This “header + contents row” combination is represented by ListRow. Body of BrowseFragment is a set of ListRow (I will use the term RowsAdapter in this post).

In the below picture, ListRow is represented by blue circle. And a blue square is a RowsAdapter, which is as set of blue circle. 

RowsAdapter1
Set of ListRow constructs RowsAdapter, which makes main body UI of BrowseFragment.

Next, let’s look inside the ListRow in more detail. The contents of the header is specified by ArrayObjectAdapter (I call RowAdapter in this post), which is a set of Object (I call CardInfo or Item in this post).  

This CardInfo can be any object, and as it will be explained in detail later, how to show this CardInfo can be specified by Presenter class. 

ListRow1
Construction of each ListRow.

To summarize,

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

Presenter class

The design of card is determined by 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)

For the details of methods, I encourage you to refer source code of Presenter class. Presenter has innerclass ViewHolder which has the reference to the View. You may access the View via viewHolder at specific event (onBind, onUnbind, etc.) listener callback method.

Implement HeadersFragment & RowsFragment (GridItemPresenter) 

Let’s proceed to hands on. Here, we will implement GridItemPresenter class.
In this sample application, Object (CardInfo/item) is String type and viewHolder holds TextView reference to show this String

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

    private class GridItemPresenter extends Presenter {
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent) {
            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) {

        }
    }

}

After defining your own Presenter, you only need to set RowsAdapter at the start time of Activity. You can do so in onActivityCreated() in MainFragment,

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.i(TAG, "onActivityCreated");
        super.onActivityCreated(savedInstanceState);

        setupUIElements();

        loadRows();
    }

    ...

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

So whole source code of MainFragment will be.

package com.corochann.androidtvapptutorial;

import android.graphics.Color;
import android.os.Bundle;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
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.Presenter;
import android.util.Log;
import android.view.Gravity;
import android.view.ViewGroup;
import android.widget.TextView;

/**
 * Created by corochann on 2015/06/28.
 */
public class MainFragment extends BrowseFragment {
    private static final String TAG = MainFragment.class.getSimpleName();

    private ArrayObjectAdapter mRowsAdapter;
    private static final int GRID_ITEM_WIDTH = 300;
    private static final int GRID_ITEM_HEIGHT = 200;

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.i(TAG, "onActivityCreated");
        super.onActivityCreated(savedInstanceState);

        setupUIElements();

        loadRows();
    }

    private void setupUIElements() {
        // setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner));
        setTitle("Hello Android TV!"); // Badge, when set, takes precedent
        // over title
        setHeadersState(HEADERS_ENABLED);
        setHeadersTransitionOnBackEnabled(true);

        // set fastLane (or headers) background color
        setBrandColor(getResources().getColor(R.color.fastlane_background));
        // set search icon color
        setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
    }

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

    private class GridItemPresenter extends Presenter {
        @Override
        public ViewHolder onCreateViewHolder(ViewGroup parent) {
            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) {

        }
    }

}

Please add following 

    <color name="default_background">#3d3d3d</color>

so that MainFragment can refer the background color setting.

(III) Build and Run!

Now you can see header & contents combination is implemented.

GridItemPresenter1

Note that we only defined Presenter, and load items to show. Other animaiton, e.g. when you select item, it will be bigger, is already implemented in SDK. So even non-designer, it’s easy to make certain level of UI for Android TV application.

Source code is uploaded on github.

See next post, How to use Presenter and ViewHolder? – Android TV application hands on tutorial 3, for CardPresenter implementation which uses ImageCardView to present a card with main image, title, and sub-text.

Introduction
– Android TV application hands on tutorial 1

androidtvapptutorial6

[updated 2015.11.16]: Revise (sentence structure refactoring).

Android TV application development introduction

Currently, we have still not enough introduction for Android TV application development yet. In this series of tutorial, I will introduce how to develop Android TV application.
The aim of this post is to understand Android “TV” specific code implementation, especially focusing on UI implementaion.

Because UI is one of the biggest difference between Android phone app and Android TV app. We need to make UI suitable for TV usage, for example we should make an app so that we can navigate the application only using ↑↓→← direction keys, instead of touchpad navigation. Because user uses remote controller, and cannot use “touch screen” function with TV. To achieve this requirement, Android open source project is providing Leanback Support library (android.support.v17.leanback) so that developers can easily implement UI which satisfies these requirements and thus suitable for TV usage. This tutorial mainly explain the usage of this Leanback library.

The target of this post is people those who,,,

  • developed Android app before, but not familiar with Android TV app.
  • begginer – intermediate level

Since Eclipse support will be finished at the end of 2015, Android studio will be used for the IDE to develop Android TV apps (So please download and setup Android studio, if you have not yet done!). Note that most of the code introduced here is from AOSP android TV sample source code. Basically this tutorial is just detail explanation of this sample source code. Let’s begin.

Launch new Android TV application

  1. Launch Android studio,

New project

 Application name: AndroidTVappTutorial

 Company Domain: corochann.com ( or you can use your own domain name )

androidtvapptutorial1

Target Android Devices

androidtvapptutorial2

Add an activity to TV

androidtvapptutorial3

Select “Add No Activity” and Finish

Android studio automatically generate the source codes to start this Tutorial.

The source code of this phase is uploaded on github.

  1. Add activity

At first, let’s make activity. Right click on “com.corochann.androidtvapptutorial”, and choose

New -> Activity -> Blank activity

Check “Launcher Activity”.

I will start from making blank activity, named “MainActivity“. 

Android studio now generate 2 files, Java class & layout/activity_main.xml. (we don’t use res/menu/menu_main.xml)

* Note: We can also find “Android TV activity”. When you select this, it will create too match files at the same time. It’s a really helpful reference, but difficult to understand what kind of functions each files takes care of. So I will create these files from scratch in this post so that we can understand each source code’s responsibility. Many of the implementation in this post is referencing this official sample program.

Next, we want to design UI of MainActivity by creating MainFragment.

  1. Add fragment

Right click on the package name (in my case com.corochann.androidtvapptutorial)

New -> Java Class -> Name: MainFragment

*  Instead of above procedure, if we choose New -> Fragment -> Blank fragment Uncheck “Create layout XML?”, it makes too match sample source code.

First, modify activity_main.xml as follows so that it only displays mainfragment.

* Click right-top “<>” icon on the code, if you want to copy & paste.

<?xml version="1.0" encoding="utf-8"?>
<fragment xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main_browse_fragment"
    android:name="com.corochann.androidtvapptutorial.MainFragment" android:layout_width="match_parent"
    android:layout_height="match_parent" tools:context=".MainActivity" tools:deviceIds="tv"
    tools:ignore="MergeRootFrame" />

Second modify MainFragment as follows.

We will make this MainFragment as a sub-class of BrowseFragment.

BrowseFragment class is supplied by Android SDK Leanback library, and it creates standard UI for Android TV application which we will see through this Tutorial.

package com.corochann.helloandroidtvfromscrach;
 
import android.os.Bundle;
import android.support.v17.leanback.app.BrowseFragment;
import android.util.Log;
 
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);
 
    }
}

(I) Build and Run it!

androidtvapptutorial5

BrowseFragment consists of HeadersFragment & RowsFragment.
Here, you can see HeaderFragment (header) part at the right side, and RowsFragment (contents) part at the left side. We will design this Header & Row combination in the following.

Before that, let’s implement UI for main color & title of this application.

source code until here is uploaded on github.

  1. Adding setupUIElements() to MainFragment.java

We will add setupUIElements() method inside MainFragment.java, to set up application information. 

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        Log.i(TAG, "onActivityCreated");
        super.onActivityCreated(savedInstanceState);

        setupUIElements();
        
    }

    private void setupUIElements() {
        // setBadgeDrawable(getActivity().getResources().getDrawable(R.drawable.videos_by_google_banner));
        setTitle("Hello Android TV!"); // Badge, when set, takes precedent
        // over title
        setHeadersState(HEADERS_ENABLED);
        setHeadersTransitionOnBackEnabled(true);

        // set fastLane (or headers) background color
        setBrandColor(getResources().getColor(R.color.fastlane_background));
        // set search icon color
        setSearchAffordanceColor(getResources().getColor(R.color.search_opaque));
    }

We have set 

  • Application title or Application icon
  • Brand color

Color information is referenced from colors.xml, which we haven’t made yet. Right click res/values and choose

New -> values Resource file
File name: colors.xml -> “OK”
Write below.

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="fastlane_background">#0096e6</color>
    <color name="search_opaque">#ffaa3f</color>
</resources>
colors
colors are shown in left side of text editor in Android studio

(II) Build and Run!

Now you can see title is shown on top-right, and header color = BrandColor on left side has changed.

androidtvapptutorial6

You can also use setBadgeDrawable() method instead of setTitle() method. As written in the comments of above code as “Badge, when set, takes precedent”, the title will change to logo if setBadgeDrawable() is used (See below picture). You can set either one of “title logo icon” by setBadgeDrawable or “title string” by setTitle.

AndroidTVsampleApp
If you use setBadgeDrawable(), logo image will appear instead of title (String) in the top right.

Source code until here is uploaded on github.

  1. Modify Android Manifest.

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. Modify AndroidManifest.xml as following.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.corochann.androidtvapptutorial" >


    <!-- 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" />


    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.Leanback" >
        <activity
            android:name=".MainActivity"
            android:icon="@drawable/app_icon_your_company"
            android:label="@string/app_name"
            android:logo="@drawable/app_icon_your_company" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

and I put app_icon_your_compan.png in main/res/drawble/ folder.

(III) Build and Run!

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" />

Also, the icon is specified in activity tag,

        <activity
            android:name=".MainActivity"
            android:icon="@drawable/app_icon_your_company"
            android:label="@string/app_name"
            android:logo="@drawable/app_icon_your_company" >

            ...
manifest_activity
Activity icon will appear in Leanback Launcher.

To show application icon in Leanback launcher

You can show application icon by declaring, 

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.Leanback" >

        ...

Note that activity icon and application icon are different.

manifest_application
application icon will appear in your Downloaded apps list.

For the Android manifest setting for Android TV, please refer details in official Android developers page.

Next chapter, Construction of BrowseFragment – Android TV application hands on tutorial 2, I will explain notion of BrowseFragmentHeadersFragmentRowsFragment, Adapter and Presenter to show selectable objects in our app.

References

no suitable constructor found for HeaderItem(int,String,)

I got following error when I try to build android TV sample application.

D:\workspace\androidstudio\AndroidTVsample\app\src\main\java\com\corochann\androidtvsample\MainFragment.java
Error:(109, 33) error: no suitable constructor found for HeaderItem(int,String,<null>)
constructor HeaderItem.HeaderItem(long,String) is not applicable
(actual and formal argument lists differ in length)
constructor HeaderItem.HeaderItem(String) is not applicable
(actual and formal argument lists differ in length)
Error:(113, 33) error: no suitable constructor found for HeaderItem(int,String,<null>)
constructor HeaderItem.HeaderItem(long,String) is not applicable
(actual and formal argument lists differ in length)
constructor HeaderItem.HeaderItem(String) is not applicable
(actual and formal argument lists differ in length)
D:\workspace\androidstudio\AndroidTVsample\app\src\main\java\com\corochann\androidtvsample\PlaybackOverlayFragment.java
Error:(316, 29) error: no suitable constructor found for HeaderItem(int,String,<null>)
constructor HeaderItem.HeaderItem(long,String) is not applicable
(actual and formal argument lists differ in length)
constructor HeaderItem.HeaderItem(String) is not applicable
(actual and formal argument lists differ in length)
D:\workspace\androidstudio\AndroidTVsample\app\src\main\java\com\corochann\androidtvsample\VideoDetailsFragment.java
Error:(156, 33) error: no suitable constructor found for HeaderItem(int,String,<null>)
constructor HeaderItem.HeaderItem(long,String) is not applicable
(actual and formal argument lists differ in length)
constructor HeaderItem.HeaderItem(String) is not applicable
(actual and formal argument lists differ in length)
Note: Some input files use or override a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
Error:Execution failed for task ':app:compileDebugJava'.
> Compilation failed; see the compiler error output for details.

The reason of this error happens is because the definition of the constructor HeaderItem.HeaderItem(long,String) has changed. We need to change

from HeaderItem(id, string, null) to HeaderItem(id, string).

So we need to delete “null” argument for all occurrences in the function.

String types not allowed (at ‘slideEdge’ with value ‘end’)

I got following error when I try to build android TV sample application.

TVDemoApp\app\build\intermediates\exploded-aar\com.android.support\leanback-v17\22.1.1\res\transition-v22\lb_browse_entrance_transition.xml
Error:(26, 26) String types not allowed (at 'slideEdge' with value 'end').
TVDemoApp\app\build\intermediates\exploded-aar\com.android.support\leanback-v17\22.1.1\res\transition-v22\lb_browse_return_transition.xml
Error:(21, 26) String types not allowed (at 'slideEdge' with value 'start').
Error:(30, 26) String types not allowed (at 'slideEdge' with value 'end').
Error:Execution failed for task ':app:processDebugResources'.
> com.android.ide.common.internal.LoggedErrorException: Failed to run command:
C:\Users\corochann\AppData\Local\Android\sdk\build-tools\21.1.1\aapt.exe package -f --no-crunch -I C:\Users\corochann\AppData\Local\Android\sdk\platforms\android-21\android.jar -M D:TVDemoApp\app\build\intermediates\manifests\full\debug\AndroidManifest.xml -S D:TVDemoApp\app\build\intermediates\res\debug -A D:TVDemoApp\app\build\intermediates\assets\debug -m -J D:TVDemoApp\app\build\generated\source\r\debug -F D:\workspace\github\bugfix_episode_4\episode_1\TVDemoApp\app\build\intermediates\res\resources-debug.ap_ --debug-mode --custom-package com.sgottard.tvdemoapp -0 apk --output-text-symbols D:TVDemoApp\app\build\intermediates\symbols\debug
Error Code:
1
Output:
D:TVDemoApp\app\build\intermediates\res\debug\transition-v22\lb_browse_entrance_transition.xml:23: error: Error: String types not allowed (at 'slideEdge' with value 'end').
D:TVDemoApp\app\build\intermediates\res\debug\transition-v22\lb_browse_return_transition.xml:18: error: Error: String types not allowed (at 'slideEdge' with value 'start').
D:TVDemoApp\app\build\intermediates\res\debug\transition-v22\lb_browse_return_transition.xml:27: error: Error: String types not allowed (at 'slideEdge' with value 'end').

How to fix

You need to update configuration of compile SDK version.

update build.gradle (Module: app) as follows.

1. specify compileSdkVersion as 22, not 21.

    compileSdkVersion 22

2. specify dependencies library version as

compile ‘com.android.support:recyclerview-v7:22.1.1
compile ‘com.android.support:leanback-v17:22.1.1
compile ‘com.android.support:appcompat-v7:22.1.1

and build again.

Reference: http://stackoverflow.com/questions/29149362/androidtv-leanback-master-build-errors

Plugin with id ‘android-sdk-manager’ not found

When you face this bug when trying to build Android source code, try following.

Add below inside “dependencies” of top folder of build.gradle (Project: TVDemoApp)

        classpath ‘com.android.tools.build:gradle:1.0.0’
        classpath ‘com.jakewharton.sdkmanager:gradle-plugin:0.12.0’

Reference:

Building Android TV sample application

AndroidTVsampleApp

Let’s look how Android TV app works by building this sample application.

At the time of writing (2015/6/25), I faced a compile error for building default sample application  for Android TV. I wrote another posts that how to resolve these issues, which are the temporary counter measure until the errors will be fixed by AOSP.

Environment

Android studio version: 1.2.2 Build 141.1980579
SDK version: API level 21 & 22.

Procedure

1. Launch Android studio → Start a new Android Studio project

2. “New project” display, fill in below.

Application name: use your own preference (Ex. HelloAndroidTV)
Company Domain: use your own preference (Ex. yourname.com)

and go “Next”.

3. “Target Android Devices” display

Uncheck “Phone and Tablet” (It is checked by Default.)
Check “TV” with Minimum SDK API level 21.

and go “Next”.

4. “Add an activity to TV”,

Select “Android TV Activity”

and go “Next”

5. “Customize the Activity” display,

Name of the Activities are already filled by default as

  • Activity Name: Main Activity
  • Main Layout Name: activity_main
  • Main Fragment: MainFragment
  • Title: MainAcitivity Title
  • Details Activity: DetailsActivity
  • Details Layout Name: activity_details
  • Details Fragment: VideoDetailsFragment

you can just press “finish” without changing names for this time.

After that, Android studio will auto generate codes, after generation click “Run app” button (shown as green triangle button) to build the source code.

If you get compile error…

Please refer following for fixing bugs.

  1. Plugin with id ‘android-sdk-manager’ not found
  2. String types not allowed (at ‘slideEdge’ with value ‘end’)
  3. no suitable constructor found for HeaderItem(int,String,)

Build it and try it!

If successful, you can launch an application shown at the top of this page. You can also try to go around inside the apps, see content details, and watch trailer!