Switch to viewbinding and di with hilt

This commit is contained in:
Willi Ye 2021-01-31 22:11:26 +01:00 committed by ◱ Mark
parent ff1b9cb510
commit f5d5caf939
41 changed files with 716 additions and 580 deletions

View File

@ -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>

View File

@ -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"

View File

@ -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))

View File

@ -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 {
binding.perfStats.apply {
postDelayed(object : Runnable {
override fun run() {
updatePerformanceStatistics()
perf_stats.text = "$fps FPS\n${frametime}ms"
perf_stats.postDelayed(this, 250)
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
}

View File

@ -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")

View File

@ -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)
binding.appList.apply {
while (itemDecorationCount > 0) removeItemDecorationAt(0)
when (layoutType) {
LayoutType.List -> app_list.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
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()

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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()

View File

@ -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
}

View File

@ -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>>)
}
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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,13 +32,12 @@ object InputManager {
*/
lateinit var eventMap : HashMap<HostEvent?, GuestEvent?>
fun init(context : Context) {
file = File("${context.applicationInfo.dataDir}/input.bin")
init {
run {
try {
if (file.exists() && file.length() != 0L) {
syncObjects()
return
return@run
}
} catch (e : Exception) {
Log.e(this.toString(), e.localizedMessage ?: "InputManager cannot read \"${file.absolutePath}\"")
@ -54,6 +57,7 @@ object InputManager {
syncFile()
}
}
/**
* This function syncs the class with data from [file]

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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
binding.stickContainer.apply {
if (guestEvent.axis == item.stick.xAxis) {
stick_container?.translationX = dipToPixels(16.5f) * coefficient
stick_container?.rotationY = 27f * coefficient
translationX = dipToPixels(16.5f) * coefficient
rotationY = 27f * coefficient
} else if (guestEvent.axis == item.stick.yAxis) {
stick_container?.translationY = dipToPixels(16.5f) * -coefficient
stick_container?.rotationX = 27f * coefficient
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)
binding.stickContainer.apply {
if (guestEvent.axis == item.stick.xAxis) {
stick_container?.translationX = dipToPixels(16.5f) * coefficient
stick_container?.rotationY = 27f * coefficient
translationX = dipToPixels(16.5f) * coefficient
rotationY = 27f * coefficient
} else if (guestEvent.axis == item.stick.yAxis) {
stick_container?.translationY = dipToPixels(16.5f) * coefficient
stick_container?.rotationX = 27f * -coefficient
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()
}
}
}

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()

View File

@ -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()
}
}
}

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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
*/

View File

@ -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())

View File

@ -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
}

View File

@ -6,7 +6,9 @@
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"

View File

@ -6,7 +6,9 @@
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"

View File

@ -1,10 +1,13 @@
<?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"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<include layout="@layout/titlebar" />
<include
android:id="@+id/titlebar"
layout="@layout/titlebar" />
<FrameLayout
android:id="@+id/settings"

View File

@ -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()