Refactor fcitx event and UI lifecycle handling of BaseInputView

This commit is contained in:
Rocka 2025-12-21 01:05:12 +08:00
parent 2fd2c92418
commit 8a6f5ebf37
No known key found for this signature in database
GPG Key ID: 28031158FFDD6853
6 changed files with 97 additions and 87 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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