mirror of
https://github.com/skyline-emu/skyline.git
synced 2024-11-23 00:19:20 +01:00
Switch to viewbinding and di with hilt
This commit is contained in:
parent
ff1b9cb510
commit
f5d5caf939
@ -110,6 +110,10 @@
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
|
@ -1,6 +1,9 @@
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-android-extensions'
|
||||
plugins {
|
||||
id 'com.android.application'
|
||||
id 'kotlin-android'
|
||||
id 'kotlin-kapt'
|
||||
id 'dagger.hilt.android.plugin'
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
@ -51,14 +54,7 @@ android {
|
||||
}
|
||||
}
|
||||
buildFeatures {
|
||||
prefab true
|
||||
viewBinding false
|
||||
}
|
||||
|
||||
/* Android Extensions */
|
||||
androidExtensions {
|
||||
/* TODO: Remove this after migrating to View Bindings */
|
||||
experimental = true
|
||||
viewBinding true
|
||||
}
|
||||
|
||||
/* Linting */
|
||||
@ -79,6 +75,10 @@ android {
|
||||
aaptOptions {
|
||||
ignoreAssetsPattern "*.md"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -99,6 +99,8 @@ dependencies {
|
||||
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
|
||||
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
|
||||
implementation 'androidx.fragment:fragment-ktx:1.2.5'
|
||||
implementation "com.google.dagger:hilt-android:$hilt_version"
|
||||
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
|
||||
|
||||
/* Kotlin */
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||
|
@ -20,8 +20,8 @@ import androidx.core.graphics.drawable.toBitmap
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.databinding.AppDialogBinding
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import kotlinx.android.synthetic.main.app_dialog.*
|
||||
|
||||
/**
|
||||
* This dialog is used to show extra game metadata and provide extra options such as pinning the game to the home screen
|
||||
@ -41,19 +41,15 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var item : AppItem
|
||||
private lateinit var binding : AppDialogBinding
|
||||
|
||||
private val item by lazy { requireArguments().getSerializable("item") as AppItem }
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
|
||||
return requireActivity().layoutInflater.inflate(R.layout.app_dialog, container)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
item = requireArguments().getSerializable("item") as AppItem
|
||||
return AppDialogBinding.inflate(inflater).also { binding = it }.root
|
||||
}
|
||||
|
||||
/**
|
||||
@ -82,19 +78,19 @@ class AppDialog : BottomSheetDialogFragment() {
|
||||
|
||||
val missingIcon = ContextCompat.getDrawable(requireActivity(), R.drawable.default_icon)!!.toBitmap(256, 256)
|
||||
|
||||
game_icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
game_title.text = item.title
|
||||
game_subtitle.text = item.subTitle ?: item.loaderResultString(requireContext())
|
||||
binding.gameIcon.setImageBitmap(item.icon ?: missingIcon)
|
||||
binding.gameTitle.text = item.title
|
||||
binding.gameSubtitle.text = item.subTitle ?: item.loaderResultString(requireContext())
|
||||
|
||||
game_play.isEnabled = item.loaderResult == LoaderResult.Success
|
||||
game_play.setOnClickListener {
|
||||
binding.gamePlay.isEnabled = item.loaderResult == LoaderResult.Success
|
||||
binding.gamePlay.setOnClickListener {
|
||||
startActivity(Intent(activity, EmulationActivity::class.java).apply { data = item.uri })
|
||||
}
|
||||
|
||||
val shortcutManager = requireActivity().getSystemService(ShortcutManager::class.java)
|
||||
game_pin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||
binding.gamePin.isEnabled = shortcutManager.isRequestPinShortcutSupported
|
||||
|
||||
game_pin.setOnClickListener {
|
||||
binding.gamePin.setOnClickListener {
|
||||
val info = ShortcutInfo.Builder(context, item.title)
|
||||
info.setShortLabel(item.title)
|
||||
info.setActivity(ComponentName(requireContext(), EmulationActivity::class.java))
|
||||
|
@ -17,13 +17,16 @@ import android.view.*
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isInvisible
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.databinding.EmuActivityBinding
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.loader.getRomFormat
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.emu_activity.*
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
@AndroidEntryPoint
|
||||
class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTouchListener {
|
||||
companion object {
|
||||
private val Tag = EmulationActivity::class.java.simpleName
|
||||
@ -33,8 +36,10 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
System.loadLibrary("skyline") // libskyline.so
|
||||
}
|
||||
|
||||
private val binding by lazy { EmuActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
/**
|
||||
* A map of [Vibrator]s that correspond to [InputManager.controllers]
|
||||
* A map of [Vibrator]s that correspond to [inputManager.controllers]
|
||||
*/
|
||||
private var vibrators = HashMap<Int, Vibrator>()
|
||||
|
||||
@ -51,6 +56,9 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
|
||||
private val settings by lazy { Settings(this) }
|
||||
|
||||
@Inject
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* This is the entry point into the emulation code for libskyline
|
||||
*
|
||||
@ -131,7 +139,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
*/
|
||||
@Suppress("unused")
|
||||
private fun initializeControllers() {
|
||||
for (controller in InputManager.controllers.values) {
|
||||
for (controller in inputManager.controllers.values) {
|
||||
if (controller.type != ControllerType.None) {
|
||||
val type = when (controller.type) {
|
||||
ControllerType.None -> throw IllegalArgumentException()
|
||||
@ -177,11 +185,11 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
/**
|
||||
* This makes the window fullscreen, sets up the performance statistics and finally calls [executeApplication] for executing the application
|
||||
*/
|
||||
@SuppressLint("SetTextI18n")
|
||||
@SuppressLint("SetTextI18n", "ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.emu_activity)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.hide(WindowInsets.Type.navigationBars() or WindowInsets.Type.systemBars() or WindowInsets.Type.systemGestures() or WindowInsets.Type.statusBars())
|
||||
@ -196,32 +204,36 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN)
|
||||
}
|
||||
|
||||
game_view.holder.addCallback(this)
|
||||
binding.gameView.holder.addCallback(this)
|
||||
|
||||
if (settings.perfStats) {
|
||||
perf_stats.postDelayed(object : Runnable {
|
||||
override fun run() {
|
||||
updatePerformanceStatistics()
|
||||
perf_stats.text = "$fps FPS\n${frametime}ms"
|
||||
perf_stats.postDelayed(this, 250)
|
||||
}
|
||||
}, 250)
|
||||
binding.perfStats.apply {
|
||||
postDelayed(object : Runnable {
|
||||
override fun run() {
|
||||
updatePerformanceStatistics()
|
||||
text = "$fps FPS\n${frametime}ms"
|
||||
postDelayed(this, 250)
|
||||
}
|
||||
}, 250)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION") val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display!! else windowManager.defaultDisplay
|
||||
display?.supportedModes?.maxByOrNull { it.refreshRate + (it.physicalHeight * it.physicalWidth) }?.let { window.attributes.preferredDisplayModeId = it.modeId }
|
||||
|
||||
game_view.setOnTouchListener(this)
|
||||
binding.gameView.setOnTouchListener(this)
|
||||
|
||||
// Hide on screen controls when first controller is not set
|
||||
on_screen_controller_view.isGone = InputManager.controllers[0]!!.type == ControllerType.None || !settings.onScreenControl
|
||||
on_screen_controller_view.setOnButtonStateChangedListener(::onButtonStateChanged)
|
||||
on_screen_controller_view.setOnStickStateChangedListener(::onStickStateChanged)
|
||||
on_screen_controller_view.recenterSticks = settings.onScreenControlRecenterSticks
|
||||
binding.onScreenControllerView.apply {
|
||||
isGone = inputManager.controllers[0]!!.type == ControllerType.None || !settings.onScreenControl
|
||||
setOnButtonStateChangedListener(::onButtonStateChanged)
|
||||
setOnStickStateChangedListener(::onStickStateChanged)
|
||||
recenterSticks = settings.onScreenControlRecenterSticks
|
||||
}
|
||||
|
||||
on_screen_controller_toggle.isGone = on_screen_controller_view.isGone
|
||||
on_screen_controller_toggle.setOnClickListener {
|
||||
on_screen_controller_view.isInvisible = !on_screen_controller_view.isInvisible
|
||||
binding.onScreenControllerToggle.apply {
|
||||
isGone = binding.onScreenControllerView.isGone
|
||||
setOnClickListener { binding.onScreenControllerView.isInvisible = !binding.onScreenControllerView.isInvisible }
|
||||
}
|
||||
|
||||
executeApplication(intent.data!!)
|
||||
@ -291,7 +303,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
else -> return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
return when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
return when (val guestEvent = inputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button != ButtonId.Menu)
|
||||
setButtonState(guestEvent.id, guestEvent.button.value(), action.state)
|
||||
@ -333,9 +345,9 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
var polarity = value >= 0
|
||||
|
||||
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
inputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
inputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -413,7 +425,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
val vibrator = if (vibrators[index] != null) {
|
||||
vibrators[index]!!
|
||||
} else {
|
||||
InputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
|
||||
inputManager.controllers[index]!!.rumbleDeviceDescriptor?.let {
|
||||
if (it == "builtin") {
|
||||
val vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
|
||||
vibrators[index] = vibrator
|
||||
@ -421,7 +433,7 @@ class EmulationActivity : AppCompatActivity(), SurfaceHolder.Callback, View.OnTo
|
||||
} else {
|
||||
for (id in InputDevice.getDeviceIds()) {
|
||||
val device = InputDevice.getDevice(id)
|
||||
if (device.descriptor == InputManager.controllers[index]!!.rumbleDeviceDescriptor) {
|
||||
if (device.descriptor == inputManager.controllers[index]!!.rumbleDeviceDescriptor) {
|
||||
vibrators[index] = device.vibrator
|
||||
device.vibrator
|
||||
}
|
||||
|
@ -20,9 +20,8 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.adapter.GenericAdapter
|
||||
import emu.skyline.adapter.HeaderViewItem
|
||||
import emu.skyline.adapter.LogViewItem
|
||||
import emu.skyline.databinding.LogActivityBinding
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.log_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@ -31,25 +30,21 @@ import java.net.URL
|
||||
import javax.net.ssl.HttpsURLConnection
|
||||
|
||||
class LogActivity : AppCompatActivity() {
|
||||
private val binding by lazy { LogActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
/**
|
||||
* The log file is used to read log entries from or to clear all entries
|
||||
*/
|
||||
private lateinit var logFile : File
|
||||
|
||||
/**
|
||||
* The adapter used for adding elements from the log to [log_list]
|
||||
*/
|
||||
private val adapter = GenericAdapter()
|
||||
|
||||
/**
|
||||
* This initializes [toolbar] and fills [log_list] with data from the logs
|
||||
*/
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.log_activity)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.titlebar.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val settings = Settings(this)
|
||||
@ -58,10 +53,9 @@ class LogActivity : AppCompatActivity() {
|
||||
val logLevel = settings.logLevel.toInt()
|
||||
val logLevels = resources.getStringArray(R.array.log_level)
|
||||
|
||||
log_list.adapter = adapter
|
||||
binding.logList.adapter = adapter
|
||||
|
||||
if (!compact)
|
||||
log_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
if (!compact) binding.logList.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
|
||||
try {
|
||||
logFile = File(applicationContext.filesDir.canonicalPath + "/skyline.log")
|
||||
|
@ -31,12 +31,14 @@ import emu.skyline.adapter.LayoutType
|
||||
import emu.skyline.data.AppItem
|
||||
import emu.skyline.data.DataItem
|
||||
import emu.skyline.data.HeaderItem
|
||||
import emu.skyline.databinding.MainActivityBinding
|
||||
import emu.skyline.loader.LoaderResult
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlin.math.ceil
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
private val binding by lazy { MainActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
private val settings by lazy { Settings(this) }
|
||||
|
||||
private val adapter = GenericAdapter()
|
||||
@ -60,24 +62,23 @@ class MainActivity : AppCompatActivity() {
|
||||
)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.main_activity)
|
||||
setContentView(binding.root)
|
||||
|
||||
PreferenceManager.setDefaultValues(this, R.xml.preferences, false)
|
||||
|
||||
setupAppList()
|
||||
|
||||
swipe_refresh_layout.apply {
|
||||
binding.swipeRefreshLayout.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) }
|
||||
post { setDistanceToTriggerSync(binding.swipeRefreshLayout.height / 3) }
|
||||
setOnRefreshListener { loadRoms(false) }
|
||||
}
|
||||
|
||||
viewModel.state.observe(owner = this, onChanged = ::handleState)
|
||||
viewModel.stateData.observe(owner = this, onChanged = ::handleState)
|
||||
loadRoms(!settings.refreshRequired)
|
||||
|
||||
search_bar.apply {
|
||||
binding.searchBar.apply {
|
||||
setLogIconListener { startActivity(Intent(context, LogActivity::class.java)) }
|
||||
setSettingsIconListener { startActivityForResult(Intent(context, SettingsActivity::class.java), 3) }
|
||||
setRefreshIconListener { loadRoms(false) }
|
||||
@ -90,7 +91,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
window.decorView.findViewById<View>(android.R.id.content).viewTreeObserver.addOnTouchModeChangeListener { isInTouchMode ->
|
||||
search_bar.refreshIconVisible = !isInTouchMode
|
||||
binding.searchBar.refreshIconVisible = !isInTouchMode
|
||||
}
|
||||
}
|
||||
|
||||
@ -118,11 +119,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setAppListDecoration() {
|
||||
while (app_list.itemDecorationCount > 0) app_list.removeItemDecorationAt(0)
|
||||
when (layoutType) {
|
||||
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
|
||||
binding.appList.apply {
|
||||
while (itemDecorationCount > 0) removeItemDecorationAt(0)
|
||||
when (layoutType) {
|
||||
LayoutType.List -> addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
|
||||
|
||||
LayoutType.Grid, LayoutType.GridCompact -> app_list.addItemDecoration(GridSpacingItemDecoration())
|
||||
LayoutType.Grid, LayoutType.GridCompact -> addItemDecoration(GridSpacingItemDecoration())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,7 +135,7 @@ class MainActivity : AppCompatActivity() {
|
||||
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 getSpanSize(position : Int) = if (layoutType == LayoutType.List || adapter.currentItems[position].fullSpan) gridSpan else 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,23 +144,24 @@ class MainActivity : AppCompatActivity() {
|
||||
when (focusDirection) {
|
||||
View.FOCUS_DOWN -> {
|
||||
findContainingItemView(focused)?.let { focusedChild ->
|
||||
val current = app_list.indexOfChild(focusedChild)
|
||||
val current = binding.appList.indexOfChild(focusedChild)
|
||||
val currentSpanIndex = (focusedChild.layoutParams as LayoutParams).spanIndex
|
||||
for (i in current + 1 until app_list.size) {
|
||||
for (i in current + 1 until binding.appList.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)
|
||||
binding.appBarLayout.setExpanded(false) // End of list, hide app bar, so bottom row is fully visible
|
||||
binding.appList.smoothScrollToPosition(adapter.itemCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View.FOCUS_UP -> {
|
||||
if (nextFocus?.isFocusable != true) {
|
||||
search_bar.requestFocus()
|
||||
app_bar_layout.setExpanded(true)
|
||||
binding.searchBar.requestFocus()
|
||||
binding.appBarLayout.setExpanded(true)
|
||||
return null
|
||||
}
|
||||
}
|
||||
@ -167,13 +171,13 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun setupAppList() {
|
||||
app_list.adapter = adapter
|
||||
binding.appList.adapter = adapter
|
||||
|
||||
val itemWidth = 225
|
||||
val metrics = resources.displayMetrics
|
||||
val gridSpan = ceil((metrics.widthPixels / metrics.density) / itemWidth).toInt()
|
||||
|
||||
app_list.layoutManager = CustomLayoutManager(gridSpan)
|
||||
binding.appList.layoutManager = CustomLayoutManager(gridSpan)
|
||||
setAppListDecoration()
|
||||
|
||||
if (settings.searchLocation.isEmpty()) {
|
||||
@ -186,18 +190,18 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
private fun handleState(state : MainState) = when (state) {
|
||||
MainState.Loading -> {
|
||||
search_bar.animateRefreshIcon()
|
||||
swipe_refresh_layout.isRefreshing = true
|
||||
binding.searchBar.animateRefreshIcon()
|
||||
binding.swipeRefreshLayout.isRefreshing = true
|
||||
}
|
||||
is MainState.Loaded -> {
|
||||
swipe_refresh_layout.isRefreshing = false
|
||||
binding.swipeRefreshLayout.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 (binding.swipeRefreshLayout.isRefreshing) return
|
||||
|
||||
if (settings.selectAction)
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
@ -206,7 +210,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun selectShowGameDialog(appItem : AppItem) {
|
||||
if (swipe_refresh_layout.isRefreshing) return
|
||||
if (binding.swipeRefreshLayout.isRefreshing) return
|
||||
|
||||
AppDialog.newInstance(appItem).show(supportFragmentManager, "game")
|
||||
}
|
||||
@ -281,7 +285,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
search_bar.apply {
|
||||
binding.searchBar.apply {
|
||||
if (hasFocus() && text.isNotEmpty()) {
|
||||
text = ""
|
||||
clearFocus()
|
||||
|
@ -31,8 +31,11 @@ class MainViewModel : ViewModel() {
|
||||
private val TAG = MainViewModel::class.java.simpleName
|
||||
}
|
||||
|
||||
private val mutableState = MutableLiveData<MainState>()
|
||||
val state : LiveData<MainState> = mutableState
|
||||
private var state
|
||||
get() = _stateData.value
|
||||
set(value) = _stateData.postValue(value)
|
||||
private val _stateData = MutableLiveData<MainState>()
|
||||
val stateData : LiveData<MainState> = _stateData
|
||||
|
||||
var searchBarAnimated = false
|
||||
|
||||
@ -66,16 +69,15 @@ class MainViewModel : ViewModel() {
|
||||
* @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)
|
||||
if (state == MainState.Loading) return
|
||||
state = MainState.Loading
|
||||
|
||||
val romsFile = File(context.filesDir.canonicalPath + "/roms.bin")
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
if (loadFromFile) {
|
||||
try {
|
||||
mutableState.postValue(MainState.Loaded(loadSerializedList(romsFile)))
|
||||
state = MainState.Loaded(loadSerializedList(romsFile))
|
||||
return@launch
|
||||
} catch (e : Exception) {
|
||||
Log.w(TAG, "Ran into exception while loading: ${e.message}")
|
||||
@ -98,9 +100,9 @@ class MainViewModel : ViewModel() {
|
||||
Log.w(TAG, "Ran into exception while saving: ${e.message}")
|
||||
}
|
||||
|
||||
mutableState.postValue(MainState.Loaded(romElements))
|
||||
state = MainState.Loaded(romElements)
|
||||
} catch (e : Exception) {
|
||||
mutableState.postValue(MainState.Error(e))
|
||||
state = MainState.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,11 +11,13 @@ import android.view.KeyEvent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.PreferenceGroup
|
||||
import emu.skyline.databinding.SettingsActivityBinding
|
||||
import emu.skyline.preference.ActivityResultDelegate
|
||||
import emu.skyline.preference.DocumentActivity
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
val binding by lazy { SettingsActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
/**
|
||||
* This is the instance of [PreferenceFragment] that is shown inside [R.id.settings]
|
||||
*/
|
||||
@ -27,9 +29,9 @@ class SettingsActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContentView(R.layout.settings_activity)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.titlebar.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
supportFragmentManager
|
||||
|
@ -6,14 +6,7 @@
|
||||
package emu.skyline
|
||||
|
||||
import android.app.Application
|
||||
import emu.skyline.input.InputManager
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
/**
|
||||
* Custom application class to initialize [InputManager]
|
||||
*/
|
||||
class SkylineApplication : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
InputManager.init(applicationContext)
|
||||
}
|
||||
}
|
||||
@HiltAndroidApp
|
||||
class SkylineApplication : Application()
|
||||
|
@ -10,42 +10,88 @@ import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.Window
|
||||
import android.widget.ImageView
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import emu.skyline.R
|
||||
import emu.skyline.data.AppItem
|
||||
import kotlinx.android.synthetic.main.app_item_grid_compact.*
|
||||
import emu.skyline.databinding.AppItemGridBinding
|
||||
import emu.skyline.databinding.AppItemGridCompactBinding
|
||||
import emu.skyline.databinding.AppItemLinearBinding
|
||||
|
||||
/**
|
||||
* This enumerates the type of layouts the menu can be in
|
||||
*/
|
||||
enum class LayoutType(val layoutRes : Int) {
|
||||
List(R.layout.app_item_linear),
|
||||
Grid(R.layout.app_item_grid),
|
||||
GridCompact(R.layout.app_item_grid_compact)
|
||||
sealed class LayoutType(val builder : (parent : ViewGroup) -> ViewBinding) {
|
||||
object List : LayoutType({ ListBinding(it) })
|
||||
object Grid : LayoutType({ GridBinding(it) })
|
||||
object GridCompact : LayoutType({ GridCompatBinding(it) })
|
||||
|
||||
companion object {
|
||||
fun values() = arrayListOf(List, Grid, GridCompact)
|
||||
}
|
||||
}
|
||||
|
||||
data class LayoutBindingFactory(private val layoutType : LayoutType) : ViewBindingFactory {
|
||||
override fun createBinding(parent : ViewGroup) = layoutType.builder(parent)
|
||||
}
|
||||
|
||||
interface LayoutBinding<V : ViewBinding> : ViewBinding {
|
||||
val binding : V
|
||||
|
||||
override fun getRoot() = binding.root
|
||||
|
||||
val textTitle : TextView
|
||||
|
||||
val textSubtitle : TextView
|
||||
|
||||
val icon : ImageView
|
||||
}
|
||||
|
||||
class ListBinding(parent : ViewGroup) : LayoutBinding<AppItemLinearBinding> {
|
||||
override val binding = AppItemLinearBinding.inflate(parent.inflater(), parent, false)
|
||||
|
||||
override val textTitle = binding.textTitle
|
||||
|
||||
override val textSubtitle = binding.textSubtitle
|
||||
|
||||
override val icon = binding.icon
|
||||
}
|
||||
|
||||
class GridBinding(parent : ViewGroup) : LayoutBinding<AppItemGridBinding> {
|
||||
override val binding = AppItemGridBinding.inflate(parent.inflater(), parent, false)
|
||||
|
||||
override val textTitle = binding.textTitle
|
||||
|
||||
override val textSubtitle = binding.textSubtitle
|
||||
|
||||
override val icon = binding.icon
|
||||
}
|
||||
|
||||
class GridCompatBinding(parent : ViewGroup) : LayoutBinding<AppItemGridCompactBinding> {
|
||||
override val binding = AppItemGridCompactBinding.inflate(parent.inflater(), parent, false)
|
||||
|
||||
override val textTitle = binding.textTitle
|
||||
|
||||
override val textSubtitle = binding.textSubtitle
|
||||
|
||||
override val icon = binding.icon
|
||||
}
|
||||
|
||||
private typealias InteractionFunction = (appItem : AppItem) -> Unit
|
||||
|
||||
private data class AppLayoutFactory(private val layoutType : LayoutType) : GenericLayoutFactory {
|
||||
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) : GenericListItem<LayoutBinding<*>>() {
|
||||
override fun getViewBindingFactory() = LayoutBindingFactory(layoutType)
|
||||
|
||||
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<LayoutBinding<*>>, position : Int) {
|
||||
holder.binding.textTitle.text = item.title
|
||||
holder.binding.textSubtitle.text = item.subTitle ?: item.loaderResultString(holder.binding.root.context)
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = item.title
|
||||
holder.text_subtitle.text = item.subTitle ?: item.loaderResultString(holder.text_subtitle.context)
|
||||
|
||||
holder.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
holder.binding.icon.setImageBitmap(item.icon ?: missingIcon)
|
||||
|
||||
if (layoutType == LayoutType.List) {
|
||||
holder.icon.setOnClickListener { showIconDialog(holder.icon.context, item) }
|
||||
holder.binding.icon.setOnClickListener { showIconDialog(it.context, item) }
|
||||
}
|
||||
|
||||
holder.itemView.findViewById<View>(R.id.item_click_layout).apply {
|
||||
@ -68,7 +114,7 @@ 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 areItemsTheSame(other : GenericListItem<LayoutBinding<*>>) = key() == other.key()
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is AppViewItem && layoutType == other.layoutType && item == other.item
|
||||
override fun areContentsTheSame(other : GenericListItem<LayoutBinding<*>>) = other is AppViewItem && layoutType == other.layoutType && item == other.item
|
||||
}
|
||||
|
@ -5,39 +5,41 @@
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
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 androidx.viewbinding.ViewBinding
|
||||
import info.debatty.java.stringsimilarity.Cosine
|
||||
import info.debatty.java.stringsimilarity.JaroWinkler
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getLayoutFactory]
|
||||
* Can handle any view types with [GenericListItem] implemented, [GenericListItem] are differentiated by the return value of [GenericListItem.getViewBindingFactory]
|
||||
*/
|
||||
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
class GenericAdapter : RecyclerView.Adapter<GenericViewHolder<ViewBinding>>(), Filterable {
|
||||
companion object {
|
||||
private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem>() {
|
||||
override fun areItemsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areItemsTheSame(newItem)
|
||||
private val DIFFER = object : DiffUtil.ItemCallback<GenericListItem<ViewBinding>>() {
|
||||
override fun areItemsTheSame(oldItem : GenericListItem<ViewBinding>, newItem : GenericListItem<ViewBinding>) = oldItem.areItemsTheSame(newItem)
|
||||
|
||||
override fun areContentsTheSame(oldItem : GenericListItem, newItem : GenericListItem) = oldItem.areContentsTheSame(newItem)
|
||||
override fun areContentsTheSame(oldItem : GenericListItem<ViewBinding>, newItem : GenericListItem<ViewBinding>) = oldItem.areContentsTheSame(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private val asyncListDiffer = AsyncListDiffer(this, DIFFER)
|
||||
private val allItems = mutableListOf<GenericListItem>()
|
||||
val currentItems : List<GenericListItem> get() = asyncListDiffer.currentList
|
||||
private val allItems = mutableListOf<GenericListItem<out ViewBinding>>()
|
||||
val currentItems : List<GenericListItem<in ViewBinding>> get() = asyncListDiffer.currentList
|
||||
|
||||
var currentSearchTerm = ""
|
||||
|
||||
private val viewTypesMapping = mutableMapOf<GenericLayoutFactory, Int>()
|
||||
private val viewTypesMapping = mutableMapOf<ViewBindingFactory, Int>()
|
||||
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createLayout(parent))
|
||||
override fun onCreateViewHolder(parent : ViewGroup, viewType : Int) = GenericViewHolder(viewTypesMapping.filterValues { it == viewType }.keys.single().createBinding(parent))
|
||||
|
||||
override fun onBindViewHolder(holder : GenericViewHolder, position : Int) {
|
||||
override fun onBindViewHolder(holder : GenericViewHolder<ViewBinding>, position : Int) {
|
||||
currentItems[position].apply {
|
||||
adapter = this@GenericAdapter
|
||||
bind(holder, position)
|
||||
@ -46,9 +48,9 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
|
||||
override fun getItemCount() = currentItems.size
|
||||
|
||||
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getLayoutFactory(), { viewTypesMapping.size })
|
||||
override fun getItemViewType(position : Int) = viewTypesMapping.getOrPut(currentItems[position].getViewBindingFactory()) { viewTypesMapping.size }
|
||||
|
||||
fun setItems(items : List<GenericListItem>) {
|
||||
fun setItems(items : List<GenericListItem<*>>) {
|
||||
allItems.clear()
|
||||
allItems.addAll(items)
|
||||
filter.filter(currentSearchTerm)
|
||||
@ -68,7 +70,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
*/
|
||||
private val cos = Cosine()
|
||||
|
||||
inner class ScoredItem(val score : Double, val item : GenericListItem)
|
||||
inner class ScoredItem(val score : Double, val item : GenericListItem<*>)
|
||||
|
||||
/**
|
||||
* This sorts the items in [allItems] in relation to how similar they are to [currentSearchTerm]
|
||||
@ -93,7 +95,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
results.values = allItems.toMutableList()
|
||||
results.count = allItems.size
|
||||
} else {
|
||||
val filterData = mutableListOf<GenericListItem>()
|
||||
val filterData = mutableListOf<GenericListItem<*>>()
|
||||
|
||||
val topResults = extractSorted()
|
||||
val avgScore = topResults.sumByDouble { it.score } / topResults.size
|
||||
@ -112,7 +114,7 @@ class GenericAdapter : RecyclerView.Adapter<GenericViewHolder>(), Filterable {
|
||||
*/
|
||||
override fun publishResults(charSequence : CharSequence, results : FilterResults) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
asyncListDiffer.submitList(results.values as List<GenericListItem>)
|
||||
asyncListDiffer.submitList(results.values as List<GenericListItem<ViewBinding>>)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,33 +5,38 @@
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.android.extensions.LayoutContainer
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
class GenericViewHolder(override val containerView : View) : RecyclerView.ViewHolder(containerView), LayoutContainer
|
||||
class GenericViewHolder<out V : ViewBinding>(val binding : V) : RecyclerView.ViewHolder(binding.root)
|
||||
|
||||
interface GenericLayoutFactory {
|
||||
fun createLayout(parent : ViewGroup) : View
|
||||
fun View.inflater() = LayoutInflater.from(context)!!
|
||||
|
||||
interface ViewBindingFactory {
|
||||
fun createBinding(parent : ViewGroup) : ViewBinding
|
||||
}
|
||||
|
||||
abstract class GenericListItem {
|
||||
abstract class GenericListItem<V : ViewBinding> {
|
||||
var adapter : GenericAdapter? = null
|
||||
|
||||
abstract fun getLayoutFactory() : GenericLayoutFactory
|
||||
abstract fun getViewBindingFactory() : ViewBindingFactory
|
||||
|
||||
abstract fun bind(holder : GenericViewHolder, position : Int)
|
||||
abstract fun bind(holder : GenericViewHolder<V>, position : Int)
|
||||
|
||||
/**
|
||||
* Used for filtering
|
||||
*/
|
||||
open fun key() : String = ""
|
||||
|
||||
open fun areItemsTheSame(other : GenericListItem) = this == other
|
||||
open fun areItemsTheSame(other : GenericListItem<V>) = this == other
|
||||
|
||||
/**
|
||||
* Will only be called when [areItemsTheSame] returns true, thus returning true by default
|
||||
*/
|
||||
open fun areContentsTheSame(other : GenericListItem) = true
|
||||
open fun areContentsTheSame(other : GenericListItem<V>) = true
|
||||
|
||||
open val fullSpan : Boolean = false
|
||||
}
|
||||
|
@ -5,24 +5,23 @@
|
||||
|
||||
package emu.skyline.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import emu.skyline.R
|
||||
import kotlinx.android.synthetic.main.section_item.*
|
||||
import emu.skyline.databinding.SectionItemBinding
|
||||
|
||||
private object HeaderLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.section_item, parent, false)
|
||||
object HeaderBindingFactory : ViewBindingFactory {
|
||||
override fun createBinding(parent : ViewGroup) = SectionItemBinding.inflate(parent.inflater(), parent, false)
|
||||
}
|
||||
|
||||
class HeaderViewItem(private val text : String) : GenericListItem() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = HeaderLayoutFactory
|
||||
class HeaderViewItem(private val text : String) : GenericListItem<SectionItemBinding>() {
|
||||
override fun getViewBindingFactory() = HeaderBindingFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = text
|
||||
override fun bind(holder : GenericViewHolder<SectionItemBinding>, position : Int) {
|
||||
holder.binding.textTitle.text = text
|
||||
}
|
||||
|
||||
override fun toString() = ""
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is HeaderViewItem && text == other.text
|
||||
override fun areItemsTheSame(other : GenericListItem<SectionItemBinding>) = other is HeaderViewItem && text == other.text
|
||||
|
||||
override val fullSpan = true
|
||||
}
|
||||
|
@ -7,25 +7,51 @@ package emu.skyline.adapter
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import emu.skyline.R
|
||||
import kotlinx.android.synthetic.main.log_item.*
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import emu.skyline.databinding.LogItemBinding
|
||||
import emu.skyline.databinding.LogItemCompactBinding
|
||||
|
||||
private data class LogLayoutFactory(private val compact : Boolean) : GenericLayoutFactory {
|
||||
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)
|
||||
data class LogBindingFactory(private val compact : Boolean) : ViewBindingFactory {
|
||||
override fun createBinding(parent : ViewGroup) = if (compact) LogCompactBinding(parent) else LogBinding(parent)
|
||||
}
|
||||
|
||||
data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem() {
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = LogLayoutFactory(compact)
|
||||
interface ILogBinding : ViewBinding {
|
||||
val binding : ViewBinding
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.text = message
|
||||
holder.text_subtitle?.text = level
|
||||
override fun getRoot() = binding.root
|
||||
|
||||
holder.itemView.setOnClickListener {
|
||||
val textTitle : TextView
|
||||
|
||||
val textSubTitle : TextView?
|
||||
}
|
||||
|
||||
class LogCompactBinding(parent : ViewGroup) : ILogBinding {
|
||||
override val binding = LogItemCompactBinding.inflate(parent.inflater(), parent, false)
|
||||
|
||||
override val textTitle = binding.textTitle
|
||||
|
||||
override val textSubTitle : Nothing? = null
|
||||
}
|
||||
|
||||
class LogBinding(parent : ViewGroup) : ILogBinding {
|
||||
override val binding = LogItemBinding.inflate(parent.inflater(), parent, false)
|
||||
|
||||
override val textTitle = binding.textTitle
|
||||
|
||||
override val textSubTitle = binding.textSubtitle
|
||||
}
|
||||
|
||||
data class LogViewItem(private val compact : Boolean, private val message : String, private val level : String) : GenericListItem<ILogBinding>() {
|
||||
override fun getViewBindingFactory() = LogBindingFactory(compact)
|
||||
|
||||
override fun bind(holder : GenericViewHolder<ILogBinding>, position : Int) {
|
||||
holder.binding.textTitle.text = message
|
||||
holder.binding.textSubTitle?.text = level
|
||||
|
||||
holder.binding.root.setOnClickListener {
|
||||
it.context.getSystemService(ClipboardManager::class.java).setPrimaryClip(ClipData.newPlainText("Log Message", "$message ($level)"))
|
||||
Toast.makeText(it.context, "Copied to clipboard", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
@ -7,25 +7,26 @@ package emu.skyline.adapter.controller
|
||||
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.databinding.ControllerItemBinding
|
||||
import emu.skyline.di.InputManagerProviderEntryPoint
|
||||
import emu.skyline.input.ButtonGuestEvent
|
||||
import emu.skyline.input.ButtonId
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
/**
|
||||
* This item is used to display a particular [button] mapping for the controller
|
||||
*/
|
||||
class ControllerButtonViewItem(private val controllerId : Int, val button : ButtonId, private val onClick : (item : ControllerButtonViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
|
||||
content = button.long?.let { holder.itemView.context.getString(it) } ?: button.toString()
|
||||
val guestEvent = ButtonGuestEvent(controllerId, button)
|
||||
subContent = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: ""
|
||||
subContent = InputManagerProviderEntryPoint.getInputManager(holder.binding.root.context).eventMap.filter { it.value is ButtonGuestEvent && it.value == guestEvent }.keys.firstOrNull()?.toString() ?: ""
|
||||
|
||||
super.bind(holder, position)
|
||||
|
||||
holder.itemView.setOnClickListener { onClick.invoke(this, position) }
|
||||
holder.binding.root.setOnClickListener { onClick.invoke(this, position) }
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && controllerId == other.controllerId
|
||||
override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerButtonViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerButtonViewItem && button == other.button
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerButtonViewItem && button == other.button
|
||||
}
|
||||
|
@ -5,36 +5,34 @@
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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 kotlinx.android.synthetic.main.controller_checkbox_item.*
|
||||
import emu.skyline.adapter.ViewBindingFactory
|
||||
import emu.skyline.adapter.inflater
|
||||
import emu.skyline.databinding.ControllerCheckboxItemBinding
|
||||
|
||||
private object ControllerCheckBoxLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_checkbox_item, parent, false)
|
||||
object ControllerCheckBoxBindingFactory : ViewBindingFactory {
|
||||
override fun createBinding(parent : ViewGroup) = ControllerCheckboxItemBinding.inflate(parent.inflater(), parent, false)
|
||||
}
|
||||
|
||||
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
|
||||
class ControllerCheckBoxViewItem(var title : String, var summary : String, var checked : Boolean, private val onCheckedChange : (item : ControllerCheckBoxViewItem, position : Int) -> Unit) : GenericListItem<ControllerCheckboxItemBinding>() {
|
||||
override fun getViewBindingFactory() = ControllerCheckBoxBindingFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
holder.text_title.isGone = title.isEmpty()
|
||||
holder.text_title.text = title
|
||||
holder.text_subtitle.isGone = summary.isEmpty()
|
||||
holder.text_subtitle.text = summary
|
||||
holder.checkbox.isChecked = checked
|
||||
override fun bind(holder : GenericViewHolder<ControllerCheckboxItemBinding>, position : Int) {
|
||||
holder.binding.textTitle.isGone = title.isEmpty()
|
||||
holder.binding.textTitle.text = title
|
||||
holder.binding.textSubtitle.isGone = summary.isEmpty()
|
||||
holder.binding.textSubtitle.text = summary
|
||||
holder.binding.checkbox.isChecked = checked
|
||||
holder.itemView.setOnClickListener {
|
||||
checked = !checked
|
||||
onCheckedChange.invoke(this, position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerCheckBoxViewItem
|
||||
override fun areItemsTheSame(other : GenericListItem<ControllerCheckboxItemBinding>) = other is ControllerCheckBoxViewItem
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerCheckBoxViewItem && title == other.title && summary == other.summary && checked == other.checked
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerCheckboxItemBinding>) = other is ControllerCheckBoxViewItem && title == other.title && summary == other.summary && checked == other.checked
|
||||
}
|
||||
|
@ -8,8 +8,9 @@ package emu.skyline.adapter.controller
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.databinding.ControllerItemBinding
|
||||
import emu.skyline.di.InputManagerProviderEntryPoint
|
||||
import emu.skyline.input.GeneralType
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.input.JoyConLeftController
|
||||
|
||||
/**
|
||||
@ -18,9 +19,9 @@ import emu.skyline.input.JoyConLeftController
|
||||
* @param type The type of controller setting this item is displaying
|
||||
*/
|
||||
class ControllerGeneralViewItem(private val controllerId : Int, val type : GeneralType, private val onClick : (item : ControllerGeneralViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
val controller = InputManager.controllers[controllerId]!!
|
||||
val controller = InputManagerProviderEntryPoint.getInputManager(context).controllers[controllerId]!!
|
||||
|
||||
content = context.getString(type.stringRes)
|
||||
subContent = when (type) {
|
||||
@ -40,7 +41,7 @@ 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 areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerGeneralViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerGeneralViewItem && type == other.type
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerGeneralViewItem && type == other.type
|
||||
}
|
||||
|
@ -8,32 +8,34 @@ package emu.skyline.adapter.controller
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.databinding.ControllerItemBinding
|
||||
import emu.skyline.di.InputManagerProviderEntryPoint
|
||||
import emu.skyline.input.AxisGuestEvent
|
||||
import emu.skyline.input.ButtonGuestEvent
|
||||
import emu.skyline.input.InputManager
|
||||
import emu.skyline.input.StickId
|
||||
|
||||
/**
|
||||
* This item is used to display all information regarding a [stick] and it's mappings for the controller
|
||||
*/
|
||||
class ControllerStickViewItem(private val controllerId : Int, val stick : StickId, private val onClick : (item : ControllerStickViewItem, position : Int) -> Unit) : ControllerViewItem(stick.toString()) {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
val inputManager = InputManagerProviderEntryPoint.getInputManager(context)
|
||||
|
||||
val buttonGuestEvent = ButtonGuestEvent(controllerId, stick.button)
|
||||
val button = InputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
val button = inputManager.eventMap.filter { it.value is ButtonGuestEvent && it.value == buttonGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
var axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, true)
|
||||
val yAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
val yAxisPlus = inputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.yAxis, false)
|
||||
val yAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
val yAxisMinus = inputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, true)
|
||||
val xAxisPlus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
val xAxisPlus = inputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
axisGuestEvent = AxisGuestEvent(controllerId, stick.xAxis, false)
|
||||
val xAxisMinus = InputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
val xAxisMinus = inputManager.eventMap.filter { it.value is AxisGuestEvent && it.value == axisGuestEvent }.keys.firstOrNull()?.toString() ?: context.getString(R.string.none)
|
||||
|
||||
subContent = "${context.getString(R.string.button)}: $button\n${context.getString(R.string.up)}: $yAxisPlus\n${context.getString(R.string.down)}: $yAxisMinus\n${context.getString(R.string.left)}: $xAxisMinus\n${context.getString(R.string.right)}: $xAxisPlus"
|
||||
|
||||
@ -42,7 +44,7 @@ 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 areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerStickViewItem && controllerId == other.controllerId
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerStickViewItem && stick == other.stick
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerStickViewItem && stick == other.stick
|
||||
}
|
||||
|
@ -8,13 +8,14 @@ package emu.skyline.adapter.controller
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.GenericListItem
|
||||
import emu.skyline.adapter.GenericViewHolder
|
||||
import emu.skyline.databinding.ControllerItemBinding
|
||||
import emu.skyline.input.ControllerType
|
||||
|
||||
/**
|
||||
* This item is used to display the [type] of the currently active controller
|
||||
*/
|
||||
class ControllerTypeViewItem(private val type : ControllerType, private val onClick : (item : ControllerTypeViewItem, position : Int) -> Unit) : ControllerViewItem() {
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
|
||||
val context = holder.itemView.context
|
||||
|
||||
content = context.getString(R.string.controller_type)
|
||||
@ -25,7 +26,7 @@ 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 areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerTypeViewItem
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerTypeViewItem && type == other.type
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerTypeViewItem && type == other.type
|
||||
}
|
||||
|
@ -5,32 +5,31 @@
|
||||
|
||||
package emu.skyline.adapter.controller
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
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 kotlinx.android.synthetic.main.controller_item.*
|
||||
import emu.skyline.adapter.ViewBindingFactory
|
||||
import emu.skyline.adapter.inflater
|
||||
import emu.skyline.databinding.ControllerItemBinding
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
private object ControllerLayoutFactory : GenericLayoutFactory {
|
||||
override fun createLayout(parent : ViewGroup) : View = LayoutInflater.from(parent.context).inflate(R.layout.controller_item, parent, false)
|
||||
object ControllerBindingFactory : ViewBindingFactory {
|
||||
override fun createBinding(parent : ViewGroup) = ControllerItemBinding.inflate(parent.inflater(), parent, false)
|
||||
}
|
||||
|
||||
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem() {
|
||||
open class ControllerViewItem(var content : String = "", var subContent : String = "", private val onClick : (() -> Unit)? = null) : GenericListItem<ControllerItemBinding>() {
|
||||
private var position = -1
|
||||
|
||||
override fun getLayoutFactory() : GenericLayoutFactory = ControllerLayoutFactory
|
||||
override fun getViewBindingFactory() = ControllerBindingFactory
|
||||
|
||||
override fun bind(holder : GenericViewHolder, position : Int) {
|
||||
override fun bind(holder : GenericViewHolder<ControllerItemBinding>, position : Int) {
|
||||
this.position = position
|
||||
holder.text_title.apply {
|
||||
holder.binding.textTitle.apply {
|
||||
isGone = content.isEmpty()
|
||||
text = content
|
||||
}
|
||||
holder.text_subtitle.apply {
|
||||
holder.binding.textSubtitle.apply {
|
||||
isGone = subContent.isEmpty()
|
||||
text = subContent
|
||||
}
|
||||
@ -39,7 +38,7 @@ open class ControllerViewItem(var content : String = "", var subContent : String
|
||||
|
||||
fun update() = adapter?.notifyItemChanged(position)
|
||||
|
||||
override fun areItemsTheSame(other : GenericListItem) = other is ControllerViewItem
|
||||
override fun areItemsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerViewItem
|
||||
|
||||
override fun areContentsTheSame(other : GenericListItem) = other is ControllerViewItem && content == other.content && subContent == other.subContent
|
||||
override fun areContentsTheSame(other : GenericListItem<ControllerItemBinding>) = other is ControllerViewItem && content == other.content && subContent == other.subContent
|
||||
}
|
||||
|
@ -0,0 +1,18 @@
|
||||
package emu.skyline.di
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface InputManagerProviderEntryPoint {
|
||||
fun inputManager() : InputManager
|
||||
|
||||
companion object {
|
||||
fun getInputManager(context : Context) = EntryPointAccessors.fromApplication(context, InputManagerProviderEntryPoint::class.java).inputManager()
|
||||
}
|
||||
}
|
@ -12,23 +12,27 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
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.databinding.ControllerActivityBinding
|
||||
import emu.skyline.input.dialog.ButtonDialog
|
||||
import emu.skyline.input.dialog.RumbleDialog
|
||||
import emu.skyline.input.dialog.StickDialog
|
||||
import emu.skyline.input.onscreen.OnScreenEditActivity
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.controller_activity.*
|
||||
import kotlinx.android.synthetic.main.titlebar.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This activity is used to change the settings for a specific controller
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ControllerActivity : AppCompatActivity() {
|
||||
private val binding by lazy { ControllerActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
/**
|
||||
* The index of the controller this activity manages
|
||||
*/
|
||||
@ -51,14 +55,17 @@ class ControllerActivity : AppCompatActivity() {
|
||||
|
||||
private val settings by lazy { Settings(this) }
|
||||
|
||||
@Inject
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* This function updates the [adapter] based on information from [InputManager]
|
||||
*/
|
||||
private fun update() {
|
||||
val items = mutableListOf<GenericListItem>()
|
||||
val items = mutableListOf<GenericListItem<*>>()
|
||||
|
||||
try {
|
||||
val controller = InputManager.controllers[id]!!
|
||||
val controller = inputManager.controllers[id]!!
|
||||
|
||||
items.add(ControllerTypeViewItem(controller.type, onControllerTypeClick))
|
||||
|
||||
@ -166,20 +173,20 @@ class ControllerActivity : AppCompatActivity() {
|
||||
|
||||
title = "${getString(R.string.config_controller)} #${id + 1}"
|
||||
|
||||
setContentView(R.layout.controller_activity)
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(toolbar)
|
||||
setSupportActionBar(binding.titlebar.toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
val layoutManager = LinearLayoutManager(this)
|
||||
controller_list.layoutManager = layoutManager
|
||||
controller_list.adapter = adapter
|
||||
binding.controllerList.layoutManager = layoutManager
|
||||
binding.controllerList.adapter = adapter
|
||||
|
||||
controller_list.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
binding.controllerList.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)
|
||||
if (layoutManager.findLastCompletelyVisibleItemPosition() == adapter.itemCount - 1) binding.titlebar.appBarLayout.setExpanded(false)
|
||||
}
|
||||
})
|
||||
|
||||
@ -190,12 +197,12 @@ class ControllerActivity : AppCompatActivity() {
|
||||
* This causes the input file to be synced when the activity has been paused
|
||||
*/
|
||||
override fun onPause() {
|
||||
InputManager.syncFile()
|
||||
inputManager.syncFile()
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
private val onControllerTypeClick = { item : ControllerTypeViewItem, _ : Int ->
|
||||
val controller = InputManager.controllers[id]!!
|
||||
val controller = inputManager.controllers[id]!!
|
||||
|
||||
val types = ControllerType.values().apply { if (id != 0) filter { !it.firstController } }
|
||||
val typeNames = types.map { getString(it.stringRes) }.toTypedArray()
|
||||
@ -206,11 +213,11 @@ class ControllerActivity : AppCompatActivity() {
|
||||
val selectedType = types[typeIndex]
|
||||
if (controller.type != selectedType) {
|
||||
if (controller is JoyConLeftController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConRightController).partnerId = null }
|
||||
controller.partnerId?.let { (inputManager.controllers[it] as JoyConRightController).partnerId = null }
|
||||
else if (controller is JoyConRightController)
|
||||
controller.partnerId?.let { (InputManager.controllers[it] as JoyConLeftController).partnerId = null }
|
||||
controller.partnerId?.let { (inputManager.controllers[it] as JoyConLeftController).partnerId = null }
|
||||
|
||||
InputManager.controllers[id] = when (selectedType) {
|
||||
inputManager.controllers[id] = when (selectedType) {
|
||||
ControllerType.None -> Controller(id, ControllerType.None)
|
||||
ControllerType.HandheldProController -> HandheldController(id)
|
||||
ControllerType.ProController -> ProController(id)
|
||||
@ -230,9 +237,9 @@ class ControllerActivity : AppCompatActivity() {
|
||||
private val onControllerGeneralClick = { item : ControllerGeneralViewItem, _ : Int ->
|
||||
when (item.type) {
|
||||
GeneralType.PartnerJoyCon -> {
|
||||
val controller = InputManager.controllers[id] as JoyConLeftController
|
||||
val controller = inputManager.controllers[id] as JoyConLeftController
|
||||
|
||||
val rJoyCons = InputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
||||
val rJoyCons = inputManager.controllers.values.filter { it.type == ControllerType.JoyConRight }
|
||||
val rJoyConNames = (listOf(getString(R.string.none)) + rJoyCons.map { "${getString(R.string.controller)} #${it.id + 1}" }).toTypedArray()
|
||||
|
||||
val partnerNameIndex = controller.partnerId?.let { partnerId ->
|
||||
@ -242,12 +249,12 @@ class ControllerActivity : AppCompatActivity() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(item.content)
|
||||
.setSingleChoiceItems(rJoyConNames, partnerNameIndex) { dialog, index ->
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
||||
(inputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = null
|
||||
|
||||
controller.partnerId = if (index == 0) null else rJoyCons[index - 1].id
|
||||
|
||||
if (controller.partnerId != null)
|
||||
(InputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
||||
(inputManager.controllers[controller.partnerId ?: -1] as JoyConRightController?)?.partnerId = controller.id
|
||||
|
||||
item.update()
|
||||
|
||||
|
@ -7,16 +7,20 @@ package emu.skyline.input
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import java.io.*
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* This object is used to manage all transactions with storing/retrieving data in relation to input
|
||||
*/
|
||||
object InputManager {
|
||||
@Singleton
|
||||
class InputManager @Inject constructor(@ApplicationContext context : Context) {
|
||||
/**
|
||||
* The underlying [File] object with the input data
|
||||
*/
|
||||
private lateinit var file : File
|
||||
private val file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||
|
||||
/**
|
||||
* A [HashMap] of all the controllers that contains their metadata
|
||||
@ -28,31 +32,31 @@ object InputManager {
|
||||
*/
|
||||
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
|
||||
|
||||
fun init(context : Context) {
|
||||
file = File("${context.applicationInfo.dataDir}/input.bin")
|
||||
|
||||
try {
|
||||
if (file.exists() && file.length() != 0L) {
|
||||
syncObjects()
|
||||
return
|
||||
init {
|
||||
run {
|
||||
try {
|
||||
if (file.exists() && file.length() != 0L) {
|
||||
syncObjects()
|
||||
return@run
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
|
||||
}
|
||||
} catch (e : Exception) {
|
||||
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
|
||||
|
||||
controllers = hashMapOf(
|
||||
0 to Controller(0, ControllerType.HandheldProController),
|
||||
1 to Controller(1, ControllerType.None),
|
||||
2 to Controller(2, ControllerType.None),
|
||||
3 to Controller(3, ControllerType.None),
|
||||
4 to Controller(4, ControllerType.None),
|
||||
5 to Controller(5, ControllerType.None),
|
||||
6 to Controller(6, ControllerType.None),
|
||||
7 to Controller(7, ControllerType.None))
|
||||
|
||||
eventMap = hashMapOf()
|
||||
|
||||
syncFile()
|
||||
}
|
||||
|
||||
controllers = hashMapOf(
|
||||
0 to Controller(0, ControllerType.HandheldProController),
|
||||
1 to Controller(1, ControllerType.None),
|
||||
2 to Controller(2, ControllerType.None),
|
||||
3 to Controller(3, ControllerType.None),
|
||||
4 to Controller(4, ControllerType.None),
|
||||
5 to Controller(5, ControllerType.None),
|
||||
6 to Controller(6, ControllerType.None),
|
||||
7 to Controller(7, ControllerType.None))
|
||||
|
||||
eventMap = hashMapOf()
|
||||
|
||||
syncFile()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -13,10 +13,12 @@ import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.controller.ControllerButtonViewItem
|
||||
import emu.skyline.databinding.ButtonDialogBinding
|
||||
import emu.skyline.input.*
|
||||
import kotlinx.android.synthetic.main.button_dialog.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
@ -24,11 +26,17 @@ import kotlin.math.abs
|
||||
*
|
||||
* @param item This is used to hold the [ControllerButtonViewItem] between instances
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class ButtonDialog @JvmOverloads constructor(private val item : ControllerButtonViewItem? = null) : BottomSheetDialogFragment() {
|
||||
private lateinit var binding : ButtonDialogBinding
|
||||
|
||||
@Inject
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.button_dialog, container)
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = ButtonDialogBinding.inflate(inflater).also { binding = it }.root
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
@ -48,20 +56,20 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
|
||||
if (item != null && context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = InputManager.controllers[context.id]!!
|
||||
val controller = inputManager.controllers[context.id]!!
|
||||
|
||||
// View focus handling so all input is always directed to this view
|
||||
view?.requestFocus()
|
||||
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||
|
||||
// Write the text for the button's icon
|
||||
button_text.text = item.button.short ?: item.button.toString()
|
||||
binding.buttonText.text = item.button.short ?: item.button.toString()
|
||||
|
||||
// Set up the reset button to clear out all entries corresponding to this button from [InputManager.eventMap]
|
||||
button_reset.setOnClickListener {
|
||||
// Set up the reset button to clear out all entries corresponding to this button from [inputManager.eventMap]
|
||||
binding.buttonReset.setOnClickListener {
|
||||
val guestEvent = ButtonGuestEvent(context.id, item.button)
|
||||
|
||||
InputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it is ButtonGuestEvent && it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
item.update()
|
||||
|
||||
@ -69,11 +77,11 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
button_layout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
button_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.buttonLayout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
binding.buttonLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
// We want the secondary progress bar to be visible through the first one
|
||||
button_seekbar.progressDrawable.alpha = 128
|
||||
binding.buttonSeekbar.progressDrawable.alpha = 128
|
||||
|
||||
var deviceId : Int? = null // The ID of the currently selected device
|
||||
var inputId : Int? = null // The key code/axis ID of the currently selected event
|
||||
@ -99,19 +107,19 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.75f).setDuration(50).start()
|
||||
button_text.animate().alpha(0.9f).setDuration(50).start()
|
||||
binding.buttonIcon.animate().alpha(0.75f).setDuration(50).start()
|
||||
binding.buttonText.animate().alpha(0.9f).setDuration(50).start()
|
||||
|
||||
button_title.text = getString(R.string.release_confirm)
|
||||
button_seekbar.visibility = View.GONE
|
||||
binding.buttonTitle.text = getString(R.string.release_confirm)
|
||||
binding.buttonSeekbar.visibility = View.GONE
|
||||
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
|
||||
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] on [KeyEvent.ACTION_UP]
|
||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
var guestEvent = inputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
inputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
@ -121,9 +129,9 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
|
||||
guestEvent = ButtonGuestEvent(context.id, item.button)
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
InputManager.eventMap[hostEvent] = guestEvent
|
||||
inputManager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
item.update()
|
||||
|
||||
@ -165,8 +173,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
inputId = axis
|
||||
axisPolarity = value >= 0
|
||||
|
||||
button_title.text = getString(R.string.hold_confirm)
|
||||
button_seekbar.visibility = View.VISIBLE
|
||||
binding.buttonTitle.text = getString(R.string.hold_confirm)
|
||||
binding.buttonSeekbar.visibility = View.VISIBLE
|
||||
|
||||
break
|
||||
}
|
||||
@ -175,10 +183,10 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
// If the currently active input is a valid axis
|
||||
if (axes.contains(inputId)) {
|
||||
val value = event.getAxisValue(inputId!!)
|
||||
val threshold = button_seekbar.progress / 100f
|
||||
val threshold = binding.buttonSeekbar.progress / 100f
|
||||
|
||||
// Update the secondary progress bar in [button_seekbar] based on the axis's value
|
||||
button_seekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
binding.buttonSeekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
|
||||
// If the axis value crosses the threshold then post [axisRunnable] with a delay and animate the views accordingly
|
||||
if (abs(value) >= threshold) {
|
||||
@ -186,10 +194,10 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
var guestEvent = inputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
inputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
@ -199,9 +207,9 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
|
||||
guestEvent = ButtonGuestEvent(controller.id, item.button, threshold)
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
InputManager.eventMap[hostEvent] = guestEvent
|
||||
inputManager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
item.update()
|
||||
|
||||
@ -211,8 +219,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
axisHandler.postDelayed(axisRunnable!!, 1000)
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
button_text.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
binding.buttonIcon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
binding.buttonText.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
} else {
|
||||
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
|
||||
if (axisRunnable != null) {
|
||||
@ -220,8 +228,8 @@ class ButtonDialog @JvmOverloads constructor(private val item : ControllerButton
|
||||
axisRunnable = null
|
||||
}
|
||||
|
||||
button_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
button_text.animate().alpha(0.35f).setDuration(50).start()
|
||||
binding.buttonIcon.animate().alpha(0.25f).setDuration(50).start()
|
||||
binding.buttonText.animate().alpha(0.35f).setDuration(50).start()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -14,22 +14,30 @@ import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.controller.ControllerGeneralViewItem
|
||||
import emu.skyline.databinding.RumbleDialogBinding
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
import kotlinx.android.synthetic.main.rumble_dialog.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* This dialog is used to set a device to pass on any rumble/force feedback data onto
|
||||
*
|
||||
* @param item This is used to hold the [ControllerGeneralViewItem] between instances
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewItem? = null) : BottomSheetDialogFragment() {
|
||||
private lateinit var binding : RumbleDialogBinding
|
||||
|
||||
@Inject
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? = inflater.inflate(R.layout.rumble_dialog, container)
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = RumbleDialogBinding.inflate(inflater).also { binding = it }.root
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
@ -49,10 +57,10 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
|
||||
|
||||
if (item != null && context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = InputManager.controllers[context.id]!!
|
||||
val controller = inputManager.controllers[context.id]!!
|
||||
|
||||
// Set up the reset button to clear out [Controller.rumbleDevice] when pressed
|
||||
rumble_reset.setOnClickListener {
|
||||
binding.rumbleReset.setOnClickListener {
|
||||
controller.rumbleDeviceDescriptor = null
|
||||
controller.rumbleDeviceName = null
|
||||
item.update()
|
||||
@ -61,10 +69,10 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
|
||||
}
|
||||
|
||||
if (context.id == 0) {
|
||||
rumble_builtin.visibility = View.VISIBLE
|
||||
binding.rumbleBuiltin.visibility = View.VISIBLE
|
||||
if (!(context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator).hasVibrator())
|
||||
rumble_builtin.isEnabled = false
|
||||
rumble_builtin.setOnClickListener {
|
||||
binding.rumbleBuiltin.isEnabled = false
|
||||
binding.rumbleBuiltin.setOnClickListener {
|
||||
controller.rumbleDeviceDescriptor = "builtin"
|
||||
controller.rumbleDeviceName = getString(R.string.builtin_vibrator)
|
||||
item.update()
|
||||
@ -74,8 +82,8 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
rumble_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
rumble_controller.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.rumbleLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.rumbleController.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
var deviceId : Int? = null // The ID of the currently selected device
|
||||
|
||||
@ -88,20 +96,20 @@ class RumbleDialog @JvmOverloads constructor(val item : ControllerGeneralViewIte
|
||||
when {
|
||||
// If the device doesn't match the currently selected device then update the UI accordingly and set [deviceId] to the current device
|
||||
deviceId != event.deviceId -> {
|
||||
rumble_controller_name.text = event.device.name
|
||||
binding.rumbleControllerName.text = event.device.name
|
||||
|
||||
if (vibrator.hasVibrator()) {
|
||||
rumble_controller_supported.text = getString(R.string.supported)
|
||||
rumble_title.text = getString(R.string.confirm_button_again)
|
||||
binding.rumbleControllerSupported.text = getString(R.string.supported)
|
||||
binding.rumbleTitle.text = getString(R.string.confirm_button_again)
|
||||
|
||||
vibrator.vibrate(VibrationEffect.createOneShot(250, VibrationEffect.DEFAULT_AMPLITUDE))
|
||||
} else {
|
||||
rumble_controller_supported.text = getString(R.string.not_supported)
|
||||
binding.rumbleControllerSupported.text = getString(R.string.not_supported)
|
||||
dialog?.setOnKeyListener { _, _, _ -> false }
|
||||
rumble_reset.requestFocus()
|
||||
binding.rumbleReset.requestFocus()
|
||||
}
|
||||
|
||||
rumble_controller_icon.animate().apply {
|
||||
binding.rumbleControllerIcon.animate().apply {
|
||||
interpolator = LinearInterpolator()
|
||||
duration = 100
|
||||
alpha(if (vibrator.hasVibrator()) 0.75f else 0.5f)
|
||||
|
@ -14,12 +14,14 @@ import android.view.*
|
||||
import android.view.animation.LinearInterpolator
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import emu.skyline.R
|
||||
import emu.skyline.adapter.controller.ControllerStickViewItem
|
||||
import emu.skyline.databinding.StickDialogBinding
|
||||
import emu.skyline.input.*
|
||||
import emu.skyline.input.MotionHostEvent.Companion.axes
|
||||
import kotlinx.android.synthetic.main.stick_dialog.*
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
|
||||
@ -28,6 +30,7 @@ import kotlin.math.max
|
||||
*
|
||||
* @param item This is used to hold the [ControllerStickViewItem] between instances
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem? = null) : BottomSheetDialogFragment() {
|
||||
/**
|
||||
* This enumerates all of the stages this dialog can be in
|
||||
@ -41,6 +44,8 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
Stick(R.string.stick_preview);
|
||||
}
|
||||
|
||||
private lateinit var binding : StickDialogBinding
|
||||
|
||||
/**
|
||||
* This is the current stage of the dialog
|
||||
*/
|
||||
@ -61,12 +66,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
*/
|
||||
private var animationStop = false
|
||||
|
||||
@Inject
|
||||
lateinit var inputManager : InputManager
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog after initial view creation
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
|
||||
return requireActivity().layoutInflater.inflate(R.layout.stick_dialog, container)
|
||||
}
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) = StickDialogBinding.inflate(inflater).also { binding = it }.root
|
||||
|
||||
/**
|
||||
* This expands the bottom sheet so that it's fully visible
|
||||
@ -91,7 +97,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
animationStop = false
|
||||
stageAnimation?.let { handler.removeCallbacks(it) }
|
||||
|
||||
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.translationY(0f)?.translationX(0f)?.rotationX(0f)?.rotationY(0f)?.start()
|
||||
binding.stickContainer.animate()
|
||||
.scaleX(1f).scaleY(1f)
|
||||
.alpha(1f)
|
||||
.translationY(0f).translationX(0f)
|
||||
.rotationX(0f)
|
||||
.rotationY(0f)
|
||||
.start()
|
||||
|
||||
when (stage) {
|
||||
DialogStage.Button -> {
|
||||
@ -99,7 +111,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.withEndAction {
|
||||
binding.stickContainer.animate().scaleX(0.85f).scaleY(0.85f).alpha(1f).withEndAction {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
@ -107,7 +119,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.scaleX(1f)?.scaleY(1f)?.alpha(0.85f)?.withEndAction {
|
||||
binding.stickContainer.animate().scaleX(1f).scaleY(1f).alpha(0.85f).withEndAction {
|
||||
if (stage != DialogStage.Button || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
@ -129,7 +141,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(300)?.translationY(dipToPixels(15f) * polarity)?.rotationX(27f * polarity)?.alpha(1f)?.withEndAction {
|
||||
binding.stickContainer.animate().setDuration(300).translationY(dipToPixels(15f) * polarity).rotationX(27f * polarity).alpha(1f).withEndAction {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
@ -137,7 +149,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(250)?.translationY(0f)?.rotationX(0f)?.alpha(0.85f)?.withEndAction {
|
||||
binding.stickContainer.animate().setDuration(250).translationY(0f).rotationX(0f).alpha(0.85f).withEndAction {
|
||||
if ((stage != DialogStage.YPlus && stage != DialogStage.YMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
@ -159,7 +171,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(300)?.translationX(dipToPixels(16.5f) * polarity)?.rotationY(27f * polarity)?.alpha(1f)?.withEndAction {
|
||||
binding.stickContainer.animate().setDuration(300).translationX(dipToPixels(16.5f) * polarity).rotationY(27f * polarity).alpha(1f).withEndAction {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
@ -167,14 +179,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@Runnable
|
||||
|
||||
stick_container?.animate()?.setDuration(250)?.translationX(0f)?.rotationY(0f)?.alpha(0.85f)?.withEndAction {
|
||||
binding.stickContainer.animate().setDuration(250).translationX(0f).rotationY(0f).alpha(0.85f).withEndAction {
|
||||
if ((stage != DialogStage.XPlus && stage != DialogStage.XMinus) || animationStop)
|
||||
return@withEndAction
|
||||
|
||||
stageAnimation?.let {
|
||||
handler.postDelayed(it, 750)
|
||||
}
|
||||
}?.start()
|
||||
}.start()
|
||||
}
|
||||
|
||||
handler.postDelayed(runnable, 300)
|
||||
@ -199,13 +211,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
if (ordinal in 0 until size) {
|
||||
stage = DialogStage.values()[ordinal]
|
||||
|
||||
stick_title.text = getString(stage.string)
|
||||
stick_subtitle.text = if (stage != DialogStage.Stick) getString(R.string.use_button_axis) else getString(R.string.use_non_stick)
|
||||
stick_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(50).start()
|
||||
stick_seekbar.visibility = View.GONE
|
||||
binding.stickTitle.text = getString(stage.string)
|
||||
binding.stickSubtitle.text = getString(if (stage != DialogStage.Stick) R.string.use_button_axis else R.string.use_non_stick)
|
||||
binding.stickIcon.animate().alpha(0.25f).setDuration(50).start()
|
||||
binding.stickName.animate().alpha(0.35f).setDuration(50).start()
|
||||
binding.stickSeekbar.visibility = View.GONE
|
||||
|
||||
stick_next.text = if (ordinal + 1 == size) getString(R.string.done) else getString(R.string.next)
|
||||
binding.stickNext.text = getString(if (ordinal + 1 == size) R.string.done else R.string.next)
|
||||
|
||||
updateAnimation()
|
||||
} else {
|
||||
@ -221,28 +233,28 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
|
||||
if (item != null && context is ControllerActivity) {
|
||||
val context = requireContext() as ControllerActivity
|
||||
val controller = InputManager.controllers[context.id]!!
|
||||
val controller = inputManager.controllers[context.id]!!
|
||||
|
||||
// View focus handling so all input is always directed to this view
|
||||
view?.requestFocus()
|
||||
view?.onFocusChangeListener = View.OnFocusChangeListener { v, hasFocus -> if (!hasFocus) v.requestFocus() }
|
||||
|
||||
// Write the text for the stick's icon
|
||||
stick_name.text = item.stick.button.short ?: item.stick.button.toString()
|
||||
binding.stickName.text = item.stick.button.short ?: item.stick.button.toString()
|
||||
|
||||
// Set up the reset button to clear out all entries corresponding to this stick from [InputManager.eventMap]
|
||||
stick_reset.setOnClickListener {
|
||||
// Set up the reset button to clear out all entries corresponding to this stick from [inputManager.eventMap]
|
||||
binding.stickReset.setOnClickListener {
|
||||
for (axis in arrayOf(item.stick.xAxis, item.stick.yAxis)) {
|
||||
for (polarity in booleanArrayOf(true, false)) {
|
||||
val guestEvent = AxisGuestEvent(context.id, axis, polarity)
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
}
|
||||
}
|
||||
|
||||
val guestEvent = ButtonGuestEvent(context.id, item.stick.button)
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
item.update()
|
||||
|
||||
@ -250,11 +262,11 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
}
|
||||
|
||||
// Ensure that layout animations are proper
|
||||
stick_layout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
stick_layout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.stickLayout.layoutTransition.setAnimateParentHierarchy(false)
|
||||
binding.stickLayout.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
// We want the secondary progress bar to be visible through the first one
|
||||
stick_seekbar.progressDrawable.alpha = 128
|
||||
binding.stickSeekbar.progressDrawable.alpha = 128
|
||||
|
||||
updateAnimation()
|
||||
|
||||
@ -266,7 +278,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
var axisPolarity = false // The polarity of the axis for the currently selected event
|
||||
var axisRunnable : Runnable? = null // The Runnable that is used for counting down till an axis is selected
|
||||
|
||||
stick_next.setOnClickListener {
|
||||
binding.stickNext.setOnClickListener {
|
||||
gotoStage(1)
|
||||
|
||||
deviceId = null
|
||||
@ -280,39 +292,41 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
// We want all input events from Joysticks and Buttons except for [KeyEvent.KEYCODE_BACK] as that will should be processed elsewhere
|
||||
((event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON) && event.keyCode != KeyEvent.KEYCODE_BACK) || event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK)) && event.repeatCount == 0 -> {
|
||||
if (stage == DialogStage.Stick) {
|
||||
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
||||
when (val guestEvent = InputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
// When the stick is being previewed after everything is mapped we do a lookup into [inputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
||||
when (val guestEvent = inputManager.eventMap[KeyHostEvent(event.device.descriptor, event.keyCode)]) {
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button == item.stick.button) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(50).scaleX(0.85f).scaleY(0.85f).start()
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
|
||||
binding.stickName.animate().alpha(0.95f).setDuration(50).start()
|
||||
} else {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(1f).scaleY(1f).start()
|
||||
|
||||
stick_icon.animate().alpha(0.25f).setDuration(25).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(25).start()
|
||||
binding.stickIcon.animate().alpha(0.25f).setDuration(25).start()
|
||||
binding.stickName.animate().alpha(0.35f).setDuration(25).start()
|
||||
}
|
||||
} else if (event.action == KeyEvent.ACTION_UP) {
|
||||
stick_next?.callOnClick()
|
||||
binding.stickNext.callOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
is AxisGuestEvent -> {
|
||||
val coefficient = if (event.action == KeyEvent.ACTION_DOWN) if (guestEvent.polarity) 1 else -1 else 0
|
||||
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
stick_container?.translationY = dipToPixels(16.5f) * -coefficient
|
||||
stick_container?.rotationX = 27f * coefficient
|
||||
binding.stickContainer.apply {
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
translationX = dipToPixels(16.5f) * coefficient
|
||||
rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
translationY = dipToPixels(16.5f) * -coefficient
|
||||
rotationX = 27f * coefficient
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
null -> if (event.action == KeyEvent.ACTION_UP) stick_next?.callOnClick()
|
||||
null -> if (event.action == KeyEvent.ACTION_UP) binding.stickNext.callOnClick()
|
||||
}
|
||||
} else if (stage != DialogStage.Stick) {
|
||||
if ((deviceId != event.deviceId || inputId != event.keyCode) && event.action == KeyEvent.ACTION_DOWN && !ignoredEvents.any { it == Objects.hash(event.deviceId, event.keyCode) }) {
|
||||
@ -330,26 +344,28 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
val coefficient = if (stage == DialogStage.YMinus || stage == DialogStage.XPlus) 1 else -1
|
||||
|
||||
when (stage) {
|
||||
DialogStage.Button -> stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start()
|
||||
DialogStage.YPlus, DialogStage.YMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationY(dipToPixels(16.5f) * coefficient)?.rotationX(27f * coefficient)?.alpha(1f)?.start()
|
||||
DialogStage.XPlus, DialogStage.XMinus -> stick_container?.animate()?.setStartDelay(0)?.setDuration(75)?.translationX(dipToPixels(16.5f) * coefficient)?.rotationY(27f * coefficient)?.alpha(1f)?.start()
|
||||
DialogStage.Button -> binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(0.85f).scaleY(0.85f).alpha(1f).start()
|
||||
|
||||
DialogStage.YPlus, DialogStage.YMinus -> binding.stickContainer.animate().setStartDelay(0).setDuration(75).translationY(dipToPixels(16.5f) * coefficient).rotationX(27f * coefficient).alpha(1f).start()
|
||||
|
||||
DialogStage.XPlus, DialogStage.XMinus -> binding.stickContainer.animate().setStartDelay(0).setDuration(75).translationX(dipToPixels(16.5f) * coefficient).rotationY(27f * coefficient).alpha(1f).start()
|
||||
else -> {
|
||||
}
|
||||
}
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
|
||||
binding.stickName.animate().alpha(0.95f).setDuration(50).start()
|
||||
|
||||
stick_subtitle.text = getString(R.string.release_confirm)
|
||||
stick_seekbar.visibility = View.GONE
|
||||
binding.stickSubtitle.text = getString(R.string.release_confirm)
|
||||
binding.stickSeekbar.visibility = View.GONE
|
||||
} else if (deviceId == event.deviceId && inputId == event.keyCode && event.action == KeyEvent.ACTION_UP) {
|
||||
// We serialize the current [deviceId] and [inputId] into a [KeyHostEvent] and map it to a corresponding [GuestEvent] and add it to [ignoredEvents] on [KeyEvent.ACTION_UP]
|
||||
val hostEvent = KeyHostEvent(event.device.descriptor, event.keyCode)
|
||||
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
var guestEvent = inputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
inputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[guestEvent.button]?.update()
|
||||
@ -364,15 +380,15 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
else -> null
|
||||
}
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
InputManager.eventMap[hostEvent] = guestEvent
|
||||
inputManager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!))
|
||||
|
||||
item.update()
|
||||
|
||||
stick_next?.callOnClick()
|
||||
binding.stickNext.callOnClick()
|
||||
}
|
||||
}
|
||||
|
||||
@ -407,7 +423,7 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
// We want all input events from Joysticks and Buttons that are [MotionEvent.ACTION_MOVE] and not from the D-pad
|
||||
if ((event.isFromSource(InputDevice.SOURCE_CLASS_JOYSTICK) || event.isFromSource(InputDevice.SOURCE_CLASS_BUTTON)) && event.action == MotionEvent.ACTION_MOVE && hat == oldHat) {
|
||||
if (stage == DialogStage.Stick) {
|
||||
// When the stick is being previewed after everything is mapped we do a lookup into [InputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
||||
// When the stick is being previewed after everything is mapped we do a lookup into [inputManager.eventMap] to find a corresponding [GuestEvent] and animate the stick correspondingly
|
||||
for (axisItem in axes.withIndex()) {
|
||||
val axis = axisItem.value
|
||||
var value = event.getAxisValue(axis)
|
||||
@ -421,9 +437,9 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
|
||||
var polarity = value >= 0
|
||||
val guestEvent = MotionHostEvent(event.device.descriptor, axis, polarity).let { hostEvent ->
|
||||
InputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
inputManager.eventMap[hostEvent] ?: if (value == 0f) {
|
||||
polarity = false
|
||||
InputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
inputManager.eventMap[hostEvent.copy(polarity = false)]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
@ -433,13 +449,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
is ButtonGuestEvent -> {
|
||||
if (guestEvent.button == item.stick.button) {
|
||||
if (abs(value) >= guestEvent.threshold) {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(50)?.scaleX(0.85f)?.scaleY(0.85f)?.start()
|
||||
stick_icon.animate().alpha(0.85f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.95f).setDuration(50).start()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(50).scaleX(0.85f).scaleY(0.85f).start()
|
||||
binding.stickIcon.animate().alpha(0.85f).setDuration(50).start()
|
||||
binding.stickName.animate().alpha(0.95f).setDuration(50).start()
|
||||
} else {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(1f)?.scaleY(1f)?.start()
|
||||
stick_icon.animate().alpha(0.25f).setDuration(25).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(25).start()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(1f).scaleY(1f).start()
|
||||
binding.stickIcon.animate().alpha(0.25f).setDuration(25).start()
|
||||
binding.stickName.animate().alpha(0.35f).setDuration(25).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -449,12 +465,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
|
||||
val coefficient = if (polarity) abs(value) else -abs(value)
|
||||
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
stick_container?.translationY = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationX = 27f * -coefficient
|
||||
binding.stickContainer.apply {
|
||||
if (guestEvent.axis == item.stick.xAxis) {
|
||||
translationX = dipToPixels(16.5f) * coefficient
|
||||
rotationY = 27f * coefficient
|
||||
} else if (guestEvent.axis == item.stick.yAxis) {
|
||||
translationY = dipToPixels(16.5f) * coefficient
|
||||
rotationX = 27f * -coefficient
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -480,10 +498,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
inputId = axis
|
||||
axisPolarity = value >= 0
|
||||
|
||||
stick_subtitle.text = getString(R.string.hold_confirm)
|
||||
binding.stickSubtitle.text = getString(R.string.hold_confirm)
|
||||
|
||||
if (stage == DialogStage.Button)
|
||||
stick_seekbar.visibility = View.VISIBLE
|
||||
binding.stickSeekbar.visibility = View.VISIBLE
|
||||
|
||||
animationStop = true
|
||||
|
||||
@ -494,13 +512,13 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
// If the currently active input is a valid axis
|
||||
if (axes.contains(inputId)) {
|
||||
val value = event.getAxisValue(inputId!!)
|
||||
val threshold = if (stage == DialogStage.Button) stick_seekbar.progress / 100f else 0.5f
|
||||
val threshold = if (stage == DialogStage.Button) binding.stickSeekbar.progress / 100f else 0.5f
|
||||
|
||||
when (stage) {
|
||||
// Update the secondary progress bar in [button_seekbar] based on the axis's value
|
||||
DialogStage.Button -> {
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(25)?.scaleX(0.85f)?.scaleY(0.85f)?.alpha(1f)?.start()
|
||||
stick_seekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(25).scaleX(0.85f).scaleY(0.85f).alpha(1f).start()
|
||||
binding.stickSeekbar.secondaryProgress = (abs(value) * 100).toInt()
|
||||
}
|
||||
|
||||
|
||||
@ -508,16 +526,16 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
DialogStage.YPlus, DialogStage.YMinus -> {
|
||||
val coefficient = if (stage == DialogStage.YMinus) abs(value) else -abs(value)
|
||||
|
||||
stick_container?.translationY = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationX = 27f * -coefficient
|
||||
binding.stickContainer.translationY = dipToPixels(16.5f) * coefficient
|
||||
binding.stickContainer.rotationX = 27f * -coefficient
|
||||
}
|
||||
|
||||
// Update the the position of the stick in the X-axis based on the axis's value
|
||||
DialogStage.XPlus, DialogStage.XMinus -> {
|
||||
val coefficient = if (stage == DialogStage.XPlus) abs(value) else -abs(value)
|
||||
|
||||
stick_container?.translationX = dipToPixels(16.5f) * coefficient
|
||||
stick_container?.rotationY = 27f * coefficient
|
||||
binding.stickContainer.translationX = dipToPixels(16.5f) * coefficient
|
||||
binding.stickContainer.rotationY = 27f * coefficient
|
||||
}
|
||||
|
||||
else -> {
|
||||
@ -530,10 +548,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
axisRunnable = Runnable {
|
||||
val hostEvent = MotionHostEvent(event.device.descriptor, inputId!!, axisPolarity)
|
||||
|
||||
var guestEvent = InputManager.eventMap[hostEvent]
|
||||
var guestEvent = inputManager.eventMap[hostEvent]
|
||||
|
||||
if (guestEvent is GuestEvent) {
|
||||
InputManager.eventMap.remove(hostEvent)
|
||||
inputManager.eventMap.remove(hostEvent)
|
||||
|
||||
if (guestEvent is ButtonGuestEvent)
|
||||
context.buttonMap[(guestEvent as ButtonGuestEvent).button]?.update()
|
||||
@ -550,9 +568,9 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
else -> null
|
||||
}
|
||||
|
||||
InputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { InputManager.eventMap.remove(it) }
|
||||
inputManager.eventMap.filterValues { it == guestEvent }.keys.forEach { inputManager.eventMap.remove(it) }
|
||||
|
||||
InputManager.eventMap[hostEvent] = guestEvent
|
||||
inputManager.eventMap[hostEvent] = guestEvent
|
||||
|
||||
ignoredEvents.add(Objects.hash(deviceId!!, inputId!!, axisPolarity))
|
||||
|
||||
@ -560,14 +578,14 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
|
||||
item.update()
|
||||
|
||||
stick_next?.callOnClick()
|
||||
binding.stickNext.callOnClick()
|
||||
}
|
||||
|
||||
handler.postDelayed(axisRunnable!!, 1000)
|
||||
}
|
||||
|
||||
stick_icon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
stick_name.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
binding.stickIcon.animate().alpha(0.85f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
binding.stickName.animate().alpha(0.95f).setInterpolator(LinearInterpolator(context, null)).setDuration(1000).start()
|
||||
} else {
|
||||
// If the axis value is below the threshold, remove [axisRunnable] from it being posted and animate the views accordingly
|
||||
if (axisRunnable != null) {
|
||||
@ -576,10 +594,10 @@ class StickDialog @JvmOverloads constructor(val item : ControllerStickViewItem?
|
||||
}
|
||||
|
||||
if (stage == DialogStage.Button)
|
||||
stick_container?.animate()?.setStartDelay(0)?.setDuration(10)?.scaleX(1f)?.scaleY(1f)?.alpha(1f)?.start()
|
||||
binding.stickContainer.animate().setStartDelay(0).setDuration(10).scaleX(1f).scaleY(1f).alpha(1f).start()
|
||||
|
||||
stick_icon.animate().alpha(0.25f).setDuration(50).start()
|
||||
stick_name.animate().alpha(0.35f).setDuration(50).start()
|
||||
binding.stickIcon.animate().alpha(0.25f).setDuration(50).start()
|
||||
binding.stickName.animate().alpha(0.35f).setDuration(50).start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,17 +16,19 @@ import androidx.core.content.ContextCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
import emu.skyline.R
|
||||
import emu.skyline.databinding.OnScreenEditActivityBinding
|
||||
import emu.skyline.utils.Settings
|
||||
import kotlinx.android.synthetic.main.on_screen_edit_activity.*
|
||||
|
||||
class OnScreenEditActivity : AppCompatActivity() {
|
||||
private val binding by lazy { OnScreenEditActivityBinding.inflate(layoutInflater) }
|
||||
|
||||
private var fullEditVisible = true
|
||||
private var editMode = false
|
||||
|
||||
private val closeAction : () -> Unit = {
|
||||
if (editMode) {
|
||||
toggleFabVisibility(true)
|
||||
on_screen_controller_view.setEditMode(false)
|
||||
binding.onScreenControllerView.setEditMode(false)
|
||||
editMode = false
|
||||
} else {
|
||||
fullEditVisible = !fullEditVisible
|
||||
@ -45,12 +47,12 @@ class OnScreenEditActivity : AppCompatActivity() {
|
||||
|
||||
private val editAction = {
|
||||
editMode = true
|
||||
on_screen_controller_view.setEditMode(true)
|
||||
binding.onScreenControllerView.setEditMode(true)
|
||||
toggleFabVisibility(false)
|
||||
}
|
||||
|
||||
private val toggleAction : () -> Unit = {
|
||||
val buttonProps = on_screen_controller_view.getButtonProps()
|
||||
val buttonProps = binding.onScreenControllerView.getButtonProps()
|
||||
val checkArray = buttonProps.map { it.second }.toBooleanArray()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
@ -62,7 +64,7 @@ class OnScreenEditActivity : AppCompatActivity() {
|
||||
}.setPositiveButton(R.string.confirm) { _, _ ->
|
||||
buttonProps.forEachIndexed { index, pair ->
|
||||
if (checkArray[index] != pair.second)
|
||||
on_screen_controller_view.setButtonEnabled(pair.first, checkArray[index])
|
||||
binding.onScreenControllerView.setButtonEnabled(pair.first, checkArray[index])
|
||||
}
|
||||
}.setNegativeButton(R.string.cancel, null)
|
||||
.setOnDismissListener { fullScreen() }
|
||||
@ -70,11 +72,11 @@ class OnScreenEditActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private val actions : List<Pair<Int, () -> Unit>> = listOf(
|
||||
Pair(R.drawable.ic_restore, { on_screen_controller_view.resetControls() }),
|
||||
Pair(R.drawable.ic_restore, { binding.onScreenControllerView.resetControls() }),
|
||||
Pair(R.drawable.ic_toggle, toggleAction),
|
||||
Pair(R.drawable.ic_edit, editAction),
|
||||
Pair(R.drawable.ic_zoom_out, { on_screen_controller_view.decreaseScale() }),
|
||||
Pair(R.drawable.ic_zoom_in, { on_screen_controller_view.increaseScale() }),
|
||||
Pair(R.drawable.ic_zoom_out, { binding.onScreenControllerView.decreaseScale() }),
|
||||
Pair(R.drawable.ic_zoom_in, { binding.onScreenControllerView.increaseScale() }),
|
||||
Pair(R.drawable.ic_close, closeAction)
|
||||
)
|
||||
|
||||
@ -82,11 +84,11 @@ class OnScreenEditActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState : Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.on_screen_edit_activity)
|
||||
on_screen_controller_view.recenterSticks = Settings(this).onScreenControlRecenterSticks
|
||||
setContentView(binding.root)
|
||||
binding.onScreenControllerView.recenterSticks = Settings(this).onScreenControlRecenterSticks
|
||||
|
||||
actions.forEach { pair ->
|
||||
fab_parent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, fab_parent, false).apply {
|
||||
binding.fabParent.addView(LayoutInflater.from(this).inflate(R.layout.on_screen_edit_mini_fab, binding.fabParent, false).apply {
|
||||
(this as FloatingActionButton).setImageDrawable(ContextCompat.getDrawable(context, pair.first))
|
||||
setOnClickListener { pair.second.invoke() }
|
||||
fabMapping[pair.first] = this
|
||||
|
@ -12,8 +12,8 @@ import android.util.AttributeSet
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.Preference.SummaryProvider
|
||||
import emu.skyline.R
|
||||
import emu.skyline.di.InputManagerProviderEntryPoint
|
||||
import emu.skyline.input.ControllerActivity
|
||||
import emu.skyline.input.InputManager
|
||||
|
||||
/**
|
||||
* This preference is used to launch [ControllerActivity] using a preference
|
||||
@ -26,6 +26,8 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
|
||||
|
||||
override var requestCode = 0
|
||||
|
||||
private val inputManager = InputManagerProviderEntryPoint.getInputManager(context)
|
||||
|
||||
init {
|
||||
for (i in 0 until attrs!!.attributeCount) {
|
||||
val attr = attrs.getAttributeName(i)
|
||||
@ -43,7 +45,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
|
||||
key = "controller_$index"
|
||||
|
||||
title = "${context.getString(R.string.config_controller)} #${index + 1}"
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { InputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } }
|
||||
summaryProvider = SummaryProvider<ControllerPreference> { inputManager.controllers[index]!!.type.stringRes.let { context.getString(it) } }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -55,7 +57,7 @@ class ControllerPreference @JvmOverloads constructor(context : Context, attrs :
|
||||
|
||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
if (this.requestCode == requestCode) {
|
||||
InputManager.syncObjects()
|
||||
inputManager.syncObjects()
|
||||
notifyChanged()
|
||||
}
|
||||
}
|
||||
|
@ -15,8 +15,8 @@ import emu.skyline.R
|
||||
/**
|
||||
* This class adapts [EditTextPreference] so that it supports setting the value as the summary automatically. Also added useful attributes.
|
||||
*/
|
||||
class CustomEditTextPreference : EditTextPreference {
|
||||
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int, defStyleRes : Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
class CustomEditTextPreference @JvmOverloads constructor(context : Context, attrs : AttributeSet? = null, defStyleAttr : Int = androidx.preference.R.attr.editTextPreferenceStyle) : EditTextPreference(context, attrs, defStyleAttr) {
|
||||
init {
|
||||
attrs?.let {
|
||||
val a = context.obtainStyledAttributes(it, R.styleable.CustomEditTextPreference, defStyleAttr, 0)
|
||||
val limit = a.getInt(R.styleable.CustomEditTextPreference_limit, -1)
|
||||
@ -33,12 +33,6 @@ class CustomEditTextPreference : EditTextPreference {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) : this(context, attrs, defStyleAttr, 0)
|
||||
|
||||
constructor(context : Context, attrs : AttributeSet?) : this(context, attrs, androidx.preference.R.attr.editTextPreferenceStyle)
|
||||
|
||||
constructor(context : Context) : this(context, null)
|
||||
|
||||
override fun onAttached() {
|
||||
super.onAttached()
|
||||
|
||||
|
@ -16,7 +16,6 @@ import com.google.android.material.snackbar.Snackbar
|
||||
import emu.skyline.KeyReader
|
||||
import emu.skyline.R
|
||||
import emu.skyline.SettingsActivity
|
||||
import kotlinx.android.synthetic.main.settings_activity.*
|
||||
|
||||
/**
|
||||
* Launches [FileActivity] and process the selected file for key import
|
||||
@ -27,14 +26,14 @@ class FilePreference @JvmOverloads constructor(context : Context?, attrs : Attri
|
||||
override fun onClick() = (context as Activity).startActivityForResult(Intent(context, FileActivity::class.java).apply { putExtra(DocumentActivity.KEY_NAME, key) }, requestCode)
|
||||
|
||||
override fun onActivityResult(requestCode : Int, resultCode : Int, data : Intent?) {
|
||||
if (this.requestCode == requestCode) {
|
||||
if (this.requestCode == requestCode && requestCode == Activity.RESULT_OK) {
|
||||
if (key == "prod_keys" || key == "title_keys") {
|
||||
val success = KeyReader.import(
|
||||
context,
|
||||
Uri.parse(PreferenceManager.getDefaultSharedPreferences(context).getString(key, "")),
|
||||
KeyReader.KeyType.parse(key)
|
||||
)
|
||||
Snackbar.make((context as SettingsActivity).settings, if (success) R.string.import_keys_success else R.string.import_keys_failed, Snackbar.LENGTH_LONG).show()
|
||||
Snackbar.make((context as SettingsActivity).binding.root, if (success) R.string.import_keys_success else R.string.import_keys_failed, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,37 +9,34 @@ import android.graphics.Rect
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import emu.skyline.R
|
||||
import kotlinx.android.synthetic.main.license_dialog.*
|
||||
import emu.skyline.databinding.LicenseDialogBinding
|
||||
|
||||
/**
|
||||
* This dialog is used to display the contents of a license for a particular project
|
||||
*/
|
||||
class LicenseDialog : DialogFragment() {
|
||||
private lateinit var binding : LicenseDialogBinding
|
||||
|
||||
/**
|
||||
* This inflates the layout of the dialog and sets the minimum width/height to 90% of the screen size
|
||||
*/
|
||||
override fun onCreateView(inflater : LayoutInflater, container : ViewGroup?, savedInstanceState : Bundle?) : View? {
|
||||
val layout = layoutInflater.inflate(R.layout.license_dialog, container)
|
||||
|
||||
val displayRectangle = Rect()
|
||||
val window : Window = requireActivity().window
|
||||
window.decorView.getWindowVisibleDisplayFrame(displayRectangle)
|
||||
|
||||
layout.minimumWidth = ((displayRectangle.width() * 0.9f).toInt())
|
||||
layout.minimumHeight = ((displayRectangle.height() * 0.9f).toInt())
|
||||
|
||||
return layout
|
||||
return LicenseDialogBinding.inflate(inflater).apply {
|
||||
root.minimumWidth = ((displayRectangle.width() * 0.9f).toInt())
|
||||
root.minimumHeight = ((displayRectangle.height() * 0.9f).toInt())
|
||||
binding = this
|
||||
}.root
|
||||
}
|
||||
|
||||
/**
|
||||
* This sets the [license_url] and [license_content] based on arguments passed
|
||||
*/
|
||||
override fun onActivityCreated(savedInstanceState : Bundle?) {
|
||||
super.onActivityCreated(savedInstanceState)
|
||||
|
||||
license_url.text = arguments?.getString("libraryUrl")!!
|
||||
license_content.text = context?.getString(arguments?.getInt("libraryLicense")!!)!!
|
||||
binding.licenseUrl.text = requireArguments().getString("libraryUrl")
|
||||
binding.licenseContent.text = getString(requireArguments().getInt("libraryLicense"))
|
||||
|
||||
dialog?.setOnKeyListener { _, keyCode, event ->
|
||||
if (keyCode == KeyEvent.KEYCODE_BUTTON_B && event.action == KeyEvent.ACTION_UP) {
|
||||
|
@ -16,63 +16,50 @@ import emu.skyline.R
|
||||
/**
|
||||
* This preference is used to show licenses and the source of a library
|
||||
*/
|
||||
class LicensePreference : Preference {
|
||||
class LicensePreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.dialogPreferenceStyle) : Preference(context, attrs, defStyleAttr) {
|
||||
/**
|
||||
* The [FragmentManager] is used to show the [LicenseDialog] fragment
|
||||
*/
|
||||
private val fragmentManager : FragmentManager
|
||||
private val fragmentManager = (context as AppCompatActivity).supportFragmentManager
|
||||
|
||||
/**
|
||||
* The tag used by this preference when launching a corresponding fragment
|
||||
*/
|
||||
private val mDialogFragmentTag = "LicensePreference"
|
||||
companion object {
|
||||
private const val LIBRARY_URL_ARG = "libraryUrl"
|
||||
private const val LIBRARY_LICENSE_ARG = "libraryLicense"
|
||||
|
||||
private val DIALOG_TAG = LicensePreference::class.java.simpleName
|
||||
}
|
||||
|
||||
/**
|
||||
* The URL of the library
|
||||
*/
|
||||
private var libraryUrl : String? = null
|
||||
private lateinit var libraryUrl : String
|
||||
|
||||
/**
|
||||
* The contents of the license of this library
|
||||
*/
|
||||
private var libraryLicense : Int? = null
|
||||
|
||||
/**
|
||||
* The constructor assigns the [fragmentManager] from the activity and finds [libraryUrl] and [libraryLicense] in the attributes
|
||||
*/
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int, defStyleRes : Int) : super(context, attrs, defStyleAttr, defStyleRes) {
|
||||
fragmentManager = (context as AppCompatActivity).supportFragmentManager
|
||||
private var libraryLicense = 0
|
||||
|
||||
init {
|
||||
for (i in 0 until attrs!!.attributeCount) {
|
||||
val attr = attrs.getAttributeName(i)
|
||||
when (attrs.getAttributeName(i)) {
|
||||
LIBRARY_URL_ARG -> libraryUrl = attrs.getAttributeValue(i)
|
||||
|
||||
if (attr.equals("libraryUrl", ignoreCase = true))
|
||||
libraryUrl = attrs.getAttributeValue(i)
|
||||
else if (attr.equals("libraryLicense", ignoreCase = true))
|
||||
libraryLicense = attrs.getAttributeValue(i).substring(1).toInt()
|
||||
LIBRARY_LICENSE_ARG -> libraryLicense = attrs.getAttributeValue(i).substring(1).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : this(context, attrs, defStyleAttr, 0)
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : this(context, attrs, R.attr.dialogPreferenceStyle)
|
||||
|
||||
constructor(context : Context?) : this(context, null)
|
||||
|
||||
/**
|
||||
* The [LicenseDialog] fragment is shown using [fragmentManager] on click with [libraryUrl] and [libraryLicense] passed as arguments
|
||||
*/
|
||||
override fun onClick() {
|
||||
if (fragmentManager.findFragmentByTag(mDialogFragmentTag) != null)
|
||||
return
|
||||
|
||||
val dialog = LicenseDialog()
|
||||
|
||||
val bundle = Bundle(2)
|
||||
bundle.putString("libraryUrl", libraryUrl!!)
|
||||
bundle.putInt("libraryLicense", libraryLicense!!)
|
||||
dialog.arguments = bundle
|
||||
|
||||
dialog.show(fragmentManager, mDialogFragmentTag)
|
||||
fragmentManager.findFragmentByTag(DIALOG_TAG) ?: run {
|
||||
LicenseDialog().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(LIBRARY_URL_ARG, libraryUrl)
|
||||
putInt(LIBRARY_LICENSE_ARG, libraryLicense)
|
||||
}
|
||||
}.show(fragmentManager, DIALOG_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,17 +9,12 @@ import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.R
|
||||
|
||||
/**
|
||||
* This preference is used to set the theme to Light/Dark mode
|
||||
*/
|
||||
class ThemePreference : ListPreference {
|
||||
constructor(context : Context?, attrs : AttributeSet?, defStyleAttr : Int) : super(context, attrs, defStyleAttr)
|
||||
|
||||
constructor(context : Context?, attrs : AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(context : Context?) : super(context)
|
||||
|
||||
class ThemePreference @JvmOverloads constructor(context : Context?, attrs : AttributeSet? = null, defStyleAttr : Int = R.attr.dialogPreferenceStyle) : ListPreference(context, attrs, defStyleAttr) {
|
||||
/**
|
||||
* This changes [AppCompatDelegate.sDefaultNightMode] based on what the user's selection is
|
||||
*/
|
||||
|
@ -16,7 +16,7 @@ inline fun <reified T> sharedPreferences(context : Context, default : T, prefix
|
||||
class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<T>, private val default : T, private val prefix : String, prefName : String?) : ReadWriteProperty<Any, T> {
|
||||
private val prefs = prefName?.let { context.getSharedPreferences(prefName, Context.MODE_PRIVATE) } ?: PreferenceManager.getDefaultSharedPreferences(context)
|
||||
|
||||
override fun setValue(thisRef : Any, property : KProperty<*>, value : T) = (prefix + pascalToSnakeCase(property.name)).let { keyName ->
|
||||
override fun setValue(thisRef : Any, property : KProperty<*>, value : T) = (prefix + camelToSnakeCase(property.name)).let { keyName ->
|
||||
prefs.edit().apply {
|
||||
when (clazz) {
|
||||
Float::class.java, java.lang.Float::class.java -> putFloat(keyName, value as Float)
|
||||
@ -27,7 +27,7 @@ class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<
|
||||
}.apply()
|
||||
}
|
||||
|
||||
override fun getValue(thisRef : Any, property : KProperty<*>) : T = (prefix + pascalToSnakeCase(property.name)).let { keyName ->
|
||||
override fun getValue(thisRef : Any, property : KProperty<*>) : T = (prefix + camelToSnakeCase(property.name)).let { keyName ->
|
||||
prefs.let {
|
||||
@Suppress("IMPLICIT_CAST_TO_ANY")
|
||||
when (clazz) {
|
||||
@ -39,7 +39,7 @@ class SharedPreferencesDelegate<T>(context : Context, private val clazz : Class<
|
||||
} as T
|
||||
}
|
||||
|
||||
private fun pascalToSnakeCase(text : String) = StringBuilder().apply {
|
||||
private fun camelToSnakeCase(text : String) = StringBuilder().apply {
|
||||
text.forEachIndexed { index, c ->
|
||||
if (index != 0 && c.isUpperCase()) append('_')
|
||||
append(c.toLowerCase())
|
||||
|
@ -11,13 +11,13 @@ 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 emu.skyline.databinding.ViewSearchBarBinding
|
||||
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) {
|
||||
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
init {
|
||||
LayoutInflater.from(context).inflate(R.layout.view_search_bar, this)
|
||||
useCompatPadding = true
|
||||
}
|
||||
|
||||
@ -32,32 +32,32 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
|
||||
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)
|
||||
fun setRefreshIconListener(listener : OnClickListener) = binding.refreshIcon.setOnClickListener(listener)
|
||||
fun setLogIconListener(listener : OnClickListener) = binding.logIcon.setOnClickListener(listener)
|
||||
fun setSettingsIconListener(listener : OnClickListener) = binding.settingsIcon.setOnClickListener(listener)
|
||||
|
||||
var refreshIconVisible = false
|
||||
set(visible) {
|
||||
field = visible
|
||||
refresh_icon.apply {
|
||||
binding.refreshIcon.apply {
|
||||
if (visible != isVisible) {
|
||||
refresh_icon.alpha = if (visible) 0f else 1f
|
||||
binding.refreshIcon.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)
|
||||
get() = binding.searchField.text
|
||||
set(value) = binding.searchField.setText(value)
|
||||
|
||||
fun startTitleAnimation() {
|
||||
motion_layout.progress = 0f
|
||||
motion_layout.transitionToEnd()
|
||||
search_field.apply {
|
||||
binding.motionLayout.progress = 0f
|
||||
binding.motionLayout.transitionToEnd()
|
||||
binding.searchField.apply {
|
||||
setOnFocusChangeListener { v, hasFocus ->
|
||||
if (hasFocus) {
|
||||
this@SearchBarView.motion_layout.progress = 1f
|
||||
binding.motionLayout.progress = 1f
|
||||
context.getSystemService(InputMethodManager::class.java).showSoftInput(v, InputMethodManager.SHOW_IMPLICIT)
|
||||
onFocusChangeListener = null
|
||||
}
|
||||
@ -66,23 +66,23 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
|
||||
}
|
||||
|
||||
fun animateRefreshIcon() {
|
||||
refresh_icon.animate().rotationBy(-180f)
|
||||
binding.refreshIcon.animate().rotationBy(-180f)
|
||||
}
|
||||
|
||||
inline fun addTextChangedListener(
|
||||
crossinline beforeTextChanged : (
|
||||
fun addTextChangedListener(
|
||||
beforeTextChanged : (
|
||||
text : CharSequence?,
|
||||
start : Int,
|
||||
count : Int,
|
||||
after : Int
|
||||
) -> Unit = { _, _, _, _ -> },
|
||||
crossinline onTextChanged : (
|
||||
onTextChanged : (
|
||||
text : CharSequence?,
|
||||
start : Int,
|
||||
before : Int,
|
||||
count : Int
|
||||
) -> Unit = { _, _, _, _ -> },
|
||||
crossinline afterTextChanged : (text : Editable?) -> Unit = {}
|
||||
afterTextChanged : (text : Editable?) -> Unit = {}
|
||||
) : TextWatcher {
|
||||
val textWatcher = object : TextWatcher {
|
||||
override fun afterTextChanged(s : Editable?) {
|
||||
@ -97,7 +97,7 @@ class SearchBarView @JvmOverloads constructor(context : Context, attrs : Attribu
|
||||
onTextChanged.invoke(text, start, before, count)
|
||||
}
|
||||
}
|
||||
search_field.addTextChangedListener(textWatcher)
|
||||
binding.searchField.addTextChangedListener(textWatcher)
|
||||
|
||||
return textWatcher
|
||||
}
|
||||
|
@ -1,16 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".input.ControllerActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".input.ControllerActivity">
|
||||
|
||||
<include layout="@layout/titlebar" />
|
||||
<include
|
||||
android:id="@+id/titlebar"
|
||||
layout="@layout/titlebar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/controller_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:id="@+id/controller_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -1,20 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".LogActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".LogActivity">
|
||||
|
||||
<include layout="@layout/titlebar" />
|
||||
<include
|
||||
android:id="@+id/titlebar"
|
||||
layout="@layout/titlebar" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/log_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fastScrollEnabled="true"
|
||||
android:focusedByDefault="true"
|
||||
android:transcriptMode="normal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:id="@+id/log_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fastScrollEnabled="true"
|
||||
android:focusedByDefault="true"
|
||||
android:transcriptMode="normal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -1,12 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
android:id="@+id/text_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:textColor="?android:attr/textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
|
@ -1,14 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<include layout="@layout/titlebar" />
|
||||
<include
|
||||
android:id="@+id/titlebar"
|
||||
layout="@layout/titlebar" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:id="@+id/settings"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
@ -1,17 +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/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:keyboardNavigationCluster="false"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
android:keyboardNavigationCluster="false"
|
||||
android:touchscreenBlocksFocus="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:elevation="16dp"
|
||||
app:layout_scrollFlags="scroll" />
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:elevation="16dp"
|
||||
app:layout_scrollFlags="scroll" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
11
build.gradle
11
build.gradle
@ -1,24 +1,25 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.4.30'
|
||||
ext.kotlin_version = '1.4.21'
|
||||
ext.lifecycle_version = '2.2.0'
|
||||
ext.hilt_version = '2.31.2-alpha'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.github.ben-manes.versions" version "0.36.0"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
|
Loading…
Reference in New Issue
Block a user