Implement swipe gestures for number row collapse/expansion

This commit is contained in:
iovxw 2026-02-03 23:15:05 +08:00
parent 1455b6090b
commit e70f74e568
No known key found for this signature in database
GPG Key ID: C84626C2D3D79995
2 changed files with 120 additions and 4 deletions

View File

@ -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<KawaiiBarComponent, FrameLayout>(),
InputBroadcastReceiver {
@ -108,6 +110,10 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
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<KawaiiBarComponent, FrameLayout>(
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<KawaiiBarComponent, FrameLayout>(
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<String, InputMethodSubtype>? = null
private val switchToVoiceInputCallback = View.OnClickListener {
@ -226,7 +260,8 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
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<KawaiiBarComponent, FrameLayout>(
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<KawaiiBarComponent, FrameLayout>(
expandButton.apply {
swipeEnabled = true
swipeThresholdY = dp(HEIGHT.toFloat())
onGestureListener = swipeDownHideKeyboardCallback
onGestureListener = swipeDownExpandCallback
}
}
}
@ -369,6 +416,7 @@ class KawaiiBarComponent : UniqueViewComponent<KawaiiBarComponent, FrameLayout>(
}
isCapabilityFlagsPassword = toolbarNumRowOnPassword && capFlags.has(CapabilityFlag.Password)
isInlineSuggestionPresent = false
numberRowState = NumberRowState.Auto
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
idleUi.inlineSuggestionsBar.clear()
}

View File

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