mirror of
https://github.com/fcitx5-android/fcitx5-android.git
synced 2026-08-02 20:16:06 +08:00
Refactor fcitx event and UI lifecycle handling of BaseInputView
This commit is contained in:
parent
2fd2c92418
commit
8a6f5ebf37
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input
|
||||
@ -13,6 +13,7 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.fcitx.fcitx5.android.core.FcitxEvent
|
||||
import org.fcitx.fcitx5.android.daemon.FcitxConnection
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.data.theme.Theme
|
||||
import org.fcitx.fcitx5.android.data.theme.ThemeManager
|
||||
import org.fcitx.fcitx5.android.data.theme.ThemePrefs
|
||||
@ -25,6 +26,11 @@ abstract class BaseInputView(
|
||||
val theme: Theme
|
||||
) : ConstraintLayout(service) {
|
||||
|
||||
/**
|
||||
* Update UI (from cached events in FcitxAPI) to match fcitx's state, before ready to receive real events
|
||||
*/
|
||||
protected abstract fun onStartHandleFcitxEvent()
|
||||
|
||||
protected abstract fun handleFcitxEvent(it: FcitxEvent<*>)
|
||||
|
||||
private var eventHandlerJob: Job? = null
|
||||
@ -37,23 +43,11 @@ abstract class BaseInputView(
|
||||
}
|
||||
}
|
||||
|
||||
open fun refreshWithCachedEvents() {
|
||||
val inputPanelData = fcitx.runImmediately { inputPanelCached }
|
||||
val inputMethodEntry = fcitx.runImmediately { inputMethodEntryCached }
|
||||
val statusAreaActions = fcitx.runImmediately { statusAreaActionsCached }
|
||||
arrayOf(
|
||||
FcitxEvent.InputPanelEvent(inputPanelData),
|
||||
FcitxEvent.IMChangeEvent(inputMethodEntry),
|
||||
FcitxEvent.StatusAreaEvent(
|
||||
FcitxEvent.StatusAreaEvent.Data(statusAreaActions, inputMethodEntry)
|
||||
)
|
||||
).forEach { handleFcitxEvent(it) }
|
||||
}
|
||||
|
||||
var handleEvents = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (field) {
|
||||
onStartHandleFcitxEvent()
|
||||
if (eventHandlerJob == null) {
|
||||
setupFcitxEventHandler()
|
||||
}
|
||||
@ -86,6 +80,20 @@ abstract class BaseInputView(
|
||||
return insetsBottom
|
||||
}
|
||||
|
||||
private val ignoreSystemWindowInsets by AppPrefs.getInstance().advanced.ignoreSystemWindowInsets
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
if (ignoreSystemWindowInsets) {
|
||||
// suppress view's own onApplyWindowInsets
|
||||
setOnApplyWindowInsetsListener { _, insets -> insets }
|
||||
} else {
|
||||
// on API 35+, we must call requestApplyInsets() manually after replacing views,
|
||||
// otherwise View#onApplyWindowInsets won't be called. ¯\_(ツ)_/¯
|
||||
requestApplyInsets()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
handleEvents = false
|
||||
super.onDetachedFromWindow()
|
||||
|
||||
@ -104,7 +104,10 @@ class CandidatesView(
|
||||
onNextPage = { fcitx.launchOnReady { it.offsetCandidatePage(1) } }
|
||||
)
|
||||
|
||||
private var bottomInsets = 0
|
||||
override fun onStartHandleFcitxEvent() {
|
||||
val inputPanelData = fcitx.runImmediately { inputPanelCached }
|
||||
handleFcitxEvent(FcitxEvent.InputPanelEvent(inputPanelData))
|
||||
}
|
||||
|
||||
override fun handleFcitxEvent(it: FcitxEvent<*>) {
|
||||
when (it) {
|
||||
@ -136,10 +139,11 @@ class CandidatesView(
|
||||
} else {
|
||||
// RecyclerView won't update its items when ancestor view is GONE
|
||||
visibility = INVISIBLE
|
||||
touchEventReceiverWindow.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private var bottomInsets = 0
|
||||
|
||||
private fun updatePosition() {
|
||||
if (visibility != VISIBLE) {
|
||||
// skip unnecessary updates
|
||||
@ -229,6 +233,13 @@ class CandidatesView(
|
||||
viewTreeObserver.addOnPreDrawListener(preDrawListener)
|
||||
}
|
||||
|
||||
override fun setVisibility(visibility: Int) {
|
||||
if (visibility != VISIBLE) {
|
||||
touchEventReceiverWindow.dismiss()
|
||||
}
|
||||
super.setVisibility(visibility)
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
viewTreeObserver.removeOnPreDrawListener(preDrawListener)
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(layoutListener)
|
||||
|
||||
@ -7,6 +7,7 @@ package org.fcitx.fcitx5.android.input
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Dialog
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.ColorStateList
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
@ -101,9 +102,14 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
private var candidatesView: CandidatesView? = null
|
||||
|
||||
private val navbarMgr = NavigationBarManager()
|
||||
private val inputDeviceMgr = InputDeviceManager onChange@{
|
||||
val w = window.window ?: return@onChange
|
||||
navbarMgr.evaluate(w, useVirtualKeyboard = it)
|
||||
private val inputDeviceMgr = InputDeviceManager { isVirtualKeyboard ->
|
||||
postFcitxJob {
|
||||
setCandidatePagingMode(if (isVirtualKeyboard) 0 else 1)
|
||||
}
|
||||
currentInputConnection?.monitorCursorAnchor(!isVirtualKeyboard)
|
||||
window.window?.let {
|
||||
navbarMgr.evaluate(it, isVirtualKeyboard)
|
||||
}
|
||||
}
|
||||
|
||||
private var capabilityFlags = CapabilityFlags.DefaultFlags
|
||||
@ -139,7 +145,6 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
val newInputView = InputView(this, fcitx, theme)
|
||||
setInputView(newInputView)
|
||||
inputDeviceMgr.setInputView(newInputView)
|
||||
navbarMgr.setupInputView(newInputView)
|
||||
inputView = newInputView
|
||||
return newInputView
|
||||
}
|
||||
@ -151,13 +156,12 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
// put CandidatesView directly under content view
|
||||
contentView.addView(newCandidatesView)
|
||||
inputDeviceMgr.setCandidatesView(newCandidatesView)
|
||||
navbarMgr.setupInputView(newCandidatesView)
|
||||
candidatesView = newCandidatesView
|
||||
return newCandidatesView
|
||||
}
|
||||
|
||||
private fun replaceInputViews(theme: Theme) {
|
||||
navbarMgr.evaluate(window.window!!)
|
||||
navbarMgr.evaluate(window.window!!, inputDeviceMgr.isVirtualKeyboard)
|
||||
replaceInputView(theme)
|
||||
replaceCandidateView(theme)
|
||||
}
|
||||
@ -216,6 +220,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
super.onCreate()
|
||||
decorView = window.window!!.decorView
|
||||
contentView = decorView.findViewById(android.R.id.content)
|
||||
lastKnownConfig = resources.configuration
|
||||
}
|
||||
|
||||
private fun handleFcitxEvent(event: FcitxEvent<*>) {
|
||||
@ -313,7 +318,7 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
if (reason != FcitxEvent.SwitchInputMethodEvent.Reason.CapabilityChanged &&
|
||||
reason != FcitxEvent.SwitchInputMethodEvent.Reason.Other
|
||||
) {
|
||||
if (inputDeviceMgr.evaluateOnInputMethodChange()) {
|
||||
if (inputDeviceMgr.evaluateOnInputMethodSwitch()) {
|
||||
// show inputView for [CandidatesView] when input method switched by user
|
||||
forceShowSelf()
|
||||
}
|
||||
@ -515,9 +520,21 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
currentInputConnection?.setSelection(end, end)
|
||||
}
|
||||
|
||||
private lateinit var lastKnownConfig: Configuration
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
postFcitxJob { reset() }
|
||||
/**
|
||||
* skip uiMode (system light/dark mode) changes, because we have [onThemeChangeListener]
|
||||
* to replace InputView(s) when needed
|
||||
* [android.inputmethodservice.InputMethodService.onConfigurationChanged] would call
|
||||
* resetStateForNewConfiguration() which calls initViews() causes InputView(s) to be replaced again
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-15.0.0_r36/core/java/android/inputmethodservice/InputMethodService.java#1984
|
||||
*/
|
||||
if (lastKnownConfig.diff(newConfig) != ActivityInfo.CONFIG_UI_MODE) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
}
|
||||
lastKnownConfig = newConfig
|
||||
}
|
||||
|
||||
override fun onWindowShown() {
|
||||
@ -528,8 +545,6 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
|
||||
Timber.w("Device does not support android.R.attr.colorAccent which it should have.")
|
||||
}
|
||||
InputFeedbacks.syncSystemPrefs()
|
||||
// navbar foreground/background color would reset every time window shows
|
||||
navbarMgr.update(window.window!!)
|
||||
}
|
||||
|
||||
override fun onCreateInputView(): View? {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input
|
||||
@ -12,7 +12,6 @@ import android.view.inputmethod.EditorInfo
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.input.candidates.floating.FloatingCandidatesMode
|
||||
import org.fcitx.fcitx5.android.utils.isTypeNull
|
||||
import org.fcitx.fcitx5.android.utils.monitorCursorAnchor
|
||||
|
||||
class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
|
||||
@ -22,12 +21,7 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
private fun setupInputViewEvents(isVirtual: Boolean) {
|
||||
val iv = inputView ?: return
|
||||
iv.handleEvents = isVirtual
|
||||
if (isVirtual) {
|
||||
iv.visibility = View.VISIBLE
|
||||
iv.refreshWithCachedEvents()
|
||||
} else {
|
||||
iv.visibility = View.GONE
|
||||
}
|
||||
iv.visibility = if (isVirtual) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setupCandidatesViewEvents(isVirtual: Boolean) {
|
||||
@ -37,8 +31,6 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
// but preserve the visibility when entering physical keyboard mode (in case it's empty)
|
||||
if (isVirtual) {
|
||||
cv.visibility = View.GONE
|
||||
} else {
|
||||
cv.refreshWithCachedEvents()
|
||||
}
|
||||
}
|
||||
|
||||
@ -49,8 +41,14 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
|
||||
var isVirtualKeyboard = true
|
||||
private set(value) {
|
||||
if (field == value) {
|
||||
return
|
||||
}
|
||||
field = value
|
||||
setupViewEvents(value)
|
||||
// fire change AFTER updating InputView(s),
|
||||
// make the view(s) ready for incoming events during `onChange`
|
||||
onChange(value)
|
||||
}
|
||||
|
||||
fun setInputView(inputView: InputView) {
|
||||
@ -63,19 +61,6 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
setupCandidatesViewEvents(this.isVirtualKeyboard)
|
||||
}
|
||||
|
||||
private fun applyMode(service: FcitxInputMethodService, useVirtualKeyboard: Boolean) {
|
||||
if (useVirtualKeyboard == isVirtualKeyboard) {
|
||||
return
|
||||
}
|
||||
// monitor CursorAnchorInfo when switching to CandidatesView
|
||||
service.currentInputConnection.monitorCursorAnchor(!useVirtualKeyboard)
|
||||
service.postFcitxJob {
|
||||
setCandidatePagingMode(if (useVirtualKeyboard) 0 else 1)
|
||||
}
|
||||
isVirtualKeyboard = useVirtualKeyboard
|
||||
onChange(isVirtualKeyboard)
|
||||
}
|
||||
|
||||
private var startedInputView = false
|
||||
private var isNullInputType = true
|
||||
|
||||
@ -91,13 +76,12 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
fun evaluateOnStartInputView(info: EditorInfo, service: FcitxInputMethodService): Boolean {
|
||||
startedInputView = true
|
||||
isNullInputType = info.isTypeNull()
|
||||
val useVirtualKeyboard = when (candidatesViewMode) {
|
||||
isVirtualKeyboard = when (candidatesViewMode) {
|
||||
FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
|
||||
FloatingCandidatesMode.InputDevice -> isVirtualKeyboard
|
||||
FloatingCandidatesMode.Disabled -> true
|
||||
}
|
||||
applyMode(service, useVirtualKeyboard)
|
||||
return useVirtualKeyboard
|
||||
return isVirtualKeyboard
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,39 +108,37 @@ class InputDeviceManager(private val onChange: (Boolean) -> Unit) {
|
||||
}
|
||||
|
||||
private fun evaluateOnKeyDownInner(service: FcitxInputMethodService) {
|
||||
val useVirtualKeyboard = when (candidatesViewMode) {
|
||||
isVirtualKeyboard = when (candidatesViewMode) {
|
||||
FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
|
||||
FloatingCandidatesMode.InputDevice -> false
|
||||
FloatingCandidatesMode.Disabled -> true
|
||||
}
|
||||
applyMode(service, useVirtualKeyboard)
|
||||
}
|
||||
|
||||
fun evaluateOnViewClicked(service: FcitxInputMethodService) {
|
||||
if (!startedInputView) return
|
||||
val useVirtualKeyboard = when (candidatesViewMode) {
|
||||
isVirtualKeyboard = when (candidatesViewMode) {
|
||||
FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
|
||||
else -> true
|
||||
}
|
||||
applyMode(service, useVirtualKeyboard)
|
||||
}
|
||||
|
||||
fun evaluateOnUpdateEditorToolType(toolType: Int, service: FcitxInputMethodService) {
|
||||
if (!startedInputView) return
|
||||
val useVirtualKeyboard = when (candidatesViewMode) {
|
||||
isVirtualKeyboard = when (candidatesViewMode) {
|
||||
FloatingCandidatesMode.SystemDefault -> service.superEvaluateInputViewShown()
|
||||
FloatingCandidatesMode.InputDevice ->
|
||||
// switch to virtual keyboard on touch screen events, otherwise preserve current mode
|
||||
if (toolType == MotionEvent.TOOL_TYPE_FINGER || toolType == MotionEvent.TOOL_TYPE_STYLUS) true else isVirtualKeyboard
|
||||
FloatingCandidatesMode.Disabled -> true
|
||||
}
|
||||
applyMode(service, useVirtualKeyboard)
|
||||
}
|
||||
|
||||
/**
|
||||
* Should be called when input method switched **by user**
|
||||
* @return should force show inputView for [CandidatesView] when input method switched by user
|
||||
*/
|
||||
fun evaluateOnInputMethodChange(): Boolean {
|
||||
fun evaluateOnInputMethodSwitch(): Boolean {
|
||||
return !isVirtualKeyboard && !startedInputView
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input
|
||||
@ -9,7 +9,6 @@ import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.WindowInsets
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InlineSuggestionsResponse
|
||||
@ -317,6 +316,19 @@ class InputView(
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartHandleFcitxEvent() {
|
||||
val inputPanelData = fcitx.runImmediately { inputPanelCached }
|
||||
val inputMethodEntry = fcitx.runImmediately { inputMethodEntryCached }
|
||||
val statusAreaActions = fcitx.runImmediately { statusAreaActionsCached }
|
||||
arrayOf(
|
||||
FcitxEvent.InputPanelEvent(inputPanelData),
|
||||
FcitxEvent.IMChangeEvent(inputMethodEntry),
|
||||
FcitxEvent.StatusAreaEvent(
|
||||
FcitxEvent.StatusAreaEvent.Data(statusAreaActions, inputMethodEntry)
|
||||
)
|
||||
).forEach { handleFcitxEvent(it) }
|
||||
}
|
||||
|
||||
override fun handleFcitxEvent(it: FcitxEvent<*>) {
|
||||
when (it) {
|
||||
is FcitxEvent.CandidateListEvent -> {
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
* SPDX-FileCopyrightText: Copyright 2024 Fcitx5 for Android Contributors
|
||||
* SPDX-FileCopyrightText: Copyright 2024-2025 Fcitx5 for Android Contributors
|
||||
*/
|
||||
|
||||
package org.fcitx.fcitx5.android.input
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.view.Window
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.core.view.WindowCompat
|
||||
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
|
||||
import org.fcitx.fcitx5.android.data.theme.Theme
|
||||
import org.fcitx.fcitx5.android.data.theme.ThemeManager
|
||||
import org.fcitx.fcitx5.android.data.theme.ThemePrefs.NavbarBackground
|
||||
@ -37,7 +35,7 @@ class NavigationBarManager {
|
||||
* Why on earth does it deprecated? It says
|
||||
* https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-15.0.0_r3/core/java/android/view/Window.java#2720
|
||||
* "If the app targets VANILLA_ICE_CREAM or above, the color will be transparent and cannot be changed"
|
||||
* but it only takes effect on API 35+ devices. Older devices still needs this.
|
||||
* but it's only true on 35+. Older versions still need this.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
navigationBarColor = color
|
||||
@ -49,7 +47,7 @@ class NavigationBarManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun evaluate(window: Window) {
|
||||
private fun evaluateWithVirtualKeyboard(window: Window) {
|
||||
when (navbarBackground) {
|
||||
NavbarBackground.None -> {
|
||||
shouldUpdateNavbarForeground = false
|
||||
@ -77,10 +75,11 @@ class NavigationBarManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun evaluate(window: Window, useVirtualKeyboard: Boolean) {
|
||||
if (useVirtualKeyboard) {
|
||||
evaluate(window)
|
||||
fun evaluate(window: Window, isVirtualKeyboard: Boolean) {
|
||||
if (isVirtualKeyboard) {
|
||||
evaluateWithVirtualKeyboard(window)
|
||||
} else {
|
||||
// always color navbar to avoid CandidatesView being drawn behind it
|
||||
shouldUpdateNavbarForeground = true
|
||||
shouldUpdateNavbarBackground = true
|
||||
window.useSystemNavbarBackground(true)
|
||||
@ -89,7 +88,7 @@ class NavigationBarManager {
|
||||
update(window)
|
||||
}
|
||||
|
||||
fun update(window: Window) {
|
||||
private fun update(window: Window) {
|
||||
val theme = ThemeManager.activeTheme
|
||||
if (shouldUpdateNavbarForeground) {
|
||||
WindowCompat.getInsetsController(window, window.decorView)
|
||||
@ -101,21 +100,4 @@ class NavigationBarManager {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val ignoreSystemWindowInsets by AppPrefs.getInstance().advanced.ignoreSystemWindowInsets
|
||||
|
||||
private val emptyOnApplyWindowInsetsListener = View.OnApplyWindowInsetsListener { _, insets ->
|
||||
insets
|
||||
}
|
||||
|
||||
fun setupInputView(v: BaseInputView) {
|
||||
if (ignoreSystemWindowInsets) {
|
||||
// suppress the view's own onApplyWindowInsets
|
||||
v.setOnApplyWindowInsetsListener(emptyOnApplyWindowInsetsListener)
|
||||
} else {
|
||||
// on API 35+, we must call requestApplyInsets() manually after replacing views,
|
||||
// otherwise View#onApplyWindowInsets won't be called. ¯\_(ツ)_/¯
|
||||
v.requestApplyInsets()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user