Android TV: Implement game selector activity in new Android TV UI

This commit is contained in:
sigmabeta 2015-07-14 22:35:52 -04:00 committed by sigmabeta
parent 3801f89125
commit 7c14996e3e
11 changed files with 358 additions and 30 deletions

View File

@ -1,7 +1,8 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
// Leanback support requires >22
compileSdkVersion 22
buildToolsVersion "22.0.1"
lintOptions {
@ -80,6 +81,9 @@ dependencies {
compile 'com.android.support:recyclerview-v7:22.2.0'
compile 'com.android.support:design:22.2.0'
// Android TV UI libraries.
compile 'com.android.support:leanback-v17:22.2.0'
// For showing the banner as a circle a-la Material Design Guidelines
compile 'de.hdodenhof:circleimageview:1.2.2'

View File

@ -33,11 +33,22 @@
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activities.TvMainActivity"
android:theme="@style/DolphinTvGamecube">
<!-- This intentfilter marks this Activity as the one that gets launched from Home screen. -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".activities.AddDirectoryActivity"
android:theme="@style/DolphinGamecube"

View File

@ -226,7 +226,7 @@ public final class MainActivity extends AppCompatActivity implements LoaderManag
GameProvider.URI_GAME, // URI of table to query
null, // Return all columns
GameDatabase.KEY_GAME_PLATFORM + " = ?", // Select by platform
new String[]{Integer.toString(id)}, // Platform id is Loader id minus 1
new String[]{Integer.toString(id)}, // Platform id is Loader id
GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
);

View File

@ -0,0 +1,141 @@
package org.dolphinemu.dolphinemu.activities;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.FragmentManager;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v17.leanback.app.BrowseFragment;
import android.support.v17.leanback.database.CursorMapper;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
import android.support.v17.leanback.widget.CursorObjectAdapter;
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.OnItemViewClickedListener;
import android.support.v17.leanback.widget.Presenter;
import android.support.v17.leanback.widget.Row;
import android.support.v17.leanback.widget.RowPresenter;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.adapters.GamePresenter;
import org.dolphinemu.dolphinemu.model.Game;
import org.dolphinemu.dolphinemu.model.GameDatabase;
import org.dolphinemu.dolphinemu.model.GameProvider;
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
public final class TvMainActivity extends Activity
{
protected BrowseFragment mBrowseFragment;
private ArrayObjectAdapter mRowsAdapter;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_tv_main);
final FragmentManager fragmentManager = getFragmentManager();
mBrowseFragment = (BrowseFragment) fragmentManager.findFragmentById(
R.id.fragment_game_list);
// Set display parameters for the BrowseFragment
mBrowseFragment.setHeadersState(BrowseFragment.HEADERS_ENABLED);
mBrowseFragment.setTitle(getString(R.string.app_name));
mBrowseFragment.setBadgeDrawable(getResources().getDrawable(
R.drawable.ic_launcher, null));
mBrowseFragment.setBrandColor(getResources().getColor(R.color.dolphin_blue_dark));
buildRowsAdapter();
mBrowseFragment.setOnItemViewClickedListener(
new OnItemViewClickedListener()
{
@Override
public void onItemClicked(Presenter.ViewHolder itemViewHolder, Object item, RowPresenter.ViewHolder rowViewHolder, Row row)
{
TvGameViewHolder holder = (TvGameViewHolder) itemViewHolder;
// Start the emulation activity and send the path of the clicked ISO to it.
Intent intent = new Intent(TvMainActivity.this, EmulationActivity.class);
intent.putExtra("SelectedGame", holder.path);
intent.putExtra("SelectedTitle", holder.title);
intent.putExtra("ScreenPath", holder.screenshotPath);
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
TvMainActivity.this,
holder.imageScreenshot,
"image_game_screenshot");
startActivity(intent, options.toBundle());
}
});
}
private void buildRowsAdapter()
{
mRowsAdapter = new ArrayObjectAdapter(new ListRowPresenter());
// For each row
for (int platformIndex = 0; platformIndex <= Game.PLATFORM_WII_WARE; ++platformIndex)
{
// Create an adapter for this row.
CursorObjectAdapter row = new CursorObjectAdapter(new GamePresenter(platformIndex));
// Add items to the adapter.
Cursor games = getContentResolver().query(
GameProvider.URI_GAME, // URI of table to query
null, // Return all columns
GameDatabase.KEY_GAME_PLATFORM + " = ?", // Select by platform
new String[]{Integer.toString(platformIndex)}, // Platform id
GameDatabase.KEY_GAME_TITLE + " asc" // Sort by game name, ascending order
);
row.swapCursor(games);
row.setMapper(new CursorMapper()
{
@Override
protected void bindColumns(Cursor cursor)
{
// No-op? Not sure what this does.
}
@Override
protected Object bind(Cursor cursor)
{
return Game.fromCursor(cursor);
}
});
String headerName;
switch (platformIndex)
{
case Game.PLATFORM_GC:
headerName = "GameCube Games";
break;
case Game.PLATFORM_WII:
headerName = "Wii Games";
break;
case Game.PLATFORM_WII_WARE:
headerName = "WiiWare";
break;
default:
headerName = "Error";
break;
}
// Create a header for this row.
HeaderItem header = new HeaderItem(platformIndex, headerName);
// Create the row, passing it the filled adapter and the header, and give it to the master adapter.
mRowsAdapter.add(new ListRow(header, row));
}
mBrowseFragment.setAdapter(mRowsAdapter);
}
}

View File

@ -0,0 +1,122 @@
package org.dolphinemu.dolphinemu.adapters;
import android.graphics.Bitmap;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import org.dolphinemu.dolphinemu.R;
import org.dolphinemu.dolphinemu.model.Game;
import org.dolphinemu.dolphinemu.viewholders.TvGameViewHolder;
/**
* The Leanback library / docs call this a Presenter, but it works very
* similarly to a RecyclerView.ViewHolder.
*/
public final class GamePresenter extends Presenter
{
private int mPlatform;
public GamePresenter(int platform)
{
mPlatform = platform;
}
public ViewHolder onCreateViewHolder(ViewGroup parent)
{
// Create a new view.
ImageCardView gameCard = new ImageCardView(parent.getContext())
{
@Override
public void setSelected(boolean selected)
{
setCardBackground(this, selected);
super.setSelected(selected);
}
};
gameCard.setMainImageAdjustViewBounds(true);
gameCard.setMainImageDimensions(480, 320);
gameCard.setMainImageScaleType(ImageView.ScaleType.CENTER_CROP);
gameCard.setFocusable(true);
gameCard.setFocusableInTouchMode(true);
setCardBackground(gameCard, false);
// Use that view to create a ViewHolder.
return new TvGameViewHolder(gameCard);
}
public void onBindViewHolder(ViewHolder viewHolder, Object item)
{
TvGameViewHolder holder = (TvGameViewHolder) viewHolder;
Game game = (Game) item;
String screenPath = game.getScreenshotPath();
// Fill in the view contents.
Picasso.with(holder.imageScreenshot.getContext())
.load(screenPath)
.fit()
.centerCrop()
.noFade()
.noPlaceholder()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_banner)
.into(holder.imageScreenshot);
holder.cardParent.setTitleText(game.getTitle());
holder.cardParent.setContentText(game.getCompany());
// TODO These shouldn't be necessary once the move to a DB-based model is complete.
holder.gameId = game.getGameId();
holder.path = game.getPath();
holder.title = game.getTitle();
holder.description = game.getDescription();
holder.country = game.getCountry();
holder.company = game.getCompany();
holder.screenshotPath = game.getScreenshotPath();
}
public void onUnbindViewHolder(ViewHolder viewHolder)
{
// no op
}
public void setCardBackground(ImageCardView view, boolean selected)
{
int backgroundColor;
if (selected)
{
switch (mPlatform)
{
case Game.PLATFORM_GC:
backgroundColor = R.color.dolphin_accent_gamecube;
break;
case Game.PLATFORM_WII:
backgroundColor = R.color.dolphin_accent_wii;
break;
case Game.PLATFORM_WII_WARE:
backgroundColor = R.color.dolphin_accent_wiiware;
break;
default:
backgroundColor = android.R.color.holo_red_dark;
break;
}
}
else
{
backgroundColor = R.color.tv_card_unselected;
}
view.setInfoAreaBackgroundColor(view.getResources().getColor(backgroundColor));
}
}

View File

@ -3,8 +3,6 @@ package org.dolphinemu.dolphinemu.model;
import android.content.ContentValues;
import android.database.Cursor;
import java.io.File;
public final class Game
{
public static final int PLATFORM_GC = 0;
@ -33,13 +31,13 @@ public final class Game
private String mDescription;
private String mPath;
private String mGameId;
private String mScreenshotFolderPath;
private String mScreenshotPath;
private String mCompany;
private int mPlatform;
private int mCountry;
public Game(int platform, String title, String description, int country, String path, String gameId, String company)
public Game(int platform, String title, String description, int country, String path, String gameId, String company, String screenshotPath)
{
mPlatform = platform;
mTitle = title;
@ -48,7 +46,7 @@ public final class Game
mPath = path;
mGameId = gameId;
mCompany = company;
mScreenshotFolderPath = PATH_SCREENSHOT_FOLDER + getGameId() + "/";
mScreenshotPath = screenshotPath;
}
public int getPlatform()
@ -86,27 +84,9 @@ public final class Game
return mGameId;
}
public String getScreenshotFolderPath()
public String getScreenshotPath()
{
return mScreenshotFolderPath;
}
public String getScreenPath()
{
// Count how many screenshots are available, so we can use the most recent one.
File screenshotFolder = new File(mScreenshotFolderPath.substring(mScreenshotFolderPath.indexOf('s') - 1));
int screenCount = 0;
if (screenshotFolder.isDirectory())
{
screenCount = screenshotFolder.list().length;
}
String screenPath = mScreenshotFolderPath
+ getGameId() + "-"
+ screenCount + ".png";
return screenPath;
return mScreenshotPath;
}
public static ContentValues asContentValues(int platform, String title, String description, int country, String path, String gameId, String company)
@ -135,6 +115,7 @@ public final class Game
cursor.getInt(GameDatabase.GAME_COLUMN_COUNTRY),
cursor.getString(GameDatabase.GAME_COLUMN_PATH),
cursor.getString(GameDatabase.GAME_COLUMN_GAME_ID),
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY));
cursor.getString(GameDatabase.GAME_COLUMN_COMPANY),
cursor.getString(GameDatabase.GAME_COLUMN_SCREENSHOT_PATH));
}
}

View File

@ -0,0 +1,40 @@
package org.dolphinemu.dolphinemu.viewholders;
import android.support.v17.leanback.widget.ImageCardView;
import android.support.v17.leanback.widget.Presenter;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
/**
* A simple class that stores references to views so that the GameAdapter doesn't need to
* keep calling findViewById(), which is expensive.
*/
public class TvGameViewHolder extends Presenter.ViewHolder
{
public ImageCardView cardParent;
public ImageView imageScreenshot;
public TextView textGameTitle;
public TextView textCompany;
public String gameId;
// TODO Not need any of this stuff. Currently only the properties dialog needs it.
public String path;
public String title;
public String description;
public int country;
public String company;
public String screenshotPath;
public TvGameViewHolder(View itemView)
{
super(itemView);
itemView.setTag(this);
cardParent = (ImageCardView) itemView;
imageScreenshot = cardParent.getMainImageView();
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<fragment
android:name="android.support.v17.leanback.app.BrowseFragment"
android:id="@+id/fragment_game_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</FrameLayout>

View File

@ -33,7 +33,6 @@
android:layout_marginLeft="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="16dp"
android:layout_toStartOf="@+id/button_details"
android:ellipsize="end"
android:lines="1"
android:maxLines="1"

View File

@ -8,4 +8,6 @@
<color name="dolphin_accent_gamecube">#651fff</color>
<color name="circle_grey">#bdbdbd</color>
<color name="tv_card_unselected">#444444</color>
</resources>

View File

@ -126,6 +126,21 @@
<item name="textAllCaps">false</item>
</style>
<!-- Android TV Themes -->
<style name="DolphinTvBase" parent="Theme.Leanback">
<item name="colorPrimary">@color/dolphin_blue</item>
<item name="colorPrimaryDark">@color/dolphin_blue_dark</item>
<!--enable window content transitions-->
<item name="android:windowContentTransitions">true</item>
<item name="android:windowAllowEnterTransitionOverlap">true</item>
<item name="android:windowAllowReturnTransitionOverlap">true</item>
</style>
<style name="DolphinTvGamecube" parent="DolphinTvBase">
<item name="colorAccent">@color/dolphin_accent_gamecube</item>
</style>
<style name="InGameMenuOption" parent="Widget.AppCompat.Button.Borderless">
<item name="android:textSize">16sp</item>
<item name="android:fontFamily">sans-serif-condensed</item>