committed by
bunnei
2 changed files with 178 additions and 244 deletions
-
244src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.java
-
178src/android/app/src/main/java/org/yuzu/yuzu_emu/adapters/GameAdapter.kt
@ -1,244 +0,0 @@ |
|||
package org.yuzu.yuzu_emu.adapters; |
|||
|
|||
import android.database.Cursor; |
|||
import android.database.DataSetObserver; |
|||
import android.graphics.Rect; |
|||
import android.graphics.drawable.Drawable; |
|||
import android.os.Build; |
|||
import android.os.SystemClock; |
|||
import android.view.LayoutInflater; |
|||
import android.view.View; |
|||
import android.view.ViewGroup; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.annotation.RequiresApi; |
|||
import androidx.core.content.ContextCompat; |
|||
import androidx.fragment.app.FragmentActivity; |
|||
import androidx.recyclerview.widget.RecyclerView; |
|||
|
|||
import org.yuzu.yuzu_emu.YuzuApplication; |
|||
import org.yuzu.yuzu_emu.R; |
|||
import org.yuzu.yuzu_emu.activities.EmulationActivity; |
|||
import org.yuzu.yuzu_emu.model.GameDatabase; |
|||
import org.yuzu.yuzu_emu.ui.DividerItemDecoration; |
|||
import org.yuzu.yuzu_emu.utils.FileUtil; |
|||
import org.yuzu.yuzu_emu.utils.Log; |
|||
import org.yuzu.yuzu_emu.utils.PicassoUtils; |
|||
import org.yuzu.yuzu_emu.viewholders.GameViewHolder; |
|||
|
|||
import java.util.stream.Stream; |
|||
|
|||
/** |
|||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of |
|||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) |
|||
* large dataset. |
|||
*/ |
|||
public final class GameAdapter extends RecyclerView.Adapter<GameViewHolder> implements |
|||
View.OnClickListener { |
|||
private Cursor mCursor; |
|||
private GameDataSetObserver mObserver; |
|||
|
|||
private boolean mDatasetValid; |
|||
private long mLastClickTime = 0; |
|||
|
|||
/** |
|||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will |
|||
* display no data until a Cursor is supplied by a CursorLoader. |
|||
*/ |
|||
public GameAdapter() { |
|||
mDatasetValid = false; |
|||
mObserver = new GameDataSetObserver(); |
|||
} |
|||
|
|||
/** |
|||
* Called by the LayoutManager when it is necessary to create a new view. |
|||
* |
|||
* @param parent The RecyclerView (I think?) the created view will be thrown into. |
|||
* @param viewType Not used here, but useful when more than one type of child will be used in the RecyclerView. |
|||
* @return The created ViewHolder with references to all the child view's members. |
|||
*/ |
|||
@Override |
|||
public GameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
|||
// Create a new view. |
|||
View gameCard = LayoutInflater.from(parent.getContext()) |
|||
.inflate(R.layout.card_game, parent, false); |
|||
|
|||
gameCard.setOnClickListener(this); |
|||
|
|||
// Use that view to create a ViewHolder. |
|||
return new GameViewHolder(gameCard); |
|||
} |
|||
|
|||
/** |
|||
* Called by the LayoutManager when a new view is not necessary because we can recycle |
|||
* an existing one (for example, if a view just scrolled onto the screen from the bottom, we |
|||
* can use the view that just scrolled off the top instead of inflating a new one.) |
|||
* |
|||
* @param holder A ViewHolder representing the view we're recycling. |
|||
* @param position The position of the 'new' view in the dataset. |
|||
*/ |
|||
@RequiresApi(api = Build.VERSION_CODES.O) |
|||
@Override |
|||
public void onBindViewHolder(@NonNull GameViewHolder holder, int position) { |
|||
if (mDatasetValid) { |
|||
if (mCursor.moveToPosition(position)) { |
|||
PicassoUtils.loadGameIcon(holder.imageIcon, |
|||
mCursor.getString(GameDatabase.GAME_COLUMN_PATH)); |
|||
|
|||
holder.textGameTitle.setText(mCursor.getString(GameDatabase.GAME_COLUMN_TITLE).replaceAll("[\\t\\n\\r]+", " ")); |
|||
holder.textGameCaption.setText(mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION)); |
|||
|
|||
// TODO These shouldn't be necessary once the move to a DB-based model is complete. |
|||
holder.gameId = mCursor.getString(GameDatabase.GAME_COLUMN_GAME_ID); |
|||
holder.path = mCursor.getString(GameDatabase.GAME_COLUMN_PATH); |
|||
holder.title = mCursor.getString(GameDatabase.GAME_COLUMN_TITLE); |
|||
holder.description = mCursor.getString(GameDatabase.GAME_COLUMN_DESCRIPTION); |
|||
holder.regions = mCursor.getString(GameDatabase.GAME_COLUMN_REGIONS); |
|||
holder.company = mCursor.getString(GameDatabase.GAME_COLUMN_CAPTION); |
|||
|
|||
final int backgroundColorId = isValidGame(holder.path) ? R.color.view_background : R.color.view_disabled; |
|||
View itemView = holder.getItemView(); |
|||
itemView.setBackgroundColor(ContextCompat.getColor(itemView.getContext(), backgroundColorId)); |
|||
} else { |
|||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid."); |
|||
} |
|||
} else { |
|||
Log.error("[GameAdapter] Can't bind view; dataset is not valid."); |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Called by the LayoutManager to find out how much data we have. |
|||
* |
|||
* @return Size of the dataset. |
|||
*/ |
|||
@Override |
|||
public int getItemCount() { |
|||
if (mDatasetValid && mCursor != null) { |
|||
return mCursor.getCount(); |
|||
} |
|||
Log.error("[GameAdapter] Dataset is not valid."); |
|||
return 0; |
|||
} |
|||
|
|||
/** |
|||
* Return the contents of the _id column for a given row. |
|||
* |
|||
* @param position The row for which Android wants an ID. |
|||
* @return A valid ID from the database, or 0 if not available. |
|||
*/ |
|||
@Override |
|||
public long getItemId(int position) { |
|||
if (mDatasetValid && mCursor != null) { |
|||
if (mCursor.moveToPosition(position)) { |
|||
return mCursor.getLong(GameDatabase.COLUMN_DB_ID); |
|||
} |
|||
} |
|||
|
|||
Log.error("[GameAdapter] Dataset is not valid."); |
|||
return 0; |
|||
} |
|||
|
|||
/** |
|||
* Tell Android whether or not each item in the dataset has a stable identifier. |
|||
* Which it does, because it's a database, so always tell Android 'true'. |
|||
* |
|||
* @param hasStableIds ignored. |
|||
*/ |
|||
@Override |
|||
public void setHasStableIds(boolean hasStableIds) { |
|||
super.setHasStableIds(true); |
|||
} |
|||
|
|||
/** |
|||
* When a load is finished, call this to replace the existing data with the newly-loaded |
|||
* data. |
|||
* |
|||
* @param cursor The newly-loaded Cursor. |
|||
*/ |
|||
public void swapCursor(Cursor cursor) { |
|||
// Sanity check. |
|||
if (cursor == mCursor) { |
|||
return; |
|||
} |
|||
|
|||
// Before getting rid of the old cursor, disassociate it from the Observer. |
|||
final Cursor oldCursor = mCursor; |
|||
if (oldCursor != null && mObserver != null) { |
|||
oldCursor.unregisterDataSetObserver(mObserver); |
|||
} |
|||
|
|||
mCursor = cursor; |
|||
if (mCursor != null) { |
|||
// Attempt to associate the new Cursor with the Observer. |
|||
if (mObserver != null) { |
|||
mCursor.registerDataSetObserver(mObserver); |
|||
} |
|||
|
|||
mDatasetValid = true; |
|||
} else { |
|||
mDatasetValid = false; |
|||
} |
|||
|
|||
notifyDataSetChanged(); |
|||
} |
|||
|
|||
/** |
|||
* Launches the game that was clicked on. |
|||
* |
|||
* @param view The card representing the game the user wants to play. |
|||
*/ |
|||
@Override |
|||
public void onClick(View view) { |
|||
// Double-click prevention, using threshold of 1000 ms |
|||
if (SystemClock.elapsedRealtime() - mLastClickTime < 1000) { |
|||
return; |
|||
} |
|||
mLastClickTime = SystemClock.elapsedRealtime(); |
|||
|
|||
GameViewHolder holder = (GameViewHolder) view.getTag(); |
|||
|
|||
EmulationActivity.launch((FragmentActivity) view.getContext(), holder.path, holder.title); |
|||
} |
|||
|
|||
public static class SpacesItemDecoration extends DividerItemDecoration { |
|||
private int space; |
|||
|
|||
public SpacesItemDecoration(Drawable divider, int space) { |
|||
super(divider); |
|||
this.space = space; |
|||
} |
|||
|
|||
@Override |
|||
public void getItemOffsets(Rect outRect, @NonNull View view, @NonNull RecyclerView parent, |
|||
@NonNull RecyclerView.State state) { |
|||
outRect.left = 0; |
|||
outRect.right = 0; |
|||
outRect.bottom = space; |
|||
outRect.top = 0; |
|||
} |
|||
} |
|||
|
|||
private boolean isValidGame(String path) { |
|||
return Stream.of( |
|||
".rar", ".zip", ".7z", ".torrent", ".tar", ".gz").noneMatch(suffix -> path.toLowerCase().endsWith(suffix)); |
|||
} |
|||
|
|||
private final class GameDataSetObserver extends DataSetObserver { |
|||
@Override |
|||
public void onChanged() { |
|||
super.onChanged(); |
|||
|
|||
mDatasetValid = true; |
|||
notifyDataSetChanged(); |
|||
} |
|||
|
|||
@Override |
|||
public void onInvalidated() { |
|||
super.onInvalidated(); |
|||
|
|||
mDatasetValid = false; |
|||
notifyDataSetChanged(); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,178 @@ |
|||
package org.yuzu.yuzu_emu.adapters |
|||
|
|||
import android.database.Cursor |
|||
import android.database.DataSetObserver |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.core.content.ContextCompat |
|||
import androidx.fragment.app.FragmentActivity |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import org.yuzu.yuzu_emu.R |
|||
import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch |
|||
import org.yuzu.yuzu_emu.model.GameDatabase |
|||
import org.yuzu.yuzu_emu.utils.Log |
|||
import org.yuzu.yuzu_emu.utils.PicassoUtils |
|||
import org.yuzu.yuzu_emu.viewholders.GameViewHolder |
|||
import java.util.* |
|||
import java.util.stream.Stream |
|||
|
|||
/** |
|||
* This adapter gets its information from a database Cursor. This fact, paired with the usage of |
|||
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) |
|||
* large dataset. |
|||
*/ |
|||
class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener { |
|||
private var cursor: Cursor? = null |
|||
private val observer: GameDataSetObserver? |
|||
private var isDatasetValid = false |
|||
|
|||
/** |
|||
* Initializes the adapter's observer, which watches for changes to the dataset. The adapter will |
|||
* display no data until a Cursor is supplied by a CursorLoader. |
|||
*/ |
|||
init { |
|||
observer = GameDataSetObserver() |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): GameViewHolder { |
|||
// Create a new view. |
|||
val gameCard = LayoutInflater.from(parent.context) |
|||
.inflate(R.layout.card_game, parent, false) |
|||
gameCard.setOnClickListener(this) |
|||
|
|||
// Use that view to create a ViewHolder. |
|||
return GameViewHolder(gameCard) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: GameViewHolder, position: Int) { |
|||
if (isDatasetValid) { |
|||
if (cursor!!.moveToPosition(position)) { |
|||
PicassoUtils.loadGameIcon( |
|||
holder.imageIcon, |
|||
cursor!!.getString(GameDatabase.GAME_COLUMN_PATH) |
|||
) |
|||
holder.textGameTitle.text = |
|||
cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE) |
|||
.replace("[\\t\\n\\r]+".toRegex(), " ") |
|||
holder.textGameCaption.text = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION) |
|||
|
|||
// TODO These shouldn't be necessary once the move to a DB-based model is complete. |
|||
holder.gameId = cursor!!.getString(GameDatabase.GAME_COLUMN_GAME_ID) |
|||
holder.path = cursor!!.getString(GameDatabase.GAME_COLUMN_PATH) |
|||
holder.title = cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE) |
|||
holder.description = cursor!!.getString(GameDatabase.GAME_COLUMN_DESCRIPTION) |
|||
holder.regions = cursor!!.getString(GameDatabase.GAME_COLUMN_REGIONS) |
|||
holder.company = cursor!!.getString(GameDatabase.GAME_COLUMN_CAPTION) |
|||
val backgroundColorId = |
|||
if (isValidGame(holder.path!!)) R.color.view_background else R.color.view_disabled |
|||
val itemView = holder.itemView |
|||
itemView.setBackgroundColor( |
|||
ContextCompat.getColor( |
|||
itemView.context, |
|||
backgroundColorId |
|||
) |
|||
) |
|||
} else { |
|||
Log.error("[GameAdapter] Can't bind view; Cursor is not valid.") |
|||
} |
|||
} else { |
|||
Log.error("[GameAdapter] Can't bind view; dataset is not valid.") |
|||
} |
|||
} |
|||
|
|||
override fun getItemCount(): Int { |
|||
if (isDatasetValid && cursor != null) { |
|||
return cursor!!.count |
|||
} |
|||
Log.error("[GameAdapter] Dataset is not valid.") |
|||
return 0 |
|||
} |
|||
|
|||
/** |
|||
* Return the contents of the _id column for a given row. |
|||
* |
|||
* @param position The row for which Android wants an ID. |
|||
* @return A valid ID from the database, or 0 if not available. |
|||
*/ |
|||
override fun getItemId(position: Int): Long { |
|||
if (isDatasetValid && cursor != null) { |
|||
if (cursor!!.moveToPosition(position)) { |
|||
return cursor!!.getLong(GameDatabase.COLUMN_DB_ID) |
|||
} |
|||
} |
|||
Log.error("[GameAdapter] Dataset is not valid.") |
|||
return 0 |
|||
} |
|||
|
|||
/** |
|||
* Tell Android whether or not each item in the dataset has a stable identifier. |
|||
* Which it does, because it's a database, so always tell Android 'true'. |
|||
* |
|||
* @param hasStableIds ignored. |
|||
*/ |
|||
override fun setHasStableIds(hasStableIds: Boolean) { |
|||
super.setHasStableIds(true) |
|||
} |
|||
|
|||
/** |
|||
* When a load is finished, call this to replace the existing data with the newly-loaded |
|||
* data. |
|||
* |
|||
* @param cursor The newly-loaded Cursor. |
|||
*/ |
|||
fun swapCursor(cursor: Cursor) { |
|||
// Sanity check. |
|||
if (cursor === this.cursor) { |
|||
return |
|||
} |
|||
|
|||
// Before getting rid of the old cursor, disassociate it from the Observer. |
|||
val oldCursor = this.cursor |
|||
if (oldCursor != null && observer != null) { |
|||
oldCursor.unregisterDataSetObserver(observer) |
|||
} |
|||
this.cursor = cursor |
|||
isDatasetValid = if (this.cursor != null) { |
|||
// Attempt to associate the new Cursor with the Observer. |
|||
if (observer != null) { |
|||
this.cursor!!.registerDataSetObserver(observer) |
|||
} |
|||
true |
|||
} else { |
|||
false |
|||
} |
|||
notifyDataSetChanged() |
|||
} |
|||
|
|||
/** |
|||
* Launches the game that was clicked on. |
|||
* |
|||
* @param view The card representing the game the user wants to play. |
|||
*/ |
|||
override fun onClick(view: View) { |
|||
val holder = view.tag as GameViewHolder |
|||
launch((view.context as FragmentActivity), holder.path, holder.title) |
|||
} |
|||
|
|||
private fun isValidGame(path: String): Boolean { |
|||
return Stream.of(".rar", ".zip", ".7z", ".torrent", ".tar", ".gz") |
|||
.noneMatch { suffix: String? -> |
|||
path.lowercase(Locale.getDefault()).endsWith(suffix!!) |
|||
} |
|||
} |
|||
|
|||
private inner class GameDataSetObserver : DataSetObserver() { |
|||
override fun onChanged() { |
|||
super.onChanged() |
|||
isDatasetValid = true |
|||
notifyDataSetChanged() |
|||
} |
|||
|
|||
override fun onInvalidated() { |
|||
super.onInvalidated() |
|||
isDatasetValid = false |
|||
notifyDataSetChanged() |
|||
} |
|||
} |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue