Refactor and Convert Adapters to RecyclerView.Adapter

This commit mainly refactors the adapters by adding spacing, comments and following other guidelines. In addition, it moves from using `BaseAdapter` to `RecyclerView.Adapter` which leads to much cleaner adapter classes.
a
This commit is contained in:
◱ PixelyIon 2020-04-12 21:43:29 +05:30 committed by ◱ PixelyIon
parent 55a9f8e937
commit d86d5c1a35
14 changed files with 358 additions and 157 deletions

View File

@ -15,7 +15,7 @@ import android.view.SurfaceHolder
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import emu.skyline.loader.getRomFormat import emu.skyline.loader.getRomFormat
import kotlinx.android.synthetic.main.game_activity.* import kotlinx.android.synthetic.main.app_activity.*
import java.io.File import java.io.File
/** /**

View File

@ -15,7 +15,11 @@ import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.adapter.LogAdapter import emu.skyline.adapter.LogAdapter
import kotlinx.android.synthetic.main.log_activity.*
import org.json.JSONObject import org.json.JSONObject
import java.io.* import java.io.*
import java.net.URL import java.net.URL
@ -32,10 +36,19 @@ class LogActivity : AppCompatActivity() {
setContentView(R.layout.log_activity) setContentView(R.layout.log_activity)
setSupportActionBar(findViewById(R.id.toolbar)) setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
val prefs = PreferenceManager.getDefaultSharedPreferences(this) val prefs = PreferenceManager.getDefaultSharedPreferences(this)
val logList = findViewById<ListView>(R.id.log_list) val compact = prefs.getBoolean("log_compact", false)
adapter = LogAdapter(this, prefs.getBoolean("log_compact", false), prefs.getString("log_level", "3")!!.toInt(), resources.getStringArray(R.array.log_level)) val logLevel = prefs.getString("log_level", "3")!!.toInt()
logList.adapter = adapter
adapter = LogAdapter(this, compact, logLevel, resources.getStringArray(R.array.log_level))
log_list.adapter = adapter
log_list.layoutManager = LinearLayoutManager(this)
if (!compact)
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
try { try {
logFile = File("${applicationInfo.dataDir}/skyline.log") logFile = File("${applicationInfo.dataDir}/skyline.log")
logFile.forEachLine { logFile.forEachLine {

View File

@ -18,6 +18,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import emu.skyline.adapter.AppAdapter import emu.skyline.adapter.AppAdapter
import emu.skyline.adapter.AppItem import emu.skyline.adapter.AppItem
@ -128,12 +131,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
open_fab.setOnClickListener(this) open_fab.setOnClickListener(this)
log_fab.setOnClickListener(this) log_fab.setOnClickListener(this)
game_list.adapter = adapter adapter = AppAdapter(this)
game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long -> app_list.adapter = adapter
val item = parent.getItemAtPosition(position)
if (item is AppItem) { app_list.layoutManager = LinearLayoutManager(this)
val intent = Intent(this, EmulationActivity::class.java) app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
intent.data = item.uri
startActivity(intent) startActivity(intent)
} }
} }
@ -181,8 +184,34 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
intent.type = "*/*" intent.type = "*/*"
startActivityForResult(intent, 2) startActivityForResult(intent, 2)
} }
R.id.app_item_linear -> {
val tag = view.tag
if (tag is AppItem) {
val intent = Intent(this, EmulationActivity::class.java)
intent.data = tag.uri
startActivity(intent)
} }
} }
}
}
override fun onLongClick(view: View?): Boolean {
when (view?.id) {
R.id.app_item_linear -> {
val tag = view.tag
if (tag is AppItem) {
val dialog = AppDialog(tag)
dialog.show(supportFragmentManager, "game")
return true
}
}
}
return false
}
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {

View File

@ -19,7 +19,10 @@ import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.ElementType.Header
import emu.skyline.adapter.ElementType.Item
import emu.skyline.loader.AppEntry import emu.skyline.loader.AppEntry
/** /**
@ -36,7 +39,7 @@ class AppItem(val meta: AppEntry) : BaseItem() {
* The title of the application * The title of the application
*/ */
val title: String val title: String
get() = meta.name + " (" + type + ")" get() = meta.name
/** /**
* The string used as the sub-title, we currently use the author * The string used as the sub-title, we currently use the author
@ -56,6 +59,9 @@ class AppItem(val meta: AppEntry) : BaseItem() {
private val type: String private val type: String
get() = meta.format.name get() = meta.format.name
/**
* The name and author is used as the key
*/
override fun key(): String? { override fun key(): String? {
return if (meta.author != null) meta.name + " " + meta.author else meta.name return if (meta.author != null) meta.name + " " + meta.author else meta.name
} }
@ -64,29 +70,41 @@ class AppItem(val meta: AppEntry) : BaseItem() {
/** /**
* This adapter is used to display all found applications using their metadata * This adapter is used to display all found applications using their metadata
*/ */
internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHeader>(), View.OnClickListener { internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHeader, RecyclerView.ViewHolder>(), View.OnClickListener {
/** /**
* This adds a string header to the view * The icon to use on items that don't have a valid icon
*/
private val missingIcon = context?.resources?.getDrawable(R.drawable.default_icon, context.theme)?.toBitmap(256, 256)
/**
* The string to use as a description for items that don't have a valid description
*/
private val missingString = context?.getString(R.string.metadata_missing)
/**
* This adds a header to the view with the contents of [string]
*/ */
fun addHeader(string: String) { fun addHeader(string: String) {
super.addHeader(BaseHeader(string)) super.addHeader(BaseHeader(string))
} }
/** /**
* The onClick handler, it's for displaying the icon preview * The onClick handler for the supplied [view], used for the icon preview
*
* @param view The specific view that was clicked
*/ */
override fun onClick(view: View) { override fun onClick(view: View) {
val position = view.tag as Int val position = view.tag as Int
if (getItem(position) is AppItem) { if (getItem(position) is AppItem) {
val item = getItem(position) as AppItem val item = getItem(position) as AppItem
if (view.id == R.id.icon) { if (view.id == R.id.icon) {
val builder = Dialog(context!!) val builder = Dialog(context!!)
builder.requestWindowFeature(Window.FEATURE_NO_TITLE) builder.requestWindowFeature(Window.FEATURE_NO_TITLE)
builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) builder.window!!.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
val imageView = ImageView(context) val imageView = ImageView(context)
imageView.setImageBitmap(item.icon) imageView.setImageBitmap(item.icon ?: missingIcon)
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)) builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
builder.show() builder.show()
} }
@ -94,60 +112,74 @@ internal class AppAdapter(val context: Context?) : HeaderAdapter<AppItem, BaseHe
} }
/** /**
* This returns the view for an element at a specific position * The ViewHolder used by items is used to hold the views associated with an item
*
* @param position The position of the requested item
* @param convertView An existing view (If any)
* @param parent The parent view group used for layout inflation
*/
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
val viewHolder: ViewHolder
val item = elementArray[visibleArray[position]]
if (view == null) {
viewHolder = ViewHolder()
if (item is AppItem) {
val inflater = LayoutInflater.from(context)
view = inflater.inflate(R.layout.game_item, parent, false)
viewHolder.icon = view.findViewById(R.id.icon)
viewHolder.title = view.findViewById(R.id.text_title)
viewHolder.subtitle = view.findViewById(R.id.text_subtitle)
view.tag = viewHolder
} else if (item is BaseHeader) {
val inflater = LayoutInflater.from(context)
view = inflater.inflate(R.layout.section_item, parent, false)
viewHolder.title = view.findViewById(R.id.text_title)
view.tag = viewHolder
}
} else {
viewHolder = view.tag as ViewHolder
}
if (item is AppItem) {
val data = getItem(position) as AppItem
viewHolder.title!!.text = data.title
viewHolder.subtitle!!.text = data.subTitle ?: context?.getString(R.string.metadata_missing)!!
viewHolder.icon!!.setImageBitmap(data.icon ?: context!!.resources.getDrawable(R.drawable.ic_missing, context.theme).toBitmap(256, 256))
viewHolder.icon!!.setOnClickListener(this)
viewHolder.icon!!.tag = position
} else {
viewHolder.title!!.text = (getItem(position) as BaseHeader).title
}
return view!!
}
/**
* The ViewHolder object is used to hold the views associated with an object
* *
* @param parent The parent view that contains all the others
* @param icon The ImageView associated with the icon * @param icon The ImageView associated with the icon
* @param title The TextView associated with the title * @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle * @param subtitle The TextView associated with the subtitle
*/ */
private class ViewHolder(var icon: ImageView? = null, var title: TextView? = null, var subtitle: TextView? = null) private class ItemViewHolder(val parent: View, var icon: ImageView, var title: TextView, var subtitle: TextView, var card: View? = null) : RecyclerView.ViewHolder(parent)
/**
* The ViewHolder used by headers is used to hold the views associated with an headers
*
* @param parent The parent view that contains all the others
* @param header The TextView associated with the header
*/
private class HeaderViewHolder(val parent: View, var header: TextView? = null) : RecyclerView.ViewHolder(parent)
/**
* 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 == Item.ordinal) {
val view = inflater.inflate(R.layout.app_item_linear, 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 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)
}
return holder!!
}
/**
* This function binds the item at [position] to the supplied [viewHolder]
*/
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item is AppItem) {
val holder = viewHolder as ItemViewHolder
holder.title.text = item.title
holder.subtitle.text = item.subTitle ?: missingString
holder.icon.setImageBitmap(item.icon ?: missingIcon)
holder.icon.setOnClickListener(this)
holder.icon.tag = position
holder.card?.tag = item
holder.parent.tag = item
} else if (item is BaseHeader) {
val holder = viewHolder as HeaderViewHolder
holder.header!!.text = item.title
}
}
} }

View File

@ -6,11 +6,10 @@
package emu.skyline.adapter package emu.skyline.adapter
import android.util.SparseIntArray import android.util.SparseIntArray
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.Filter import android.widget.Filter
import android.widget.Filterable import android.widget.Filterable
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import info.debatty.java.stringsimilarity.Cosine import info.debatty.java.stringsimilarity.Cosine
import info.debatty.java.stringsimilarity.JaroWinkler import info.debatty.java.stringsimilarity.JaroWinkler
import java.io.* import java.io.*
@ -18,6 +17,9 @@ import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
/**
* An enumeration of the type of elements in this adapter
*/
enum class ElementType(val type: Int) { enum class ElementType(val type: Int) {
Header(0x0), Header(0x0),
Item(0x1) Item(0x1)
@ -43,13 +45,30 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
abstract fun key(): String? abstract fun key(): String?
} }
internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHeader?> : BaseAdapter(), Filterable, Serializable { /**
* 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() var elementArray: ArrayList<BaseElement?> = ArrayList()
/**
* This holds the indices of all the visible items in [elementArray]
*/
var visibleArray: ArrayList<Int> = ArrayList() 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 = "" private var searchTerm = ""
fun addItem(element: ItemType) { /**
elementArray.add(element) * This functions adds [item] to [elementArray] and [visibleArray] based on the filter
*/
fun addItem(item: ItemType) {
elementArray.add(item)
if (searchTerm.isNotEmpty()) if (searchTerm.isNotEmpty())
filter.filter(searchTerm) filter.filter(searchTerm)
else { else {
@ -58,18 +77,19 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
} }
} }
fun addHeader(element: HeaderType) { /**
elementArray.add(element) * This function adds [header] to [elementArray] and [visibleArray] based on if the filter is active
if (searchTerm.isNotEmpty()) */
filter.filter(searchTerm) fun addHeader(header: HeaderType) {
else { elementArray.add(header)
if (searchTerm.isEmpty())
visibleArray.add(elementArray.size - 1) visibleArray.add(elementArray.size - 1)
notifyDataSetChanged() notifyDataSetChanged()
} }
}
internal inner class State(val elementArray: ArrayList<BaseElement?>) : Serializable
/**
* This serializes [elementArray] into [file]
*/
@Throws(IOException::class) @Throws(IOException::class)
fun save(file: File) { fun save(file: File) {
val fileObj = FileOutputStream(file) val fileObj = FileOutputStream(file)
@ -79,6 +99,9 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
fileObj.close() fileObj.close()
} }
/**
* This reads in [elementArray] from [file]
*/
@Throws(IOException::class, ClassNotFoundException::class) @Throws(IOException::class, ClassNotFoundException::class)
open fun load(file: File) { open fun load(file: File) {
val fileObj = FileInputStream(file) val fileObj = FileInputStream(file)
@ -90,82 +113,124 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
filter.filter(searchTerm) filter.filter(searchTerm)
} }
/**
* This clears the view by clearing [elementArray] and [visibleArray]
*/
fun clear() { fun clear() {
elementArray.clear() elementArray.clear()
visibleArray.clear() visibleArray.clear()
notifyDataSetChanged() notifyDataSetChanged()
} }
override fun getCount(): Int { /**
return visibleArray.size * This returns the amount of elements that should be drawn to the list
} */
override fun getItemCount(): Int = visibleArray.size
override fun getItem(index: Int): BaseElement? {
return elementArray[visibleArray[index]] /**
} * This returns a particular element at [position]
*/
override fun getItemId(position: Int): Long { fun getItem(position: Int): BaseElement? {
return position.toLong() 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 { override fun getItemViewType(position: Int): Int {
return elementArray[visibleArray[position]]!!.elementType.type return elementArray[visibleArray[position]]!!.elementType.type
} }
override fun getViewTypeCount(): Int { /**
return ElementType.values().size * This returns an instance of the filter object which is used to search for items in the view
} */
abstract override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
override fun getFilter(): Filter { override fun getFilter(): Filter {
return object : Filter() { return object : Filter() {
inner class ScoredItem(val score: Double, val index: Int, val item:String) {} /**
* 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> { fun extractSorted(term: String, keyArray: ArrayList<String>): Array<ScoredItem> {
val scoredItems: MutableList<ScoredItem> = ArrayList() val scoredItems: MutableList<ScoredItem> = ArrayList()
val jw = JaroWinkler() keyArray.forEachIndexed { index, item ->
val cos = Cosine() val similarity = (jw.similarity(term, item) + cos.similarity(term, item)) / 2
if (similarity != 0.0)
scoredItems.add(ScoredItem(similarity, index))
}
keyArray.forEachIndexed { index, item -> scoredItems.add(ScoredItem((jw.similarity(term, item) + cos.similarity(term, item)) / 2, index, item)) }
scoredItems.sortWith(compareByDescending { it.score }) scoredItems.sortWith(compareByDescending { it.score })
return scoredItems.toTypedArray() return scoredItems.toTypedArray()
} }
override fun performFiltering(charSequence: CharSequence): FilterResults { /**
* This performs filtering on the items in [elementArray] based on similarity to [term]
*/
override fun performFiltering(term: CharSequence): FilterResults {
val results = FilterResults() val results = FilterResults()
searchTerm = (charSequence as String).toLowerCase(Locale.getDefault()) searchTerm = (term as String).toLowerCase(Locale.getDefault())
if (charSequence.isEmpty()) {
if (term.isEmpty()) {
results.values = elementArray.indices.toMutableList() results.values = elementArray.indices.toMutableList()
results.count = elementArray.size results.count = elementArray.size
} else { } else {
val filterData = ArrayList<Int>() val filterData = ArrayList<Int>()
val keyArray = ArrayList<String>() val keyArray = ArrayList<String>()
val keyIndex = SparseIntArray() val keyIndex = SparseIntArray()
for (index in elementArray.indices) { for (index in elementArray.indices) {
val item = elementArray[index]!! val item = elementArray[index]!!
if (item is BaseItem) { if (item is BaseItem) {
keyIndex.append(keyArray.size, index) keyIndex.append(keyArray.size, index)
keyArray.add(item.key()!!.toLowerCase(Locale.getDefault())) keyArray.add(item.key()!!.toLowerCase(Locale.getDefault()))
} }
} }
val topResults = extractSorted(searchTerm, keyArray) val topResults = extractSorted(searchTerm, keyArray)
val avgScore = topResults.sumByDouble { it.score } / topResults.size val avgScore = topResults.sumByDouble { it.score } / topResults.size
for (result in topResults) for (result in topResults)
if (result.score > avgScore) if (result.score > avgScore)
filterData.add(keyIndex[result.index]) filterData.add(keyIndex[result.index])
results.values = filterData results.values = filterData
results.count = filterData.size results.count = filterData.size
} }
return results return results
} }
/**
* This publishes the results that were calculated in [performFiltering] to the view
*/
override fun publishResults(charSequence: CharSequence, results: FilterResults) { override fun publishResults(charSequence: CharSequence, results: FilterResults) {
if (results.values is ArrayList<*>) { if (results.values is ArrayList<*>) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
visibleArray = results.values as ArrayList<Int> visibleArray = results.values as ArrayList<Int>
notifyDataSetChanged() notifyDataSetChanged()
} }
} }

View File

@ -14,77 +14,118 @@ import android.view.View.OnLongClickListener
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import emu.skyline.R import emu.skyline.R
/**
* This class is used to hold all data about a log entry
*/
internal class LogItem(val message: String, val level: String) : BaseItem() { internal class LogItem(val message: String, val level: String) : BaseItem() {
/**
* The log message itself is used as the search key
*/
override fun key(): String? { override fun key(): String? {
return message return message
} }
} }
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>(), OnLongClickListener { /**
* 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 {
private val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager private val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
/**
* This function adds a line to this log adapter
*/
fun add(logLine: String) { fun add(logLine: String) {
try { try {
val logMeta = logLine.split("|", limit = 3) val logMeta = logLine.split("|", limit = 3)
if (logMeta[0].startsWith("1")) { if (logMeta[0].startsWith("1")) {
val level = logMeta[1].toInt() val level = logMeta[1].toInt()
if (level > debug_level) return if (level > debug_level) return
addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level])) addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level]))
} else { } else {
addHeader(BaseHeader(logMeta[1])) addHeader(BaseHeader(logMeta[1]))
} }
} catch (ignored: IndexOutOfBoundsException) { } catch (ignored: IndexOutOfBoundsException) {
} catch (ignored: NumberFormatException) {
} }
} }
/**
* The ViewHolder used by items is used to hold the views associated with an item
*
* @param parent The parent view that contains all the others
* @param title The TextView associated with the title
* @param subtitle The TextView associated with the subtitle
*/
private class ItemViewHolder(val parent: View, var title: TextView, var subtitle: TextView? = null) : RecyclerView.ViewHolder(parent)
/**
* The ViewHolder used by headers is used to hold the views associated with an headers
*
* @param parent The parent view that contains all the others
* @param header The TextView associated with the header
*/
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 { override fun onLongClick(view: View): Boolean {
val item = getItem((view.tag as ViewHolder).position) as LogItem val item = view.tag as LogItem
clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")")) clipboard.setPrimaryClip(ClipData.newPlainText("Log Message", item.message + " (" + item.level + ")"))
Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show() Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
return false return false
} }
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { /**
var view = convertView * This function creates the view-holder of type [viewType] with the layout parent as [parent]
val viewHolder: ViewHolder */
val item = elementArray[visibleArray[position]] override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
if (view == null) {
viewHolder = ViewHolder()
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(context)
if (item is LogItem) { var holder: RecyclerView.ViewHolder? = null
if (viewType == ElementType.Item.ordinal) {
if (compact) { if (compact) {
view = inflater.inflate(R.layout.log_item_compact, parent, false) val view = inflater.inflate(R.layout.log_item_compact, parent, false)
viewHolder.txtTitle = view.findViewById(R.id.text_title) holder = ItemViewHolder(view, view.findViewById(R.id.text_title))
} else {
view = inflater.inflate(R.layout.log_item, parent, false)
viewHolder.txtTitle = view.findViewById(R.id.text_title)
viewHolder.txtSub = view.findViewById(R.id.text_subtitle)
}
view.setOnLongClickListener(this) view.setOnLongClickListener(this)
} else if (item is BaseHeader) {
view = inflater.inflate(R.layout.section_item, parent, false)
viewHolder.txtTitle = view.findViewById(R.id.text_title)
}
view!!.tag = viewHolder
} else { } else {
viewHolder = view.tag as ViewHolder 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)
} }
} 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!!
}
/**
* This function binds the item at [position] to the supplied [viewHolder]
*/
override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
val item = getItem(position)
if (item is LogItem) { if (item is LogItem) {
viewHolder.txtTitle!!.text = item.message val holder = viewHolder as ItemViewHolder
if (!compact) viewHolder.txtSub!!.text = item.level
holder.title.text = item.message
holder.subtitle?.text = item.level
holder.parent.tag = item
} else if (item is BaseHeader) { } else if (item is BaseHeader) {
viewHolder.txtTitle!!.text = item.title val holder = viewHolder as HeaderViewHolder
}
viewHolder.position = position
return view
}
private class ViewHolder { holder.header.text = item.title
var txtTitle: TextView? = null }
var txtSub: TextView? = null
var position = 0
} }
} }

View File

@ -0,0 +1,14 @@
# Credits (Drawables)
## [Material Design Icons](https://material.io/resources/icons) (Apache-2)
* ic_clear
* ic_log
* ic_open
* ic_play
* ic_refresh
* ic_search
* ic_settings
* ic_share
## [Default Icon](https://github.com/switchbrew/libnx/blob/master/nx/default_icon.jpg)
We've recieved permission to use the icon from it's author [jaames](https://github.com/jaames)
## Skyline Logo
Skyline's logo was designed by [PixelyIon](https://github.com/PixelyIon)

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@ -1,7 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/app_item_linear"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical" android:orientation="vertical"
android:padding="15dp"> android:padding="15dp">
@ -12,20 +17,21 @@
android:layout_alignParentStart="true" android:layout_alignParentStart="true"
android:layout_alignParentTop="false" android:layout_alignParentTop="false"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:contentDescription="@string/icon" android:layout_marginEnd="10dp"
android:src="@drawable/ic_missing" /> android:contentDescription="@string/icon" />
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="60dp" android:layout_alignTop="@id/icon"
android:layout_marginTop="5dp" android:layout_toEndOf="@id/icon"
android:textAppearance="?android:attr/textAppearanceListItem" /> android:textAppearance="?android:attr/textAppearanceListItem"
tools:ignore="RelativeOverlap" />
<TextView <TextView
android:id="@+id/text_subtitle" android:id="@+id/text_subtitle"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_below="@id/text_title" android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title" android:layout_alignStart="@id/text_title"

View File

@ -18,10 +18,10 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<ListView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/log_list" android:id="@+id/log_list"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="wrap_content"
android:fastScrollEnabled="true" android:fastScrollEnabled="true"
android:transcriptMode="normal" android:transcriptMode="normal"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

View File

@ -4,7 +4,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:orientation="vertical" android:orientation="vertical"
android:padding="6dp"> android:padding="1dp">
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"

View File

@ -8,6 +8,8 @@
<androidx.appcompat.widget.Toolbar <androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"

View File

@ -3,8 +3,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clickable="false" android:clickable="false"
android:orientation="vertical" android:padding="10dp">
android:padding="12dp">
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_title"
@ -14,6 +13,6 @@
android:layout_marginTop="2dp" android:layout_marginTop="2dp"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:textColor="?colorSecondary" android:textColor="?colorSecondary"
android:textSize="13sp" /> android:textSize="15sp" />
</RelativeLayout> </RelativeLayout>