mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-21 21:59:17 +01:00
Redesign UI
* Change colors * Replace toolbar in main activity * Initial implementation of ViewModel
This commit is contained in:
parent
80ab22a627
commit
9f6a5df5e0
@ -68,6 +68,7 @@ android {
|
||||
|
||||
/* NDK */
|
||||
ndkVersion '22.0.7026061'
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
version '3.18.1+'
|
||||
@ -85,15 +86,22 @@ dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
||||
|
||||
/* Google */
|
||||
def lifecycle_version = "2.2.0"
|
||||
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||
implementation 'androidx.preference:preference-ktx:1.1.1'
|
||||
implementation 'com.google.android.material:material:1.3.0'
|
||||
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 */
|
||||
implementation "androidx.core:core-ktx:1.3.2"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
||||
/* Other Java */
|
||||
implementation 'info.debatty:java-string-similarity:2.0.0'
|
||||
|
@ -38,12 +38,6 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@ -262,14 +256,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
vibrators.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets [surface] to [holder].surface and passes it into libskyline
|
||||
*/
|
||||
override fun surfaceCreated(holder : SurfaceHolder) {
|
||||
Log.d(Tag, "surfaceCreated Holder: $holder")
|
||||
surface = holder.surface
|
||||
while (emulationThread.isAlive)
|
||||
if (setSurface(surface))
|
||||
if (setSurface(holder.surface))
|
||||
return
|
||||
}
|
||||
|
||||
@ -280,14 +270,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
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) {
|
||||
Log.d(Tag, "surfaceDestroyed Holder: $holder")
|
||||
surface = null
|
||||
while (emulationThread.isAlive)
|
||||
if (setSurface(surface))
|
||||
if (setSurface(null))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -32,54 +32,49 @@ object KeyReader {
|
||||
return false
|
||||
|
||||
val fileName = DocumentFile.fromSingleUri(context, uri)!!.name
|
||||
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() ?: false)
|
||||
if (fileName?.substringAfterLast('.')?.startsWith("keys")?.not() == true)
|
||||
return false
|
||||
|
||||
val tmpOutputFile = File("${context.filesDir.canonicalFile}/${keyType.fileName}.tmp")
|
||||
|
||||
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 {
|
||||
for (line in it) {
|
||||
val pair = line.split("=")
|
||||
if (pair.size != 2)
|
||||
return@useLines false
|
||||
val key = pair[0].trim()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
val key = pair[0].trim()
|
||||
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
|
||||
}
|
||||
writer.append("$key=$value\n")
|
||||
}
|
||||
|
||||
outputStream.append("$key=$value\n")
|
||||
true
|
||||
}
|
||||
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 {
|
||||
for (c in str)
|
||||
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F'))
|
||||
return false
|
||||
if (!(c in '0'..'9' || c in 'a'..'f' || c in 'A'..'F')) return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
@ -66,22 +66,23 @@ class LogActivity : AppCompatActivity() {
|
||||
try {
|
||||
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
|
||||
|
||||
logFile.forEachLine { logLine ->
|
||||
adapter.setItems(logFile.readLines().mapNotNull { logLine ->
|
||||
try {
|
||||
val logMeta = logLine.split("|", limit = 3)
|
||||
|
||||
if (logMeta[0].startsWith("1")) {
|
||||
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 {
|
||||
adapter.addItem(HeaderViewItem(logMeta[1]))
|
||||
return@mapNotNull HeaderViewItem(logMeta[1])
|
||||
}
|
||||
} catch (ignored : IndexOutOfBoundsException) {
|
||||
} catch (ignored : NumberFormatException) {
|
||||
}
|
||||
}
|
||||
null
|
||||
})
|
||||
} catch (e : FileNotFoundException) {
|
||||
Log.w("Logger", "IO Error during access of log file: " + e.message)
|
||||
Toast.makeText(applicationContext, getString(R.string.file_missing), Toast.LENGTH_LONG).show()
|
||||
|
@ -5,22 +5,20 @@
|
||||
|
||||
package emu.skyline
|
||||
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.graphics.Color
|
||||
import android.graphics.Rect
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.use
|
||||
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.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@ -34,222 +32,137 @@ import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.DataItem
|
||||
import emu.skyline.data.HeaderItem
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.loader.RomFile
|
||||
import emu.skyline.loader.RomFormat
|
||||
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.titlebar.*
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.math.ceil
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
companion object {
|
||||
private val TAG = MainActivity::class.java.simpleName
|
||||
}
|
||||
|
||||
private val settings by lazy { Settings(this) }
|
||||
|
||||
/**
|
||||
* The adapter used for adding elements to [app_list]
|
||||
*/
|
||||
private val adapter = GenericAdapter()
|
||||
|
||||
private var reloading = AtomicBoolean()
|
||||
|
||||
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 viewModel by viewModels<MainViewModel>()
|
||||
|
||||
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?) {
|
||||
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)
|
||||
|
||||
setContentView(R.layout.main_activity)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
|
||||
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()
|
||||
|
||||
app_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
var y = 0
|
||||
swipe_refresh_layout.apply {
|
||||
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) {
|
||||
y += dy
|
||||
viewModel.state.observe(owner = this, onChanged = ::handleState)
|
||||
loadRoms(!settings.refreshRequired)
|
||||
|
||||
if (!app_list.isInTouchMode)
|
||||
toolbar_layout.setExpanded(y == 0)
|
||||
|
||||
super.onScrolled(recyclerView, dx, dy)
|
||||
search_bar.apply {
|
||||
setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
|
||||
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
|
||||
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
|
||||
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener {
|
||||
if (!it) {
|
||||
toolbar_layout.setExpanded(false)
|
||||
private inner class GridSpacingItemDecoration : RecyclerView.ItemDecoration() {
|
||||
private val padding = resources.getDimensionPixelSize(R.dimen.grid_padding)
|
||||
|
||||
controller_fabs.visibility = View.VISIBLE
|
||||
ObjectAnimator.ofFloat(controller_fabs, "translationX", 0f).apply {
|
||||
duration = 250
|
||||
start()
|
||||
override fun getItemOffsets(outRect : Rect, view : View, parent : RecyclerView, state : RecyclerView.State) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
|
||||
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() {
|
||||
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
||||
when (layoutType) {
|
||||
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 gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
app_list.layoutManager = GridLayoutManager(this, gridSpan).apply {
|
||||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position] is HeaderViewItem) gridSpan else 1
|
||||
}
|
||||
}
|
||||
app_list.layoutManager = CustomLayoutManager(gridSpan)
|
||||
setAppListDecoration()
|
||||
|
||||
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
|
||||
|
||||
startActivityForResult(intent, 1)
|
||||
} else {
|
||||
refreshAdapter(!settings.refreshRequired)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This inflates the layout for the menu [R.menu.toolbar_main] and sets up searching the logs
|
||||
*/
|
||||
override fun onCreateOptionsMenu(menu : Menu) : Boolean {
|
||||
menuInflater.inflate(R.menu.toolbar_main, menu)
|
||||
|
||||
val searchView = menu.findItem(R.id.action_search_main).actionView as SearchView
|
||||
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query : String) : Boolean {
|
||||
searchView.clearFocus()
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText : String) : Boolean {
|
||||
adapter.filter.filter(newText)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
private fun handleState(state : MainState) = when (state) {
|
||||
MainState.Loading -> {
|
||||
search_bar.animateRefreshIcon()
|
||||
swipe_refresh_layout.isRefreshing = true
|
||||
}
|
||||
is MainState.Loaded -> {
|
||||
swipe_refresh_layout.isRefreshing = false
|
||||
populateAdapter(state.items)
|
||||
}
|
||||
is MainState.Error -> Snackbar.make(findViewById(android.R.id.content), getString(R.string.error) + ": ${state.ex.localizedMessage}", Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun selectStartGame(appItem : AppItem) {
|
||||
if (swipe_refresh_layout.isRefreshing) return
|
||||
|
||||
if (settings.selectAction)
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
else if (appItem.loaderResult == LoaderResult.Success)
|
||||
@ -308,26 +206,24 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun selectShowGameDialog(appItem : AppItem) {
|
||||
if (swipe_refresh_layout.isRefreshing) return
|
||||
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
}
|
||||
|
||||
/**
|
||||
* This handles menu interaction for [R.id.action_settings] and [R.id.action_refresh]
|
||||
*/
|
||||
override fun onOptionsItemSelected(item : MenuItem) : Boolean {
|
||||
return when (item.itemId) {
|
||||
R.id.action_settings -> {
|
||||
startActivityForResult(Intent(this, SettingsActivity::class.java), 3)
|
||||
true
|
||||
}
|
||||
private fun loadRoms(loadFromFile : Boolean) {
|
||||
viewModel.loadRoms(this, loadFromFile, Uri.parse(settings.searchLocation))
|
||||
settings.refreshRequired = false
|
||||
}
|
||||
|
||||
R.id.action_refresh -> {
|
||||
refreshAdapter(false)
|
||||
true
|
||||
private fun populateAdapter(items : List<DataItem>) {
|
||||
adapter.setItems(items.map {
|
||||
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)
|
||||
settings.searchLocation = uri.toString()
|
||||
|
||||
refreshAdapter(!settings.refreshRequired)
|
||||
loadRoms(!settings.refreshRequired)
|
||||
}
|
||||
|
||||
2 -> {
|
||||
@ -360,9 +256,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
3 -> {
|
||||
if (settings.refreshRequired) refreshAdapter(false)
|
||||
}
|
||||
3 -> if (settings.refreshRequired) loadRoms(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -371,7 +265,7 @@ class MainActivity : AppCompatActivity() {
|
||||
super.onResume()
|
||||
|
||||
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) {
|
||||
appViewItem.layoutType = layoutType
|
||||
layoutTypeChanged = true
|
||||
@ -381,14 +275,19 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (layoutTypeChanged) {
|
||||
adapter.notifyAllItemsChanged()
|
||||
setAppListDecoration()
|
||||
adapter.notifyItemRangeChanged(0, adapter.currentItems.size)
|
||||
}
|
||||
}
|
||||
|
||||
val gridCardMagin = resources.getDimensionPixelSize(R.dimen.app_card_margin_half)
|
||||
when (layoutType) {
|
||||
LayoutType.List -> app_list.post { app_list.setPadding(0, 0, 0, fab_parent.height) }
|
||||
LayoutType.Grid, LayoutType.GridCompact -> app_list.post { app_list.setPadding(gridCardMagin, 0, gridCardMagin, fab_parent.height) }
|
||||
override fun onBackPressed() {
|
||||
search_bar.apply {
|
||||
if (hasFocus() && text.isNotEmpty()) {
|
||||
text = ""
|
||||
clearFocus()
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
107
app/src/main/java/emu/skyline/MainViewModel.kt
Normal file
107
app/src/main/java/emu/skyline/MainViewModel.kt
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import emu.skyline.preference.ActivityResultDelegate
|
||||
import emu.skyline.preference.DocumentActivity
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
@ -46,8 +45,6 @@ class SettingsActivity : AppCompatActivity() {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
preferenceFragment.delegateActivityResult(requestCode, resultCode, data)
|
||||
|
||||
settings
|
||||
}
|
||||
|
||||
/**
|
||||
@ -101,4 +98,9 @@ class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
setResult(RESULT_OK)
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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 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) }
|
||||
}
|
||||
|
||||
when (layoutType) {
|
||||
LayoutType.List -> holder.itemView
|
||||
LayoutType.Grid, LayoutType.GridCompact -> holder.card_app_item_grid
|
||||
}.apply {
|
||||
holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
|
||||
setOnClickListener { onClick.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 areItemsTheSame(other : GenericListItem) = key() == other.key()
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item
|
||||
}
|
||||
|
@ -8,20 +8,30 @@ package emu.skyline.adapter
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Filter
|
||||
import android.widget.Filterable
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
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 {
|
||||
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
|
||||
val allItems = mutableListOf<GenericViewHolderBinder>()
|
||||
private var filteredItems = listOf<GenericViewHolderBinder>()
|
||||
override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
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>()
|
||||
|
||||
@ -38,19 +48,10 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
|
||||
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
|
||||
|
||||
fun addItem(item : GenericViewHolderBinder) {
|
||||
allItems.add(item)
|
||||
notifyItemInserted(currentItems.size)
|
||||
}
|
||||
|
||||
fun removeAllItems() {
|
||||
val size = currentItems.size
|
||||
fun setItems(items : List<GenericListItem>) {
|
||||
allItems.clear()
|
||||
notifyItemRangeRemoved(0, size)
|
||||
}
|
||||
|
||||
fun notifyAllItemsChanged() {
|
||||
notifyItemRangeChanged(0, currentItems.size)
|
||||
allItems.addAll(items)
|
||||
filter.filter(currentSearchTerm)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,7 +68,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
*/
|
||||
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]
|
||||
@ -92,7 +93,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
results.values = allItems.toMutableList()
|
||||
results.count = allItems.size
|
||||
} else {
|
||||
val filterData = mutableListOf<GenericViewHolderBinder>()
|
||||
val filterData = mutableListOf<GenericListItem>()
|
||||
|
||||
val topResults = extractSorted()
|
||||
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) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
filteredItems = results.values as List<GenericViewHolderBinder>
|
||||
|
||||
notifyDataSetChanged()
|
||||
asyncListDiffer.submitList(results.values as List<GenericListItem>)
|
||||
}
|
||||
}
|
||||
}
|
@ -16,7 +16,7 @@ interface GenericLayoutFactory {
|
||||
fun createLayout(parent : ViewGroup) : View
|
||||
}
|
||||
|
||||
abstract class GenericViewHolderBinder {
|
||||
abstract class GenericListItem {
|
||||
var adapter : GenericAdapter? = null
|
||||
|
||||
abstract fun getLayoutFactory() : GenericLayoutFactory
|
||||
@ -27,4 +27,11 @@ abstract class GenericViewHolderBinder {
|
||||
* Used for filtering
|
||||
*/
|
||||
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
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
||||
class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
|
||||
class HeaderViewItem(private val text : String) : GenericListItem() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
@ -23,4 +23,6 @@ class HeaderViewItem(private val text : String) : GenericViewHolderBinder() {
|
||||
}
|
||||
|
||||
override fun toString() = ""
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
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 bind(holder : GenericViewHolder, position : Int) {
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.ButtonGuestEvent
|
||||
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) }
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button
|
||||
}
|
||||
|
@ -12,14 +12,14 @@ import androidx.core.view.isGone
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericLayoutFactory
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.adapter.GenericViewHolderBinder
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import kotlinx.android.synthetic.main.controller_checkbox_item.*
|
||||
|
||||
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
|
||||
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 bind(holder : GenericViewHolder, position : Int) {
|
||||
|
@ -6,6 +6,7 @@
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.GeneralType
|
||||
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) }
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.input.AxisGuestEvent
|
||||
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) }
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerStickViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
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) }
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerTypeViewItem
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type
|
||||
}
|
||||
|
@ -11,15 +11,15 @@ import android.view.ViewGroup
|
||||
import androidx.core.view.isGone
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericLayoutFactory
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.adapter.GenericViewHolderBinder
|
||||
import kotlinx.android.synthetic.main.controller_item.*
|
||||
|
||||
private object ControllerLayoutFactory : GenericLayoutFactory {
|
||||
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
|
||||
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
|
||||
|
@ -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 ""
|
||||
}
|
@ -5,6 +5,56 @@
|
||||
|
||||
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
|
||||
|
||||
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 ""
|
||||
}
|
||||
|
@ -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
|
@ -10,9 +10,11 @@ import android.os.Bundle
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericAdapter
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.HeaderViewItem
|
||||
import emu.skyline.adapter.controller.*
|
||||
import emu.skyline.input.dialog.ButtonDialog
|
||||
@ -53,99 +55,103 @@ class ControllerActivity : AppCompatActivity() {
|
||||
* This function updates the [adapter] based on information from [InputManager]
|
||||
*/
|
||||
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)
|
||||
return
|
||||
if (controller.type == ControllerType.None)
|
||||
return
|
||||
|
||||
if (id == 0 && controller.type.firstController) {
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.osc)))
|
||||
if (id == 0 && controller.type.firstController) {
|
||||
items.add(HeaderViewItem(getString(R.string.osc)))
|
||||
|
||||
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 ->
|
||||
item.summary = oscSummary.invoke(item.checked)
|
||||
settings.onScreenControl = item.checked
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
val oscSummary = { checked : Boolean -> getString(if (checked) R.string.osc_shown else R.string.osc_not_shown) }
|
||||
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_enable), oscSummary.invoke(settings.onScreenControl), settings.onScreenControl) { item, position ->
|
||||
item.summary = oscSummary.invoke(item.checked)
|
||||
settings.onScreenControl = item.checked
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
|
||||
adapter.addItem(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
|
||||
settings.onScreenControlRecenterSticks = item.checked
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
items.add(ControllerCheckBoxViewItem(getString(R.string.osc_recenter_sticks), "", settings.onScreenControlRecenterSticks) { item, position ->
|
||||
settings.onScreenControlRecenterSticks = item.checked
|
||||
adapter.notifyItemChanged(position)
|
||||
})
|
||||
|
||||
adapter.addItem(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
|
||||
startActivity(Intent(this, OnScreenEditActivity::class.java))
|
||||
}))
|
||||
}
|
||||
items.add(ControllerViewItem(content = getString(R.string.osc_edit), onClick = {
|
||||
startActivity(Intent(this, OnScreenEditActivity::class.java))
|
||||
}))
|
||||
}
|
||||
|
||||
var wroteTitle = false
|
||||
var wroteTitle = false
|
||||
|
||||
for (item in GeneralType.values()) {
|
||||
if (item.compatibleControllers == null || item.compatibleControllers.contains(controller.type)) {
|
||||
for (item in GeneralType.values()) {
|
||||
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) {
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.general)))
|
||||
items.add(HeaderViewItem(getString(R.string.sticks)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
adapter.addItem(ControllerGeneralViewItem(id, item, onControllerGeneralClick))
|
||||
}
|
||||
}
|
||||
val stickItem = ControllerStickViewItem(id, stick, onControllerStickClick)
|
||||
|
||||
wroteTitle = false
|
||||
|
||||
for (stick in controller.type.sticks) {
|
||||
if (!wroteTitle) {
|
||||
adapter.addItem(HeaderViewItem(getString(R.string.sticks)))
|
||||
wroteTitle = true
|
||||
items.add(stickItem)
|
||||
buttonMap[stick.button] = stickItem
|
||||
axisMap[stick.xAxis] = stickItem
|
||||
axisMap[stick.yAxis] = stickItem
|
||||
}
|
||||
|
||||
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)
|
||||
buttonMap[stick.button] = stickItem
|
||||
axisMap[stick.xAxis] = stickItem
|
||||
axisMap[stick.yAxis] = stickItem
|
||||
}
|
||||
val buttonArrays = arrayOf(dpadButtons, faceButtons, shoulderTriggerButtons, shoulderRailButtons)
|
||||
|
||||
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))
|
||||
for (buttonArray in buttonArrays) {
|
||||
wroteTitle = false
|
||||
|
||||
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
|
||||
|
||||
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) {
|
||||
adapter.addItem(HeaderViewItem(getString(buttonArray.first)))
|
||||
items.add(HeaderViewItem(getString(R.string.misc_buttons)))
|
||||
wroteTitle = true
|
||||
}
|
||||
|
||||
val buttonItem = ControllerButtonViewItem(id, button, onControllerButtonClick)
|
||||
|
||||
adapter.addItem(buttonItem)
|
||||
items.add(buttonItem)
|
||||
buttonMap[button] = buttonItem
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
} finally {
|
||||
adapter.setItems(items)
|
||||
}
|
||||
}
|
||||
|
||||
@ -165,9 +171,18 @@ class ControllerActivity : AppCompatActivity() {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
controller_list.layoutManager = LinearLayoutManager(this)
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
controller_list.layoutManager = layoutManager
|
||||
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()
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@
|
||||
|
||||
package emu.skyline.input.onscreen
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import android.view.WindowInsetsController
|
||||
@ -17,7 +17,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import emu.skyline.R
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.main_activity.fab_parent
|
||||
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
|
||||
|
||||
class OnScreenEditActivity : AppCompatActivity() {
|
||||
@ -87,10 +86,8 @@ class OnScreenEditActivity : AppCompatActivity() {
|
||||
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
|
||||
|
||||
actions.forEach { pair ->
|
||||
fab_parent.addView(FloatingActionButton(this).apply {
|
||||
size = FloatingActionButton.SIZE_MINI
|
||||
setColorFilter(Color.WHITE)
|
||||
setImageDrawable(ContextCompat.getDrawable(context, pair.first))
|
||||
fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply {
|
||||
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
|
||||
setOnClickListener { pair.second.invoke() }
|
||||
fabMapping[pair.first] = this
|
||||
})
|
||||
|
@ -63,7 +63,7 @@ enum class LoaderResult(val value : Int) {
|
||||
/**
|
||||
* 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 ->
|
||||
val nameIndex : Int = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
cursor.moveToFirst()
|
||||
|
@ -10,6 +10,7 @@ import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceManager
|
||||
import emu.skyline.utils.Settings
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
Settings(this).refreshRequired = true
|
||||
PreferenceManager.getDefaultSharedPreferences(this).edit()
|
||||
.putString(keyName, uri.toString())
|
||||
.putBoolean("refresh_required", true)
|
||||
.apply()
|
||||
}
|
||||
finish()
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
104
app/src/main/java/emu/skyline/views/SearchBarView.kt
Normal file
104
app/src/main/java/emu/skyline/views/SearchBarView.kt
Normal 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
|
||||
}
|
||||
}
|
@ -56,7 +56,9 @@
|
||||
android:layout_marginEnd="6dp"
|
||||
android:focusedByDefault="true"
|
||||
android:text="@string/play"
|
||||
app:icon="@drawable/ic_play" />
|
||||
android:textColor="?attr/colorAccent"
|
||||
app:icon="@drawable/ic_play"
|
||||
app:iconTint="?attr/colorAccent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/game_pin"
|
||||
@ -64,8 +66,8 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:text="@string/pin" />
|
||||
|
||||
android:text="@string/pin"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
@ -1,65 +1,55 @@
|
||||
<?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:tools="http://schemas.android.com/tools"
|
||||
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
|
||||
android:id="@+id/card_app_item_grid"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/item_click_layout"
|
||||
android:layout_width="match_parent"
|
||||
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">
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="4dp">
|
||||
|
||||
<LinearLayout
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/icon"
|
||||
tools:src="@drawable/default_icon" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:contentDescription="@string/icon"
|
||||
tools:src="@drawable/default_icon" />
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:singleLine="true"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="12sp"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="15dp"
|
||||
android:paddingTop="10dp"
|
||||
android:paddingEnd="15dp"
|
||||
android:singleLine="true"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
tools:text="Title" />
|
||||
|
||||
<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>
|
||||
<TextView
|
||||
android:id="@+id/text_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:singleLine="true"
|
||||
android:textSize="12sp"
|
||||
tools:text="Subtitle" />
|
||||
</LinearLayout>
|
||||
|
@ -1,77 +1,70 @@
|
||||
<?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:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/item_click_layout"
|
||||
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
|
||||
android:id="@+id/card_app_item_grid"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
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">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
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
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="match_parent"
|
||||
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" />
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="242.25"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/icon"
|
||||
app:layout_constraintStart_toStartOf="@id/icon"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:alpha="242.25"
|
||||
android:ellipsize="marquee"
|
||||
android:marqueeRepeatLimit="marquee_forever"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?android:attr/textAppearanceListItem"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/text_subtitle"
|
||||
app:layout_constraintEnd_toEndOf="@id/icon"
|
||||
app:layout_constraintStart_toStartOf="@id/icon"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
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>
|
||||
<TextView
|
||||
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>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
@ -2,6 +2,7 @@
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/item_click_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
@ -14,6 +15,7 @@
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:contentDescription="@string/icon"
|
||||
android:focusable="false"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@drawable/default_icon" />
|
||||
|
@ -76,6 +76,6 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
android:text="@string/reset"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
</LinearLayout>
|
||||
|
@ -2,76 +2,45 @@
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/coordinatorLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
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
|
||||
android:id="@+id/app_list"
|
||||
<com.google.android.material.appbar.CollapsingToolbarLayout
|
||||
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_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
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>
|
||||
android:layout_marginTop="-4dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<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>
|
||||
|
@ -9,7 +9,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<emu.skyline.views.CustomLinearLayout
|
||||
<LinearLayout
|
||||
android:id="@+id/fab_parent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
10
app/src/main/res/layout/on_screen_edit_mini_fab.xml
Normal file
10
app/src/main/res/layout/on_screen_edit_mini_fab.xml
Normal 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" />
|
@ -60,7 +60,6 @@
|
||||
android:textAppearance="?android:attr/textAppearanceListItemSecondary"
|
||||
android:textColor="@android:color/tertiary_text_light"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<LinearLayout
|
||||
@ -76,6 +75,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/builtin"
|
||||
android:textColor="?attr/colorAccent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<Button
|
||||
@ -84,8 +84,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
|
||||
android:text="@string/reset"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
@ -4,8 +4,9 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textColor="?colorSecondary"
|
||||
android:textSize="15sp" />
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
@ -22,7 +22,7 @@
|
||||
android:animateLayoutChanges="true"
|
||||
android:text="@string/stick_button"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1"
|
||||
android:textColor="@color/colorPrimary"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="16sp" />
|
||||
|
||||
<TextView
|
||||
@ -116,13 +116,15 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:text="@string/reset" />
|
||||
android:text="@string/reset"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/stick_next"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/next" />
|
||||
android:text="@string/next"
|
||||
android:textColor="?attr/colorAccent" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
@ -1,18 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
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_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:layout_scrollFlags="scroll|exitUntilCollapsed"
|
||||
app:liftOnScroll="true">
|
||||
android:keyboardNavigationCluster="false"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
style="@style/Widget.MaterialComponents.Toolbar.Primary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:theme="@style/AppTheme.ActionBar"
|
||||
app:layout_scrollFlags="scroll|enterAlways|snap" />
|
||||
app:elevation="16dp"
|
||||
app:layout_scrollFlags="scroll" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
107
app/src/main/res/layout/view_search_bar.xml
Normal file
107
app/src/main/res/layout/view_search_bar.xml
Normal 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>
|
@ -6,15 +6,18 @@
|
||||
android:icon="@drawable/ic_search"
|
||||
android:title="@string/search"
|
||||
app:actionViewClass="androidx.appcompat.widget.SearchView"
|
||||
app:iconTint="?android:attr/textColorSecondary"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
<item
|
||||
android:id="@+id/action_share_log"
|
||||
android:icon="@drawable/ic_share"
|
||||
android:title="@string/share"
|
||||
app:iconTint="?android:attr/textColorSecondary"
|
||||
app:showAsAction="ifRoom" />
|
||||
<item
|
||||
android:id="@+id/action_clear"
|
||||
android:icon="@drawable/ic_clear"
|
||||
android:title="@string/clear"
|
||||
app:iconTint="?android:attr/textColorSecondary"
|
||||
app:showAsAction="ifRoom" />
|
||||
</menu>
|
||||
|
@ -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>
|
8
app/src/main/res/values-night-v27/styles.xml
Normal file
8
app/src/main/res/values-night-v27/styles.xml
Normal 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>
|
@ -1,9 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#FFFF0000</color>
|
||||
<color name="colorPrimaryDark">#BFFF0000</color>
|
||||
<color name="colorOnPrimary">#D7000000</color>
|
||||
<color name="colorSecondary">#FFFF0000</color>
|
||||
<color name="colorSecondaryDark">#BFFF0000</color>
|
||||
<color name="colorOnSecondary">#DF000000</color>
|
||||
<color name="colorPrimary">#FF424242</color>
|
||||
<color name="colorPrimaryDark">@android:color/black</color>
|
||||
</resources>
|
||||
|
9
app/src/main/res/values-night/styles.xml
Normal file
9
app/src/main/res/values-night/styles.xml
Normal 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>
|
8
app/src/main/res/values-v27/styles.xml
Normal file
8
app/src/main/res/values-v27/styles.xml
Normal 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>
|
@ -1,9 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#FFFF0000</color>
|
||||
<color name="colorPrimaryDark">#BFFF0000</color>
|
||||
<color name="colorOnPrimary">#DFFFFFFF</color>
|
||||
<color name="colorSecondary">#FFFF0000</color>
|
||||
<color name="colorSecondaryDark">#BFFF0000</color>
|
||||
<color name="colorOnSecondary">#DFFFFFFF</color>
|
||||
<color name="colorPrimary">@color/cardview_light_background</color>
|
||||
<color name="colorPrimaryDark">@android:color/white</color>
|
||||
<color name="colorAccent">#FFFF0000</color>
|
||||
</resources>
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<dimen name="app_card_margin">8dp</dimen>
|
||||
<dimen name="app_card_margin_half">4dp</dimen>
|
||||
</resources>
|
||||
<dimen name="grid_padding">8dp</dimen>
|
||||
</resources>
|
||||
|
@ -1,20 +1,15 @@
|
||||
<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="colorPrimaryVariant">@color/colorPrimaryDark</item>
|
||||
<item name="colorOnPrimary">@color/colorOnPrimary</item>
|
||||
<item name="colorSecondary">@color/colorSecondary</item>
|
||||
<item name="colorSecondaryVariant">@color/colorSecondaryDark</item>
|
||||
<item name="colorOnSecondary">@color/colorOnSecondary</item>
|
||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||
<item name="colorAccent">@color/colorAccent</item>
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.ActionBar" parent="">
|
||||
<item name="android:textColorPrimary">@color/colorOnPrimary</item>
|
||||
<item name="android:textColorSecondary">@color/colorOnPrimary</item>
|
||||
</style>
|
||||
<style name="AppTheme" parent="BaseAppTheme" />
|
||||
|
||||
<style name="roundedAppImage" parent="">
|
||||
<style name="roundedAppImage">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">6dp</item>
|
||||
</style>
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user