Redesign UI

* Change colors
* Replace toolbar in main activity
* Initial implementation of ViewModel
This commit is contained in:
Willi Ye 2021-01-30 14:59:11 +01:00 committed by ◱ Mark
parent 80ab22a627
commit 9f6a5df5e0
51 changed files with 1014 additions and 772 deletions

View File

@ -68,6 +68,7 @@ android {
/* NDK */ /* NDK */
ndkVersion '22.0.7026061' ndkVersion '22.0.7026061'
externalNativeBuild { externalNativeBuild {
cmake { cmake {
version '3.18.1+' version '3.18.1+'
@ -85,15 +86,22 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
/* Google */ /* Google */
def lifecycle_version = "2.2.0"
implementation "androidx.core:core-ktx:1.3.2"
implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'com.google.android.material:material:1.3.0' implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.documentfile:documentfile:1.0.1' implementation 'androidx.documentfile:documentfile:1.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation 'androidx.fragment:fragment-ktx:1.2.5'
/* Kotlin */ /* Kotlin */
implementation "androidx.core:core-ktx:1.3.2" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
/* Other Java */ /* Other Java */
implementation 'info.debatty:java-string-similarity:2.0.0' implementation 'info.debatty:java-string-similarity:2.0.0'

View File

@ -38,12 +38,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
*/ */
private var vibrators = HashMap<Int, Vibrator>() private var vibrators = HashMap<Int, Vibrator>()
/**
* The surface object used for displaying frames
*/
@Volatile
private var surface : Surface? = null
/** /**
* A boolean flag denoting if the emulation thread should call finish() or not * A boolean flag denoting if the emulation thread should call finish() or not
*/ */
@ -262,14 +256,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
vibrators.clear() vibrators.clear()
} }
/**
* This sets [surface] to [holder].surface and passes it into libskyline
*/
override fun surfaceCreated(holder : SurfaceHolder) { override fun surfaceCreated(holder : SurfaceHolder) {
Log.d(Tag, "surfaceCreated Holder: $holder") Log.d(Tag, "surfaceCreated Holder: $holder")
surface = holder.surface
while (emulationThread.isAlive) while (emulationThread.isAlive)
if (setSurface(surface)) if (setSurface(holder.surface))
return return
} }
@ -280,14 +270,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height") Log.d(Tag, "surfaceChanged Holder: $holder, Format: $format, Width: $width, Height: $height")
} }
/**
* This sets [surface] to null and passes it into libskyline
*/
override fun surfaceDestroyed(holder : SurfaceHolder) { override fun surfaceDestroyed(holder : SurfaceHolder) {
Log.d(Tag, "surfaceDestroyed Holder: $holder") Log.d(Tag, "surfaceDestroyed Holder: $holder")
surface = null
while (emulationThread.isAlive) while (emulationThread.isAlive)
if (setSurface(surface)) if (setSurface(null))
return return
} }

View File

@ -32,54 +32,49 @@ object KeyReader {
return false return false
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() ?: false) if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() == true)
return false return false
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp") val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
val inputStream = context.contentResolver.openInputStream(uri) val inputStream = context.contentResolver.openInputStream(uri)
val outputStream = tmpOutputFile.bufferedWriter() tmpOutputFile.bufferedWriter().use { writer ->
val valid = inputStream!!.bufferedReader().useLines {
for (line in it) {
val pair = line.split("=")
if (pair.size != 2)
return@useLines false
val valid = inputStream!!.bufferedReader().useLines { val key = pair[0].trim()
for (line in it) { val value = pair[1].trim()
val pair = line.split("=") when (keyType) {
if (pair.size != 2) KeyType.Title -> {
return@useLines false if (key.length != 32 && !isHexString(key))
return@useLines false
if (value.length != 32 && !isHexString(value))
return@useLines false
}
KeyType.Prod -> {
if (!key.contains("_"))
return@useLines false
if (!isHexString(value))
return@useLines false
}
}
val key = pair[0].trim() writer.append("$key=$value\n")
val value = pair[1].trim()
when (keyType) {
KeyType.Title -> {
if (key.length != 32 && !isHexString(key))
return@useLines false
if (value.length != 32 && !isHexString(value))
return@useLines false
}
KeyType.Prod -> {
if (!key.contains("_"))
return@useLines false
if (!isHexString(value))
return@useLines false
}
} }
true
outputStream.append("$key=$value\n")
} }
true
if (valid) tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
return valid
} }
outputStream.flush()
outputStream.close()
if (valid)
tmpOutputFile.renameTo(File("${tmpOutputFile.parent}/${keyType.fileName}"))
return valid
} }
private fun isHexString(str : String) : Boolean { private fun isHexString(str : String) : Boolean {
for (c in str) for (c in str)
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) return false
return false
return true return true
} }
} }

View File

@ -66,22 +66,23 @@ class LogActivity : AppCompatActivity() {
try { try {
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log") logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
logFile.forEachLine { logLine -> adapter.setItems(logFile.readLines().mapNotNull { logLine ->
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 > logLevel) return@forEachLine if (level > logLevel) return@mapNotNull null
adapter.addItem(LogViewItem(compact, "(" + logMeta[2] + ") " + logMeta[3].replace('\\', '\n'), logLevels[level])) return@mapNotNull LogViewItem(compact, "(" + logMeta[2] + ") " + logMeta[3].replace('\\', '\n'), logLevels[level])
} else { } else {
adapter.addItem(HeaderViewItem(logMeta[1])) return@mapNotNull HeaderViewItem(logMeta[1])
} }
} catch (ignored : IndexOutOfBoundsException) { } catch (ignored : IndexOutOfBoundsException) {
} catch (ignored : NumberFormatException) { } catch (ignored : NumberFormatException) {
} }
} null
})
} catch (e : FileNotFoundException) { } catch (e : FileNotFoundException) {
Log.w("Logger", "IO Error during access of log file: " + e.message) Log.w("Logger", "IO Error during access of log file: " + e.message)
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show() Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()

View File

@ -5,22 +5,20 @@
package emu.skyline package emu.skyline
import android.animation.ObjectAnimator
import android.content.Intent import android.content.Intent
import android.content.pm.ActivityInfo import android.graphics.Color
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.Menu
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.SearchView
import androidx.core.animation.doOnEnd
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.res.use
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
import androidx.documentfile.provider.DocumentFile import androidx.core.view.size
import androidx.lifecycle.observe
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
@ -34,222 +32,137 @@ import emu.skyline.data.AppItem
import emu.skyline.data.DataItem import emu.skyline.data.DataItem
import emu.skyline.data.HeaderItem import emu.skyline.data.HeaderItem
import emu.skyline.loader.LoaderResult import emu.skyline.loader.LoaderResult
import emu.skyline.loader.RomFile
import emu.skyline.loader.RomFormat
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import emu.skyline.utils.loadSerializedList
import emu.skyline.utils.serialize
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.titlebar.*
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.concurrent.thread
import kotlin.math.ceil import kotlin.math.ceil
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
companion object {
private val TAG = MainActivity::class.java.simpleName
}
private val settings by lazy { Settings(this) } private val settings by lazy { Settings(this) }
/**
* The adapter used for adding elements to [app_list]
*/
private val adapter = GenericAdapter() private val adapter = GenericAdapter()
private var reloading = AtomicBoolean()
private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()] private val layoutType get() = LayoutType.values()[settings.layoutType.toInt()]
private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) } private val missingIcon by lazy { ContextCompat.getDrawable(this, R.drawable.default_icon)!!.toBitmap(256, 256) }
private val viewModel by viewModels<MainViewModel>()
private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog) private fun AppItem.toViewItem() = AppViewItem(layoutType, this, missingIcon, ::selectStartGame, ::selectShowGameDialog)
/**
* This adds all files in [directory] with [extension] as an entry in [adapter] using [RomFile] to load metadata
*/
private fun addEntries(extension : String, romFormat : RomFormat, directory : DocumentFile, romElements : ArrayList<DataItem>, found : Boolean = false) : Boolean {
var foundCurrent = found
directory.listFiles().forEach { file ->
if (file.isDirectory) {
foundCurrent = addEntries(extension, romFormat, file, romElements, foundCurrent)
} else {
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
RomFile(this, romFormat, file.uri).let { romFile ->
val finalFoundCurrent = foundCurrent
runOnUiThread {
if (!finalFoundCurrent) {
romElements.add(HeaderItem(romFormat.name))
adapter.addItem(HeaderViewItem(romFormat.name))
}
romElements.add(AppItem(romFile.appEntry).also {
adapter.addItem(it.toViewItem())
})
}
foundCurrent = true
}
}
}
}
return foundCurrent
}
/**
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
*
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
*/
private fun refreshAdapter(loadFromFile : Boolean) {
val romsFile = File(applicationContext.filesDir.canonicalPath + "/roms.bin")
if (loadFromFile) {
try {
loadSerializedList<DataItem>(romsFile).forEach {
if (it is HeaderItem)
adapter.addItem(HeaderViewItem(it.title))
else if (it is AppItem)
adapter.addItem(it.toViewItem())
}
return
} catch (e : Exception) {
Log.w(TAG, "Ran into exception while loading: ${e.message}")
}
}
if (reloading.getAndSet(true)) return
thread(start = true) {
val snackbar = Snackbar.make(coordinatorLayout, getString(R.string.searching_roms), Snackbar.LENGTH_INDEFINITE)
runOnUiThread {
snackbar.show()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
}
try {
runOnUiThread { adapter.removeAllItems() }
val searchLocation = DocumentFile.fromTreeUri(this, Uri.parse(settings.searchLocation))!!
val romElements = ArrayList<DataItem>()
addEntries("nro", RomFormat.NRO, searchLocation, romElements)
addEntries("nso", RomFormat.NSO, searchLocation, romElements)
addEntries("nca", RomFormat.NCA, searchLocation, romElements)
addEntries("xci", RomFormat.XCI, searchLocation, romElements)
addEntries("nsp", RomFormat.NSP, searchLocation, romElements)
runOnUiThread {
if (romElements.isEmpty()) {
romElements.add(HeaderItem(getString(R.string.no_rom)))
adapter.addItem(HeaderViewItem(getString(R.string.no_rom)))
}
try {
romElements.serialize(romsFile)
} catch (e : IOException) {
Log.w(TAG, "Ran into exception while saving: ${e.message}")
}
}
settings.refreshRequired = false
} catch (e : IllegalArgumentException) {
runOnUiThread {
settings.searchLocation = ""
val intent = intent
finish()
startActivity(intent)
}
} catch (e : Exception) {
runOnUiThread {
Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${e.localizedMessage}", Snackbar.LENGTH_SHORT).show()
}
}
runOnUiThread {
snackbar.dismiss()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
reloading.set(false)
}
}
/**
* This initializes [toolbar], [open_fab], [log_fab] and [app_list]
*/
override fun onCreate(savedInstanceState : Bundle?) { override fun onCreate(savedInstanceState : Bundle?) {
AppCompatDelegate.setDefaultNightMode(
when ((settings.appTheme.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
}
)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity) setContentView(R.layout.main_activity)
setSupportActionBar(toolbar)
PreferenceManager.setDefaultValues(this, R.xml.preferences, false) PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
AppCompatDelegate.setDefaultNightMode(when ((settings.appTheme.toInt())) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
2 -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> AppCompatDelegate.MODE_NIGHT_UNSPECIFIED
})
refresh_fab.setOnClickListener { refreshAdapter(false) }
settings_fab.setOnClickListener { startActivityForResult(Intent(this, SettingsActivity::class.java), 3) }
open_fab.setOnClickListener {
startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "*/*"
}, 2)
}
log_fab.setOnClickListener { startActivity(Intent(this, LogActivity::class.java)) }
setupAppList() setupAppList()
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() { swipe_refresh_layout.apply {
var y = 0 setProgressBackgroundColorSchemeColor(obtainStyledAttributes(intArrayOf(R.attr.colorPrimary)).use { it.getColor(0, Color.BLACK) })
setColorSchemeColors(obtainStyledAttributes(intArrayOf(R.attr.colorAccent)).use { it.getColor(0, Color.BLACK) })
post { setDistanceToTriggerSync(swipe_refresh_layout.height / 3) }
setOnRefreshListener { loadRoms(false) }
}
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) { viewModel.state.observe(owner = this, onChanged = ::handleState)
y += dy loadRoms(!settings.refreshRequired)
if (!app_list.isInTouchMode) search_bar.apply {
toolbar_layout.setExpanded(y == 0) setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
super.onScrolled(recyclerView, dx, dy) setRefreshIconListener { loadRoms(false) }
addTextChangedListener(afterTextChanged = { editable ->
editable?.let { text -> adapter.filter.filter(text.toString()) }
})
if (!viewModel.searchBarAnimated) {
viewModel.searchBarAnimated = true
post { startTitleAnimation() }
} }
}) }
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
search_bar.refreshIconVisible = !isInTouchMode
}
}
val controllerFabX = controller_fabs.translationX private inner class GridSpacingItemDecoration : RecyclerView.ItemDecoration() {
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { private val padding = resources.getDimensionPixelSize(R.dimen.grid_padding)
if (!it) {
toolbar_layout.setExpanded(false)
controller_fabs.visibility = View.VISIBLE override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) {
ObjectAnimator.ofFloat(controller_fabs, "translationX", 0f).apply { super.getItemOffsets(outRect, view, parent, state)
duration = 250
start() val gridLayoutManager = parent.layoutManager as GridLayoutManager
val layoutParams = view.layoutParams as GridLayoutManager.LayoutParams
when (layoutParams.spanIndex) {
0 -> outRect.left = padding
gridLayoutManager.spanCount - 1 -> outRect.right = padding
else -> {
outRect.left = padding / 2
outRect.right = padding / 2
} }
} else {
ObjectAnimator.ofFloat(controller_fabs, "translationX", controllerFabX).apply {
duration = 250
start()
}.doOnEnd { controller_fabs.visibility = View.GONE }
} }
if (layoutParams.spanSize == gridLayoutManager.spanCount) outRect.right = padding
} }
} }
private fun setAppListDecoration() { private fun setAppListDecoration() {
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
when (layoutType) { when (layoutType) {
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
LayoutType.Grid, LayoutType.GridCompact -> if (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0) LayoutType.Grid, LayoutType.GridCompact -> app_list.addItemDecoration(GridSpacingItemDecoration())
}
}
/**
* This layout manager handles situations where [onFocusSearchFailed] gets called, when possible we always want to focus on the item with the same span index
*/
private inner class CustomLayoutManager(gridSpan : Int) : GridLayoutManager(this, gridSpan) {
init {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
}
}
override fun onFocusSearchFailed(focused : View, focusDirection : Int, recycler : RecyclerView.Recycler, state : RecyclerView.State) : View? {
val nextFocus = super.onFocusSearchFailed(focused, focusDirection, recycler, state)
when (focusDirection) {
View.FOCUS_DOWN -> {
findContainingItemView(focused)?.let { focusedChild ->
val current = app_list.indexOfChild(focusedChild)
val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex
for (i in current + 1 until app_list.size) {
val candidate = getChildAt(i)!!
// Return candidate when span index matches
if (currentSpanIndex == (candidate.layoutParams as LayoutParams).spanIndex) return candidate
}
if (nextFocus == null) {
app_bar_layout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible
app_list.smoothScrollToPosition(adapter.itemCount)
}
}
}
View.FOCUS_UP -> {
if (nextFocus?.isFocusable != true) {
search_bar.requestFocus()
app_bar_layout.setExpanded(true)
return null
}
}
}
return nextFocus
} }
} }
@ -260,11 +173,7 @@ class MainActivity : AppCompatActivity() {
val metrics = resources.displayMetrics val metrics = resources.displayMetrics
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt() val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply { app_list.layoutManager = CustomLayoutManager(gridSpan)
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
}
}
setAppListDecoration() setAppListDecoration()
if (settings.searchLocation.isEmpty()) { if (settings.searchLocation.isEmpty()) {
@ -272,35 +181,24 @@ class MainActivity : AppCompatActivity() {
intent.flags = Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION or Intent.FLAG_GRANT_PREFIX_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION 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) startActivityForResult(intent, 1)
} else {
refreshAdapter(!settings.refreshRequired)
} }
} }
/** private fun handleState(state : MainState) = when (state) {
* This inflates the layout for the menu [R.menu.toolbar_main] and sets up searching the logs MainState.Loading -> {
*/ search_bar.animateRefreshIcon()
override fun onCreateOptionsMenu(menu : Menu) : Boolean { swipe_refresh_layout.isRefreshing = true
menuInflater.inflate(R.menu.toolbar_main, menu) }
is MainState.Loaded -> {
val searchView = menu.findItem(R.id.action_search_main).actionView as SearchView swipe_refresh_layout.isRefreshing = false
populateAdapter(state.items)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { }
override fun onQueryTextSubmit(query : String) : Boolean { is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
searchView.clearFocus()
return false
}
override fun onQueryTextChange(newText : String) : Boolean {
adapter.filter.filter(newText)
return true
}
})
return super.onCreateOptionsMenu(menu)
} }
private fun selectStartGame(appItem : AppItem) { private fun selectStartGame(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
if (settings.selectAction) if (settings.selectAction)
AppDialog.newInstance(appItem).show(supportFragmentManager, "game") AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
else if (appItem.loaderResult == LoaderResult.Success) else if (appItem.loaderResult == LoaderResult.Success)
@ -308,26 +206,24 @@ class MainActivity : AppCompatActivity() {
} }
private fun selectShowGameDialog(appItem : AppItem) { private fun selectShowGameDialog(appItem : AppItem) {
if (swipe_refresh_layout.isRefreshing) return
AppDialog.newInstance(appItem).show(supportFragmentManager, "game") AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
} }
/** private fun loadRoms(loadFromFile : Boolean) {
* This handles menu interaction for [R.id.action_settings] and [R.id.action_refresh] viewModel.loadRoms(this, loadFromFile, Uri.parse(settings.searchLocation))
*/ settings.refreshRequired = false
override fun onOptionsItemSelected(item : MenuItem) : Boolean { }
return when (item.itemId) {
R.id.action_settings -> {
startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
true
}
R.id.action_refresh -> { private fun populateAdapter(items : List<DataItem>) {
refreshAdapter(false) adapter.setItems(items.map {
true when (it) {
is HeaderItem -> HeaderViewItem(it.title)
is AppItem -> it.toViewItem()
} }
})
else -> super.onOptionsItemSelected(item) if (items.isEmpty()) adapter.setItems(listOf(HeaderViewItem(getString(R.string.no_rom))))
}
} }
/** /**
@ -343,7 +239,7 @@ class MainActivity : AppCompatActivity() {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
settings.searchLocation = uri.toString() settings.searchLocation = uri.toString()
refreshAdapter(!settings.refreshRequired) loadRoms(!settings.refreshRequired)
} }
2 -> { 2 -> {
@ -360,9 +256,7 @@ class MainActivity : AppCompatActivity() {
} }
} }
3 -> { 3 -> if (settings.refreshRequired) loadRoms(false)
if (settings.refreshRequired) refreshAdapter(false)
}
} }
} }
} }
@ -371,7 +265,7 @@ class MainActivity : AppCompatActivity() {
super.onResume() super.onResume()
var layoutTypeChanged = false var layoutTypeChanged = false
for (appViewItem in adapter.allItems.filterIsInstance(AppViewItem::class.java)) { for (appViewItem in adapter.currentItems.filterIsInstance(AppViewItem::class.java)) {
if (layoutType != appViewItem.layoutType) { if (layoutType != appViewItem.layoutType) {
appViewItem.layoutType = layoutType appViewItem.layoutType = layoutType
layoutTypeChanged = true layoutTypeChanged = true
@ -381,14 +275,19 @@ class MainActivity : AppCompatActivity() {
} }
if (layoutTypeChanged) { if (layoutTypeChanged) {
adapter.notifyAllItemsChanged()
setAppListDecoration() setAppListDecoration()
adapter.notifyItemRangeChanged(0, adapter.currentItems.size)
} }
}
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half) override fun onBackPressed() {
when (layoutType) { search_bar.apply {
LayoutType.List -> app_list.post { app_list.setPadding(0, 0, 0, fab_parent.height) } if (hasFocus() && text.isNotEmpty()) {
LayoutType.Grid, LayoutType.GridCompact -> app_list.post { app_list.setPadding(gridCardMagin, 0, gridCardMagin, fab_parent.height) } text = ""
clearFocus()
} else {
super.onBackPressed()
}
} }
} }
} }

View File

@ -0,0 +1,107 @@
package emu.skyline
import android.content.Context
import android.net.Uri
import android.util.Log
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import emu.skyline.data.AppItem
import emu.skyline.data.DataItem
import emu.skyline.data.HeaderItem
import emu.skyline.loader.RomFile
import emu.skyline.loader.RomFormat
import emu.skyline.utils.loadSerializedList
import emu.skyline.utils.serialize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.io.IOException
sealed class MainState {
object Loading : MainState()
class Loaded(val items : List<DataItem>) : MainState()
class Error(val ex : Exception) : MainState()
}
class MainViewModel : ViewModel() {
companion object {
private val TAG = MainViewModel::class.java.simpleName
}
private val mutableState = MutableLiveData<MainState>()
val state : LiveData<MainState> = mutableState
var searchBarAnimated = false
/**
* This adds all files in [directory] with [extension] as an entry using [RomFile] to load metadata
*/
private fun addEntries(context : Context, extension : String, romFormat : RomFormat, directory : DocumentFile, romElements : ArrayList<DataItem>, found : Boolean = false) : Boolean {
var foundCurrent = found
directory.listFiles().forEach { file ->
if (file.isDirectory) {
foundCurrent = addEntries(context, extension, romFormat, file, romElements, foundCurrent)
} else {
if (extension.equals(file.name?.substringAfterLast("."), ignoreCase = true)) {
RomFile(context, romFormat, file.uri).let { romFile ->
if (!foundCurrent) romElements.add(HeaderItem(romFormat.name))
romElements.add(AppItem(romFile.appEntry))
foundCurrent = true
}
}
}
}
return foundCurrent
}
/**
* This refreshes the contents of the adapter by either trying to load cached adapter data or searches for them to recreate a list
*
* @param loadFromFile If this is false then trying to load cached adapter data is skipped entirely
*/
fun loadRoms(context : Context, loadFromFile : Boolean, searchLocation : Uri) {
if (mutableState.value == MainState.Loading) return
mutableState.postValue(MainState.Loading)
val romsFile = File(context.filesDir.canonicalPath + "/roms.bin")
viewModelScope.launch(Dispatchers.IO) {
if (loadFromFile) {
try {
mutableState.postValue(MainState.Loaded(loadSerializedList(romsFile)))
return@launch
} catch (e : Exception) {
Log.w(TAG, "Ran into exception while loading: ${e.message}")
}
}
try {
val searchDocument = DocumentFile.fromTreeUri(context, searchLocation)!!
val romElements = ArrayList<DataItem>()
addEntries(context, "nro", RomFormat.NRO, searchDocument, romElements)
addEntries(context, "nso", RomFormat.NSO, searchDocument, romElements)
addEntries(context, "nca", RomFormat.NCA, searchDocument, romElements)
addEntries(context, "xci", RomFormat.XCI, searchDocument, romElements)
addEntries(context, "nsp", RomFormat.NSP, searchDocument, romElements)
try {
romElements.serialize(romsFile)
} catch (e : IOException) {
Log.w(TAG, "Ran into exception while saving: ${e.message}")
}
mutableState.postValue(MainState.Loaded(romElements))
} catch (e : Exception) {
mutableState.postValue(MainState.Error(e))
}
}
}
}

View File

@ -13,7 +13,6 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceGroup import androidx.preference.PreferenceGroup
import emu.skyline.preference.ActivityResultDelegate import emu.skyline.preference.ActivityResultDelegate
import emu.skyline.preference.DocumentActivity import emu.skyline.preference.DocumentActivity
import kotlinx.android.synthetic.main.settings_activity.*
import kotlinx.android.synthetic.main.titlebar.* import kotlinx.android.synthetic.main.titlebar.*
class SettingsActivity : AppCompatActivity() { class SettingsActivity : AppCompatActivity() {
@ -46,8 +45,6 @@ class SettingsActivity : AppCompatActivity() {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
preferenceFragment.delegateActivityResult(requestCode, resultCode, data) preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
settings
} }
/** /**
@ -101,4 +98,9 @@ class SettingsActivity : AppCompatActivity() {
return super.onKeyUp(keyCode, event) return super.onKeyUp(keyCode, event)
} }
override fun finish() {
setResult(RESULT_OK)
super.finish()
}
} }

View File

@ -35,7 +35,7 @@ private data class AppLayoutFactory(private val layoutType : LayoutType) : Gener
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false) override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(layoutType.layoutRes, parent, false)
} }
class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericViewHolderBinder() { class AppViewItem(var layoutType : LayoutType, private val item : AppItem, private val missingIcon : Bitmap, private val onClick : InteractionFunction, private val onLongClick : InteractionFunction) : GenericListItem() {
override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType) override fun getLayoutFactory() : GenericLayoutFactory = AppLayoutFactory(layoutType)
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder, position : Int) {
@ -48,10 +48,7 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) } holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
} }
when (layoutType) { holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
LayoutType.List -> holder.itemView
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
}.apply {
setOnClickListener { onClick.invoke(item) } setOnClickListener { onClick.invoke(item) }
setOnLongClickListener { true.also { onLongClick.invoke(item) } } setOnLongClickListener { true.also { onLongClick.invoke(item) } }
} }
@ -70,4 +67,8 @@ class AppViewItem(var layoutType : LayoutType, private val item : AppItem, priva
} }
override fun key() = item.key() override fun key() = item.key()
override fun areItemsTheSame(other : GenericListItem) = key() == other.key()
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item
} }

View File

@ -8,20 +8,30 @@ package emu.skyline.adapter
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Filter import android.widget.Filter
import android.widget.Filterable import android.widget.Filterable
import androidx.recyclerview.widget.AsyncListDiffer
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView 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.util.* import java.util.*
/** /**
* Can handle any view types with [GenericViewHolderBinder] implemented, [GenericViewHolderBinder] are differentiated by the return value of [GenericViewHolderBinder.getLayoutFactory] * Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getLayoutFactory]
*/ */
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable { class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
var currentSearchTerm = "" companion object {
private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem>() {
override fun areItemsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areItemsTheSame(newItem)
val currentItems get() = if (currentSearchTerm.isEmpty()) allItems else filteredItems override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem)
val allItems = mutableListOf<GenericViewHolderBinder>() }
private var filteredItems = listOf<GenericViewHolderBinder>() }
private val asyncListDiffer = AsyncListDiffer(this, DIFFER)
private val allItems = mutableListOf<GenericListItem>()
val currentItems : List<GenericListItem> get() = asyncListDiffer.currentList
var currentSearchTerm = ""
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>() private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
@ -38,19 +48,10 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size }) override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
fun addItem(item : GenericViewHolderBinder) { fun setItems(items : List<GenericListItem>) {
allItems.add(item)
notifyItemInserted(currentItems.size)
}
fun removeAllItems() {
val size = currentItems.size
allItems.clear() allItems.clear()
notifyItemRangeRemoved(0, size) allItems.addAll(items)
} filter.filter(currentSearchTerm)
fun notifyAllItemsChanged() {
notifyItemRangeChanged(0, currentItems.size)
} }
/** /**
@ -67,7 +68,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
*/ */
private val cos = Cosine() private val cos = Cosine()
inner class ScoredItem(val score : Double, val item : GenericViewHolderBinder) inner class ScoredItem(val score : Double, val item : GenericListItem)
/** /**
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm] * This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
@ -92,7 +93,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
results.values = allItems.toMutableList() results.values = allItems.toMutableList()
results.count = allItems.size results.count = allItems.size
} else { } else {
val filterData = mutableListOf<GenericViewHolderBinder>() val filterData = mutableListOf<GenericListItem>()
val topResults = extractSorted() val topResults = extractSorted()
val avgScore = topResults.sumByDouble { it.score } / topResults.size val avgScore = topResults.sumByDouble { it.score } / topResults.size
@ -111,9 +112,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
*/ */
override fun publishResults(charSequence : CharSequence, results : FilterResults) { override fun publishResults(charSequence : CharSequence, results : FilterResults) {
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
filteredItems = results.values as List<GenericViewHolderBinder> asyncListDiffer.submitList(results.values as List<GenericListItem>)
notifyDataSetChanged()
} }
} }
} }

View File

@ -16,7 +16,7 @@ interface GenericLayoutFactory {
fun createLayout(parent : ViewGroup) : View fun createLayout(parent : ViewGroup) : View
} }
abstract class GenericViewHolderBinder { abstract class GenericListItem {
var adapter : GenericAdapter? = null var adapter : GenericAdapter? = null
abstract fun getLayoutFactory() : GenericLayoutFactory abstract fun getLayoutFactory() : GenericLayoutFactory
@ -27,4 +27,11 @@ abstract class GenericViewHolderBinder {
* Used for filtering * Used for filtering
*/ */
open fun key() : String = "" open fun key() : String = ""
open fun areItemsTheSame(other : GenericListItem) = this == other
/**
* Will only be called when [areItemsTheSame] returns true, thus returning true by default
*/
open fun areContentsTheSame(other : GenericListItem) = true
} }

View File

@ -15,7 +15,7 @@ private object HeaderLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false) override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
} }
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() { class HeaderViewItem(private val text : String) : GenericListItem() {
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder, position : Int) {
@ -23,4 +23,6 @@ class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
} }
override fun toString() = "" override fun toString() = ""
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text
} }

View File

@ -18,7 +18,7 @@ private data class LogLayoutFactory(private val compact : Boolean) : GenericLayo
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false) override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(if (compact) R.layout.log_item_compact else R.layout.log_item, parent, false)
} }
class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericViewHolderBinder() { data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem() {
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact) override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder, position : Int) {

View File

@ -5,6 +5,7 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.ButtonGuestEvent import emu.skyline.input.ButtonGuestEvent
import emu.skyline.input.ButtonId import emu.skyline.input.ButtonId
@ -23,4 +24,8 @@ class ControllerButtonViewItem(private val controllerId : Int, val button : Butt
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button
} }

View File

@ -12,14 +12,14 @@ import androidx.core.view.isGone
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder import emu.skyline.adapter.GenericListItem
import kotlinx.android.synthetic.main.controller_checkbox_item.* import kotlinx.android.synthetic.main.controller_checkbox_item.*
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory { private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false) override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
} }
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericViewHolderBinder() { class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericListItem() {
override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory override fun getLayoutFactory() : GenericLayoutFactory = ControllerCheckBoxLayoutFactory
override fun bind(holder : GenericViewHolder, position : Int) { override fun bind(holder : GenericViewHolder, position : Int) {

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.GeneralType import emu.skyline.input.GeneralType
import emu.skyline.input.InputManager import emu.skyline.input.InputManager
@ -38,4 +39,8 @@ class ControllerGeneralViewItem(private val controllerId : Int, val type : Gener
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type
} }

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.AxisGuestEvent import emu.skyline.input.AxisGuestEvent
import emu.skyline.input.ButtonGuestEvent import emu.skyline.input.ButtonGuestEvent
@ -40,4 +41,8 @@ class ControllerStickViewItem(private val controllerId : Int, val stick : StickI
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerStickViewItem && controllerId == other.controllerId
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick
} }

View File

@ -6,6 +6,7 @@
package emu.skyline.adapter.controller package emu.skyline.adapter.controller
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.input.ControllerType import emu.skyline.input.ControllerType
@ -23,4 +24,8 @@ class ControllerTypeViewItem(private val type : ControllerType, private val onCl
holder.itemView.setOnClickListener { onClick.invoke(this, position) } holder.itemView.setOnClickListener { onClick.invoke(this, position) }
} }
override fun areItemsTheSame(other : GenericListItem) = other is ControllerTypeViewItem
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type
} }

View File

@ -11,15 +11,15 @@ import android.view.ViewGroup
import androidx.core.view.isGone import androidx.core.view.isGone
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericLayoutFactory import emu.skyline.adapter.GenericLayoutFactory
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.GenericViewHolder import emu.skyline.adapter.GenericViewHolder
import emu.skyline.adapter.GenericViewHolderBinder
import kotlinx.android.synthetic.main.controller_item.* import kotlinx.android.synthetic.main.controller_item.*
private object ControllerLayoutFactory : GenericLayoutFactory { private object ControllerLayoutFactory : GenericLayoutFactory {
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false) override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
} }
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericViewHolderBinder() { open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem() {
private var position = -1 private var position = -1
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory

View File

@ -1,60 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.data
import android.content.Context
import emu.skyline.R
import emu.skyline.loader.AppEntry
import emu.skyline.loader.LoaderResult
/**
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
*/
class AppItem(private val meta : AppEntry) : DataItem {
/**
* The icon of the application
*/
val icon get() = meta.icon
/**
* The title of the application
*/
val title get() = meta.name
/**
* The string used as the sub-title, we currently use the author
*/
val subTitle get() = meta.author
/**
* The URI of the application's image file
*/
val uri get() = meta.uri
/**
* The format of the application ROM as a string
*/
private val type get() = meta.format.name
val loaderResult get() = meta.loaderResult
fun loaderResultString(context : Context) = context.getString(when (meta.loaderResult) {
LoaderResult.Success -> R.string.metadata_missing
LoaderResult.ParsingError -> R.string.invalid_file
LoaderResult.MissingTitleKey -> R.string.missing_title_key
LoaderResult.MissingHeaderKey,
LoaderResult.MissingTitleKek,
LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys
})
/**
* The name and author is used as the key
*/
fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
}

View File

@ -5,6 +5,56 @@
package emu.skyline.data package emu.skyline.data
import android.content.Context
import emu.skyline.R
import emu.skyline.loader.AppEntry
import emu.skyline.loader.LoaderResult
import java.io.Serializable import java.io.Serializable
interface DataItem : Serializable sealed class DataItem : Serializable
class HeaderItem(val title : String) : DataItem()
/**
* This class is a wrapper around [AppEntry], it is used for passing around game metadata
*/
data class AppItem(private val meta : AppEntry) : DataItem() {
/**
* The icon of the application
*/
val icon get() = meta.icon
/**
* The title of the application
*/
val title get() = meta.name
/**
* The string used as the sub-title, we currently use the author
*/
val subTitle get() = meta.author
/**
* The URI of the application's image file
*/
val uri get() = meta.uri
val loaderResult get() = meta.loaderResult
fun loaderResultString(context : Context) = context.getString(when (meta.loaderResult) {
LoaderResult.Success -> R.string.metadata_missing
LoaderResult.ParsingError -> R.string.invalid_file
LoaderResult.MissingTitleKey -> R.string.missing_title_key
LoaderResult.MissingHeaderKey,
LoaderResult.MissingTitleKek,
LoaderResult.MissingKeyArea -> R.string.incomplete_prod_keys
})
/**
* The name and author is used as the key
*/
fun key() = meta.name + if (meta.author != null) " ${meta.author}" else ""
}

View File

@ -1,8 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.data
class HeaderItem(val title : String) : DataItem

View File

@ -10,9 +10,11 @@ import android.os.Bundle
import android.view.KeyEvent import android.view.KeyEvent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import emu.skyline.R import emu.skyline.R
import emu.skyline.adapter.GenericAdapter import emu.skyline.adapter.GenericAdapter
import emu.skyline.adapter.GenericListItem
import emu.skyline.adapter.HeaderViewItem import emu.skyline.adapter.HeaderViewItem
import emu.skyline.adapter.controller.* import emu.skyline.adapter.controller.*
import emu.skyline.input.dialog.ButtonDialog import emu.skyline.input.dialog.ButtonDialog
@ -53,99 +55,103 @@ class ControllerActivity : AppCompatActivity() {
* This function updates the [adapter] based on information from [InputManager] * This function updates the [adapter] based on information from [InputManager]
*/ */
private fun update() { private fun update() {
adapter.removeAllItems() val items = mutableListOf<GenericListItem>()
val controller = InputManager.controllers[id]!! try {
val controller = InputManager.controllers[id]!!
adapter.addItem(ControllerTypeViewItem(controller.type, onControllerTypeClick)) items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick))
if (controller.type == ControllerType.None) if (controller.type == ControllerType.None)
return return
if (id == 0 && controller.type.firstController) { if (id == 0 && controller.type.firstController) {
adapter.addItem(HeaderViewItem(getString(R.string.osc))) items.add(HeaderViewItem(getString(R.string.osc)))
val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) } val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position -> items.add(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
item.summary = oscSummary.invoke(item.checked) item.summary = oscSummary.invoke(item.checked)
settings.onScreenControl = item.checked settings.onScreenControl = item.checked
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
}) })
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position -> items.add(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
settings.onScreenControlRecenterSticks = item.checked settings.onScreenControlRecenterSticks = item.checked
adapter.notifyItemChanged(position) adapter.notifyItemChanged(position)
}) })
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = { items.add(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
startActivity(Intent(this, OnScreenEditActivity::class.java)) startActivity(Intent(this, OnScreenEditActivity::class.java))
})) }))
} }
var wroteTitle = false var wroteTitle = false
for (item in GeneralType.values()) { for (item in GeneralType.values()) {
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) { if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
if (!wroteTitle) {
items.add(HeaderViewItem(getString(R.string.general)))
wroteTitle = true
}
items.add(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
}
}
wroteTitle = false
for (stick in controller.type.sticks) {
if (!wroteTitle) { if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(R.string.general))) items.add(HeaderViewItem(getString(R.string.sticks)))
wroteTitle = true wroteTitle = true
} }
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick)) val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
}
}
wroteTitle = false items.add(stickItem)
buttonMap[stick.button] = stickItem
for (stick in controller.type.sticks) { axisMap[stick.xAxis] = stickItem
if (!wroteTitle) { axisMap[stick.yAxis] = stickItem
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
wroteTitle = true
} }
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick) val dpadButtons = Pair(R.string.dpad, arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight))
val faceButtons = Pair(R.string.face_buttons, arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y))
val shoulderTriggerButtons = Pair(R.string.shoulder_trigger, arrayOf(ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR))
val shoulderRailButtons = Pair(R.string.shoulder_rail, arrayOf(ButtonId.LeftSL, ButtonId.LeftSR, ButtonId.RightSL, ButtonId.RightSR))
adapter.addItem(stickItem) val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons)
buttonMap[stick.button] = stickItem
axisMap[stick.xAxis] = stickItem
axisMap[stick.yAxis] = stickItem
}
val dpadButtons = Pair(R.string.dpad, arrayOf(ButtonId.DpadUp, ButtonId.DpadDown, ButtonId.DpadLeft, ButtonId.DpadRight)) for (buttonArray in buttonArrays) {
val faceButtons = Pair(R.string.face_buttons, arrayOf(ButtonId.A, ButtonId.B, ButtonId.X, ButtonId.Y)) wroteTitle = false
val shoulderTriggerButtons = Pair(R.string.shoulder_trigger, arrayOf(ButtonId.L, ButtonId.R, ButtonId.ZL, ButtonId.ZR))
val shoulderRailButtons = Pair(R.string.shoulder_rail, arrayOf(ButtonId.LeftSL, ButtonId.LeftSR, ButtonId.RightSL, ButtonId.RightSR))
val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons) for (button in controller.type.buttons.filter { it in buttonArray.second }) {
if (!wroteTitle) {
items.add(HeaderViewItem(getString(buttonArray.first)))
wroteTitle = true
}
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
items.add(buttonItem)
buttonMap[button] = buttonItem
}
}
for (buttonArray in buttonArrays) {
wroteTitle = false wroteTitle = false
for (button in controller.type.buttons.filter { it in buttonArray.second }) { for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
if (!wroteTitle) { if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(buttonArray.first))) items.add(HeaderViewItem(getString(R.string.misc_buttons)))
wroteTitle = true wroteTitle = true
} }
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick) val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem) items.add(buttonItem)
buttonMap[button] = buttonItem buttonMap[button] = buttonItem
} }
} } finally {
adapter.setItems(items)
wroteTitle = false
for (button in controller.type.buttons.filterNot { item -> buttonArrays.any { item in it.second } }.plus(ButtonId.Menu)) {
if (!wroteTitle) {
adapter.addItem(HeaderViewItem(getString(R.string.misc_buttons)))
wroteTitle = true
}
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
adapter.addItem(buttonItem)
buttonMap[button] = buttonItem
} }
} }
@ -165,9 +171,18 @@ class ControllerActivity : AppCompatActivity() {
setSupportActionBar(toolbar) setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
controller_list.layoutManager = LinearLayoutManager(this) val layoutManager = LinearLayoutManager(this)
controller_list.layoutManager = layoutManager
controller_list.adapter = adapter controller_list.adapter = adapter
controller_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView : RecyclerView, dx : Int, dy : Int) {
super.onScrolled(recyclerView, dx, dy)
if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) app_bar_layout.setExpanded(false)
}
})
update() update()
} }

View File

@ -5,9 +5,9 @@
package emu.skyline.input.onscreen package emu.skyline.input.onscreen
import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowInsetsController import android.view.WindowInsetsController
@ -17,7 +17,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton
import emu.skyline.R import emu.skyline.R
import emu.skyline.utils.Settings import emu.skyline.utils.Settings
import kotlinx.android.synthetic.main.main_activity.fab_parent
import kotlinx.android.synthetic.main.on_screen_edit_activity.* import kotlinx.android.synthetic.main.on_screen_edit_activity.*
class OnScreenEditActivity : AppCompatActivity() { class OnScreenEditActivity : AppCompatActivity() {
@ -87,10 +86,8 @@ class OnScreenEditActivity : AppCompatActivity() {
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
actions.forEach { pair -> actions.forEach { pair ->
fab_parent.addView(FloatingActionButton(this).apply { fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply {
size = FloatingActionButton.SIZE_MINI (this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
setColorFilter(Color.WHITE)
setImageDrawable(ContextCompat.getDrawable(context, pair.first))
setOnClickListener { pair.second.invoke() } setOnClickListener { pair.second.invoke() }
fabMapping[pair.first] = this fabMapping[pair.first] = this
}) })

View File

@ -63,7 +63,7 @@ enum class LoaderResult(val value : Int) {
/** /**
* This class is used to hold an application's metadata in a serializable way * This class is used to hold an application's metadata in a serializable way
*/ */
class AppEntry(var name : String, var author : String?, var icon : Bitmap?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult) : Serializable { data class AppEntry(var name : String, var author : String?, var icon : Bitmap?, var format : RomFormat, var uri : Uri, var loaderResult : LoaderResult) : Serializable {
constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor -> constructor(context : Context, format : RomFormat, uri : Uri, loaderResult : LoaderResult) : this(context.contentResolver.query(uri, null, null, null, null)?.use { cursor ->
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst() cursor.moveToFirst()

View File

@ -10,6 +10,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import emu.skyline.utils.Settings
/** /**
* This activity is used to launch a document picker and saves the result to preferences * This activity is used to launch a document picker and saves the result to preferences
@ -45,9 +46,9 @@ abstract class DocumentActivity : AppCompatActivity() {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
Settings(this).refreshRequired = true
PreferenceManager.getDefaultSharedPreferences(this).edit() PreferenceManager.getDefaultSharedPreferences(this).edit()
.putString(keyName, uri.toString()) .putString(keyName, uri.toString())
.putBoolean("refresh_required", true)
.apply() .apply()
} }
finish() finish()

View File

@ -1,44 +0,0 @@
/*
* SPDX-License-Identifier: MPL-2.0
* Copyright © 2020 Skyline Team and Contributors (https://github.com/skyline-emu/)
*/
package emu.skyline.views
import android.content.Context
import android.graphics.Rect
import android.util.AttributeSet
import android.view.View
import android.widget.LinearLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import com.google.android.material.snackbar.Snackbar.SnackbarLayout
/**
* Custom linear layout with support for [CoordinatorLayout] to move children, when [com.google.android.material.snackbar.Snackbar] shows up
*/
class CustomLinearLayout : LinearLayout, CoordinatorLayout.AttachedBehavior {
constructor(context : Context) : this(context, null)
constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, 0)
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr)
override fun getBehavior() : CoordinatorLayout.Behavior<CustomLinearLayout> = MoveUpwardBehavior()
override fun requestFocus(direction: Int, previouslyFocusedRect: Rect): Boolean = getChildAt(if (direction == View.FOCUS_UP) childCount - 1 else 0 )?.requestFocus() ?: false
/**
* Defines behaviour when [com.google.android.material.snackbar.Snackbar] is shown
* Simply sets an offset to y translation to move children out of the way
*/
class MoveUpwardBehavior : CoordinatorLayout.Behavior<CustomLinearLayout>() {
override fun layoutDependsOn(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) : Boolean = dependency is SnackbarLayout
override fun onDependentViewChanged(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) : Boolean {
child.translationY = (0f).coerceAtMost(dependency.translationY - dependency.height)
return true
}
override fun onDependentViewRemoved(parent : CoordinatorLayout, child : CustomLinearLayout, dependency : View) {
child.translationY = 0f
}
}
}

View File

@ -0,0 +1,104 @@
package emu.skyline.views
import android.content.Context
import android.text.Editable
import android.text.TextWatcher
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.inputmethod.InputMethodManager
import androidx.core.view.MarginLayoutParamsCompat
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import com.google.android.material.card.MaterialCardView
import emu.skyline.R
import kotlinx.android.synthetic.main.view_search_bar.view.*
import kotlin.math.roundToInt
class SearchBarView @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = com.google.android.material.R.attr.materialCardViewStyle) : MaterialCardView(context, attrs, defStyleAttr) {
init {
LayoutInflater.from(context).inflate(R.layout.view_search_bar, this)
useCompatPadding = true
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val margin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16f, context.resources.displayMetrics).roundToInt()
MarginLayoutParamsCompat.setMarginStart(layoutParams as MarginLayoutParams?, margin)
MarginLayoutParamsCompat.setMarginEnd(layoutParams as MarginLayoutParams?, margin)
radius = margin / 2f
cardElevation = radius / 2f
}
fun setRefreshIconListener(listener : OnClickListener) = refresh_icon.setOnClickListener(listener)
fun setLogIconListener(listener : OnClickListener) = log_icon.setOnClickListener(listener)
fun setSettingsIconListener(listener : OnClickListener) = settings_icon.setOnClickListener(listener)
var refreshIconVisible = false
set(visible) {
field = visible
refresh_icon.apply {
if (visible != isVisible) {
refresh_icon.alpha = if (visible) 0f else 1f
animate().alpha(if (visible) 1f else 0f).withStartAction { isVisible = true }.withEndAction { isInvisible = !visible }.apply { duration = 500 }.start()
}
}
}
var text : CharSequence
get() = search_field.text
set(value) = search_field.setText(value)
fun startTitleAnimation() {
motion_layout.progress = 0f
motion_layout.transitionToEnd()
search_field.apply {
setOnFocusChangeListener { v, hasFocus ->
if (hasFocus) {
this@SearchBarView.motion_layout.progress = 1f
context.getSystemService(InputMethodManager::class.java).showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
onFocusChangeListener = null
}
}
}
}
fun animateRefreshIcon() {
refresh_icon.animate().rotationBy(-180f)
}
inline fun addTextChangedListener(
crossinline beforeTextChanged : (
text : CharSequence?,
start : Int,
count : Int,
after : Int
) -> Unit = { _, _, _, _ -> },
crossinline onTextChanged : (
text : CharSequence?,
start : Int,
before : Int,
count : Int
) -> Unit = { _, _, _, _ -> },
crossinline afterTextChanged : (text : Editable?) -> Unit = {}
) : TextWatcher {
val textWatcher = object : TextWatcher {
override fun afterTextChanged(s : Editable?) {
afterTextChanged.invoke(s)
}
override fun beforeTextChanged(text : CharSequence?, start : Int, count : Int, after : Int) {
beforeTextChanged.invoke(text, start, count, after)
}
override fun onTextChanged(text : CharSequence?, start : Int, before : Int, count : Int) {
onTextChanged.invoke(text, start, before, count)
}
}
search_field.addTextChangedListener(textWatcher)
return textWatcher
}
}

View File

@ -56,7 +56,9 @@
android:layout_marginEnd="6dp" android:layout_marginEnd="6dp"
android:focusedByDefault="true" android:focusedByDefault="true"
android:text="@string/play" android:text="@string/play"
app:icon="@drawable/ic_play" /> android:textColor="?attr/colorAccent"
app:icon="@drawable/ic_play"
app:iconTint="?attr/colorAccent" />
<Button <Button
android:id="@+id/game_pin" android:id="@+id/game_pin"
@ -64,8 +66,8 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="6dp" android:layout_marginStart="6dp"
android:text="@string/pin" /> android:text="@string/pin"
android:textColor="?attr/colorAccent" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,65 +1,55 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:foreground="?attr/selectableItemBackground"
android:orientation="vertical">
<androidx.cardview.widget.CardView <com.google.android.material.card.MaterialCardView
android:id="@+id/card_app_item_grid" android:id="@+id/item_click_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_marginStart="12dp"
android:layout_margin="@dimen/app_card_margin_half" android:layout_marginTop="12dp"
android:clickable="true" android:layout_marginEnd="12dp"
android:focusable="true" android:layout_marginBottom="12dp"
android:foreground="?attr/selectableItemBackground" app:cardCornerRadius="16dp"
app:cardCornerRadius="4dp" app:cardElevation="4dp">
app:cardElevation="@dimen/app_card_margin"
app:cardUseCompatPadding="true">
<LinearLayout <ImageView
android:id="@+id/icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:adjustViewBounds="true"
android:orientation="vertical"> android:contentDescription="@string/icon"
tools:src="@drawable/default_icon" />
</com.google.android.material.card.MaterialCardView>
<ImageView <TextView
android:id="@+id/icon" 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:adjustViewBounds="true" android:layout_marginStart="20dp"
android:contentDescription="@string/icon" android:layout_marginEnd="20dp"
tools:src="@drawable/default_icon" /> android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="12sp"
tools:text="Title" />
<TextView <TextView
android:id="@+id/text_title" 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:ellipsize="marquee" android:layout_marginStart="20dp"
android:marqueeRepeatLimit="marquee_forever" android:layout_marginEnd="20dp"
android:paddingStart="15dp" android:layout_marginBottom="16dp"
android:paddingTop="10dp" android:ellipsize="marquee"
android:paddingEnd="15dp" android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true" android:singleLine="true"
android:textAlignment="center" android:textSize="12sp"
android:textAppearance="?android:attr/textAppearanceListItem" tools:text="Subtitle" />
tools:text="Title" /> </LinearLayout>
<TextView
android:id="@+id/text_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:paddingBottom="15dp"
android:singleLine="true"
android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light"
tools:text="Subtitle" />
</LinearLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@ -1,77 +1,70 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_click_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<androidx.cardview.widget.CardView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/card_app_item_grid"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <ImageView
android:id="@+id/icon"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content"
android:adjustViewBounds="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" />
<ImageView <TextView
android:id="@+id/icon" android:id="@+id/text_title"
android:layout_width="match_parent" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:adjustViewBounds="true" android:alpha="242.25"
android:contentDescription="@string/icon" android:ellipsize="marquee"
android:foreground="@drawable/background_gradient" android:marqueeRepeatLimit="marquee_forever"
app:layout_constraintBottom_toBottomOf="parent" android:paddingStart="8dp"
app:layout_constraintEnd_toEndOf="parent" android:paddingEnd="8dp"
app:layout_constraintStart_toStartOf="parent" android:singleLine="true"
app:layout_constraintTop_toTopOf="parent" android:textAppearance="?android:attr/textAppearanceListItem"
tools:src="@drawable/default_icon" /> android:textColor="@android:color/white"
android:textStyle="bold"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Title" />
<TextView <TextView
android:id="@+id/text_title" android:id="@+id/text_subtitle"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:alpha="242.25" android:layout_marginBottom="8dp"
android:ellipsize="marquee" android:alpha="242.25"
android:marqueeRepeatLimit="marquee_forever" android:ellipsize="marquee"
android:paddingStart="8dp" android:fadingEdge="horizontal"
android:paddingEnd="8dp" android:marqueeRepeatLimit="marquee_forever"
android:singleLine="true" android:paddingStart="8dp"
android:textAppearance="?android:attr/textAppearanceListItem" android:paddingEnd="8dp"
android:textColor="@android:color/white" android:singleLine="true"
android:textStyle="bold" android:textAppearance="?android:attr/textAppearanceListItemSecondary"
app:layout_constrainedWidth="true" android:textColor="@android:color/white"
app:layout_constraintBottom_toTopOf="@id/text_subtitle" app:layout_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="@id/icon" app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon" app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Title" /> tools:text="Subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView </com.google.android.material.card.MaterialCardView>
android:id="@+id/text_subtitle"
android:layout_width="0dp"
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_constraintBottom_toBottomOf="@id/icon"
app:layout_constraintEnd_toEndOf="@id/icon"
app:layout_constraintStart_toStartOf="@id/icon"
tools:text="Subtitle" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</FrameLayout>

View File

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout 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:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/item_click_layout"
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:background="?attr/selectableItemBackground"
@ -14,6 +15,7 @@
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:contentDescription="@string/icon" android:contentDescription="@string/icon"
android:focusable="false"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/default_icon" /> tools:src="@drawable/default_icon" />

View File

@ -76,6 +76,6 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="end" android:layout_gravity="end"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/reset" /> android:text="@string/reset"
android:textColor="?attr/colorAccent" />
</LinearLayout> </LinearLayout>

View File

@ -2,76 +2,45 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".MainActivity"> tools:context=".MainActivity">
<include layout="@layout/titlebar" /> <com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fitsSystemWindows="true"
android:keyboardNavigationCluster="false"
android:touchscreenBlocksFocus="false"
app:elevation="0dp">
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/app_list" android:layout_width="match_parent"
android:layout_height="62dp"
android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll">
<emu.skyline.views.SearchBarView
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:layout_marginTop="-4dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior">
<emu.skyline.views.CustomLinearLayout
android:id="@+id/fab_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:orientation="vertical">
<emu.skyline.views.CustomLinearLayout
android:id="@+id/controller_fabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="true"
android:orientation="vertical"
android:translationX="72dp"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/refresh_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:maxImageSize="26dp"
app:srcCompat="@drawable/ic_refresh" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/settings_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:maxImageSize="26dp"
app:srcCompat="@drawable/ic_settings" />
</emu.skyline.views.CustomLinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/open_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:maxImageSize="26dp"
app:srcCompat="@drawable/ic_open" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/log_fab"
style="@style/Widget.MaterialComponents.FloatingActionButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:maxImageSize="26dp"
app:srcCompat="@drawable/ic_log" />
</emu.skyline.views.CustomLinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="4dp" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -9,7 +9,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" /> android:layout_height="match_parent" />
<emu.skyline.views.CustomLinearLayout <LinearLayout
android:id="@+id/fab_parent" android:id="@+id/fab_parent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.floatingactionbutton.FloatingActionButton 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="wrap_content"
android:layout_height="wrap_content"
app:backgroundTint="?attr/colorPrimary"
app:fabSize="mini"
app:tint="?attr/colorAccent"
tools:ignore="ContentDescription" />

View File

@ -60,7 +60,6 @@
android:textAppearance="?android:attr/textAppearanceListItemSecondary" android:textAppearance="?android:attr/textAppearanceListItemSecondary"
android:textColor="@android:color/tertiary_text_light" android:textColor="@android:color/tertiary_text_light"
android:textSize="13sp" /> android:textSize="13sp" />
</RelativeLayout> </RelativeLayout>
<LinearLayout <LinearLayout
@ -76,6 +75,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/builtin" android:text="@string/builtin"
android:textColor="?attr/colorAccent"
android:visibility="gone" /> android:visibility="gone" />
<Button <Button
@ -84,8 +84,7 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/reset" /> android:text="@string/reset"
android:textColor="?attr/colorAccent" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -4,8 +4,9 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="16dp" android:layout_marginTop="8dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginBottom="4dp" android:layout_marginBottom="8dp"
android:textColor="?colorSecondary" android:textColor="?android:attr/textColorPrimary"
android:textSize="15sp" /> android:textSize="18sp"
android:textStyle="bold" />

View File

@ -22,7 +22,7 @@
android:animateLayoutChanges="true" android:animateLayoutChanges="true"
android:text="@string/stick_button" android:text="@string/stick_button"
android:textAppearance="@style/TextAppearance.AppCompat.Display1" android:textAppearance="@style/TextAppearance.AppCompat.Display1"
android:textColor="@color/colorPrimary" android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp" /> android:textSize="16sp" />
<TextView <TextView
@ -116,13 +116,15 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/reset" /> android:text="@string/reset"
android:textColor="?attr/colorAccent" />
<Button <Button
android:id="@+id/stick_next" android:id="@+id/stick_next"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/next" /> android:text="@string/next"
android:textColor="?attr/colorAccent" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View File

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android" <com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/toolbar_layout" android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:layout_scrollFlags="scroll|exitUntilCollapsed" android:keyboardNavigationCluster="false"
app:liftOnScroll="true"> android:touchscreenBlocksFocus="false">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar" android:id="@+id/toolbar"
style="@style/Widget.MaterialComponents.Toolbar.Primary"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
android:theme="@style/AppTheme.ActionBar" app:elevation="16dp"
app:layout_scrollFlags="scroll|enterAlways|snap" /> app:layout_scrollFlags="scroll" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<merge 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"
tools:parentTag="com.google.android.material.card.MaterialCardView">
<androidx.constraintlayout.motion.widget.MotionLayout
android:id="@+id/motion_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutDescription="@xml/view_search_bar_xml_constraintlayout_scene"
app:motionProgress="1"
tools:motionDebug="SHOW_ALL">
<ImageView
android:id="@+id/search_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="16dp"
android:contentDescription="@string/search"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_search"
app:tint="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/search_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:background="@null"
android:focusedByDefault="false"
android:hint="@string/search"
android:importantForAutofill="no"
android:inputType="text"
android:singleLine="true"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/refresh_icon"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/search_icon"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="LabelFor" />
<ImageView
android:id="@+id/refresh_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/refresh"
android:padding="5dp"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/log_icon"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_refresh"
app:tint="?android:attr/textColorSecondary"
tools:visibility="visible" />
<ImageView
android:id="@+id/log_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="4dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/log"
android:padding="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/settings_icon"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_log"
app:tint="?android:attr/textColorSecondary" />
<ImageView
android:id="@+id/settings_icon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_gravity="center_vertical"
android:layout_marginEnd="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/settings"
android:padding="5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings"
app:tint="?android:attr/textColorSecondary" />
<TextView
android:id="@+id/skyline_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="sans-serif-medium"
android:text="@string/app_name"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp" />
</androidx.constraintlayout.motion.widget.MotionLayout>
</merge>

View File

@ -6,15 +6,18 @@
android:icon="@drawable/ic_search" android:icon="@drawable/ic_search"
android:title="@string/search" android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom|withText" /> app:showAsAction="ifRoom|withText" />
<item <item
android:id="@+id/action_share_log" android:id="@+id/action_share_log"
android:icon="@drawable/ic_share" android:icon="@drawable/ic_share"
android:title="@string/share" android:title="@string/share"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item <item
android:id="@+id/action_clear" android:id="@+id/action_clear"
android:icon="@drawable/ic_clear" android:icon="@drawable/ic_clear"
android:title="@string/clear" android:title="@string/clear"
app:iconTint="?android:attr/textColorSecondary"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
</menu> </menu>

View File

@ -1,21 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_search_main"
android:icon="@drawable/ic_search"
android:iconTint="@color/colorOnPrimary"
android:title="@string/search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|withText" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings"
android:title="@string/settings"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_refresh"
android:icon="@drawable/ic_refresh"
android:title="@string/refresh"
app:showAsAction="ifRoom" />
</menu>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="BaseAppTheme">
<item name="android:windowLightNavigationBar">false</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
</resources>

View File

@ -1,9 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#FFFF0000</color> <color name="colorPrimary">#FF424242</color>
<color name="colorPrimaryDark">#BFFF0000</color> <color name="colorPrimaryDark">@android:color/black</color>
<color name="colorOnPrimary">#D7000000</color>
<color name="colorSecondary">#FFFF0000</color>
<color name="colorSecondaryDark">#BFFF0000</color>
<color name="colorOnSecondary">#DF000000</color>
</resources> </resources>

View File

@ -0,0 +1,9 @@
<resources>
<style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowLightStatusBar">false</item>
</style>
</resources>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="AppTheme" parent="BaseAppTheme">
<item name="android:windowLightNavigationBar">true</item>
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
</style>
</resources>

View File

@ -1,9 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="colorPrimary">#FFFF0000</color> <color name="colorPrimary">@color/cardview_light_background</color>
<color name="colorPrimaryDark">#BFFF0000</color> <color name="colorPrimaryDark">@android:color/white</color>
<color name="colorOnPrimary">#DFFFFFFF</color> <color name="colorAccent">#FFFF0000</color>
<color name="colorSecondary">#FFFF0000</color>
<color name="colorSecondaryDark">#BFFF0000</color>
<color name="colorOnSecondary">#DFFFFFFF</color>
</resources> </resources>

View File

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

View File

@ -1,20 +1,15 @@
<resources> <resources>
<style name="AppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> <style name="BaseAppTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar">
<item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryVariant">@color/colorPrimaryDark</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorOnPrimary">@color/colorOnPrimary</item> <item name="colorAccent">@color/colorAccent</item>
<item name="colorSecondary">@color/colorSecondary</item> <item name="android:windowLightStatusBar">true</item>
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
<item name="colorOnSecondary">@color/colorOnSecondary</item>
</style> </style>
<style name="AppTheme.ActionBar" parent=""> <style name="AppTheme" parent="BaseAppTheme" />
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
<item name="android:textColorSecondary">@color/colorOnPrimary</item>
</style>
<style name="roundedAppImage" parent=""> <style name="roundedAppImage">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSize">6dp</item> <item name="cornerSize">6dp</item>
</style> </style>

View File

@ -0,0 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:motion="http://schemas.android.com/apk/res-auto">
<Transition
motion:autoTransition="none"
motion:constraintSetEnd="@+id/end"
motion:constraintSetStart="@id/start"
motion:duration="2000">
<KeyFrameSet>
<KeyPosition
motion:motionTarget="@+id/skyline_text"
motion:framePosition="50"
motion:keyPositionType="deltaRelative"
motion:percentX="0" />
<KeyPosition
motion:motionTarget="@+id/skyline_text"
motion:framePosition="90"
motion:keyPositionType="deltaRelative"
motion:percentX="1" />
<KeyAttribute
motion:motionTarget="@+id/search_field"
motion:framePosition="90"
android:alpha="0" />
<KeyAttribute
motion:motionTarget="@+id/skyline_text"
motion:framePosition="55"
android:scaleY="1"
android:scaleX="1" />
<KeyPosition
motion:motionTarget="@+id/skyline_text"
motion:framePosition="70"
motion:keyPositionType="deltaRelative"
motion:percentX="-0.5" />
</KeyFrameSet>
</Transition>
<ConstraintSet android:id="@+id/start">
<Constraint
android:id="@+id/skyline_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintStart_toStartOf="parent"
motion:layout_constraintTop_toTopOf="parent" />
<Constraint
android:id="@+id/search_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="invisible"
motion:layout_constrainedWidth="true"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@id/refresh_icon"
motion:layout_constraintHorizontal_bias="0"
motion:layout_constraintStart_toEndOf="@id/search_icon"
motion:layout_constraintTop_toTopOf="parent"
motion:transitionEasing="linear"
android:alpha="0" />
</ConstraintSet>
<ConstraintSet android:id="@+id/end">
<Constraint
android:id="@+id/skyline_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="invisible"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toEndOf="parent"
motion:layout_constraintHorizontal_bias="0.0"
motion:layout_constraintStart_toStartOf="@id/search_field"
motion:layout_constraintTop_toTopOf="parent"
android:scaleX="0.5"
android:scaleY="0.5" />
<Constraint
android:id="@+id/search_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:visibility="visible"
motion:layout_constrainedWidth="true"
motion:layout_constraintBottom_toBottomOf="parent"
motion:layout_constraintEnd_toStartOf="@id/refresh_icon"
motion:layout_constraintHorizontal_bias="0"
motion:layout_constraintStart_toEndOf="@id/search_icon"
motion:layout_constraintTop_toTopOf="parent"
android:alpha="1" />
</ConstraintSet>
</MotionScene>

0
gradlew vendored Normal file → Executable file
View File