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 androidx.appcompat.app.AppCompatActivity
import emu.skyline.loader.getRomFormat
import kotlinx.android.synthetic.main.game_activity.*
import kotlinx.android.synthetic.main.app_activity.*
import java.io.File
/**

View File

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

View File

@ -18,6 +18,9 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView
import androidx.documentfile.provider.DocumentFile
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 emu.skyline.adapter.AppAdapter
import emu.skyline.adapter.AppItem
@ -128,12 +131,12 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
setSupportActionBar(toolbar)
open_fab.setOnClickListener(this)
log_fab.setOnClickListener(this)
game_list.adapter = adapter
game_list.onItemClickListener = OnItemClickListener { parent: AdapterView<*>, _: View?, position: Int, _: Long ->
val item = parent.getItemAtPosition(position)
if (item is AppItem) {
val intent = Intent(this, EmulationActivity::class.java)
intent.data = item.uri
adapter = AppAdapter(this)
app_list.adapter = adapter
app_list.layoutManager = LinearLayoutManager(this)
app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
startActivity(intent)
}
}
@ -181,9 +184,35 @@ class MainActivity : AppCompatActivity(), View.OnClickListener {
intent.type = "*/*"
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 {
return when (item.itemId) {
R.id.action_settings -> {

View File

@ -19,7 +19,10 @@ import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.TextView
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
/**
@ -36,7 +39,7 @@ class AppItem(val meta: AppEntry) : BaseItem() {
* The title of the application
*/
val title: String
get() = meta.name + " (" + type + ")"
get() = meta.name
/**
* 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
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
}
@ -64,29 +70,41 @@ class AppItem(val meta: AppEntry) : BaseItem() {
/**
* 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) {
super.addHeader(BaseHeader(string))
}
/**
* The onClick handler, it's for displaying the icon preview
*
* @param view The specific view that was clicked
* 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)
imageView.setImageBitmap(item.icon ?: missingIcon)
builder.addContentView(imageView, RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
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
*
* @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
* 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 icon The ImageView associated with the icon
* @param title The TextView associated with the title
* @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
import android.util.SparseIntArray
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.Filter
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.JaroWinkler
import java.io.*
@ -18,6 +17,9 @@ import java.util.*
import kotlin.collections.ArrayList
/**
* An enumeration of the type of elements in this adapter
*/
enum class ElementType(val type: Int) {
Header(0x0),
Item(0x1)
@ -43,13 +45,30 @@ abstract class BaseItem : BaseElement(ElementType.Item) {
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()
/**
* 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 = ""
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())
filter.filter(searchTerm)
else {
@ -58,18 +77,19 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
}
}
fun addHeader(element: HeaderType) {
elementArray.add(element)
if (searchTerm.isNotEmpty())
filter.filter(searchTerm)
else {
/**
* 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)
notifyDataSetChanged()
}
notifyDataSetChanged()
}
internal inner class State(val elementArray: ArrayList<BaseElement?>) : Serializable
/**
* This serializes [elementArray] into [file]
*/
@Throws(IOException::class)
fun save(file: File) {
val fileObj = FileOutputStream(file)
@ -79,6 +99,9 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
fileObj.close()
}
/**
* This reads in [elementArray] from [file]
*/
@Throws(IOException::class, ClassNotFoundException::class)
open fun load(file: File) {
val fileObj = FileInputStream(file)
@ -90,82 +113,124 @@ internal abstract class HeaderAdapter<ItemType : BaseItem?, HeaderType : BaseHea
filter.filter(searchTerm)
}
/**
* This clears the view by clearing [elementArray] and [visibleArray]
*/
fun clear() {
elementArray.clear()
visibleArray.clear()
notifyDataSetChanged()
}
override fun getCount(): Int {
return visibleArray.size
}
override fun getItem(index: Int): BaseElement? {
return elementArray[visibleArray[index]]
}
override fun getItemId(position: Int): Long {
return position.toLong()
/**
* 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.type
}
override fun getViewTypeCount(): Int {
return ElementType.values().size
}
abstract override fun getView(position: Int, convertView: View?, parent: ViewGroup): View
/**
* 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() {
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> {
val scoredItems : MutableList<ScoredItem> = ArrayList()
val scoredItems: MutableList<ScoredItem> = ArrayList()
val jw = JaroWinkler()
val cos = Cosine()
keyArray.forEachIndexed { index, item ->
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 })
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()
searchTerm = (charSequence as String).toLowerCase(Locale.getDefault())
if (charSequence.isEmpty()) {
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) {
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()
}
}

View File

@ -14,77 +14,118 @@ 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
/**
* This class is used to hold all data about a log entry
*/
internal class LogItem(val message: String, val level: String) : BaseItem() {
/**
* The log message itself is used as the search key
*/
override fun key(): String? {
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
/**
* This function adds a line to this log adapter
*/
fun add(logLine: String) {
try {
val logMeta = logLine.split("|", limit = 3)
if (logMeta[0].startsWith("1")) {
val level = logMeta[1].toInt()
if (level > debug_level) return
addItem(LogItem(logMeta[2].replace('\\', '\n'), level_str[level]))
} else {
addHeader(BaseHeader(logMeta[1]))
}
} 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 {
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 + ")"))
Toast.makeText(view.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
return false
}
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()
val inflater = LayoutInflater.from(context)
if (item is LogItem) {
if (compact) {
view = inflater.inflate(R.layout.log_item_compact, parent, false)
viewHolder.txtTitle = 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)
}
/**
* 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)
} 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 {
viewHolder = view.tag as ViewHolder
} 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) {
viewHolder.txtTitle!!.text = item.message
if (!compact) viewHolder.txtSub!!.text = item.level
val holder = viewHolder as ItemViewHolder
holder.title.text = item.message
holder.subtitle?.text = item.level
holder.parent.tag = item
} else if (item is BaseHeader) {
viewHolder.txtTitle!!.text = item.title
val holder = viewHolder as HeaderViewHolder
holder.header.text = item.title
}
viewHolder.position = position
return view
}
private class ViewHolder {
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"?>
<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_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:orientation="vertical"
android:padding="15dp">
@ -12,20 +17,21 @@
android:layout_alignParentStart="true"
android:layout_alignParentTop="false"
android:layout_centerVertical="true"
android:contentDescription="@string/icon"
android:src="@drawable/ic_missing" />
android:layout_marginEnd="10dp"
android:contentDescription="@string/icon" />
<TextView
android:id="@+id/text_title"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="60dp"
android:layout_marginTop="5dp"
android:textAppearance="?android:attr/textAppearanceListItem" />
android:layout_alignTop="@id/icon"
android:layout_toEndOf="@id/icon"
android:textAppearance="?android:attr/textAppearanceListItem"
tools:ignore="RelativeOverlap" />
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/text_title"
android:layout_alignStart="@id/text_title"

View File

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

View File

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

View File

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

View File

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