diff --git a/app/src/main/java/com/kingzcheung/xime/keyboard/KeyboardDimensions.kt b/app/src/main/java/com/kingzcheung/xime/keyboard/KeyboardDimensions.kt index a1eb341f..bc46684d 100644 --- a/app/src/main/java/com/kingzcheung/xime/keyboard/KeyboardDimensions.kt +++ b/app/src/main/java/com/kingzcheung/xime/keyboard/KeyboardDimensions.kt @@ -10,7 +10,7 @@ object KeyboardDimensions { // 按键尺寸 val KeyHeight = 44.dp - val BubbleHeightDown = 36.dp // 下滑气泡主体高度 + val BubbleHeightDown = 40.dp // 下滑气泡主体高度 val BubbleCornerRadius = 8.dp // 气泡底部pointer(覆盖按键的部分�? diff --git a/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt b/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt index 85906f42..ea926865 100644 --- a/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt +++ b/app/src/main/java/com/kingzcheung/xime/service/XimeInputMethodService.kt @@ -884,14 +884,25 @@ class XimeInputMethodService : InputMethodService(), LifecycleOwner, SavedStateR * 实测关闭面板后 insets 残留数秒(仅在下一次输入会话/窗口事件时才恢复)。 * 这里通过容器做一次真实尺寸变化(+1dp)再立即还原(同帧内完成,视觉不可见), * 强制系统按 keyboardContentTopPx 重算并下发,消除残留。 + * + * [insetsRecomputePending] 防重入:+1dp 到 resetHeight 还原之间为"飞行中", + * 期间再次调用会读到 +1dp 后的当前高度再叠加 +1——某些 ROM 上 onApplyWindowInsets + * 回调时机不同导致 reset 滞后,+1dp 反复叠加/循环 → 键盘底部被持续垫高(2.7.0 用户反馈)。 */ + private var insetsRecomputePending = false + internal fun forceInsetsRecompute() { if (!::keyboardContainer.isInitialized) return + if (insetsRecomputePending) return val density = resources.displayMetrics.density val h = keyboardContainer.height if (h > 0) { + insetsRecomputePending = true keyboardContainer.updateHeight((h / density).toInt() + 1) - keyboardContainer.post { keyboardContainer.resetHeight() } + keyboardContainer.post { + keyboardContainer.resetHeight() + insetsRecomputePending = false + } } else { keyboardContainer.post { keyboardContainer.requestLayout() } } @@ -1252,7 +1263,10 @@ class XimeInputMethodService : InputMethodService(), LifecycleOwner, SavedStateR .align(androidx.compose.ui.Alignment.BottomCenter) .then(if (state.isFloatingMode) Modifier else Modifier.offset(y = (-activeBottomDp).dp)) .onGloballyPositioned { - if (!state.isFloatingMode && !state.isCompact) { + // insetsRecomputePending 期间的高度摆动是 +1dp hack + // 自身引起的,忽略之:否则 y 在 h/h+1 间交替变化, + // 每次都 != keyboardContentTopPx → 无限循环触发重算。 + if (!state.isFloatingMode && !state.isCompact && !insetsRecomputePending) { val y = it.positionInWindow().y.toInt() if (y != keyboardContentTopPx) { keyboardContentTopPx = y diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/CommonSymbolKeyboardLayout.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/CommonSymbolKeyboardLayout.kt index c8b7c488..09f31514 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/CommonSymbolKeyboardLayout.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/CommonSymbolKeyboardLayout.kt @@ -87,27 +87,28 @@ fun CommonSymbolKeyboardLayout( val isLandscape = !isFloatingMode && configuration.screenWidthDp > configuration.screenHeightDp val isDarkTheme = keyTextColor == Color(0xFFE8EAED) val suppressCursorMove = LocalSuppressCursorMove.current - var swipeState by remember { mutableStateOf(SwipeState()) } + val swipeBubble = rememberSwipeBubbleController() var keyboardBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } - var lastKeyBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } fun processSwipeState(state: SwipeState, bounds: Rect) { - swipeState = state - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top, + swipeBubble.update( + state, + Rect( + left = bounds.left - keyboardBounds.left, + top = bounds.top - keyboardBounds.top, + right = bounds.right - keyboardBounds.left, + bottom = bounds.bottom - keyboardBounds.top, + ) ) } val bubbleData = rememberSwipeBubbleDrawData( - swipeState = swipeState, - keyBounds = lastKeyBounds, + swipeState = swipeBubble.state, + keyBounds = swipeBubble.keyBounds, keyBackgroundColor = bubbleBackgroundColor, keyTextColor = keyTextColor, accentColor = specialKeyTextColor, - keyWidth = if (swipeState.isSwiping || swipeState.isPressed) lastKeyBounds.width else 0f, + keyWidth = if (swipeBubble.state.isSwiping || swipeBubble.state.isPressed) swipeBubble.keyBounds.width else 0f, keyboardWidth = keyboardBounds.width, ) diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/KeyboardLayout.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/KeyboardLayout.kt index 185a92f9..e2acf0ce 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/KeyboardLayout.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/KeyboardLayout.kt @@ -213,9 +213,8 @@ fun KeyboardLayout( SubcharHelper.init(context) } - var swipeState by remember { mutableStateOf(SwipeState()) } + val swipeBubble = rememberSwipeBubbleController() var keyboardBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } - var lastKeyBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } // 监听手势配置版本号,部署后强制刷新键帽显示 val cfgVer by KeysConfigHelper.configVersion.collectAsState() @@ -226,23 +225,24 @@ fun KeyboardLayout( } else { state } - swipeState = newState - - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top + swipeBubble.update( + newState, + Rect( + left = bounds.left - keyboardBounds.left, + top = bounds.top - keyboardBounds.top, + right = bounds.right - keyboardBounds.left, + bottom = bounds.bottom - keyboardBounds.top + ) ) } val bubbleData = rememberSwipeBubbleDrawData( - swipeState = swipeState, - keyBounds = lastKeyBounds, + swipeState = swipeBubble.state, + keyBounds = swipeBubble.keyBounds, keyBackgroundColor = bubbleBgColor, keyTextColor = keyTextColor, accentColor = specialKeyTextColor, - keyWidth = if (swipeState.isSwiping || swipeState.isPressed) lastKeyBounds.width else 0f, + keyWidth = if (swipeBubble.state.isSwiping || swipeBubble.state.isPressed) swipeBubble.keyBounds.width else 0f, keyboardWidth = keyboardBounds.width ) diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/NumberKeyboardLayout.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/NumberKeyboardLayout.kt index d615a9ad..9729e1bd 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/NumberKeyboardLayout.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/NumberKeyboardLayout.kt @@ -79,22 +79,36 @@ fun NumberKeyboardLayout( "\\", "|", ";", ":", "'", "\"", "<", ">" ) - var swipeState by remember { mutableStateOf(SwipeState()) } + val swipeBubble = rememberSwipeBubbleController() var keyboardBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } - var lastKeyBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } val isDarkTheme = keyTextColor == Color(0xFFE8EAED) val bubbleData = rememberSwipeBubbleDrawData( - swipeState = swipeState, - keyBounds = lastKeyBounds, + swipeState = swipeBubble.state, + keyBounds = swipeBubble.keyBounds, keyBackgroundColor = bubbleBackgroundColor, keyTextColor = keyTextColor, accentColor = specialKeyTextColor, - keyWidth = if (swipeState.isSwiping || swipeState.isPressed) lastKeyBounds.width else 0f, + keyWidth = if (swipeBubble.state.isSwiping || swipeBubble.state.isPressed) swipeBubble.keyBounds.width else 0f, keyboardWidth = keyboardBounds.width ) + fun processSwipeState(state: SwipeState, bounds: Rect) { + val newState = if (state.isSwipeDown && state.swipeText != null) { + state.copy(charInfos = SubcharHelper.parseSwipeDownText(state.swipeText)) + } else state + swipeBubble.update( + newState, + Rect( + left = bounds.left - keyboardBounds.left, + top = bounds.top - keyboardBounds.top, + right = bounds.right - keyboardBounds.left, + bottom = bounds.bottom - keyboardBounds.top + ) + ) + } + CompositionLocalProvider(LocalKeyCornerRadius provides keyCornerRadius) { Box( modifier = modifier @@ -171,18 +185,7 @@ fun NumberKeyboardLayout( onKeyPressDown = onKeyPressDown, compactMode = true, specialKeyTextColor = specialKeyTextColor, - onSwipeStateChange = { state, bounds -> - val newState = if (state.isSwipeDown && state.swipeText != null) { - state.copy(charInfos = SubcharHelper.parseSwipeDownText(state.swipeText)) - } else state - swipeState = newState - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top - ) - } + onSwipeStateChange = ::processSwipeState ) } } @@ -208,18 +211,8 @@ fun NumberKeyboardLayout( shadowShapeRadius = shadowShapeRadius, onKeyPressDown = onKeyPressDown, specialKeyTextColor = specialKeyTextColor, - onSwipeStateChange = { state, bounds -> - val newState = if (state.isSwipeDown && state.swipeText != null) { - state.copy(charInfos = SubcharHelper.parseSwipeDownText(state.swipeText)) - } else state - swipeState = newState - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top - ) - }) + onSwipeStateChange = ::processSwipeState + ) } } } diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/StrokeKeyboardLayout.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/StrokeKeyboardLayout.kt index e20848de..1fbaa180 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/StrokeKeyboardLayout.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/StrokeKeyboardLayout.kt @@ -128,19 +128,18 @@ private fun StrokeKeyboardSwipeOverlay( val configuration = LocalConfiguration.current val isLandscape = !isFloatingMode && configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE - var swipeState by remember { mutableStateOf(SwipeState()) } + val swipeBubble = rememberSwipeBubbleController() var keyboardBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } - var lastKeyBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } val isDarkTheme = keyTextColor == Color(0xFFE8EAED) val bubbleData = rememberSwipeBubbleDrawData( - swipeState = swipeState, - keyBounds = lastKeyBounds, + swipeState = swipeBubble.state, + keyBounds = swipeBubble.keyBounds, keyBackgroundColor = bubbleBackgroundColor, keyTextColor = keyTextColor, accentColor = specialKeyTextColor, - keyWidth = if (swipeState.isSwiping || swipeState.isPressed) lastKeyBounds.width else 0f, + keyWidth = if (swipeBubble.state.isSwiping || swipeBubble.state.isPressed) swipeBubble.keyBounds.width else 0f, keyboardWidth = keyboardBounds.width ) @@ -150,12 +149,14 @@ private fun StrokeKeyboardSwipeOverlay( } else { state } - swipeState = newState - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top + swipeBubble.update( + newState, + Rect( + left = bounds.left - keyboardBounds.left, + top = bounds.top - keyboardBounds.top, + right = bounds.right - keyboardBounds.left, + bottom = bounds.bottom - keyboardBounds.top + ) ) } diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/SwipeBubble.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/SwipeBubble.kt index 7c627b64..9a6a9025 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/SwipeBubble.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/SwipeBubble.kt @@ -5,7 +5,11 @@ import android.graphics.Paint import android.graphics.Path import android.graphics.Typeface import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.DrawScope @@ -18,9 +22,12 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kingzcheung.xime.keyboard.KeyboardDimensions import com.kingzcheung.xime.settings.SettingsPreferences +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch private val BubbleBodyHeight = KeyboardDimensions.BubbleHeightDown -private val BubblePointerHeight = KeyboardDimensions.BubblePointerHeight private val BubbleCornerRadius = KeyboardDimensions.BubbleCornerRadius private val BubbleScreenMargin = 4.dp @@ -61,6 +68,52 @@ data class BubbleDrawData( val accentArgb: Int = 0xFF8F73E2.toInt(), ) +/** 按压气泡抬起后的滞留时长:随抬起瞬间消失看不清键位提示(主流输入法约 50~80ms),取中值。 */ +private const val PRESS_BUBBLE_RELEASE_DELAY_MS = 60L + +/** + * 各布局共享的气泡状态持有器。 + * + * 封装"按压气泡抬起后短暂滞留"逻辑:抬起瞬间不清空状态,而是保留气泡内容与位置 + * [PRESS_BUBBLE_RELEASE_DELAY_MS] 毫秒再消失;期间任意新手势(新键按压/滑动/长按) + * 会立即取消滞留并切换为新气泡,快速连打无延迟感。 + * 仅对按压气泡滞留——滑动选择与长按选择的气泡在松手时语义上已结束(候选已提交),立即消失。 + */ +class SwipeBubbleController(private val scope: CoroutineScope) { + var state by mutableStateOf(SwipeState()) + private set + var keyBounds by mutableStateOf(Rect(0f, 0f, 0f, 0f)) + private set + private var releaseJob: Job? = null + + fun update(newState: SwipeState, bounds: Rect) { + releaseJob?.cancel() + releaseJob = null + val prev = state + val gestureActive = newState.isSwiping || newState.isPressed || newState.isLongPress + if (!gestureActive && + prev.isPressed && prev.pressedText != null && + !prev.isSwiping && !prev.isLongPress + ) { + state = prev + keyBounds = bounds + releaseJob = scope.launch { + delay(PRESS_BUBBLE_RELEASE_DELAY_MS) + state = SwipeState() + } + return + } + state = newState + keyBounds = bounds + } +} + +@Composable +fun rememberSwipeBubbleController(): SwipeBubbleController { + val scope = rememberCoroutineScope() + return remember { SwipeBubbleController(scope) } +} + @Composable fun rememberSwipeBubbleDrawData( swipeState: SwipeState, @@ -83,12 +136,14 @@ fun rememberSwipeBubbleDrawData( val density = LocalDensity.current val bodyHeightPx = with(density) { BubbleBodyHeight.toPx() } - val pointerHeightPx = with(density) { (BubblePointerHeight + 5.dp).toPx() } + // 尖端完整覆盖按下的按键(与按键同高),宽体锚定按键顶部悬在上方(见 boxTop)。 + // 全部基于真实按键 bounds 计算,不再用 KeyHeight 估算值—— + // 键盘高度被调大时估算失准,宽体会下沉进按键被手指挡住。 + val pointerHeightPx = keyBounds.height val cornerRadiusPx = with(density) { BubbleCornerRadius.toPx() } val screenMarginPx = with(density) { BubbleScreenMargin.toPx() } val keyWidthPx = keyWidth val minBodyWidthPx = keyWidthPx * 1.8f - val totalHeightPx = bodyHeightPx + pointerHeightPx val shadowRadiusPx = with(density) { 4.dp.toPx() } val accentArgb = accentColor.toArgb() @@ -102,7 +157,7 @@ fun rememberSwipeBubbleDrawData( val textPaint = remember { Paint().apply { - textSize = with(density) { 14.sp.toPx() } + textSize = with(density) { 16.sp.toPx() } isAntiAlias = true } } @@ -117,9 +172,9 @@ fun rememberSwipeBubbleDrawData( maxOf(textPaint.measureText(displayText!!) + with(density) { 20.dp.toPx() }, minBodyWidthPx) } - val textSizePx = with(density) { 14.sp.toPx() } - val selectedFontSizePx = with(density) { 18.sp.toPx() } - val normalFontSizePx = with(density) { 14.sp.toPx() } + val textSizePx = with(density) { 16.sp.toPx() } + val selectedFontSizePx = with(density) { 20.sp.toPx() } + val normalFontSizePx = with(density) { 16.sp.toPx() } val selectedBgRadiusPx = with(density) { 6.dp.toPx() } val pointerCenterX = keyBounds.left + keyBounds.width / 2f @@ -131,7 +186,9 @@ fun rememberSwipeBubbleDrawData( val pointerLeft = pointerCenterX - keyWidthPx / 2f val pointerRight = pointerLeft + keyWidthPx val boxLeft = minOf(bodyLeft, pointerLeft) - val boxTop = keyBounds.top + keyBounds.height - totalHeightPx + // 宽体(显示文字的主体)底部对齐按键顶部,悬在按键正上方不被手指遮挡; + // 尖端从宽体底部向下延伸 pointerHeightPx(≈按键上半部)指示归属键。 + val boxTop = keyBounds.top - bodyHeightPx val boxRight = maxOf(bodyRight, pointerRight) val rightRoom = bodyRight - pointerRight @@ -295,6 +352,7 @@ fun DrawScope.drawSwipeBubble(data: BubbleDrawData) { bubbleLabelPaint.color = if (index == data.selectedLongPressIndex) accentColor else data.textColor bubbleLabelPaint.textSize = fontSize bubbleLabelPaint.textAlign = Paint.Align.CENTER + bubbleLabelPaint.isFakeBoldText = true val textY = data.bodyHeightPx / 2f - (bubbleLabelPaint.fontMetrics.ascent + bubbleLabelPaint.fontMetrics.descent) / 2f canvas.drawText(item, itemLeft + cellWidth / 2f, textY, bubbleLabelPaint) } @@ -307,6 +365,7 @@ fun DrawScope.drawSwipeBubble(data: BubbleDrawData) { bubbleTextPaint.textSize = data.textSizePx bubbleTextPaint.textAlign = Paint.Align.CENTER bubbleTextPaint.typeface = data.chaiTypeface + bubbleTextPaint.isFakeBoldText = true val textCenterX = data.pathBodyLeft + data.pathBodyWidth / 2f val textY = data.bodyHeightPx / 2f - (bubbleTextPaint.fontMetrics.ascent + bubbleTextPaint.fontMetrics.descent) / 2f canvas.drawText(data.displayText, textCenterX, textY, bubbleTextPaint) diff --git a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/T9KeyboardLayout.kt b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/T9KeyboardLayout.kt index 7fcf5db0..ef79bcb2 100644 --- a/app/src/main/java/com/kingzcheung/xime/ui/keyboard/T9KeyboardLayout.kt +++ b/app/src/main/java/com/kingzcheung/xime/ui/keyboard/T9KeyboardLayout.kt @@ -184,9 +184,8 @@ private fun T9KeyboardSwipeOverlay( specialKeyTextColor: Color = Color.White, candidateState: State = remember { mutableStateOf(CandidateState()) }, ) { - var swipeState by remember { mutableStateOf(SwipeState()) } + val swipeBubble = rememberSwipeBubbleController() var keyboardBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } - var lastKeyBounds by remember { mutableStateOf(Rect(0f, 0f, 0f, 0f)) } fun processSwipeState(state: SwipeState, bounds: Rect) { val newState = if (state.isSwipeDown && state.swipeText != null) { @@ -194,24 +193,26 @@ private fun T9KeyboardSwipeOverlay( } else { state } - swipeState = newState - lastKeyBounds = Rect( - left = bounds.left - keyboardBounds.left, - top = bounds.top - keyboardBounds.top, - right = bounds.right - keyboardBounds.left, - bottom = bounds.bottom - keyboardBounds.top + swipeBubble.update( + newState, + Rect( + left = bounds.left - keyboardBounds.left, + top = bounds.top - keyboardBounds.top, + right = bounds.right - keyboardBounds.left, + bottom = bounds.bottom - keyboardBounds.top + ) ) } val isDarkTheme = keyTextColor == Color(0xFFE8EAED) val bubbleData = rememberSwipeBubbleDrawData( - swipeState = swipeState, - keyBounds = lastKeyBounds, + swipeState = swipeBubble.state, + keyBounds = swipeBubble.keyBounds, keyBackgroundColor = bubbleBackgroundColor, keyTextColor = keyTextColor, accentColor = specialKeyTextColor, - keyWidth = if (swipeState.isSwiping || swipeState.isPressed) lastKeyBounds.width else 0f, + keyWidth = if (swipeBubble.state.isSwiping || swipeBubble.state.isPressed) swipeBubble.keyBounds.width else 0f, keyboardWidth = keyboardBounds.width )