skyline/app/src/main/java/emu/skyline/adapter/HeaderAdapter.kt
◱ PixelyIon 75d485a9a7 Addition of Controller Configuration UI
This commit adds in the UI for Controller Configuration to Settings, in addition to introducing the storage and loading of aforementioned configurations to a file that can be saved/loaded at runtime. This commit also fixes updating of individual fields in Settings when changed from an external activity.
2020-08-21 11:48:29 +00:00

248 lines
8.0 KiB
Kotlin

/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.adapter
import android.util.SparseIntArray
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.*
import java.util.*
import kotlin.collections.ArrayList
/**
* An enumeration of the type of elements in this adapter
*/
enum class ElementType {
Header,
Item,
}
/**
* This is an abstract class that all adapter element classes inherit from
*/
abstract class BaseElement constructor(val elementType : ElementType) : Serializable
/**
* This is an abstract class that all adapter header classes inherit from
*/
class BaseHeader constructor(val title : String) : BaseElement(ElementType.Header)
/**
* This adapter has the ability to have 2 types of elements specifically headers and items
*/
abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?, ViewHolder : RecyclerView.ViewHolder?> : RecyclerView.Adapter<ViewHolder>(), Filterable, Serializable {
/**
* This holds all the elements in an array even if they may not be visible
*/
var elementArray : ArrayList<BaseElement?> = ArrayList()
/**
* This holds the indices of all the visible items in [elementArray]
*/
var visibleArray : ArrayList<Int> = ArrayList()
/**
* This holds the search term if there is any, to filter any items added during a search
*/
private var searchTerm = ""
/**
* This functions adds [item] to [elementArray] and [visibleArray] based on the filter
*/
fun addItem(item : ItemType) {
elementArray.add(item)
if (searchTerm.isNotEmpty()) {
filter.filter(searchTerm)
} else {
visibleArray.add(elementArray.size - 1)
notifyItemInserted(visibleArray.size)
}
}
/**
* This function adds [header] to [elementArray] and [visibleArray] based on if the filter is active
*/
fun addHeader(header : HeaderType) {
elementArray.add(header)
if (searchTerm.isEmpty()) {
visibleArray.add(elementArray.size - 1)
notifyItemInserted(visibleArray.size)
}
}
/**
* This serializes [elementArray] into [file]
*/
@Throws(IOException::class)
fun save(file : File) {
val fileObj = FileOutputStream(file)
val out = ObjectOutputStream(fileObj)
out.writeObject(elementArray)
out.close()
fileObj.close()
}
/**
* This reads in [elementArray] from [file]
*/
@Throws(IOException::class, ClassNotFoundException::class)
open fun load(file : File) {
val fileObj = FileInputStream(file)
val input = ObjectInputStream(fileObj)
@Suppress("UNCHECKED_CAST")
elementArray = input.readObject() as ArrayList<BaseElement?>
input.close()
fileObj.close()
filter.filter(searchTerm)
}
/**
* This clears the view by clearing [elementArray] and [visibleArray]
*/
fun clear() {
elementArray.clear()
visibleArray.clear()
notifyDataSetChanged()
}
/**
* This returns the amount of elements that should be drawn to the list
*/
override fun getItemCount() : Int = visibleArray.size
/**
* This returns a particular element at [position]
*/
fun getItem(position : Int) : BaseElement? {
return elementArray[visibleArray[position]]
}
/**
* This returns the type of an element at the specified position
*
* @param position The position of the element
*/
override fun getItemViewType(position : Int) : Int {
return elementArray[visibleArray[position]]!!.elementType.ordinal
}
/**
* This returns an instance of the filter object which is used to search for items in the view
*/
override fun getFilter() = 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()
/**
* 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()
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()
}
/**
* 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())
if (term.isEmpty()) {
results.values = elementArray.indices.toMutableList()
results.count = elementArray.size
} else {
val filterData = ArrayList<Int>()
val keyArray = ArrayList<String>()
val keyIndex = SparseIntArray()
for (index in elementArray.indices) {
val item = elementArray[index]!!
if (item is BaseItem && item.key() != null) {
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
}
/**
* 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()
}
}
}
}
/**
* This class is used to lookup the span based on the type of the element
*
* @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>, private val headerSpan : Int) : GridLayoutManager.SpanSizeLookup() {
/**
* This returns the size of the span based on the type of the element at [position]
*/
override fun getSpanSize(position : Int) : Int {
val item = adapter.getItem(position)!!
return if (item.elementType == ElementType.Item)
1
else
headerSpan
}
}