From c3e54d1abf1ae7af20af9e244177638c5e374ac4 Mon Sep 17 00:00:00 2001 From: Willi Ye Date: Fri, 17 Jul 2020 17:53:44 +0200 Subject: [PATCH] Redesign cards in grid view * Refactor some classes and clean up * Refresh style on the fly --- app/src/main/java/emu/skyline/AppDialog.kt | 57 +++-- app/src/main/java/emu/skyline/MainActivity.kt | 125 +++++------ .../java/emu/skyline/adapter/AppAdapter.kt | 197 ++++++++---------- .../java/emu/skyline/adapter/HeaderAdapter.kt | 155 +++++++------- .../java/emu/skyline/adapter/LogAdapter.kt | 53 +++-- app/src/main/java/emu/skyline/data/AppItem.kt | 52 +++++ .../main/java/emu/skyline/data/BaseItem.kt | 19 ++ .../main/java/emu/skyline/loader/RomFile.kt | 15 +- .../main/res/drawable/background_gradient.xml | 9 + app/src/main/res/layout/app_item_grid.xml | 35 ++-- .../main/res/layout/app_item_grid_compact.xml | 81 +++++++ app/src/main/res/layout/app_item_linear.xml | 35 ++-- app/src/main/res/values/array.xml | 2 + app/src/main/res/values/dimens.xml | 5 + 14 files changed, 479 insertions(+), 361 deletions(-) create mode 100644 app/src/main/java/emu/skyline/data/AppItem.kt create mode 100644 app/src/main/java/emu/skyline/data/BaseItem.kt create mode 100644 app/src/main/res/drawable/background_gradient.xml create mode 100644 app/src/main/res/layout/app_item_grid_compact.xml create mode 100644 app/src/main/res/values/dimens.xml diff --git a/app/src/main/java/emu/skyline/AppDialog.kt b/app/src/main/java/emu/skyline/AppDialog.kt index 3451bca4..a8e56e20 100644 --- a/app/src/main/java/emu/skyline/AppDialog.kt +++ b/app/src/main/java/emu/skyline/AppDialog.kt @@ -15,10 +15,11 @@ import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment -import emu.skyline.adapter.AppItem +import emu.skyline.data.AppItem import kotlinx.android.synthetic.main.app_dialog.* /** @@ -26,7 +27,7 @@ import kotlinx.android.synthetic.main.app_dialog.* * * @param item This is used to hold the [AppItem] between instances */ -class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() { +class AppDialog(val item : AppItem) : BottomSheetDialogFragment() { /** * This inflates the layout of the dialog after initial view creation @@ -47,10 +48,9 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() { dialog?.setOnKeyListener { _, keyCode, event -> if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_DOWN) { dialog?.onBackPressed() - true - } else { - false + return@setOnKeyListener true } + false } } @@ -60,38 +60,35 @@ class AppDialog(val item : AppItem? = null) : BottomSheetDialogFragment() { override fun onActivityCreated(savedInstanceState : Bundle?) { super.onActivityCreated(savedInstanceState) - if (item is AppItem) { - val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context?.theme)?.toBitmap(256, 256) + val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256) - game_icon.setImageBitmap(item.icon ?: missingIcon) - game_title.text = item.title - game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing) + game_icon.setImageBitmap(item.icon ?: missingIcon) + game_title.text = item.title + game_subtitle.text = item.subTitle ?: getString(R.string.metadata_missing) - game_play.setOnClickListener { - val intent = Intent(activity, EmulationActivity::class.java) - intent.data = item.uri + game_play.setOnClickListener { + val intent = Intent(activity, EmulationActivity::class.java) + intent.data = item.uri - startActivity(intent) - } + startActivity(intent) + } - val shortcutManager = activity?.getSystemService(ShortcutManager::class.java)!! - game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported + val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java) + game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported - game_pin.setOnClickListener { - val info = ShortcutInfo.Builder(context, item.title) - info.setShortLabel(item.meta.name) - info.setActivity(ComponentName(context!!, EmulationActivity::class.java)) - info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon)) + game_pin.setOnClickListener { + val info = ShortcutInfo.Builder(context, item.title) + info.setShortLabel(item.meta.name) + info.setActivity(ComponentName(requireActivity(), EmulationActivity::class.java)) + info.setIcon(Icon.createWithAdaptiveBitmap(item.icon ?: missingIcon)) - val intent = Intent(context, EmulationActivity::class.java) - intent.data = item.uri - intent.action = Intent.ACTION_VIEW + val intent = Intent(context, EmulationActivity::class.java) + intent.data = item.uri + intent.action = Intent.ACTION_VIEW - info.setIntent(intent) + info.setIntent(intent) - shortcutManager.requestPinShortcut(info.build(), null) - } - } else - activity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit() + shortcutManager.requestPinShortcut(info.build(), null) + } } } diff --git a/app/src/main/java/emu/skyline/MainActivity.kt b/app/src/main/java/emu/skyline/MainActivity.kt index 96a2fe62..dc009545 100644 --- a/app/src/main/java/emu/skyline/MainActivity.kt +++ b/app/src/main/java/emu/skyline/MainActivity.kt @@ -25,9 +25,9 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar import emu.skyline.adapter.AppAdapter -import emu.skyline.adapter.AppItem import emu.skyline.adapter.GridLayoutSpan import emu.skyline.adapter.LayoutType +import emu.skyline.data.AppItem import emu.skyline.loader.RomFile import emu.skyline.loader.RomFormat import kotlinx.android.synthetic.main.main_activity.* @@ -37,7 +37,7 @@ import java.io.IOException import kotlin.concurrent.thread import kotlin.math.ceil -class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClickListener { +class MainActivity : AppCompatActivity(), View.OnClickListener { /** * This is used to get/set shared preferences */ @@ -51,14 +51,14 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick /** * This adds all files in [directory] with [extension] as an entry in [adapter] using [loader] to load metadata */ - private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean { + private fun addEntries(romFormat : RomFormat, directory : DocumentFile, found : Boolean = false) : Boolean { var foundCurrent = found directory.listFiles().forEach { file -> if (file.isDirectory) { - foundCurrent = addEntries(extension, romFormat, file, foundCurrent) + foundCurrent = addEntries(romFormat, file, foundCurrent) } else { - if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { + if (romFormat.extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) { val romFd = contentResolver.openFileDescriptor(file.uri, "r")!! val romFile = RomFile(this, romFormat, romFd) @@ -111,14 +111,17 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick try { runOnUiThread { adapter.clear() } - var foundRoms = addEntries("nro", RomFormat.NRO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) - foundRoms = foundRoms or addEntries("nso", RomFormat.NSO, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) - foundRoms = foundRoms or addEntries("nca", RomFormat.NCA, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) - foundRoms = foundRoms or addEntries("nsp", RomFormat.NSP, DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!!) + val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(sharedPreferences.getString("search_location", "")))!! + + var foundRoms = addEntries(RomFormat.NRO, searchLocation) + foundRoms = foundRoms or addEntries(RomFormat.NSO, searchLocation) + foundRoms = foundRoms or addEntries(RomFormat.NCA, searchLocation) + foundRoms = foundRoms or addEntries(RomFormat.NSP, searchLocation) runOnUiThread { - if (!foundRoms) + if (!foundRoms) { adapter.addHeader(getString(R.string.no_rom)) + } try { adapter.save(File("${applicationInfo.dataDir}/roms.bin")) @@ -172,28 +175,7 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick open_fab.setOnClickListener(this) log_fab.setOnClickListener(this) - val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()] - - adapter = AppAdapter(this, layoutType) - app_list.adapter = adapter - - when (layoutType) { - LayoutType.List -> { - app_list.layoutManager = LinearLayoutManager(this) - app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) - } - - LayoutType.Grid -> { - val itemWidth = 225 - val metrics = resources.displayMetrics - val span = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt() - - val layoutManager = GridLayoutManager(this, span) - layoutManager.spanSizeLookup = GridLayoutSpan(adapter, span) - - app_list.layoutManager = layoutManager - } - } + setupAppList() app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() { var y : Int = 0 @@ -202,23 +184,43 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick y += dy if (!app_list.isInTouchMode) { - if (y == 0) - toolbar_layout.setExpanded(true) - else - toolbar_layout.setExpanded(false) + toolbar_layout.setExpanded(y == 0) } super.onScrolled(recyclerView, dx, dy) } }) + } + + private fun setupAppList() { + val itemWidth = 225 + val metrics = resources.displayMetrics + val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt() + + val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()] + + adapter = AppAdapter(layoutType = layoutType, gridSpan = gridSpan, onClick = selectStartGame, onLongClick = selectShowGameDialog) + app_list.adapter = adapter + + app_list.layoutManager = when (layoutType) { + LayoutType.List -> LinearLayoutManager(this).also { app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) } + LayoutType.Grid, LayoutType.GridCompact -> GridLayoutManager(this, gridSpan).apply { + spanSizeLookup = GridLayoutSpan(adapter, gridSpan).also { + if (app_list.itemDecorationCount > 0) { + app_list.removeItemDecorationAt(0) + } + } + } + } if (sharedPreferences.getString("search_location", "") == "") { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION startActivityForResult(intent, 1) - } else + } else { refreshAdapter(!sharedPreferences.getBoolean("refresh_required", false)) + } } /** @@ -245,12 +247,11 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick } /** - * This handles on-click interaction with [R.id.log_fab], [R.id.open_fab], [R.id.app_item_linear] and [R.id.app_item_grid] + * This handles on-click interaction with [R.id.log_fab], [R.id.open_fab] */ override fun onClick(view : View) { when (view.id) { R.id.log_fab -> startActivity(Intent(this, LogActivity::class.java)) - R.id.open_fab -> { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) @@ -258,40 +259,19 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick startActivityForResult(intent, 2) } - - R.id.app_item_linear, R.id.app_item_grid -> { - val tag = view.tag - if (tag is AppItem) { - if (sharedPreferences.getBoolean("select_action", false)) { - val dialog = AppDialog(tag) - dialog.show(supportFragmentManager, "game") - } else { - val intent = Intent(this, EmulationActivity::class.java) - intent.data = tag.uri - startActivity(intent) - } - } - } } } - /** - * This handles long-click interaction with [R.id.app_item_linear] and [R.id.app_item_grid] - */ - override fun onLongClick(view : View?) : Boolean { - when (view?.id) { - R.id.app_item_linear, R.id.app_item_grid -> { - val tag = view.tag - if (tag is AppItem) { - val dialog = AppDialog(tag) - dialog.show(supportFragmentManager, "game") - - return true - } - } + private val selectStartGame : (appItem : AppItem) -> Unit = { + if (sharedPreferences.getBoolean("select_action", false)) { + AppDialog(it).show(supportFragmentManager, "game") + } else { + startActivity(Intent(this, EmulationActivity::class.java).apply { data = it.uri }) } + } - return false + private val selectShowGameDialog : (appItem : AppItem) -> Unit = { + AppDialog(it).show(supportFragmentManager, "game") } /** @@ -350,4 +330,13 @@ class MainActivity : AppCompatActivity(), View.OnClickListener, View.OnLongClick } } } + + override fun onResume() { + super.onResume() + + val layoutType = LayoutType.values()[sharedPreferences.getString("layout_type", "1")!!.toInt()] + if (layoutType != adapter.layoutType) { + setupAppList() + } + } } diff --git a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt b/app/src/main/java/emu/skyline/adapter/AppAdapter.kt index e5c297b4..8ffbdf96 100644 --- a/app/src/main/java/emu/skyline/adapter/AppAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/AppAdapter.kt @@ -7,80 +7,42 @@ package emu.skyline.adapter import android.app.Dialog import android.content.Context -import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.ColorDrawable -import android.net.Uri +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.Window import android.widget.ImageView +import android.widget.LinearLayout import android.widget.RelativeLayout import android.widget.TextView +import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import androidx.recyclerview.widget.RecyclerView import emu.skyline.R -import emu.skyline.adapter.ElementType.Header -import emu.skyline.adapter.ElementType.Item -import emu.skyline.loader.AppEntry - -/** - * This class is a wrapper around [AppEntry], it is used for passing around game metadata - */ -class AppItem(val meta : AppEntry) : BaseItem() { - /** - * The icon of the application - */ - val icon : Bitmap? - get() = meta.icon - - /** - * The title of the application - */ - val title : String - get() = meta.name - - /** - * The string used as the sub-title, we currently use the author - */ - val subTitle : String? - get() = meta.author - - /** - * The URI of the application's image file - */ - val uri : Uri - get() = meta.uri - - /** - * The format of the application ROM as a string - */ - private val type : String - get() = meta.format.name - - /** - * The name and author is used as the key - */ - override fun key() : String? { - return if (meta.author != null) meta.name + " " + meta.author else meta.name - } -} +import emu.skyline.data.AppItem /** * This enumerates the type of layouts the menu can be in */ -enum class LayoutType { - List, - Grid, +enum class LayoutType(val layoutRes : Int) { + List(R.layout.app_item_linear), + Grid(R.layout.app_item_grid), + GridCompact(R.layout.app_item_grid_compact) } +private typealias AppInteractionFunc = (appItem : AppItem) -> Unit + /** * This adapter is used to display all found applications using their metadata */ -internal class AppAdapter(val context : Context?, private val layoutType : LayoutType) : HeaderAdapter(), View.OnClickListener { - private val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context.theme)?.toBitmap(256, 256) - private val missingString = context?.getString(R.string.metadata_missing) +internal class AppAdapter(val layoutType : LayoutType, private val gridSpan : Int, private val onClick : AppInteractionFunc, private val onLongClick : AppInteractionFunc) : HeaderAdapter() { + + private lateinit var context : Context + private val missingIcon by lazy { ContextCompat.getDrawable(context, R.drawable.default_icon)!!.toBitmap(256, 256) } + private val missingString by lazy { context.getString(R.string.metadata_missing) } /** * This adds a header to the view with the contents of [string] @@ -89,29 +51,6 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou super.addHeader(BaseHeader(string)) } - /** - * The onClick handler for the supplied [view], used for the icon preview - */ - override fun onClick(view : View) { - val position = view.tag as Int - - if (getItem(position) is AppItem) { - val item = getItem(position) as AppItem - - if (view.id == R.id.icon) { - val builder = Dialog(context!!) - builder.requestWindowFeature(Window.FEATURE_NO_TITLE) - builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) - - val imageView = ImageView(context) - imageView.setImageBitmap(item.icon ?: missingIcon) - - builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) - builder.show() - } - } - } - /** * The ViewHolder used by items is used to hold the views associated with an item * @@ -134,38 +73,49 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou * This function creates the view-holder of type [viewType] with the layout parent as [parent] */ override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder { + context = parent.context + val inflater = LayoutInflater.from(context) - var holder : RecyclerView.ViewHolder? = null - - if (viewType == Item.ordinal) { - val view = inflater.inflate(if (layoutType == LayoutType.List) R.layout.app_item_linear else R.layout.app_item_grid, parent, false) - holder = ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)) - - if (layoutType == LayoutType.List) { - if (context is View.OnClickListener) - view.setOnClickListener(context as View.OnClickListener) - - if (context is View.OnLongClickListener) - view.setOnLongClickListener(context as View.OnLongClickListener) - } else { - holder.card = view.findViewById(R.id.app_item_grid) - - if (context is View.OnClickListener) - holder.card!!.setOnClickListener(context as View.OnClickListener) - - if (context is View.OnLongClickListener) - holder.card!!.setOnLongClickListener(context as View.OnLongClickListener) - - holder.title.isSelected = true - } - } else if (viewType == Header.ordinal) { - val view = inflater.inflate(R.layout.section_item, parent, false) - holder = HeaderViewHolder(view) - - holder.header = view.findViewById(R.id.text_title) + val view = when (ElementType.values()[viewType]) { + ElementType.Item -> inflater.inflate(layoutType.layoutRes, parent, false) + ElementType.Header -> inflater.inflate(R.layout.section_item, parent, false) } - return holder!! + Log.i("blaa", "onCreateViewHolder") + + return when (ElementType.values()[viewType]) { + ElementType.Item -> { + ItemViewHolder(view, view.findViewById(R.id.icon), view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)).apply { + if (layoutType == LayoutType.List) { + view.apply { + if (context is View.OnClickListener) { + setOnClickListener(context as View.OnClickListener) + } + if (context is View.OnLongClickListener) { + setOnLongClickListener(context as View.OnLongClickListener) + } + } + } else { + card = view.findViewById(R.id.app_item_grid) + card!!.apply { + if (context is View.OnClickListener) { + setOnClickListener(context as View.OnClickListener) + } + if (context is View.OnLongClickListener) { + setOnLongClickListener(context as View.OnLongClickListener) + } + } + + title.isSelected = true + } + } + } + ElementType.Header -> { + HeaderViewHolder(view).apply { + header = view.findViewById(R.id.text_title) + } + } + } } /** @@ -183,16 +133,45 @@ internal class AppAdapter(val context : Context?, private val layoutType : Layou holder.icon.setImageBitmap(item.icon ?: missingIcon) if (layoutType == LayoutType.List) { - holder.icon.setOnClickListener(this) - holder.icon.tag = position + holder.icon.setOnClickListener { showIconDialog(item) } } - holder.card?.tag = item - holder.parent.tag = item + when (layoutType) { + LayoutType.List -> holder.itemView + LayoutType.Grid, LayoutType.GridCompact -> holder.card!! + }.apply { + setOnClickListener { onClick.invoke(item) } + setOnLongClickListener { true.also { onLongClick.invoke(item) } } + } + + // Increase margin of edges to avoid huge gap in between items + if (layoutType == LayoutType.Grid || layoutType == LayoutType.GridCompact) { + + holder.itemView.layoutParams = LinearLayout.LayoutParams(holder.itemView.layoutParams.width, holder.itemView.layoutParams.height).apply { + if (position % gridSpan == 0) { + marginStart = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2 + } else if (position % gridSpan == gridSpan - 1) { + marginEnd = holder.itemView.resources.getDimensionPixelSize(R.dimen.app_card_margin) * 2 + } + } + holder.itemView.requestLayout() + } } else if (item is BaseHeader) { val holder = viewHolder as HeaderViewHolder holder.header!!.text = item.title } } + + private fun showIconDialog(appItem : AppItem) { + val builder = Dialog(context) + builder.requestWindowFeature(Window.FEATURE_NO_TITLE) + builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) + + val imageView = ImageView(context) + imageView.setImageBitmap(appItem.icon ?: missingIcon) + + builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) + builder.show() + } } diff --git a/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt b/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt index f6265612..af7cabdd 100644 --- a/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt @@ -10,6 +10,7 @@ import android.widget.Filter import android.widget.Filterable import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView +import emu.skyline.data.BaseItem import info.debatty.java.stringsimilarity.Cosine import info.debatty.java.stringsimilarity.JaroWinkler import java.io.* @@ -35,16 +36,6 @@ abstract class BaseElement constructor(val elementType : ElementType) : Serializ */ class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header) -/** - * This is an abstract class that all adapter item classes inherit from - */ -abstract class BaseItem : BaseElement(ElementType.Item) { - /** - * This function returns a string used for searching - */ - abstract fun key() : String? -} - /** * This adapter has the ability to have 2 types of elements specifically headers and items */ @@ -69,9 +60,9 @@ abstract class HeaderAdapter) : Array { - val scoredItems : MutableList = ArrayList() + /** + * This sorts the items in [keyArray] in relation to how similar they are to [term] + */ + fun extractSorted(term : String, keyArray : ArrayList) : Array { + val scoredItems : MutableList = ArrayList() - keyArray.forEachIndexed { index, item -> - val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2 + keyArray.forEachIndexed { index, item -> + val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2 - if (similarity != 0.0) - scoredItems.add(ScoredItem(similarity, index)) - } - - scoredItems.sortWith(compareByDescending { it.score }) - - return scoredItems.toTypedArray() + if (similarity != 0.0) + scoredItems.add(ScoredItem(similarity, index)) } - /** - * This performs filtering on the items in [elementArray] based on similarity to [term] - */ - override fun performFiltering(term : CharSequence) : FilterResults { - val results = FilterResults() - searchTerm = (term as String).toLowerCase(Locale.getDefault()) + scoredItems.sortWith(compareByDescending { it.score }) - if (term.isEmpty()) { - results.values = elementArray.indices.toMutableList() - results.count = elementArray.size - } else { - val filterData = ArrayList() + return scoredItems.toTypedArray() + } - val keyArray = ArrayList() - val keyIndex = SparseIntArray() + /** + * This performs filtering on the items in [elementArray] based on similarity to [term] + */ + override fun performFiltering(term : CharSequence) : FilterResults { + val results = FilterResults() + searchTerm = (term as String).toLowerCase(Locale.getDefault()) - for (index in elementArray.indices) { - val item = elementArray[index]!! + if (term.isEmpty()) { + results.values = elementArray.indices.toMutableList() + results.count = elementArray.size + } else { + val filterData = ArrayList() - if (item is BaseItem) { - keyIndex.append(keyArray.size, index) - keyArray.add(item.key()!!.toLowerCase(Locale.getDefault())) - } + val keyArray = ArrayList() + val keyIndex = SparseIntArray() + + for (index in elementArray.indices) { + val item = elementArray[index]!! + + if (item is BaseItem) { + keyIndex.append(keyArray.size, index) + keyArray.add(item.key()!!.toLowerCase(Locale.getDefault())) } - - val topResults = extractSorted(searchTerm, keyArray) - val avgScore = topResults.sumByDouble { it.score } / topResults.size - - for (result in topResults) - if (result.score > avgScore) - filterData.add(keyIndex[result.index]) - - results.values = filterData - results.count = filterData.size } - return results + val topResults = extractSorted(searchTerm, keyArray) + val avgScore = topResults.sumByDouble { it.score } / topResults.size + + for (result in topResults) + if (result.score > avgScore) + filterData.add(keyIndex[result.index]) + + results.values = filterData + results.count = filterData.size } - /** - * This publishes the results that were calculated in [performFiltering] to the view - */ - override fun publishResults(charSequence : CharSequence, results : FilterResults) { - if (results.values is ArrayList<*>) { - @Suppress("UNCHECKED_CAST") - visibleArray = results.values as ArrayList + return results + } - notifyDataSetChanged() - } + /** + * This publishes the results that were calculated in [performFiltering] to the view + */ + override fun publishResults(charSequence : CharSequence, results : FilterResults) { + if (results.values is ArrayList<*>) { + @Suppress("UNCHECKED_CAST") + visibleArray = results.values as ArrayList + + notifyDataSetChanged() } } } @@ -244,7 +233,7 @@ abstract class HeaderAdapter(val adapter : HeaderAdapter, var headerSpan : Int) : GridLayoutManager.SpanSizeLookup() { +class GridLayoutSpan(val adapter : HeaderAdapter, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() { /** * This returns the size of the span based on the type of the element at [position] */ diff --git a/app/src/main/java/emu/skyline/adapter/LogAdapter.kt b/app/src/main/java/emu/skyline/adapter/LogAdapter.kt index 6d5d4ebc..5f350cc3 100644 --- a/app/src/main/java/emu/skyline/adapter/LogAdapter.kt +++ b/app/src/main/java/emu/skyline/adapter/LogAdapter.kt @@ -10,12 +10,12 @@ import android.content.ClipboardManager import android.content.Context import android.view.LayoutInflater import android.view.View -import android.view.View.OnLongClickListener import android.view.ViewGroup import android.widget.TextView import android.widget.Toast import androidx.recyclerview.widget.RecyclerView import emu.skyline.R +import emu.skyline.data.BaseItem /** * This class is used to hold all data about a log entry @@ -32,7 +32,7 @@ internal class LogItem(val message : String, val level : String) : BaseItem() { /** * This adapter is used for displaying logs outputted by the application */ -internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array) : HeaderAdapter(), OnLongClickListener { +internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array) : HeaderAdapter() { private val clipboard : ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager /** @@ -72,41 +72,33 @@ internal class LogAdapter internal constructor(val context : Context, val compac */ private class HeaderViewHolder(val parent : View, var header : TextView) : RecyclerView.ViewHolder(parent) - /** - * The onLongClick handler for the supplied [view], used to copy a log into the clipboard - */ - override fun onLongClick(view : View) : Boolean { - val item = view.tag as LogItem - clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")")) - Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show() - return false - } - /** * This function creates the view-holder of type [viewType] with the layout parent as [parent] */ override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) : RecyclerView.ViewHolder { val inflater = LayoutInflater.from(context) - var holder : RecyclerView.ViewHolder? = null - if (viewType == ElementType.Item.ordinal) { - if (compact) { - val view = inflater.inflate(R.layout.log_item_compact, parent, false) - holder = ItemViewHolder(view, view.findViewById(R.id.text_title)) - - view.setOnLongClickListener(this) - } else { - val view = inflater.inflate(R.layout.log_item, parent, false) - holder = ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)) - - view.setOnLongClickListener(this) + val view = when (ElementType.values()[viewType]) { + ElementType.Item -> { + inflater.inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false) + } + ElementType.Header -> { + inflater.inflate(R.layout.log_item, parent, false) } - } else if (viewType == ElementType.Header.ordinal) { - val view = inflater.inflate(R.layout.section_item, parent, false) - holder = HeaderViewHolder(view, view.findViewById(R.id.text_title)) } - return holder!! + return when (ElementType.values()[viewType]) { + ElementType.Item -> { + if (compact) { + ItemViewHolder(view, view.findViewById(R.id.text_title)) + } else { + ItemViewHolder(view, view.findViewById(R.id.text_title), view.findViewById(R.id.text_subtitle)) + } + } + ElementType.Header -> { + HeaderViewHolder(view, view.findViewById(R.id.text_title)) + } + } } /** @@ -121,7 +113,10 @@ internal class LogAdapter internal constructor(val context : Context, val compac holder.title.text = item.message holder.subtitle?.text = item.level - holder.parent.tag = item + holder.parent.setOnClickListener { + clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")")) + Toast.makeText(holder.itemView.context, "Copied to clipboard", Toast.LENGTH_LONG).show() + } } else if (item is BaseHeader) { val holder = viewHolder as HeaderViewHolder diff --git a/app/src/main/java/emu/skyline/data/AppItem.kt b/app/src/main/java/emu/skyline/data/AppItem.kt new file mode 100644 index 00000000..443b51f9 --- /dev/null +++ b/app/src/main/java/emu/skyline/data/AppItem.kt @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.data + +import android.graphics.Bitmap +import android.net.Uri +import emu.skyline.loader.AppEntry + +/** + * This class is a wrapper around [AppEntry], it is used for passing around game metadata + */ +class AppItem(val meta : AppEntry) : BaseItem() { + /** + * The icon of the application + */ + val icon : Bitmap? + get() = meta.icon + + /** + * The title of the application + */ + val title : String + get() = meta.name + + /** + * The string used as the sub-title, we currently use the author + */ + val subTitle : String? + get() = meta.author + + /** + * The URI of the application's image file + */ + val uri : Uri + get() = meta.uri + + /** + * The format of the application ROM as a string + */ + private val type : String + get() = meta.format.name + + /** + * The name and author is used as the key + */ + override fun key() : String? { + return if (meta.author != null) meta.name + " " + meta.author else meta.name + } +} diff --git a/app/src/main/java/emu/skyline/data/BaseItem.kt b/app/src/main/java/emu/skyline/data/BaseItem.kt new file mode 100644 index 00000000..8206caf0 --- /dev/null +++ b/app/src/main/java/emu/skyline/data/BaseItem.kt @@ -0,0 +1,19 @@ +/* + * SPDX-License-Identifier: MPL-2.0 + * Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/) + */ + +package emu.skyline.data + +import emu.skyline.adapter.BaseElement +import emu.skyline.adapter.ElementType + +/** + * This is an abstract class that all adapter item classes inherit from + */ +abstract class BaseItem : BaseElement(ElementType.Item) { + /** + * This function returns a string used for searching + */ + abstract fun key() : String? +} diff --git a/app/src/main/java/emu/skyline/loader/RomFile.kt b/app/src/main/java/emu/skyline/loader/RomFile.kt index 98ca4e58..2189a875 100644 --- a/app/src/main/java/emu/skyline/loader/RomFile.kt +++ b/app/src/main/java/emu/skyline/loader/RomFile.kt @@ -13,7 +13,6 @@ import android.net.Uri import android.os.Build import android.os.ParcelFileDescriptor import android.provider.OpenableColumns -import android.view.Surface import java.io.IOException import java.io.ObjectInputStream import java.io.ObjectOutputStream @@ -23,12 +22,12 @@ import java.util.* /** * An enumeration of all supported ROM formats */ -enum class RomFormat(val format: Int){ - NRO(0), - NSO(1), - NCA(2), - XCI(3), - NSP(4), +enum class RomFormat(val extension : String) { + NRO("nro"), + NSO("nso"), + NCA("nca"), + XCI("xci"), + NSP("nsp"), } /** @@ -185,7 +184,7 @@ internal class RomFile(val context : Context, val format : RomFormat, val file : fun getAppEntry(uri : Uri) : AppEntry { return if (hasAssets(instance)) { val rawIcon = getIcon(instance) - val icon = if (rawIcon.size != 0) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null + val icon = if (rawIcon.isNotEmpty()) BitmapFactory.decodeByteArray(rawIcon, 0, rawIcon.size) else null AppEntry(getApplicationName(instance), getApplicationPublisher(instance), format, uri, icon) } else { diff --git a/app/src/main/res/drawable/background_gradient.xml b/app/src/main/res/drawable/background_gradient.xml new file mode 100644 index 00000000..2d6eebc0 --- /dev/null +++ b/app/src/main/res/drawable/background_gradient.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_item_grid.xml b/app/src/main/res/layout/app_item_grid.xml index 8d023883..b79b30d9 100644 --- a/app/src/main/res/layout/app_item_grid.xml +++ b/app/src/main/res/layout/app_item_grid.xml @@ -1,21 +1,24 @@ - + android:layout_height="wrap_content"> - + app:cardCornerRadius="4dp" + app:cardElevation="@dimen/app_card_margin" + app:cardUseCompatPadding="true"> - + tools:src="@drawable/default_icon" /> + tools:text="Title" /> - + android:textColor="@android:color/tertiary_text_light" + tools:text="Subtitle" /> + - + diff --git a/app/src/main/res/layout/app_item_grid_compact.xml b/app/src/main/res/layout/app_item_grid_compact.xml new file mode 100644 index 00000000..a27fd687 --- /dev/null +++ b/app/src/main/res/layout/app_item_grid_compact.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/app_item_linear.xml b/app/src/main/res/layout/app_item_linear.xml index c275b7e6..73d8aa55 100644 --- a/app/src/main/res/layout/app_item_linear.xml +++ b/app/src/main/res/layout/app_item_linear.xml @@ -1,40 +1,43 @@ - + android:padding="16dp"> + android:contentDescription="@string/icon" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/default_icon" /> + app:layout_constraintBottom_toTopOf="@+id/text_subtitle" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintTop_toTopOf="parent" + tools:text="Title" /> - + android:textColor="@android:color/tertiary_text_light" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toEndOf="@id/icon" + app:layout_constraintTop_toBottomOf="@+id/text_title" + tools:text="SubTitle" /> + diff --git a/app/src/main/res/values/array.xml b/app/src/main/res/values/array.xml index 37bc2d27..ffba65c5 100644 --- a/app/src/main/res/values/array.xml +++ b/app/src/main/res/values/array.xml @@ -15,10 +15,12 @@ List Grid + Grid Compact 0 1 + 2 Light diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 00000000..0e947602 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 8dp + 4dp + \ No newline at end of file