From 7b45df0cc2c8a29f7335ac59064e499e70576117 Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Sat, 6 Mar 2021 14:42:40 +1000 Subject: [PATCH] Android: Move game list over to RecyclerView --- .../stenzek/duckstation/GameGridFragment.java | 1 + .../github/stenzek/duckstation/GameList.java | 40 --- .../stenzek/duckstation/GameListEntry.java | 86 +------ .../stenzek/duckstation/GameListFragment.java | 231 +++++++++++++++--- .../main/res/layout/fragment_game_list.xml | 2 +- .../res/layout/layout_game_list_entry.xml | 9 +- 6 files changed, 204 insertions(+), 165 deletions(-) diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java index 4ccb1682a..0c71a1a48 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameGridFragment.java @@ -17,6 +17,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.widget.PopupMenu; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java index f136b579b..f6d0bd8de 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameList.java @@ -19,12 +19,10 @@ public class GameList { private Activity mContext; private GameListEntry[] mEntries; - private ListViewAdapter mAdapter; private ArrayList mRefreshListeners = new ArrayList<>(); public GameList(Activity context) { mContext = context; - mAdapter = new ListViewAdapter(); mEntries = new GameListEntry[0]; } @@ -58,7 +56,6 @@ public class GameList { e.printStackTrace(); } mEntries = newEntries; - mAdapter.notifyDataSetChanged(); for (OnRefreshListener listener : mRefreshListeners) listener.onGameListRefresh(); }); @@ -72,41 +69,4 @@ public class GameList { public GameListEntry getEntry(int index) { return mEntries[index]; } - - private class ListViewAdapter extends BaseAdapter { - @Override - public int getCount() { - return mEntries.length; - } - - @Override - public Object getItem(int position) { - return mEntries[position]; - } - - @Override - public long getItemId(int position) { - return position; - } - - @Override - public int getViewTypeCount() { - return 1; - } - - @Override - public View getView(int position, View convertView, ViewGroup parent) { - if (convertView == null) { - convertView = LayoutInflater.from(mContext) - .inflate(R.layout.game_list_view_entry, parent, false); - } - - mEntries[position].fillView(convertView); - return convertView; - } - } - - public BaseAdapter getListViewAdapter() { - return mAdapter; - } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java index a450e907b..2806dc365 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListEntry.java @@ -80,6 +80,8 @@ public class GameListEntry { return mFileTitle; } + public long getSize() { return mSize; } + public String getModifiedTime() { return mModifiedTime; } @@ -105,88 +107,4 @@ public class GameListEntry { else return path; } - - private String getSubTitle() { - String fileName = getFileNameForPath(mPath); - String sizeString = String.format("%.2f MB", (double) mSize / 1048576.0); - return String.format("%s (%s)", fileName, sizeString); - } - - public void fillView(View view) { - ((TextView) view.findViewById(R.id.game_list_view_entry_title)).setText(mTitle); - ((TextView) view.findViewById(R.id.game_list_view_entry_subtitle)).setText(getSubTitle()); - - int regionDrawableId; - switch (mRegion) { - case NTSC_J: - regionDrawableId = R.drawable.flag_jp; - break; - case PAL: - regionDrawableId = R.drawable.flag_eu; - break; - case Other: - regionDrawableId = R.drawable.ic_baseline_help_24; - break; - case NTSC_U: - default: - regionDrawableId = R.drawable.flag_us; - break; - } - - ((ImageView) view.findViewById(R.id.game_list_view_entry_region_icon)) - .setImageDrawable(ContextCompat.getDrawable(view.getContext(), regionDrawableId)); - - int typeDrawableId; - switch (mType) { - case PSExe: - typeDrawableId = R.drawable.ic_emblem_system; - break; - - case Playlist: - typeDrawableId = R.drawable.ic_baseline_playlist_play_24; - break; - - case PSF: - typeDrawableId = R.drawable.ic_baseline_library_music_24; - break; - - case Disc: - default: - typeDrawableId = R.drawable.ic_media_cdrom; - break; - } - - ImageView icon = ((ImageView) view.findViewById(R.id.game_list_view_entry_type_icon)); - icon.setImageDrawable(ContextCompat.getDrawable(view.getContext(), typeDrawableId)); - - if (mCoverPath != null) { - new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, mCoverPath); - } - - int compatibilityDrawableId; - switch (mCompatibilityRating) { - case DoesntBoot: - compatibilityDrawableId = R.drawable.ic_star_1; - break; - case CrashesInIntro: - compatibilityDrawableId = R.drawable.ic_star_2; - break; - case CrashesInGame: - compatibilityDrawableId = R.drawable.ic_star_3; - break; - case GraphicalAudioIssues: - compatibilityDrawableId = R.drawable.ic_star_4; - break; - case NoIssues: - compatibilityDrawableId = R.drawable.ic_star_5; - break; - case Unknown: - default: - compatibilityDrawableId = R.drawable.ic_star_0; - break; - } - - ((ImageView) view.findViewById(R.id.game_list_view_compatibility_icon)) - .setImageDrawable(ContextCompat.getDrawable(view.getContext(), compatibilityDrawableId)); - } } diff --git a/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java b/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java index 394fea861..21f02f5a3 100644 --- a/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java +++ b/android/app/src/main/java/com/github/stenzek/duckstation/GameListFragment.java @@ -1,20 +1,29 @@ package com.github.stenzek.duckstation; +import android.os.AsyncTask; import android.os.Bundle; import android.view.Gravity; +import android.view.LayoutInflater; import android.view.MenuItem; import android.view.View; +import android.view.ViewGroup; import android.widget.AdapterView; -import android.widget.ListView; +import android.widget.ImageView; import android.widget.PopupMenu; +import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; -public class GameListFragment extends Fragment { +public class GameListFragment extends Fragment implements GameList.OnRefreshListener { private MainActivity mParent; - private ListView mGameListView; + private RecyclerView mRecyclerView; + private GameListFragment.ViewAdapter mAdapter; public GameListFragment(MainActivity parent) { super(R.layout.fragment_game_list); @@ -29,40 +38,190 @@ public class GameListFragment extends Fragment { public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - mGameListView = view.findViewById(R.id.game_list_view); - mGameListView.setAdapter(getGameList().getListViewAdapter()); - mGameListView.setOnItemClickListener(new AdapterView.OnItemClickListener() { - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - mParent.startEmulation(getGameList().getEntry(position).getPath(), mParent.shouldResumeStateByDefault()); + mAdapter = new GameListFragment.ViewAdapter(mParent, getGameList()); + getGameList().addRefreshListener(this); + + mRecyclerView = view.findViewById(R.id.game_list_view); + mRecyclerView.setAdapter(mAdapter); + mRecyclerView.setLayoutManager(new LinearLayoutManager(mParent)); + mRecyclerView.addItemDecoration(new DividerItemDecoration(mRecyclerView.getContext(), + DividerItemDecoration.VERTICAL)); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + + getGameList().removeRefreshListener(this); + } + + @Override + public void onGameListRefresh() { + mAdapter.notifyDataSetChanged(); + } + + private static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, View.OnLongClickListener { + private MainActivity mParent; + private View mItemView; + private GameListEntry mEntry; + + public ViewHolder(@NonNull MainActivity parent, @NonNull View itemView) { + super(itemView); + mParent = parent; + mItemView = itemView; + mItemView.setOnClickListener(this); + mItemView.setOnLongClickListener(this); + } + + private String getSubTitle() { + String fileName = GameListEntry.getFileNameForPath(mEntry.getPath()); + String sizeString = String.format("%.2f MB", (double) mEntry.getSize() / 1048576.0); + return String.format("%s (%s)", fileName, sizeString); + } + + public void bindToEntry(GameListEntry entry) { + mEntry = entry; + + ((TextView) mItemView.findViewById(R.id.game_list_view_entry_title)).setText(entry.getTitle()); + ((TextView) mItemView.findViewById(R.id.game_list_view_entry_subtitle)).setText(getSubTitle()); + + int regionDrawableId; + switch (entry.getRegion()) { + case NTSC_J: + regionDrawableId = R.drawable.flag_jp; + break; + case PAL: + regionDrawableId = R.drawable.flag_eu; + break; + case Other: + regionDrawableId = R.drawable.ic_baseline_help_24; + break; + case NTSC_U: + default: + regionDrawableId = R.drawable.flag_us; + break; } - }); - mGameListView.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { - @Override - public boolean onItemLongClick(AdapterView parent, View view, int position, - long id) { - PopupMenu menu = new PopupMenu(getContext(), view, Gravity.RIGHT | Gravity.TOP); - menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu()); - menu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() { - @Override - public boolean onMenuItemClick(MenuItem item) { - int id = item.getItemId(); - if (id == R.id.game_list_entry_menu_start_game) { - mParent.startEmulation(getGameList().getEntry(position).getPath(), false); - return true; - } else if (id == R.id.game_list_entry_menu_resume_game) { - mParent.startEmulation(getGameList().getEntry(position).getPath(), true); - return true; - } else if (id == R.id.game_list_entry_menu_properties) { - mParent.openGameProperties(getGameList().getEntry(position).getPath()); - return true; - } - return false; + + ((ImageView) mItemView.findViewById(R.id.game_list_view_entry_region_icon)) + .setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), regionDrawableId)); + + int typeDrawableId; + switch (entry.getType()) { + case PSExe: + typeDrawableId = R.drawable.ic_emblem_system; + break; + + case Playlist: + typeDrawableId = R.drawable.ic_baseline_playlist_play_24; + break; + + case PSF: + typeDrawableId = R.drawable.ic_baseline_library_music_24; + break; + + case Disc: + default: + typeDrawableId = R.drawable.ic_media_cdrom; + break; + } + + ImageView icon = ((ImageView) mItemView.findViewById(R.id.game_list_view_entry_type_icon)); + icon.setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), typeDrawableId)); + + final String coverPath = entry.getCoverPath(); + if (coverPath != null) { + new ImageLoadTask(icon).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, coverPath); + } + + int compatibilityDrawableId; + switch (entry.getCompatibilityRating()) { + case DoesntBoot: + compatibilityDrawableId = R.drawable.ic_star_1; + break; + case CrashesInIntro: + compatibilityDrawableId = R.drawable.ic_star_2; + break; + case CrashesInGame: + compatibilityDrawableId = R.drawable.ic_star_3; + break; + case GraphicalAudioIssues: + compatibilityDrawableId = R.drawable.ic_star_4; + break; + case NoIssues: + compatibilityDrawableId = R.drawable.ic_star_5; + break; + case Unknown: + default: + compatibilityDrawableId = R.drawable.ic_star_0; + break; + } + + ((ImageView) mItemView.findViewById(R.id.game_list_view_compatibility_icon)) + .setImageDrawable(ContextCompat.getDrawable(mItemView.getContext(), compatibilityDrawableId)); + } + + @Override + public void onClick(View v) { + mParent.startEmulation(mEntry.getPath(), mParent.shouldResumeStateByDefault()); + } + + @Override + public boolean onLongClick(View v) { + androidx.appcompat.widget.PopupMenu menu = new androidx.appcompat.widget.PopupMenu(mParent, v, Gravity.RIGHT | Gravity.TOP); + menu.getMenuInflater().inflate(R.menu.menu_game_list_entry, menu.getMenu()); + menu.setOnMenuItemClickListener(new androidx.appcompat.widget.PopupMenu.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.game_list_entry_menu_start_game) { + mParent.startEmulation(mEntry.getPath(), false); + return true; + } else if (id == R.id.game_list_entry_menu_resume_game) { + mParent.startEmulation(mEntry.getPath(), true); + return true; + } else if (id == R.id.game_list_entry_menu_properties) { + mParent.openGameProperties(mEntry.getPath()); + return true; } - }); - menu.show(); - return true; - } - }); + return false; + } + }); + menu.show(); + return true; + } + } + + private static class ViewAdapter extends RecyclerView.Adapter { + private MainActivity mParent; + private LayoutInflater mInflater; + private GameList mGameList; + + public ViewAdapter(@NonNull MainActivity parent, @NonNull GameList gameList) { + mParent = parent; + mInflater = LayoutInflater.from(parent); + mGameList = gameList; + } + + @NonNull + @Override + public GameListFragment.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new GameListFragment.ViewHolder(mParent, mInflater.inflate(R.layout.layout_game_list_entry, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull GameListFragment.ViewHolder holder, int position) { + GameListEntry entry = mGameList.getEntry(position); + holder.bindToEntry(entry); + } + + @Override + public int getItemCount() { + return mGameList.getEntryCount(); + } + + @Override + public int getItemViewType(int position) { + return R.layout.layout_game_list_entry; + } } } diff --git a/android/app/src/main/res/layout/fragment_game_list.xml b/android/app/src/main/res/layout/fragment_game_list.xml index 627c0178b..6d8d8f069 100644 --- a/android/app/src/main/res/layout/fragment_game_list.xml +++ b/android/app/src/main/res/layout/fragment_game_list.xml @@ -5,7 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:background="?android:attr/selectableItemBackground"> @@ -21,7 +24,6 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="10dp" - android:layout_marginTop="8dp" android:layout_marginRight="80dp" android:focusable="false" android:focusableInTouchMode="false" @@ -50,7 +52,6 @@ android:id="@+id/game_list_view_compatibility_icon" android:layout_width="64dp" android:layout_height="16dp" - android:layout_marginTop="8dp" android:layout_marginEnd="8dp" android:focusable="false" android:focusableInTouchMode="false"