android: Replace Picasso with Coil

master
Charles Lombardo 2023-03-14 20:23:00 +07:00 committed by bunnei
parent 37cc94526b
commit 3fcc6b1104
7 changed files with 41 additions and 138 deletions

@ -135,9 +135,7 @@ dependencies {
implementation 'com.google.android.material:material:1.8.0' implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.preference:preference:1.2.0' implementation 'androidx.preference:preference:1.2.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1"
implementation "io.coil-kt:coil:2.2.2"
// For loading huge screenshots from the disk.
implementation 'com.squareup.picasso:picasso:2.71828'
// Allows FRP-style asynchronous operations in Android. // Allows FRP-style asynchronous operations in Android.
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'

@ -5,17 +5,27 @@ package org.yuzu.yuzu_emu.adapters
import android.database.Cursor import android.database.Cursor
import android.database.DataSetObserver import android.database.DataSetObserver
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.FragmentActivity import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import coil.load
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.yuzu.yuzu_emu.NativeLibrary
import org.yuzu.yuzu_emu.R import org.yuzu.yuzu_emu.R
import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch import org.yuzu.yuzu_emu.activities.EmulationActivity.Companion.launch
import org.yuzu.yuzu_emu.model.GameDatabase import org.yuzu.yuzu_emu.model.GameDatabase
import org.yuzu.yuzu_emu.utils.Log import org.yuzu.yuzu_emu.utils.Log
import org.yuzu.yuzu_emu.utils.PicassoUtils
import org.yuzu.yuzu_emu.viewholders.GameViewHolder import org.yuzu.yuzu_emu.viewholders.GameViewHolder
import java.util.* import java.util.*
import java.util.stream.Stream import java.util.stream.Stream
@ -25,7 +35,8 @@ import java.util.stream.Stream
* ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly) * ContentProviders and Loaders, allows for efficient display of a limited view into a (possibly)
* large dataset. * large dataset.
*/ */
class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener { class GameAdapter(private val activity: AppCompatActivity) : RecyclerView.Adapter<GameViewHolder>(),
View.OnClickListener {
private var cursor: Cursor? = null private var cursor: Cursor? = null
private val observer: GameDataSetObserver? private val observer: GameDataSetObserver?
private var isDatasetValid = false private var isDatasetValid = false
@ -51,10 +62,21 @@ class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener
override fun onBindViewHolder(holder: GameViewHolder, position: Int) { override fun onBindViewHolder(holder: GameViewHolder, position: Int) {
if (isDatasetValid) { if (isDatasetValid) {
if (cursor!!.moveToPosition(position)) { if (cursor!!.moveToPosition(position)) {
PicassoUtils.loadGameIcon( holder.imageIcon.scaleType = ImageView.ScaleType.CENTER_CROP
holder.imageIcon, activity.lifecycleScope.launch {
cursor!!.getString(GameDatabase.GAME_COLUMN_PATH) withContext(Dispatchers.IO) {
) val uri =
Uri.parse(cursor!!.getString(GameDatabase.GAME_COLUMN_PATH)).toString()
val bitmap = decodeGameIcon(uri)
withContext(Dispatchers.Main) {
holder.imageIcon.load(bitmap) {
error(R.drawable.no_icon)
crossfade(true)
}
}
}
}
holder.textGameTitle.text = holder.textGameTitle.text =
cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE) cursor!!.getString(GameDatabase.GAME_COLUMN_TITLE)
.replace("[\\t\\n\\r]+".toRegex(), " ") .replace("[\\t\\n\\r]+".toRegex(), " ")
@ -165,6 +187,16 @@ class GameAdapter : RecyclerView.Adapter<GameViewHolder>(), View.OnClickListener
} }
} }
private fun decodeGameIcon(uri: String): Bitmap {
val data = NativeLibrary.GetIcon(uri)
return BitmapFactory.decodeByteArray(
data,
0,
data.size,
BitmapFactory.Options()
)
}
private inner class GameDataSetObserver : DataSetObserver() { private inner class GameDataSetObserver : DataSetObserver() {
override fun onChanged() { override fun onChanged() {
super.onChanged() super.onChanged()

@ -57,7 +57,6 @@ class MainActivity : AppCompatActivity(), MainView {
PlatformGamesFragment.TAG PlatformGamesFragment.TAG
) as PlatformGamesFragment? ) as PlatformGamesFragment?
} }
PicassoUtils.init()
// Dismiss previous notifications (should not happen unless a crash occurred) // Dismiss previous notifications (should not happen unless a crash occurred)
EmulationActivity.tryDismissRunningNotification(this) EmulationActivity.tryDismissRunningNotification(this)

@ -10,6 +10,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.ViewTreeObserver.OnGlobalLayoutListener
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -40,7 +41,7 @@ class PlatformGamesFragment : Fragment(), PlatformGamesView {
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
adapter = GameAdapter() adapter = GameAdapter(requireActivity() as AppCompatActivity)
// Organize our grid layout based on the current view. // Organize our grid layout based on the current view.
if (isAdded) { if (isAdded) {

@ -1,25 +0,0 @@
// SPDX-FileCopyrightText: 2023 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
package org.yuzu.yuzu_emu.utils
import android.graphics.BitmapFactory
import com.squareup.picasso.Picasso
import com.squareup.picasso.Request
import com.squareup.picasso.RequestHandler
import org.yuzu.yuzu_emu.NativeLibrary
class GameIconRequestHandler : RequestHandler() {
override fun canHandleRequest(data: Request): Boolean {
return "content" == data.uri.scheme
}
override fun load(request: Request, networkPolicy: Int): Result {
val gamePath = request.uri.toString()
val data = NativeLibrary.GetIcon(gamePath)
val options = BitmapFactory.Options()
options.inMutable = true
val bitmap = BitmapFactory.decodeByteArray(data, 0, data.size, options)
return Result(bitmap, Picasso.LoadedFrom.DISK)
}
}

@ -1,45 +0,0 @@
package org.yuzu.yuzu_emu.utils;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import com.squareup.picasso.Transformation;
public class PicassoRoundedCornersTransformation implements Transformation {
@Override
public Bitmap transform(Bitmap icon) {
final int width = icon.getWidth();
final int height = icon.getHeight();
final Rect rect = new Rect(0, 0, width, height);
final int size = Math.min(width, height);
final int x = (width - size) / 2;
final int y = (height - size) / 2;
Bitmap squaredBitmap = Bitmap.createBitmap(icon, x, y, size, size);
if (squaredBitmap != icon) {
icon.recycle();
}
Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(output);
BitmapShader shader = new BitmapShader(squaredBitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP);
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setShader(shader);
canvas.drawRoundRect(new RectF(rect), 10, 10, paint);
squaredBitmap.recycle();
return output;
}
@Override
public String key() {
return "circle";
}
}

@ -1,57 +0,0 @@
package org.yuzu.yuzu_emu.utils;
import android.graphics.Bitmap;
import android.net.Uri;
import android.widget.ImageView;
import com.squareup.picasso.Picasso;
import org.yuzu.yuzu_emu.YuzuApplication;
import org.yuzu.yuzu_emu.R;
import java.io.IOException;
import androidx.annotation.Nullable;
public class PicassoUtils {
private static boolean mPicassoInitialized = false;
public static void init() {
if (mPicassoInitialized) {
return;
}
Picasso picassoInstance = new Picasso.Builder(YuzuApplication.getAppContext())
.addRequestHandler(new GameIconRequestHandler())
.build();
Picasso.setSingletonInstance(picassoInstance);
mPicassoInitialized = true;
}
public static void loadGameIcon(ImageView imageView, String gamePath) {
Picasso
.get()
.load(Uri.parse(gamePath))
.fit()
.centerInside()
.config(Bitmap.Config.RGB_565)
.error(R.drawable.no_icon)
.transform(new PicassoRoundedCornersTransformation())
.into(imageView);
}
// Blocking call. Load image from file and crop/resize it to fit in width x height.
@Nullable
public static Bitmap LoadBitmapFromFile(String uri, int width, int height) {
try {
return Picasso.get()
.load(Uri.parse(uri))
.config(Bitmap.Config.ARGB_8888)
.centerCrop()
.resize(width, height)
.get();
} catch (IOException e) {
return null;
}
}
}