From e70f74e5682e753072602d1dcc24ab789c202023 Mon Sep 17 00:00:00 2001 From: iovxw Date: Tue, 3 Feb 2026 23:15:05 +0800 Subject: [PATCH] Implement swipe gestures for number row collapse/expansion --- .../android/input/bar/KawaiiBarComponent.kt | 56 +++++++++++++-- .../android/input/bar/ui/idle/NumberRow.kt | 68 +++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt index bfd212b6..6e79571a 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/KawaiiBarComponent.kt @@ -78,6 +78,8 @@ import java.util.concurrent.Executor import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.min +import kotlin.math.abs +import timber.log.Timber class KawaiiBarComponent : UniqueViewComponent(), InputBroadcastReceiver { @@ -108,6 +110,10 @@ class KawaiiBarComponent : UniqueViewComponent( private var isKeyboardLayoutNumber: Boolean = false private var isToolbarManuallyToggled: Boolean = false + private enum class NumberRowState { Auto, ForceShow, ForceHide } + + private var numberRowState = NumberRowState.Auto + @Keep private val onClipboardUpdateListener = ClipboardManager.OnClipboardUpdateListener { @@ -165,9 +171,10 @@ class KawaiiBarComponent : UniqueViewComponent( private fun evalIdleUiState(fromUser: Boolean = false) { val newState = when { + numberRowState == NumberRowState.ForceShow -> IdleUi.State.NumberRow isClipboardFresh -> IdleUi.State.Clipboard isInlineSuggestionPresent -> IdleUi.State.InlineSuggestion - isCapabilityFlagsPassword && !isKeyboardLayoutNumber -> IdleUi.State.NumberRow + isCapabilityFlagsPassword && !isKeyboardLayoutNumber && numberRowState != NumberRowState.ForceHide -> IdleUi.State.NumberRow /** * state matrix: * expandToolbarByDefault @@ -186,13 +193,40 @@ class KawaiiBarComponent : UniqueViewComponent( service.requestHideSelf(0) } - private val swipeDownHideKeyboardCallback = CustomGestureView.OnGestureListener { _, e -> + private val swipeDownExpandCallback = CustomGestureView.OnGestureListener { _, e -> if (e.type == CustomGestureView.GestureType.Up && e.totalY > 0) { service.requestHideSelf(0) true } else false } + // Combined gesture: determine primary direction by comparing totalX and totalY. + // - If horizontal is dominant and left, show number row (when allowed). + // - If vertical is dominant and down, hide keyboard. + private val swipeHideKeyboardCallback = CustomGestureView.OnGestureListener { _, e -> + if (e.type != CustomGestureView.GestureType.Up) return@OnGestureListener false + + val absX = abs(e.totalX) + val absY = abs(e.totalY) + + when { + // Horizontal && left (show number row) + absX > absY && e.totalX < 0 -> { + if (isCapabilityFlagsPassword && !isKeyboardLayoutNumber) { + numberRowState = NumberRowState.ForceShow + evalIdleUiState(fromUser = true) + true + } else false + } + // Vertical && down (hide keyboard) + absY > absX && e.totalY > 0 -> { + service.requestHideSelf(0) + true + } + else -> false + } + } + private var voiceInputSubtype: Pair? = null private val switchToVoiceInputCallback = View.OnClickListener { @@ -226,7 +260,8 @@ class KawaiiBarComponent : UniqueViewComponent( setOnClickListener(hideKeyboardCallback) swipeEnabled = true swipeThresholdY = dp(HEIGHT.toFloat()) - onGestureListener = swipeDownHideKeyboardCallback + swipeThresholdX = swipeThresholdY + onGestureListener = swipeHideKeyboardCallback } buttonsUi.apply { undoButton.setOnClickListener { @@ -262,6 +297,18 @@ class KawaiiBarComponent : UniqueViewComponent( true } } + numberRow.apply { + shouldCollapse = { start, current -> + val dir = if (context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_LTR) 1 else -1 + val sx = start.x * dir + val cx = current.x * dir + cx > sx && abs(cx - sx) > dp(HEIGHT.toFloat()) + } + onCollapseListener = { + numberRowState = NumberRowState.ForceHide + evalIdleUiState(fromUser = true) + } + } } } @@ -270,7 +317,7 @@ class KawaiiBarComponent : UniqueViewComponent( expandButton.apply { swipeEnabled = true swipeThresholdY = dp(HEIGHT.toFloat()) - onGestureListener = swipeDownHideKeyboardCallback + onGestureListener = swipeDownExpandCallback } } } @@ -369,6 +416,7 @@ class KawaiiBarComponent : UniqueViewComponent( } isCapabilityFlagsPassword = toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password) isInlineSuggestionPresent = false + numberRowState = NumberRowState.Auto if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { idleUi.inlineSuggestionsBar.clear() } diff --git a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt index 0da95932..f874b68c 100644 --- a/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt +++ b/app/src/main/java/org/fcitx/fcitx5/android/input/bar/ui/idle/NumberRow.kt @@ -8,12 +8,80 @@ import android.annotation.SuppressLint import android.content.Context import org.fcitx.fcitx5.android.core.KeySym import org.fcitx.fcitx5.android.data.theme.Theme +import android.graphics.PointF +import android.view.MotionEvent +import android.view.ViewGroup +import android.view.ViewGroup.LayoutParams import org.fcitx.fcitx5.android.input.keyboard.BaseKeyboard import org.fcitx.fcitx5.android.input.keyboard.KeyAction +import org.fcitx.fcitx5.android.input.keyboard.KeyActionListener import org.fcitx.fcitx5.android.input.keyboard.KeyDef +import org.fcitx.fcitx5.android.input.keyboard.CustomGestureView +import org.fcitx.fcitx5.android.input.popup.PopupActionListener +import kotlin.math.abs +import timber.log.Timber @SuppressLint("ViewConstructor") class NumberRow(ctx: Context, theme: Theme) : BaseKeyboard(ctx, theme, Layout) { + + private var gestureStartEvent: MotionEvent? = null + private var collapseGestureTriggerd: Boolean = false + + var onCollapseListener: (() -> Unit)? = null + + // return true if the swipe distance is enough to trigger collapse + var shouldCollapse: (start: PointF, current: PointF) -> Boolean = { _, _ -> false } + + private fun checkGesture(ev: MotionEvent): Boolean { + val startEvent = gestureStartEvent ?: return false + val firstPointerId = startEvent.getPointerId(startEvent.actionIndex) + if (ev.getPointerId(ev.actionIndex) == firstPointerId) { + val start = PointF(startEvent.x, startEvent.y) + val current = PointF(ev.getX(ev.actionIndex), ev.getY(ev.actionIndex)) + if (shouldCollapse(start, current)) { + Timber.d("NumberRow: intercepted gesture from child keyboard to handle swipe") + resetState() + collapseGestureTriggerd = true + return true + } + } + return false + } + + private fun resetState() { + gestureStartEvent?.recycle() + gestureStartEvent = null + collapseGestureTriggerd = false + } + + override fun onInterceptTouchEvent(ev: MotionEvent): Boolean { + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> gestureStartEvent = MotionEvent.obtain(ev) + MotionEvent.ACTION_MOVE -> { + if (checkGesture(ev)) return true + } + MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> resetState() + } + return super.onInterceptTouchEvent(ev) + } + + override fun onTouchEvent(ev: MotionEvent): Boolean { + var handled = false + when (ev.actionMasked) { + MotionEvent.ACTION_DOWN -> gestureStartEvent = MotionEvent.obtain(ev) + MotionEvent.ACTION_MOVE -> checkGesture(ev) + MotionEvent.ACTION_UP -> { + if (collapseGestureTriggerd) { + resetState() + onCollapseListener?.invoke() + handled = true + } + } + MotionEvent.ACTION_CANCEL -> resetState() + } + return super.onTouchEvent(ev) || handled + } + companion object { val Layout = listOf( listOf("1", "2", "3", "4", "5", "6", "7", "8", "9", "0").map { digit ->