Redesign cards in grid view

* Refactor some classes and clean up
* Refresh style on the fly
This commit is contained in:
Willi Ye 2020-07-17 17:53:44 +02:00 committed by ◱ PixelyIon
parent b23779bda1
commit c3e54d1abf
14 changed files with 479 additions and 361 deletions

View File

@ -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)
}
}
}

View File

@ -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()
}
}
}

View File

@ -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<AppItem, BaseHeader, RecyclerView.ViewHolder>(), 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<AppItem, BaseHeader, RecyclerView.ViewHolder>() {
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()
}
}

View File

@ -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<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
*/
fun addItem(item : ItemType) {
elementArray.add(item)
if (searchTerm.isNotEmpty())
if (searchTerm.isNotEmpty()) {
filter.filter(searchTerm)
else {
} else {
visibleArray.add(elementArray.size - 1)
notifyDataSetChanged()
}
@ -146,93 +137,91 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
/**
* This returns an instance of the filter object which is used to search for items in the view
*/
override fun getFilter() : Filter {
return object : Filter() {
/**
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
*/
private val jw = JaroWinkler()
override fun getFilter() : Filter = object : Filter() {
/**
* We use Jaro-Winkler distance for string similarity (https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance)
*/
private val jw = JaroWinkler()
/**
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
*/
private val cos = Cosine()
/**
* We use Cosine similarity for string similarity (https://en.wikipedia.org/wiki/Cosine_similarity)
*/
private val cos = Cosine()
/**
* This class is used to store the results of the item sorting
*
* @param score The score of this result
* @param index The index of this item
*/
inner class ScoredItem(val score : Double, val index : Int) {}
/**
* This class is used to store the results of the item sorting
*
* @param score The score of this result
* @param index The index of this item
*/
inner class ScoredItem(val score : Double, val index : Int) {}
/**
* This sorts the items in [keyArray] in relation to how similar they are to [term]
*/
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
val scoredItems : MutableList<ScoredItem> = ArrayList()
/**
* This sorts the items in [keyArray] in relation to how similar they are to [term]
*/
fun extractSorted(term : String, keyArray : ArrayList<String>) : Array<ScoredItem> {
val scoredItems : MutableList<ScoredItem> = 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<Int>()
return scoredItems.toTypedArray()
}
val keyArray = ArrayList<String>()
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<Int>()
if (item is BaseItem) {
keyIndex.append(keyArray.size, index)
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
}
val keyArray = ArrayList<String>()
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<Int>
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<Int>
notifyDataSetChanged()
}
}
}
@ -244,7 +233,7 @@ abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, Vie
* @param adapter The adapter which is used to deduce the type of the item based on the position
* @param headerSpan The span size to return for headers
*/
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, var headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
class GridLayoutSpan<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?>(val adapter : HeaderAdapter<ItemType, HeaderType, ViewHolder>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
/**
* This returns the size of the span based on the type of the element at [position]
*/

View File

@ -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<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>(), OnLongClickListener {
internal class LogAdapter internal constructor(val context : Context, val compact : Boolean, private val debug_level : Int, private val level_str : Array<String>) : HeaderAdapter<LogItem, BaseHeader, RecyclerView.ViewHolder>() {
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

View File

@ -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
}
}

View File

@ -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?
}

View File

@ -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 {

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<gradient
android:angle="270"
android:endColor="#80000000"
android:centerColor="#00000000"
android:startColor="#00000000"
android:type="linear" />
</shape>

View File

@ -1,21 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center">
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
<androidx.cardview.widget.CardView
android:id="@+id/app_item_grid"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_margin="15dp"
android:layout_gravity="center"
android:layout_margin="@dimen/app_card_margin_half"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
card_view:cardCornerRadius="4dp">
app:cardCornerRadius="4dp"
app:cardElevation="@dimen/app_card_margin"
app:cardUseCompatPadding="true">
<RelativeLayout
<LinearLayout
android:layout_width="155dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
@ -25,16 +28,13 @@
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="155dp"
android:layout_alignParentTop="false"
android:layout_centerHorizontal="true"
android:contentDescription="@string/icon"
android:scaleType="centerCrop" />
tools:src="@drawable/default_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/icon"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="15dp"
@ -43,14 +43,12 @@
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:ignore="RelativeOverlap" />
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
@ -58,7 +56,8 @@
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light" />
</RelativeLayout>
android:textColor="@android:color/tertiary_text_light"
tools:text="Subtitle" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,81 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.cardview.widget.CardView
android:id="@+id/app_item_grid"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="@dimen/app_card_margin_half"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardCornerRadius="4dp"
app:cardElevation="@dimen/app_card_margin"
app:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="155dp"
android:layout_height="155dp">
<ImageView
android:id="@+id/icon"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentTop="false"
android:layout_centerHorizontal="true"
android:contentDescription="@string/icon"
android:foreground="@drawable/background_gradient"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:alpha="242.25"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItem"
android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:alpha="242.25"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/white"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toStartOf="parent"
tools:text="Subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@ -1,40 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_item_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:padding="15dp">
android:padding="16dp">
<ImageView
android:id="@+id/icon"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_alignParentStart="true"
android:layout_alignParentTop="false"
android:layout_centerVertical="true"
android:layout_marginEnd="10dp"
android:contentDescription="@string/icon" />
android:contentDescription="@string/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/icon"
android:layout_toEndOf="@id/icon"
android:layout_marginStart="10dp"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:ignore="RelativeOverlap" />
app:layout_constraintBottom_toTopOf="@+id/text_subtitle"
app:layout_constraintStart_toEndOf="@id/icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title"
android:layout_marginStart="10dp"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light" />
</RelativeLayout>
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" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -15,10 +15,12 @@
<string-array name="layout_type">
<item>List</item>
<item>Grid</item>
<item>Grid Compact</item>
</string-array>
<string-array name="layout_type_val">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="app_theme">
<item>Light</item>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="app_card_margin">8dp</dimen>
<dimen name="app_card_margin_half">4dp</dimen>
</resources>