Test floating keyboard

This commit is contained in:
VibeHero 2026-06-04 22:54:46 +08:00
parent e205195f38
commit c7938b928f
7 changed files with 357 additions and 17 deletions

View File

@ -35,6 +35,10 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
val pid = int("pid", 0)
val editorInfoInspector = bool("editor_info_inspector", false)
val needNotifications = bool("need_notifications", true)
val floatingKeyboardXRatio = float("floating_keyboard_x_ratio", 0.5f)
val floatingKeyboardYRatio = float("floating_keyboard_y_ratio", 1f)
val floatingKeyboardXRatioLandscape = float("floating_keyboard_x_ratio_landscape", 0.5f)
val floatingKeyboardYRatioLandscape = float("floating_keyboard_y_ratio_landscape", 1f)
}
inner class Advanced : ManagedPreferenceCategory(R.string.advanced, sharedPreferences) {
@ -133,6 +137,8 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
val expandToolbarByDefault =
switch(R.string.expand_toolbar_by_default, "expand_toolbar_by_default", false)
val inlineSuggestions = switch(R.string.inline_suggestions, "inline_suggestions", true)
val floatingKeyboardEnabled =
switch(R.string.floating_keyboard, "floating_keyboard_enabled", false)
val toolbarNumRowOnPassword =
switch(R.string.toolbar_num_row_on_password, "toolbar_num_row_on_password", true)
val popupOnKeyPress = switch(R.string.popup_on_key_press, "popup_on_key_press", true)
@ -239,6 +245,26 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
keyboardBottomPaddingLandscape = secondary
}
val floatingKeyboardWidthPercent: ManagedPreference.PInt
val floatingKeyboardWidthPercentLandscape: ManagedPreference.PInt
init {
val (primary, secondary) = twinInt(
R.string.floating_keyboard_width,
R.string.portrait,
"floating_keyboard_width_percent",
80,
R.string.landscape,
"floating_keyboard_width_percent_landscape",
80,
55,
100,
"%"
) { floatingKeyboardEnabled.getValue() }
floatingKeyboardWidthPercent = primary
floatingKeyboardWidthPercentLandscape = secondary
}
val horizontalCandidateStyle = enumList(
R.string.horizontal_candidate_style,
"horizontal_candidate_style",
@ -436,4 +462,4 @@ class AppPrefs(private val sharedPreferences: SharedPreferences) {
fun getInstance() = instance!!
}
}
}

View File

@ -11,6 +11,7 @@ import android.content.pm.ActivityInfo
import android.content.res.ColorStateList
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.Icon
import android.os.Build
import android.os.Bundle
@ -598,14 +599,28 @@ class FcitxInputMethodService : LifecycleInputMethodService() {
}
private var inputViewLocation = intArrayOf(0, 0)
private val floatingKeyboardTouchableRect = Rect()
override fun onComputeInsets(outInsets: Insets) {
if (inputDeviceMgr.isVirtualKeyboard) {
inputView?.keyboardView?.getLocationInWindow(inputViewLocation)
outInsets.apply {
contentTopInsets = inputViewLocation[1]
visibleTopInsets = inputViewLocation[1]
touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
if (
inputView?.getFloatingKeyboardTouchableRect(floatingKeyboardTouchableRect) == true
) {
val n = decorView.findViewById<View>(android.R.id.navigationBarBackground)?.height ?: 0
val h = decorView.height - n
outInsets.apply {
contentTopInsets = h
visibleTopInsets = floatingKeyboardTouchableRect.top
touchableInsets = Insets.TOUCHABLE_INSETS_REGION
touchableRegion.set(floatingKeyboardTouchableRect)
}
} else {
inputView?.keyboardView?.getLocationInWindow(inputViewLocation)
outInsets.apply {
contentTopInsets = inputViewLocation[1]
visibleTopInsets = inputViewLocation[1]
touchableInsets = Insets.TOUCHABLE_INSETS_VISIBLE
}
}
} else {
val n = decorView.findViewById<View>(android.R.id.navigationBarBackground)?.height ?: 0

View File

@ -7,7 +7,9 @@ package org.fcitx.fcitx5.android.input
import android.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.view.MotionEvent
import android.view.View
import android.view.WindowInsets
import android.view.inputmethod.EditorInfo
@ -15,6 +17,7 @@ import android.view.inputmethod.InlineSuggestionsResponse
import android.widget.ImageView
import androidx.annotation.Keep
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
import androidx.core.view.updateLayoutParams
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.CapabilityFlags
@ -63,6 +66,10 @@ import splitties.views.dsl.core.view
import splitties.views.dsl.core.withTheme
import splitties.views.dsl.core.wrapContent
import splitties.views.imageDrawable
import splitties.views.imageResource
import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
@SuppressLint("ViewConstructor")
class InputView(
@ -71,6 +78,13 @@ class InputView(
theme: Theme
) : BaseInputView(service, fcitx, theme) {
companion object {
private const val MIN_FLOATING_KEYBOARD_WIDTH_PERCENT = 55
private const val MAX_FLOATING_KEYBOARD_WIDTH_PERCENT = 100
private const val FLOATING_DRAG_HANDLE_HEIGHT_DP = 24
private const val FLOATING_RESIZE_HANDLE_SIZE_DP = 40
}
private val keyBorder by ThemeManager.prefs.keyBorder
private val customBackground = imageView {
@ -91,6 +105,12 @@ class InputView(
// bottomMargin as WindowInsets (Navigation Bar) offset
setOnClickListener(placeholderOnClickListener)
}
private val resizeHandle = imageView {
imageResource = R.drawable.ic_baseline_drag_handle_24
scaleType = ImageView.ScaleType.CENTER
alpha = 0.72f
contentDescription = context.getString(R.string.resize_floating_keyboard)
}
private val scope = DynamicScope()
private val themedContext = context.withTheme(R.style.Theme_InputViewTheme)
@ -129,23 +149,30 @@ class InputView(
}
private val keyboardPrefs = AppPrefs.getInstance().keyboard
private val internalPrefs = AppPrefs.getInstance().internal
private val focusChangeResetKeyboard by keyboardPrefs.focusChangeResetKeyboard
private var floatingKeyboardEnabled by keyboardPrefs.floatingKeyboardEnabled
private val keyboardHeightPercent = keyboardPrefs.keyboardHeightPercent
private val keyboardHeightPercentLandscape = keyboardPrefs.keyboardHeightPercentLandscape
private val keyboardSidePadding = keyboardPrefs.keyboardSidePadding
private val keyboardSidePaddingLandscape = keyboardPrefs.keyboardSidePaddingLandscape
private val keyboardBottomPadding = keyboardPrefs.keyboardBottomPadding
private val keyboardBottomPaddingLandscape = keyboardPrefs.keyboardBottomPaddingLandscape
private val floatingKeyboardWidthPercent = keyboardPrefs.floatingKeyboardWidthPercent
private val floatingKeyboardWidthPercentLandscape = keyboardPrefs.floatingKeyboardWidthPercentLandscape
private val keyboardSizePrefs = listOf(
keyboardPrefs.floatingKeyboardEnabled,
keyboardHeightPercent,
keyboardHeightPercentLandscape,
keyboardSidePadding,
keyboardSidePaddingLandscape,
keyboardBottomPadding,
keyboardBottomPaddingLandscape,
floatingKeyboardWidthPercent,
floatingKeyboardWidthPercentLandscape,
)
private val keyboardHeightPx: Int
@ -154,7 +181,11 @@ class InputView(
Configuration.ORIENTATION_LANDSCAPE -> keyboardHeightPercentLandscape
else -> keyboardHeightPercent
}.getValue()
return resources.displayMetrics.heightPixels * percent / 100
return (if (floatingKeyboardEnabled) {
height.takeIf { it > 0 } ?: resources.displayMetrics.heightPixels
} else {
resources.displayMetrics.heightPixels
}) * percent / 100
}
private val keyboardSidePaddingPx: Int
@ -175,15 +206,109 @@ class InputView(
return dp(value)
}
private val floatingKeyboardWidthPx: Int
get() {
val parentWidth = width.takeIf { it > 0 } ?: resources.displayMetrics.widthPixels
return parentWidth * activeFloatingKeyboardWidthPercent.getValue() / 100
}
private val floatingDragHandleHeightPx: Int
get() = dp(FLOATING_DRAG_HANDLE_HEIGHT_DP)
private val activeFloatingKeyboardWidthPercent
get() = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> floatingKeyboardWidthPercentLandscape
else -> floatingKeyboardWidthPercent
}
private val activeFloatingKeyboardXRatio
get() = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> internalPrefs.floatingKeyboardXRatioLandscape
else -> internalPrefs.floatingKeyboardXRatio
}
private val activeFloatingKeyboardYRatio
get() = when (resources.configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> internalPrefs.floatingKeyboardYRatioLandscape
else -> internalPrefs.floatingKeyboardYRatio
}
private var navBarBottomInset = 0
private var floatingKeyboardX = 0f
private var floatingKeyboardY = 0f
private var floatingDragStartRawX = 0f
private var floatingDragStartRawY = 0f
private var floatingDragStartKeyboardX = 0f
private var floatingDragStartKeyboardY = 0f
private var floatingResizeStartWidth = 0
private val inputViewLocation = intArrayOf(0, 0)
@Keep
private val onKeyboardSizeChangeListener = ManagedPreferenceProvider.OnChangeListener { key ->
if (keyboardSizePrefs.any { it.key == key }) {
if (keyboardPrefs.floatingKeyboardEnabled.key == key) {
applyKeyboardMode()
} else if (keyboardSizePrefs.any { it.key == key }) {
updateKeyboardSize()
}
}
val keyboardView: View
private val floatingKeyboardDragListener = OnTouchListener { _, event ->
if (!floatingKeyboardEnabled) return@OnTouchListener false
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
floatingDragStartRawX = event.rawX
floatingDragStartRawY = event.rawY
floatingDragStartKeyboardX = floatingKeyboardX
floatingDragStartKeyboardY = floatingKeyboardY
true
}
MotionEvent.ACTION_MOVE -> {
updateFloatingKeyboardPosition(
floatingDragStartKeyboardX + event.rawX - floatingDragStartRawX,
floatingDragStartKeyboardY + event.rawY - floatingDragStartRawY,
persist = true
)
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> true
else -> false
}
}
private val floatingKeyboardResizeListener = OnTouchListener { _, event ->
if (!floatingKeyboardEnabled) return@OnTouchListener false
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
floatingDragStartRawX = event.rawX
floatingResizeStartWidth = keyboardView.width
true
}
MotionEvent.ACTION_MOVE -> {
val parentWidth = width.takeIf { it > 0 } ?: return@OnTouchListener true
val delta = if (layoutDirection == LAYOUT_DIRECTION_RTL) {
floatingDragStartRawX - event.rawX
} else {
event.rawX - floatingDragStartRawX
}
val newWidth = (floatingResizeStartWidth + delta).roundToInt()
val percent = (newWidth * 100f / parentWidth)
.roundToInt()
.coerceIn(MIN_FLOATING_KEYBOARD_WIDTH_PERCENT, MAX_FLOATING_KEYBOARD_WIDTH_PERCENT)
activeFloatingKeyboardWidthPercent.setValue(percent)
updateKeyboardSize()
true
}
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> true
else -> false
}
}
init {
// MUST call before any operation
setupScope()
@ -240,18 +365,33 @@ class InputView(
endToStartOf(rightPaddingSpace)
bottomOfParent()
})
add(resizeHandle, lParams(dp(FLOATING_RESIZE_HANDLE_SIZE_DP), dp(FLOATING_RESIZE_HANDLE_SIZE_DP)) {
endOfParent()
bottomOfParent()
})
}
bottomPaddingSpace.setOnTouchListener(floatingKeyboardDragListener)
resizeHandle.setOnTouchListener(floatingKeyboardResizeListener)
updateKeyboardSize()
add(preedit.ui.root, lParams(matchParent, wrapContent) {
above(keyboardView)
centerHorizontally()
})
add(keyboardView, lParams(matchParent, wrapContent) {
centerHorizontally()
bottomOfParent()
})
if (floatingKeyboardEnabled) {
add(keyboardView, lParams(floatingKeyboardWidthPx, wrapContent) {
startOfParent()
topOfParent()
})
} else {
add(keyboardView, lParams(matchParent, wrapContent) {
centerHorizontally()
bottomOfParent()
})
}
post { applyKeyboardMode() }
add(popup.root, lParams(matchParent, matchParent) {
centerVertically()
centerHorizontally()
@ -265,13 +405,34 @@ class InputView(
height = keyboardHeightPx
}
bottomPaddingSpace.updateLayoutParams {
height = keyboardBottomPaddingPx
height = if (floatingKeyboardEnabled) {
max(keyboardBottomPaddingPx, floatingDragHandleHeightPx)
} else {
keyboardBottomPaddingPx
}
}
val sidePadding = keyboardSidePaddingPx
if (sidePadding == 0) {
if (floatingKeyboardEnabled) {
leftPaddingSpace.visibility = GONE
rightPaddingSpace.visibility = GONE
resizeHandle.visibility = VISIBLE
windowManager.view.updateLayoutParams<LayoutParams> {
startToEnd = unset
endToStart = unset
startOfParent()
endOfParent()
}
bottomPaddingSpace.updateLayoutParams<LayoutParams> {
startToEnd = unset
endToStart = unset
startOfParent()
endOfParent()
}
} else if (sidePadding == 0) {
// hide side padding space views when unnecessary
leftPaddingSpace.visibility = GONE
rightPaddingSpace.visibility = GONE
resizeHandle.visibility = GONE
windowManager.view.updateLayoutParams<LayoutParams> {
startToEnd = unset
endToStart = unset
@ -281,6 +442,7 @@ class InputView(
} else {
leftPaddingSpace.visibility = VISIBLE
rightPaddingSpace.visibility = VISIBLE
resizeHandle.visibility = GONE
leftPaddingSpace.updateLayoutParams {
width = sidePadding
}
@ -294,17 +456,117 @@ class InputView(
endToStartOf(rightPaddingSpace)
}
}
preedit.ui.root.setPadding(sidePadding, 0, sidePadding, 0)
kawaiiBar.view.setPadding(sidePadding, 0, sidePadding, 0)
val inputSidePadding = if (floatingKeyboardEnabled) 0 else sidePadding
preedit.ui.root.setPadding(inputSidePadding, 0, inputSidePadding, 0)
kawaiiBar.view.setPadding(inputSidePadding, 0, inputSidePadding, 0)
if (floatingKeyboardEnabled) {
if (keyboardView.parent != null) {
keyboardView.updateLayoutParams<LayoutParams> {
width = floatingKeyboardWidthPx
}
}
keyboardView.post {
restoreFloatingKeyboardPosition()
}
}
service.window.window?.decorView?.requestLayout()
}
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
navBarBottomInset = getNavBarBottomInset(insets)
bottomPaddingSpace.updateLayoutParams<LayoutParams> {
bottomMargin = getNavBarBottomInset(insets)
bottomMargin = navBarBottomInset
}
if (floatingKeyboardEnabled) {
keyboardView.post {
restoreFloatingKeyboardPosition()
}
}
return insets
}
private fun applyKeyboardMode() {
keyboardView.updateLayoutParams<LayoutParams> {
if (floatingKeyboardEnabled) {
width = floatingKeyboardWidthPx
height = wrapContent
startToStart = PARENT_ID
topToTop = PARENT_ID
endToEnd = unset
bottomToBottom = unset
} else {
width = matchParent
height = wrapContent
startToStart = unset
topToTop = unset
endToEnd = unset
bottomToBottom = PARENT_ID
centerHorizontally()
}
}
keyboardView.translationX = 0f
keyboardView.translationY = 0f
updateKeyboardSize()
kawaiiBar.updateFloatingKeyboardButton()
if (floatingKeyboardEnabled) {
keyboardView.post { restoreFloatingKeyboardPosition() }
}
}
private fun restoreFloatingKeyboardPosition() {
if (!floatingKeyboardEnabled || width <= 0 || keyboardView.width <= 0) return
val maxX = max(0, width - keyboardView.width).toFloat()
val maxY = max(0, height - navBarBottomInset - keyboardView.height).toFloat()
val x = maxX * activeFloatingKeyboardXRatio.getValue().coerceIn(0f, 1f)
val y = maxY * activeFloatingKeyboardYRatio.getValue().coerceIn(0f, 1f)
updateFloatingKeyboardPosition(x, y)
}
private fun updateFloatingKeyboardPosition(x: Float, y: Float, persist: Boolean = false) {
if (!floatingKeyboardEnabled) return
val maxX = max(0, width - keyboardView.width).toFloat()
val maxY = max(0, height - navBarBottomInset - keyboardView.height).toFloat()
floatingKeyboardX = min(max(x, 0f), maxX)
floatingKeyboardY = min(max(y, 0f), maxY)
keyboardView.translationX = floatingKeyboardX
keyboardView.translationY = floatingKeyboardY
if (persist) {
activeFloatingKeyboardXRatio.setValue(if (maxX > 0f) floatingKeyboardX / maxX else 0.5f)
activeFloatingKeyboardYRatio.setValue(if (maxY > 0f) floatingKeyboardY / maxY else 1f)
}
service.window.window?.decorView?.requestLayout()
}
fun getFloatingKeyboardTouchableRect(outRect: Rect): Boolean {
if (!floatingKeyboardEnabled || keyboardView.width <= 0 || keyboardView.height <= 0) {
return false
}
keyboardView.getLocationInWindow(inputViewLocation)
outRect.set(
inputViewLocation[0],
inputViewLocation[1],
inputViewLocation[0] + keyboardView.width,
inputViewLocation[1] + keyboardView.height
)
return true
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (floatingKeyboardEnabled) {
keyboardView.post {
restoreFloatingKeyboardPosition()
}
}
}
fun toggleFloatingKeyboard() {
floatingKeyboardEnabled = !floatingKeyboardEnabled
applyKeyboardMode()
}
fun isFloatingKeyboardEnabled() = floatingKeyboardEnabled
/**
* called when [InputView] is about to show, or restart
*/

View File

@ -55,6 +55,7 @@ import org.fcitx.fcitx5.android.input.candidates.horizontal.HorizontalCandidateC
import org.fcitx.fcitx5.android.input.clipboard.ClipboardWindow
import org.fcitx.fcitx5.android.input.dependency.UniqueViewComponent
import org.fcitx.fcitx5.android.input.dependency.context
import org.fcitx.fcitx5.android.input.dependency.inputView
import org.fcitx.fcitx5.android.input.dependency.inputMethodService
import org.fcitx.fcitx5.android.input.dependency.theme
import org.fcitx.fcitx5.android.input.editing.TextEditingWindow
@ -92,6 +93,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
private val theme by manager.theme()
private val service by manager.inputMethodService()
private val windowManager: InputWindowManager by manager.must()
private val inputView by manager.inputView()
private val horizontalCandidate: HorizontalCandidateComponent by manager.must()
private val commonKeyActionListener: CommonKeyActionListener by manager.must()
private val popup: PopupComponent by manager.must()
@ -313,6 +315,10 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
clipboardButton.setOnClickListener {
windowManager.attachWindow(ClipboardWindow())
}
floatingKeyboardButton.setOnClickListener {
inputView.toggleFloatingKeyboard()
updateFloatingKeyboardButton()
}
moreButton.setOnClickListener {
windowManager.attachWindow(StatusAreaWindow())
}
@ -343,6 +349,18 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
}
}
fun updateFloatingKeyboardButton() {
idleUi.buttonsUi.floatingKeyboardButton.apply {
if (inputView.isFloatingKeyboardEnabled()) {
setIcon(R.drawable.ic_baseline_keyboard_24)
contentDescription = context.getString(R.string.dock_keyboard)
} else {
setIcon(R.drawable.ic_baseline_open_in_full_24)
contentDescription = context.getString(R.string.float_keyboard)
}
}
}
private val candidateUi by lazy {
CandidateUi(context, theme, horizontalCandidate.view).apply {
expandButton.apply {
@ -458,6 +476,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
shouldShowVoiceInput,
if (shouldShowVoiceInput) switchToVoiceInputCallback else hideKeyboardCallback
)
updateFloatingKeyboardButton()
evalIdleUiState()
}

View File

@ -44,6 +44,10 @@ class ButtonsBarUi(override val ctx: Context, private val theme: Theme) : Ui {
contentDescription = ctx.getString(R.string.clipboard)
}
val floatingKeyboardButton = toolButton(R.drawable.ic_baseline_open_in_full_24).apply {
contentDescription = ctx.getString(R.string.float_keyboard)
}
val moreButton = toolButton(R.drawable.ic_baseline_more_horiz_24).apply {
contentDescription = ctx.getString(R.string.status_area)
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M5,5h6v2H8.41l3.3,3.29 -1.42,1.42L7,8.41V11H5V5zM13,5h6v6h-2V8.41l-3.29,3.3 -1.42,-1.42 3.3,-3.29H13V5zM10.29,12.29l1.42,1.42L8.41,17H11v2H5v-6h2v2.59l3.29,-3.3zM13.71,12.29l3.29,3.3V13h2v6h-6v-2h2.59l-3.3,-3.29 1.42,-1.42z" />
</vector>

View File

@ -62,6 +62,8 @@
<string name="delete">Delete</string>
<string name="clipboard">Clipboard</string>
<string name="keyboard_height">Keyboard height</string>
<string name="floating_keyboard">Floating keyboard</string>
<string name="floating_keyboard_width">Floating keyboard width</string>
<string name="virtual_keyboard">Virtual Keyboard</string>
<string name="clipboard_suggestion_timeout">Clipboard suggestion timeout</string>
<string name="version">Version</string>
@ -292,6 +294,9 @@
<string name="hide_candidates_list">Hide candidates list</string>
<string name="switch_to_voice_input">Switch to voice input</string>
<string name="hide_keyboard">Hide keyboard</string>
<string name="float_keyboard">Float keyboard</string>
<string name="dock_keyboard">Dock keyboard</string>
<string name="resize_floating_keyboard">Resize floating keyboard</string>
<string name="private_mode">Private mode (tap to toggle toolbar)</string>
<string name="hide_toolbar">Hide toolbar</string>
<string name="expand_toolbar">Expand toolbar</string>