mirror of
https://github.com/osfans/trime.git
synced 2026-11-08 01:16:38 +08:00
refactor: split key view and simplify keyboard layout and gesture architecture
- introduce KeyView as a dedicated key component for rendering, event handling, and popup interaction - remove KeyboardGestureFrame and merge gesture handling into GestureFrame - remove KeyboardSizeCalculator and KeyboardSize; move size calculation into Keyboard - remove proximity_correction and vertical_correction from theme configuration - simplify KeyboardActionListener.onPress(): remove isSound parameter and vibration handling - centralize vibration feedback management in GestureFrame - update KeyboardWindow to receive keyboardActionListener via constructor
This commit is contained in:
parent
6cf035b316
commit
91622f9307
@ -17,11 +17,11 @@ height:
|
||||
4: &jpgd4 48 #主键盘
|
||||
5: &jpgd5 39.5 #符号键盘
|
||||
6: &hgap 4 #键盘横缝大小
|
||||
7: &sgap 8 #键盘竖缝大小
|
||||
7: &sgap 12 #键盘竖缝大小
|
||||
|
||||
#键盘高度
|
||||
round_corner:
|
||||
1: &round1 12 #按键圆角半径
|
||||
1: &round1 6 #按键圆角半径
|
||||
2: &round2 0 #候选窗口圆角
|
||||
3: &round3 12 #回车键圆角
|
||||
|
||||
@ -55,14 +55,12 @@ style:
|
||||
popup_bottom_margin: 68 #按键提示底边距
|
||||
popup_key_height: 48 #按键提示内键高度
|
||||
popup_text_size: 23 #按键提示字号
|
||||
proximity_correction: true #按键纠错
|
||||
reset_ascii_mode: false #显示键盘时重置为中文状态
|
||||
round_corner: *round1 #按键圆角半径
|
||||
shadow_radius: 0.0 #按键阴影半径
|
||||
symbol_font: symbol.ttf #符号字体
|
||||
symbol_text_size: 10 #符号字号
|
||||
text_font: latin.ttf #编码字体
|
||||
vertical_correction: -5
|
||||
vertical_gap: *sgap #键盘行距
|
||||
enter_label_mode: 0 #是否使用App提供的ActionLabel内容作为Enter键的文本(由于多数App没有适配ActionLable,实际影响不大)。0不使用,1只使用actionlabel,2优先使用,3当其他方式没有获得label时才读取actionlabel
|
||||
enter_labels: # 定义Enter键的文本
|
||||
|
||||
@ -43,14 +43,12 @@ style:
|
||||
preview_height: 60 #按鍵提示高度
|
||||
preview_offset: -12 #按鍵提示縱向偏移
|
||||
preview_text_size: 40 #按鍵提示字號
|
||||
proximity_correction: true #按鍵糾錯
|
||||
reset_ascii_mode: false #顯示鍵盤時重置爲中文狀態
|
||||
round_corner: 8 #按鍵圓角半徑
|
||||
shadow_radius: 0.0 #按鍵陰影半徑
|
||||
symbol_font: symbol.ttf #符號字型
|
||||
symbol_text_size: 10 #符號字號
|
||||
text_font: latin.ttf #編碼字型
|
||||
vertical_correction: -10
|
||||
vertical_gap: 1 #鍵盤行距
|
||||
#background_folder: #背景图保存在background目录下的哪个子目录
|
||||
enter_label_mode: 0 #是否使用App提供的ActionLabel内容作为Enter键的文本(由于多数App没有适配ActionLable,实际影响不大)。0不使用,1只使用actionlabel,2优先使用,3当其他方式没有获得label时才读取actionlabel
|
||||
|
||||
@ -45,8 +45,8 @@ class GeneralStyleMapper(
|
||||
keySymbolOffsetY = getFloat("key_symbol_offset_y"),
|
||||
keyHintOffsetX = getFloat("key_hint_offset_x"),
|
||||
keyHintOffsetY = getFloat("key_hint_offset_y"),
|
||||
keyPressOffsetX = getInt("key_press_offset_x"),
|
||||
keyPressOffsetY = getInt("key_press_offset_y"),
|
||||
keyPressOffsetX = getFloat("key_press_offset_x"),
|
||||
keyPressOffsetY = getFloat("key_press_offset_y"),
|
||||
keyWidth = getFloat("key_width"),
|
||||
labelTextSize = getFloat("label_text_size"),
|
||||
labelFont = getStringList("label_font"),
|
||||
@ -59,14 +59,12 @@ class GeneralStyleMapper(
|
||||
popupKeyHeight = getInt("popup_key_height"),
|
||||
popupFont = getStringList("popup_font"),
|
||||
popupTextSize = getFloat("popup_text_size"),
|
||||
proximityCorrection = getBoolean("proximity_correction"),
|
||||
resetASCIIMode = getBoolean("reset_ascii_mode"),
|
||||
roundCorner = getFloat("round_corner"),
|
||||
shadowRadius = getFloat("shadow_radius"),
|
||||
symbolFont = getStringList("symbol_font"),
|
||||
symbolTextSize = getFloat("symbol_text_size"),
|
||||
textFont = getStringList("text_font"),
|
||||
verticalCorrection = getInt("vertical_correction"),
|
||||
verticalGap = getInt("vertical_gap"),
|
||||
backgroundFolder = getString("background_folder"),
|
||||
enterLabelMode = getInt("enter_label_mode"),
|
||||
|
||||
@ -42,8 +42,8 @@ class TextKeyboardMapper(
|
||||
keySymbolOffsetY = it.getFloat("key_symbol_offset_y"),
|
||||
keyHintOffsetX = it.getFloat("key_hint_offset_x"),
|
||||
keyHintOffsetY = it.getFloat("key_hint_offset_y"),
|
||||
keyPressOffsetX = it.getInt("key_press_offset_x"),
|
||||
keyPressOffsetY = it.getInt("key_press_offset_y"),
|
||||
keyPressOffsetX = it.getFloat("key_press_offset_x"),
|
||||
keyPressOffsetY = it.getFloat("key_press_offset_y"),
|
||||
keyTextColor = it.getString("key_text_color"),
|
||||
keyBackColor = it.getString("key_back_color"),
|
||||
keySymbolColor = it.getString("key_symbol_color"),
|
||||
@ -92,8 +92,8 @@ class TextKeyboardMapper(
|
||||
keySymbolOffsetY = getFloat("key_symbol_offset_y"),
|
||||
keyHintOffsetX = getFloat("key_hint_offset_x"),
|
||||
keyHintOffsetY = getFloat("key_hint_offset_y"),
|
||||
keyPressOffsetX = getInt("key_press_offset_x"),
|
||||
keyPressOffsetY = getInt("key_press_offset_y"),
|
||||
keyPressOffsetX = getFloat("key_press_offset_x"),
|
||||
keyPressOffsetY = getFloat("key_press_offset_y"),
|
||||
importPreset = getString("import_preset"),
|
||||
keys = keys,
|
||||
)
|
||||
|
||||
@ -44,8 +44,8 @@ data class GeneralStyle(
|
||||
val keySymbolOffsetY: Float = 0f,
|
||||
val keyHintOffsetX: Float = 0f,
|
||||
val keyHintOffsetY: Float = 0f,
|
||||
val keyPressOffsetX: Int = 0,
|
||||
val keyPressOffsetY: Int = 0,
|
||||
val keyPressOffsetX: Float = 0f,
|
||||
val keyPressOffsetY: Float = 0f,
|
||||
val keyWidth: Float = 0f,
|
||||
val labelTextSize: Float = 0f,
|
||||
val labelFont: List<String> = emptyList(),
|
||||
@ -58,14 +58,12 @@ data class GeneralStyle(
|
||||
val popupKeyHeight: Int = 0,
|
||||
val popupFont: List<String> = emptyList(),
|
||||
val popupTextSize: Float = 0f,
|
||||
val proximityCorrection: Boolean = false,
|
||||
val resetASCIIMode: Boolean = false,
|
||||
val roundCorner: Float = 0f,
|
||||
val shadowRadius: Float = 0f,
|
||||
val symbolFont: List<String> = emptyList(),
|
||||
val symbolTextSize: Float = 0f,
|
||||
val textFont: List<String> = emptyList(),
|
||||
val verticalCorrection: Int = 0,
|
||||
val verticalGap: Int = 0,
|
||||
val backgroundFolder: String = "backgrounds",
|
||||
val enterLabelMode: Int = 0,
|
||||
|
||||
@ -36,8 +36,8 @@ data class TextKeyboard(
|
||||
val keySymbolOffsetY: Float,
|
||||
val keyHintOffsetX: Float,
|
||||
val keyHintOffsetY: Float,
|
||||
val keyPressOffsetX: Int,
|
||||
val keyPressOffsetY: Int,
|
||||
val keyPressOffsetX: Float,
|
||||
val keyPressOffsetY: Float,
|
||||
val importPreset: String,
|
||||
val keys: List<TextKey>,
|
||||
) : Parcelable {
|
||||
@ -65,8 +65,8 @@ data class TextKeyboard(
|
||||
val keySymbolOffsetY: Float,
|
||||
val keyHintOffsetX: Float,
|
||||
val keyHintOffsetY: Float,
|
||||
val keyPressOffsetX: Int,
|
||||
val keyPressOffsetY: Int,
|
||||
val keyPressOffsetX: Float,
|
||||
val keyPressOffsetY: Float,
|
||||
val keyTextColor: String,
|
||||
val keyBackColor: String,
|
||||
val keySymbolColor: String,
|
||||
|
||||
@ -35,6 +35,7 @@ import com.osfans.trime.ime.core.TrimeInputMethodService
|
||||
import com.osfans.trime.ime.dependency.InputDependencyManager
|
||||
import com.osfans.trime.ime.keyboard.CommonKeyboardActionListener
|
||||
import com.osfans.trime.ime.keyboard.GestureFrame
|
||||
import com.osfans.trime.ime.keyboard.KeyBehavior
|
||||
import com.osfans.trime.ime.keyboard.KeyboardWindow
|
||||
import com.osfans.trime.ime.switches.SwitchOptionWindow
|
||||
import com.osfans.trime.ime.window.BoardWindow
|
||||
@ -117,8 +118,8 @@ class InputBarDelegate : InputBroadcastReceiver {
|
||||
alwaysUi.updateState(newState)
|
||||
}
|
||||
|
||||
private val swipeDownHideKeyboardCallback: ((GestureFrame.SwipeDirection) -> Unit) = { d ->
|
||||
if (d == GestureFrame.SwipeDirection.Down) {
|
||||
private val swipeDownHideKeyboardCallback: ((KeyBehavior) -> Unit) = { d ->
|
||||
if (d == KeyBehavior.SWIPE_DOWN) {
|
||||
service.requestHideSelf(0)
|
||||
}
|
||||
}
|
||||
@ -133,7 +134,7 @@ class InputBarDelegate : InputBroadcastReceiver {
|
||||
}.apply {
|
||||
hideKeyboardButton.apply {
|
||||
setOnClickListener { service.requestHideSelf(0) }
|
||||
onSwipeListener = swipeDownHideKeyboardCallback
|
||||
onSwipe = swipeDownHideKeyboardCallback
|
||||
}
|
||||
clipboardUi.suggestionView.apply {
|
||||
setOnClickListener {
|
||||
@ -157,7 +158,7 @@ class InputBarDelegate : InputBroadcastReceiver {
|
||||
private val candidateUi by lazy {
|
||||
CandidateUi(context, candidate.view).apply {
|
||||
unrollButton.apply {
|
||||
onSwipeListener = swipeDownHideKeyboardCallback
|
||||
onSwipe = swipeDownHideKeyboardCallback
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -114,8 +114,6 @@ class CandidateItemUi(
|
||||
/**
|
||||
* candidate long press feedback is handled by `showCandidateActionMenu`
|
||||
*/
|
||||
longPressFeedbackEnabled = false
|
||||
|
||||
add(
|
||||
content,
|
||||
lParams(matchParent, matchParent) {
|
||||
|
||||
@ -115,10 +115,9 @@ class CommonKeyboardActionListener {
|
||||
|
||||
val listener by lazy {
|
||||
object : KeyboardActionListener {
|
||||
override fun onPress(keyEventCode: Int, isSound: Boolean) {
|
||||
override fun onPress(keyEventCode: Int) {
|
||||
InputFeedbackManager.run {
|
||||
keyPressVibrate(service.window.window!!.decorView)
|
||||
if (isSound) keyPressSound(keyEventCode)
|
||||
keyPressSound(keyEventCode)
|
||||
keyPressSpeak(keyEventCode)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
|
||||
* SPDX-FileCopyrightText: 2015 - 2026 Rime community
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
@ -7,8 +7,8 @@ package com.osfans.trime.ime.keyboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.SystemClock
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.widget.FrameLayout
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
@ -20,43 +20,53 @@ import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
|
||||
open class GestureFrame(context: Context) : FrameLayout(context) {
|
||||
enum class SwipeDirection {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
private var startX = 0f
|
||||
private var startY = 0f
|
||||
private var lastX = 0f
|
||||
private var startTime = 0L
|
||||
|
||||
private var isLongPressed = false
|
||||
private var slideActivated = false
|
||||
private var shouldPerformSwipe = false
|
||||
|
||||
private var longPressJob: Job? = null
|
||||
private var repeatJob: Job? = null
|
||||
private var doubleTapJob: Job? = null
|
||||
|
||||
private var lastTapTime = 0L
|
||||
private var lastSwipeBehavior: KeyBehavior = KeyBehavior.CLICK
|
||||
|
||||
private val lifecycleScope by lazy {
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope!!
|
||||
}
|
||||
|
||||
private var touchId = 0
|
||||
var onClick: (() -> Unit)? = null
|
||||
var onDoubleClick: (() -> Unit)? = null
|
||||
var onLazyDoubleClick: (() -> Unit)? = null
|
||||
var onLongClick: (() -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var longPressTriggered = false
|
||||
private var longPressJob: Job? = null
|
||||
var onSwipeLeft: (() -> Unit)? = null
|
||||
var onSwipeRight: (() -> Unit)? = null
|
||||
var onSwipeUp: (() -> Unit)? = null
|
||||
var onSwipeDown: (() -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
var longPressFeedbackEnabled = true
|
||||
var onSlide: ((delta: Int, x: Float, y: Float) -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var swipeTriggered = false
|
||||
private var shouldPerformSwipe = false
|
||||
private var swipeStartX = -1f
|
||||
private var swipeStartY = -1f
|
||||
private var swipeLastX = -1f
|
||||
var onPress: (() -> Unit)? = null
|
||||
var onRelease: ((behavior: KeyBehavior, longPress: Boolean) -> Unit)? = null
|
||||
var onCancel: (() -> Unit)? = null
|
||||
var onMove: ((x: Float, y: Float, longPress: Boolean) -> Unit)? = null
|
||||
var onSwipe: ((behavior: KeyBehavior) -> Unit)? = null
|
||||
|
||||
@Volatile
|
||||
private var slideActivated = false
|
||||
var isRepeatable = false
|
||||
var isSlideCursor = false
|
||||
var isSlideDelete = false
|
||||
|
||||
private var lastTapTime = 0L
|
||||
private var maybeDoubleTap = false
|
||||
|
||||
var onDoubleTapListener: (() -> Unit)? = null
|
||||
var onSwipeListener: ((SwipeDirection) -> Unit)? = null
|
||||
var onSlideListener: ((Int) -> Unit)? = null
|
||||
var onReleaseListener: (() -> Unit)? = null
|
||||
var hasLongPress = false
|
||||
var hasDouble = false
|
||||
var hasLazyDouble = false
|
||||
var hasPopup = false
|
||||
|
||||
init {
|
||||
// disable system sound effect and haptic feedback
|
||||
@ -67,149 +77,245 @@ open class GestureFrame(context: Context) : FrameLayout(context) {
|
||||
isFocusableInTouchMode = false
|
||||
}
|
||||
|
||||
fun getSwipeDirection(dx: Float, dy: Float): SwipeDirection = if (abs(dx) > abs(dy)) {
|
||||
if (dx > 0) SwipeDirection.Right else SwipeDirection.Left
|
||||
} else {
|
||||
if (dy > 0) SwipeDirection.Down else SwipeDirection.Up
|
||||
}
|
||||
|
||||
fun getNStep(start: Float, end: Float, step: Float): Int = (if (start < end) 1 else -1) * floor(abs(end - start) / step).toInt()
|
||||
|
||||
private fun resetState() {
|
||||
onReleaseListener?.invoke()
|
||||
swipeStartX = -1f
|
||||
swipeStartY = -1f
|
||||
swipeLastX = -1f
|
||||
longPressTriggered = false
|
||||
longPressJob?.cancel()
|
||||
longPressJob = null
|
||||
shouldPerformSwipe = false
|
||||
swipeTriggered = false
|
||||
slideActivated = false
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
when (event.actionMasked) {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
if (!isEnabled) return false
|
||||
touchId = (touchId) and 0xFFFF
|
||||
val currentTouchId = touchId
|
||||
drawableHotspotChanged(x, y)
|
||||
isPressed = true
|
||||
InputFeedbackManager.keyPressVibrate(this)
|
||||
longPressJob?.cancel()
|
||||
longPressJob = lifecycleScope.launch {
|
||||
delay(longPressTimeout.toLong())
|
||||
if (touchId != currentTouchId) return@launch
|
||||
if (!(swipeTriggered || longPressTriggered || shouldPerformSwipe)) {
|
||||
longPressTriggered = performLongClick()
|
||||
if (longPressFeedbackEnabled && longPressTriggered) {
|
||||
InputFeedbackManager.keyPressVibrate(this@GestureFrame, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
swipeStartX = x
|
||||
swipeStartY = y
|
||||
swipeLastX = x
|
||||
return true
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (!isEnabled) return false
|
||||
drawableHotspotChanged(x, y)
|
||||
val dx = x - swipeStartX
|
||||
val dy = y - swipeStartY
|
||||
startX = event.x
|
||||
startY = event.y
|
||||
lastX = startX
|
||||
startTime = SystemClock.elapsedRealtime()
|
||||
|
||||
if (!longPressTriggered) {
|
||||
if (!shouldPerformSwipe && (abs(dx) > SWIPE_THRESHOLD || abs(dy) > SWIPE_THRESHOLD)) {
|
||||
shouldPerformSwipe = true
|
||||
swipeTriggered = true
|
||||
}
|
||||
}
|
||||
val onSlide = onSlideListener
|
||||
if (onSlide != null) {
|
||||
if (!slideActivated) {
|
||||
if (abs(dx) >= SWIPE_THRESHOLD) {
|
||||
val startX = swipeStartX
|
||||
slideActivated = true
|
||||
swipeLastX = startX + (if (dx > 0) SWIPE_THRESHOLD else -SWIPE_THRESHOLD)
|
||||
}
|
||||
}
|
||||
if (slideActivated) {
|
||||
val startX = swipeStartX
|
||||
val lastX = swipeLastX
|
||||
val totalPast = getNStep(startX, lastX, SLIDE_STEP_SIZE)
|
||||
val totalNow = getNStep(startX, x, SLIDE_STEP_SIZE)
|
||||
val delta = totalNow - totalPast
|
||||
if (delta != 0) {
|
||||
onSlide(delta)
|
||||
}
|
||||
swipeLastX = x
|
||||
}
|
||||
} else if (longPressTriggered) {
|
||||
val lastX = swipeLastX
|
||||
val delta = getNStep(lastX, x, SWIPE_MOVE_SIZE)
|
||||
if (delta != 0) {
|
||||
swipeLastX += delta.toFloat() * SWIPE_MOVE_SIZE
|
||||
}
|
||||
isLongPressed = false
|
||||
slideActivated = false
|
||||
shouldPerformSwipe = false
|
||||
lastSwipeBehavior = KeyBehavior.CLICK
|
||||
|
||||
if (vibrateOnKeyPress) InputFeedbackManager.keyPressVibrate(this)
|
||||
onPress?.invoke()
|
||||
|
||||
if (hasLongPress || isRepeatable || hasPopup) {
|
||||
startLongPressJob()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
val dx = x - startX
|
||||
val dy = y - startY
|
||||
|
||||
if (!isLongPressed) {
|
||||
val behavior = detectSwipe(dx, dy)
|
||||
if (behavior != lastSwipeBehavior) {
|
||||
lastSwipeBehavior = behavior
|
||||
if (behavior != KeyBehavior.CLICK) {
|
||||
onSwipe?.invoke(behavior)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMove?.invoke(x, y, isLongPressed)
|
||||
|
||||
if ((isSlideCursor || isSlideDelete) && onSlide != null && !isLongPressed) {
|
||||
if (!slideActivated) {
|
||||
if (abs(dx) >= swipeTravel) {
|
||||
slideActivated = true
|
||||
lastX = startX
|
||||
}
|
||||
}
|
||||
|
||||
if (slideActivated) {
|
||||
val step = getNStep(lastX, x, slideStepSize.toFloat())
|
||||
if (step != 0) {
|
||||
onSlide?.invoke(step, x, y)
|
||||
lastX = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
isPressed = false
|
||||
val dx = x - swipeStartX
|
||||
val dy = y - swipeStartY
|
||||
val x = event.x
|
||||
val y = event.y
|
||||
|
||||
if (vibrateOnKeyRelease) InputFeedbackManager.keyPressVibrate(this)
|
||||
cancelJobs()
|
||||
|
||||
if (slideActivated) {
|
||||
val onSlide = onSlideListener
|
||||
if (onSlide != null) {
|
||||
onSlide(0)
|
||||
resetState()
|
||||
return true
|
||||
}
|
||||
onSlide?.invoke(0, x, y)
|
||||
onCancel?.invoke()
|
||||
return true
|
||||
}
|
||||
|
||||
if (isLongPressed) {
|
||||
dispatchBehavior(KeyBehavior.LONG_CLICK, true)
|
||||
return true
|
||||
}
|
||||
|
||||
if (shouldPerformSwipe) {
|
||||
if (!longPressTriggered) {
|
||||
onSwipeListener?.invoke(getSwipeDirection(dx, dy))
|
||||
dispatchBehavior(lastSwipeBehavior, false)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!hasDouble && !hasLazyDouble) {
|
||||
dispatchBehavior(KeyBehavior.CLICK, false)
|
||||
return true
|
||||
}
|
||||
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val delta = now - lastTapTime
|
||||
if (delta <= doubleTapTimeout) {
|
||||
lastTapTime = 0
|
||||
doubleTapJob?.cancel()
|
||||
if (hasDouble) {
|
||||
dispatchBehavior(KeyBehavior.DOUBLE_CLICK, false)
|
||||
} else {
|
||||
dispatchBehavior(KeyBehavior.LAZY_DOUBLE_CLICK, false)
|
||||
}
|
||||
} else {
|
||||
if (!longPressTriggered) {
|
||||
val onDoubleTap = onDoubleTapListener
|
||||
if (onDoubleTap != null) {
|
||||
val now = System.currentTimeMillis()
|
||||
if (maybeDoubleTap && now - lastTapTime <= longPressTimeout) {
|
||||
maybeDoubleTap = false
|
||||
onDoubleTap()
|
||||
} else {
|
||||
maybeDoubleTap = true
|
||||
performClick()
|
||||
lastTapTime = now
|
||||
if (hasLazyDouble && !hasDouble) {
|
||||
doubleTapJob = lifecycleScope.launch {
|
||||
delay(doubleTapTimeout.toLong())
|
||||
if (lastTapTime == now) {
|
||||
lastTapTime = 0
|
||||
dispatchBehavior(KeyBehavior.CLICK, false)
|
||||
}
|
||||
lastTapTime = now
|
||||
} else {
|
||||
performClick()
|
||||
}
|
||||
} else {
|
||||
dispatchBehavior(KeyBehavior.CLICK, false)
|
||||
}
|
||||
}
|
||||
resetState()
|
||||
return true
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
isPressed = false
|
||||
resetState()
|
||||
cancelJobs()
|
||||
|
||||
isLongPressed = false
|
||||
slideActivated = false
|
||||
shouldPerformSwipe = false
|
||||
|
||||
onCancel?.invoke()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SWIPE_THRESHOLD = 24f
|
||||
private const val SWIPE_MOVE_SIZE = 24f
|
||||
private const val SLIDE_STEP_SIZE = 12f
|
||||
private fun startLongPressJob() {
|
||||
longPressJob = lifecycleScope.launch {
|
||||
delay(longPressTimeout.toLong())
|
||||
|
||||
if (shouldPerformSwipe || slideActivated) return@launch
|
||||
isLongPressed = true
|
||||
|
||||
if (vibrateOnKeyPress) InputFeedbackManager.keyPressVibrate(this@GestureFrame, true)
|
||||
|
||||
if (isRepeatable) {
|
||||
startRepeatJob()
|
||||
} else {
|
||||
performLongClick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startRepeatJob() {
|
||||
repeatJob = lifecycleScope.launch {
|
||||
try {
|
||||
while (true) {
|
||||
if (vibrateOnKeyRepeat) InputFeedbackManager.keyPressVibrate(this@GestureFrame)
|
||||
dispatchBehavior(KeyBehavior.CLICK, true)
|
||||
delay(repeatInterval.toLong())
|
||||
}
|
||||
} finally {
|
||||
onCancel?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectSwipe(dx: Float, dy: Float): KeyBehavior {
|
||||
val absDx = abs(dx)
|
||||
val absDy = abs(dy)
|
||||
|
||||
val distance = if (absDx > absDy) absDx else absDy
|
||||
val elapsed = SystemClock.elapsedRealtime() - startTime
|
||||
|
||||
val velocity = if (elapsed > 0) {
|
||||
(distance / elapsed) * 1000f
|
||||
} else {
|
||||
0f
|
||||
}
|
||||
|
||||
val isSwipe = distance >= swipeTravel || velocity >= swipeVelocity
|
||||
shouldPerformSwipe = isSwipe
|
||||
|
||||
if (!isSwipe) return KeyBehavior.CLICK
|
||||
return if (absDx > absDy) {
|
||||
if (dx > 0) KeyBehavior.SWIPE_RIGHT else KeyBehavior.SWIPE_LEFT
|
||||
} else {
|
||||
if (dy > 0) KeyBehavior.SWIPE_DOWN else KeyBehavior.SWIPE_UP
|
||||
}
|
||||
}
|
||||
|
||||
private fun dispatchBehavior(
|
||||
behavior: KeyBehavior,
|
||||
longPress: Boolean,
|
||||
) {
|
||||
onRelease?.invoke(behavior, longPress)
|
||||
when (behavior) {
|
||||
KeyBehavior.CLICK -> performClick()
|
||||
KeyBehavior.DOUBLE_CLICK -> onDoubleClick?.invoke()
|
||||
KeyBehavior.LAZY_DOUBLE_CLICK -> onLazyDoubleClick?.invoke()
|
||||
KeyBehavior.SWIPE_LEFT -> onSwipeLeft?.invoke()
|
||||
KeyBehavior.SWIPE_RIGHT -> onSwipeRight?.invoke()
|
||||
KeyBehavior.SWIPE_UP -> onSwipeUp?.invoke()
|
||||
KeyBehavior.SWIPE_DOWN -> onSwipeDown?.invoke()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun cancelJobs() {
|
||||
longPressJob?.cancel()
|
||||
repeatJob?.cancel()
|
||||
doubleTapJob?.cancel()
|
||||
}
|
||||
|
||||
fun getNStep(start: Float, end: Float, step: Float): Int = (if (start < end) 1 else -1) *
|
||||
floor(abs(end - start) / step).toInt()
|
||||
|
||||
override fun setOnLongClickListener(l: OnLongClickListener?) {
|
||||
hasLongPress = l != null
|
||||
super.setOnLongClickListener(l)
|
||||
}
|
||||
|
||||
override fun performClick(): Boolean {
|
||||
super.performClick()
|
||||
onClick?.invoke()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun performLongClick(): Boolean {
|
||||
val handled = super.performLongClick()
|
||||
onLongClick?.invoke()
|
||||
return handled || onLongClick != null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val swipeTravel by AppPrefs.defaultInstance().keyboard.swipeTravel
|
||||
private val swipeVelocity by AppPrefs.defaultInstance().keyboard.swipeVelocity
|
||||
private val longPressTimeout by AppPrefs.defaultInstance().keyboard.longPressTimeout
|
||||
private val repeatInterval by AppPrefs.defaultInstance().keyboard.repeatInterval
|
||||
private val doubleTapTimeout by AppPrefs.defaultInstance().keyboard.doubleTapTimeout
|
||||
private val slideStepSize by AppPrefs.defaultInstance().keyboard.slideStepSize
|
||||
private val vibrateOnKeyPress by AppPrefs.defaultInstance().keyboard.vibrateOnKeyPress
|
||||
private val vibrateOnKeyRelease by AppPrefs.defaultInstance().keyboard.vibrateOnKeyRelease
|
||||
private val vibrateOnKeyRepeat by AppPrefs.defaultInstance().keyboard.vibrateOnKeyRepeat
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,8 +64,8 @@ class Key(
|
||||
get() = field + keyOffsetX
|
||||
var keyHintOffsetY = 0f
|
||||
get() = field + keyOffsetY
|
||||
var keyPressOffsetX = 0
|
||||
var keyPressOffsetY = 0
|
||||
var keyPressOffsetX = 0f
|
||||
var keyPressOffsetY = 0f
|
||||
|
||||
// get color from key customization or just fallback to specified color
|
||||
private fun getColor(
|
||||
@ -85,8 +85,7 @@ class Key(
|
||||
src: TextKeyboard.TextKey.() -> String,
|
||||
fallback: String,
|
||||
) = selfConfig?.let {
|
||||
if (src(it).isEmpty()) null
|
||||
ColorManager.getDrawable(src(it))
|
||||
if (src(it).isEmpty()) null else ColorManager.getDrawable(src(it))
|
||||
} ?: ColorManager.getDrawable(fallback)
|
||||
|
||||
private val keyBackground by lazy { getDrawable({ keyBackColor }, "key_back_color") }
|
||||
@ -125,10 +124,10 @@ class Key(
|
||||
return isOn
|
||||
}
|
||||
|
||||
private val keyOffsetX: Int
|
||||
get() = if (isPressed) keyPressOffsetX else 0
|
||||
private val keyOffsetY: Int
|
||||
get() = if (isPressed) keyPressOffsetY else 0
|
||||
private val keyOffsetX: Float
|
||||
get() = if (isPressed) keyPressOffsetX else 0f
|
||||
private val keyOffsetY: Float
|
||||
get() = if (isPressed) keyPressOffsetY else 0f
|
||||
|
||||
/**
|
||||
* Informs the key that it has been pressed, in case it needs to change its appearance or state.
|
||||
|
||||
401
app/src/main/java/com/osfans/trime/ime/keyboard/KeyView.kt
Normal file
401
app/src/main/java/com/osfans/trime/ime/keyboard/KeyView.kt
Normal file
@ -0,0 +1,401 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015 - 2026 Rime community
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.osfans.trime.ime.keyboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.KeyEvent
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.osfans.trime.daemon.RimeDaemon
|
||||
import com.osfans.trime.data.prefs.AppPrefs
|
||||
import com.osfans.trime.data.theme.ColorManager
|
||||
import com.osfans.trime.data.theme.FontManager
|
||||
import com.osfans.trime.ime.core.TrimeInputMethodService
|
||||
import com.osfans.trime.ime.popup.PopupAction
|
||||
import com.osfans.trime.ime.popup.PopupDelegate
|
||||
import com.osfans.trime.util.sp
|
||||
import splitties.dimensions.dp
|
||||
import timber.log.Timber
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility", "ViewConstructor")
|
||||
class KeyView(
|
||||
context: Context,
|
||||
private val key: Key,
|
||||
private val keyboard: Keyboard,
|
||||
private val keyboardView: KeyboardView,
|
||||
private val keyboardActionListener: KeyboardActionListener,
|
||||
) : GestureFrame(context) {
|
||||
|
||||
private val service: TrimeInputMethodService
|
||||
get() = keyboardView.service
|
||||
|
||||
private val popup: PopupDelegate
|
||||
get() = keyboardView.popup
|
||||
|
||||
private val rime get() = RimeDaemon.getFirstSessionOrNull()!!
|
||||
|
||||
private val hookShiftArrow: Boolean by lazy {
|
||||
AppPrefs.defaultInstance().keyboard.hookShiftArrow.getValue()
|
||||
}
|
||||
|
||||
private val deletedTextBuffer = ArrayDeque<String>()
|
||||
|
||||
private var keyPressed = false
|
||||
override fun isPressed(): Boolean = keyPressed
|
||||
|
||||
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
private val symbolPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private var cachedIcon: IconicsDrawable? = null
|
||||
private var cachedIconName: String? = null
|
||||
|
||||
private val cachedLocation = intArrayOf(0, 0)
|
||||
private val cachedBounds = Rect()
|
||||
private var boundsValid = false
|
||||
|
||||
val bounds: Rect
|
||||
get() = cachedBounds.also {
|
||||
if (!boundsValid) updateBounds()
|
||||
}
|
||||
|
||||
fun updateBounds() {
|
||||
val (x, y) = cachedLocation.also { getLocationInWindow(it) }
|
||||
cachedBounds.set(x, y, x + width, y + height)
|
||||
boundsValid = true
|
||||
}
|
||||
|
||||
init {
|
||||
setWillNotDraw(false)
|
||||
isRepeatable = key.click?.isRepeatable ?: false
|
||||
isSlideCursor = key.click?.isSlideCursor ?: false
|
||||
isSlideDelete = key.click?.isSlideDelete ?: false
|
||||
hasLongPress = key.hasAction(KeyBehavior.LONG_CLICK)
|
||||
hasDouble = key.hasAction(KeyBehavior.DOUBLE_CLICK)
|
||||
hasLazyDouble = key.hasAction(KeyBehavior.LAZY_DOUBLE_CLICK)
|
||||
hasPopup = key.popup.isNotEmpty()
|
||||
|
||||
onPress = {
|
||||
if (keyboard.firstPressedKeyIndex == -1) keyboard.firstPressedKeyIndex = id
|
||||
setPressedState(true)
|
||||
key.getCode(KeyBehavior.CLICK).let { keyboardActionListener.onPress(it) }
|
||||
showPopupPreview()
|
||||
}
|
||||
|
||||
onRelease = { behavior, isFromLongPress ->
|
||||
Timber.d("KeyView release: label=${key.getLabel()}, behavior=$behavior, fromLongPress=$isFromLongPress")
|
||||
if (isFromLongPress) {
|
||||
if (hasPopup) {
|
||||
val triggerAction = PopupAction.TriggerAction(id)
|
||||
popup.listener.onPopupAction(triggerAction)
|
||||
triggerAction.outAction?.let { action ->
|
||||
keyboardActionListener.onAction(KeyAction(action))
|
||||
setPressedState(false)
|
||||
dismissPopupPreview()
|
||||
}
|
||||
} else if (isRepeatable) {
|
||||
key.getAction(KeyBehavior.CLICK)?.let { processKeyAction(it, KeyBehavior.CLICK) }
|
||||
}
|
||||
} else {
|
||||
when (behavior) {
|
||||
KeyBehavior.CLICK -> {
|
||||
val pressedIdx = keyboard.firstPressedKeyIndex
|
||||
val actionBehavior = if (pressedIdx != -1 && pressedIdx != id) KeyBehavior.COMBO else behavior
|
||||
key.getAction(actionBehavior)?.let { processKeyAction(it, actionBehavior) }
|
||||
}
|
||||
KeyBehavior.DOUBLE_CLICK, KeyBehavior.LAZY_DOUBLE_CLICK,
|
||||
KeyBehavior.SWIPE_UP, KeyBehavior.SWIPE_DOWN, KeyBehavior.SWIPE_LEFT, KeyBehavior.SWIPE_RIGHT,
|
||||
->
|
||||
key.getAction(behavior)?.let { processKeyAction(it, behavior) }
|
||||
else -> {}
|
||||
}
|
||||
|
||||
setPressedState(false)
|
||||
dismissPopupPreview()
|
||||
}
|
||||
if (keyboard.firstPressedKeyIndex == id) keyboard.firstPressedKeyIndex = -1
|
||||
}
|
||||
|
||||
onSwipe = { direction ->
|
||||
setPressedState(true)
|
||||
showPopupPreview(direction)
|
||||
}
|
||||
|
||||
onSlide = { delta, _, _ ->
|
||||
service.let { svc ->
|
||||
if (isSlideCursor) {
|
||||
repeat(kotlin.math.abs(delta)) {
|
||||
val keyCode = if (delta > 0) KeyEvent.KEYCODE_DPAD_RIGHT else KeyEvent.KEYCODE_DPAD_LEFT
|
||||
svc.sendDownUpKeyEvent(keyCode)
|
||||
}
|
||||
} else if (isSlideDelete) {
|
||||
val ic = svc.currentInputConnection
|
||||
when {
|
||||
delta < 0 -> {
|
||||
val beforeText = ic.getTextBeforeCursor(1, 0) ?: ""
|
||||
if (beforeText.isNotEmpty()) {
|
||||
deletedTextBuffer.addFirst(beforeText.toString())
|
||||
ic.deleteSurroundingText(1, 0)
|
||||
}
|
||||
}
|
||||
|
||||
delta > 0 -> {
|
||||
if (deletedTextBuffer.isNotEmpty()) {
|
||||
ic.commitText(deletedTextBuffer.removeFirst(), 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onLongClick = {
|
||||
if (key.popup.isNotEmpty()) {
|
||||
dismissPopupPreview()
|
||||
showPopupKeyboard()
|
||||
} else if (hasLongPress) {
|
||||
key.getAction(KeyBehavior.LONG_CLICK)?.let {
|
||||
processKeyAction(it, KeyBehavior.LONG_CLICK)
|
||||
setPressedState(false)
|
||||
dismissPopupPreview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMove = { x, y, isLongPress ->
|
||||
if (isLongPress && hasPopup) {
|
||||
popup.listener.onPopupAction(PopupAction.ChangeFocusAction(id, x, y))
|
||||
}
|
||||
}
|
||||
|
||||
onCancel = {
|
||||
deletedTextBuffer.clear()
|
||||
setPressedState(false)
|
||||
dismissPopupPreview()
|
||||
}
|
||||
}
|
||||
|
||||
fun setPressedState(pressed: Boolean) {
|
||||
if (keyPressed != pressed) {
|
||||
keyPressed = pressed
|
||||
if (pressed) {
|
||||
key.onPressed()
|
||||
} else {
|
||||
key.onReleased()
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
private fun processKeyAction(action: KeyAction, behavior: KeyBehavior) {
|
||||
Timber.d("processKeyAction: label=${key.getLabel()}, code=${action.code}, type=$behavior")
|
||||
|
||||
if (action.isModifierKey) {
|
||||
keyboard.clickModifierKey(
|
||||
action.isShiftLock xor (behavior == KeyBehavior.LONG_CLICK),
|
||||
action.modifierKeyOnMask,
|
||||
)
|
||||
keyboardView.invalidateAllKeys()
|
||||
return
|
||||
}
|
||||
|
||||
keyboardActionListener.onAction(action)
|
||||
|
||||
val hookArrow = if (hookShiftArrow) {
|
||||
when (action.code) {
|
||||
in KeyEvent.KEYCODE_DPAD_UP..KeyEvent.KEYCODE_DPAD_RIGHT -> true
|
||||
KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.KEYCODE_MOVE_END -> true
|
||||
else -> false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
if (!hookArrow) {
|
||||
if (keyboard.refreshModifier()) {
|
||||
keyboardView.invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPopupKeyboard() {
|
||||
val popupKeys = key.popup
|
||||
if (popupKeys.isEmpty()) return
|
||||
|
||||
popup.listener.onPopupAction(
|
||||
PopupAction.ShowKeyboardAction(id, popupKeys, bounds),
|
||||
)
|
||||
}
|
||||
|
||||
private fun showPopupPreview(behavior: KeyBehavior = KeyBehavior.CLICK) {
|
||||
key.getPreviewText(behavior).takeIf { it.isNotEmpty() }?.let { previewText ->
|
||||
val context = if (previewText.isIconFont) {
|
||||
previewText
|
||||
} else {
|
||||
String(Character.toChars(previewText.codePointAt(0)))
|
||||
}
|
||||
popup.listener.onPopupAction(PopupAction.PreviewAction(id, context, bounds))
|
||||
}
|
||||
}
|
||||
|
||||
private fun dismissPopupPreview() {
|
||||
popup.listener.onPopupAction(
|
||||
PopupAction.DismissAction(id),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val desiredWidth = key.width + paddingLeft + paddingRight
|
||||
val desiredHeight = key.height + paddingTop + paddingBottom
|
||||
|
||||
setMeasuredDimension(
|
||||
resolveSize(desiredWidth, widthMeasureSpec),
|
||||
resolveSize(desiredHeight, heightMeasureSpec),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
||||
super.onLayout(changed, left, top, right, bottom)
|
||||
boundsValid = false
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
drawBackground(canvas, key)
|
||||
|
||||
val label = if (key.code == KeyEvent.KEYCODE_ENTER && keyboardView.labelEnter.isNotEmpty()) {
|
||||
keyboardView.labelEnter
|
||||
} else {
|
||||
key.getLabel()
|
||||
}
|
||||
if (label.isNotEmpty()) {
|
||||
drawLabel(canvas, label, key)
|
||||
}
|
||||
|
||||
val symbol = key.symbolLabel
|
||||
if (symbol.isNotEmpty()) {
|
||||
drawSymbol(canvas, symbol, key)
|
||||
}
|
||||
|
||||
val hint = key.hint
|
||||
if (hint.isNotEmpty()) {
|
||||
drawSymbol(canvas, hint, key, isTop = false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawBackground(canvas: Canvas, k: Key) {
|
||||
val bg = k.getBackgroundDrawable() ?: return
|
||||
|
||||
if (bg is GradientDrawable) {
|
||||
(k.roundCorner ?: keyboard.roundCorner).takeIf { it > 0f }?.let { bg.cornerRadius = dp(it) }
|
||||
(k.keyBorder ?: keyboard.keyBorder).takeIf { it > 0 }?.let { bg.setStroke(dp(it), ColorManager.getColor("key_border_color")) }
|
||||
}
|
||||
|
||||
bg.setBounds(
|
||||
paddingLeft,
|
||||
paddingTop,
|
||||
width - paddingRight,
|
||||
height - paddingBottom,
|
||||
)
|
||||
bg.draw(canvas)
|
||||
}
|
||||
|
||||
private fun drawLabel(canvas: Canvas, label: String, k: Key) {
|
||||
val textColor = k.getTextColor()
|
||||
val textSize = if (k.keyTextSize > 0) sp(k.keyTextSize) else sp(if (label.length > 1) keyboardView.labelTextSize else keyboardView.keyTextSize)
|
||||
|
||||
if (label.isIconFont) {
|
||||
drawIcon(canvas, label, textSize, textColor)
|
||||
} else {
|
||||
textPaint.apply {
|
||||
color = textColor
|
||||
this.textSize = textSize
|
||||
typeface = FontManager.getTypeface("key_font")
|
||||
clearShadowLayer()
|
||||
}
|
||||
|
||||
val centerX = (width - paddingLeft - paddingRight) / 2f + paddingLeft
|
||||
val centerY = (height - paddingTop - paddingBottom) / 2f + paddingTop
|
||||
val adjustmentY = (textPaint.textSize - textPaint.descent()) / 2f
|
||||
val offsetX = k.keyTextOffsetX
|
||||
val offsetY = k.keyTextOffsetY
|
||||
|
||||
canvas.drawText(label, centerX + sp(offsetX), centerY + adjustmentY + sp(offsetY), textPaint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawIcon(canvas: Canvas, iconName: String, size: Float, color: Int) {
|
||||
val iconSize = (size / resources.displayMetrics.density).toInt()
|
||||
val cmdName = iconName.toIconName()
|
||||
|
||||
val icon = if (cachedIconName == cmdName) {
|
||||
cachedIcon!!
|
||||
} else {
|
||||
IconicsDrawable(context, cmdName).apply {
|
||||
sizeDp = iconSize
|
||||
cachedIcon = this
|
||||
cachedIconName = cmdName
|
||||
}
|
||||
}
|
||||
|
||||
icon.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
|
||||
val centerX = (width - paddingLeft - paddingRight) / 2f + paddingLeft + sp(key.keyTextOffsetX)
|
||||
val centerY = (height - paddingTop - paddingBottom) / 2f + paddingTop
|
||||
|
||||
icon.setBounds(
|
||||
(centerX - icon.intrinsicWidth / 2).toInt(),
|
||||
(centerY - icon.intrinsicHeight / 2).toInt(),
|
||||
(centerX + icon.intrinsicWidth / 2).toInt(),
|
||||
(centerY + icon.intrinsicHeight / 2).toInt(),
|
||||
)
|
||||
icon.draw(canvas)
|
||||
}
|
||||
|
||||
private fun drawSymbol(canvas: Canvas, text: String, k: Key, isTop: Boolean = true) {
|
||||
val showSymbol = rime.run { !getRuntimeOption("_hide_key_symbol") }
|
||||
val showHint = rime.run { !getRuntimeOption("_hide_key_hint") }
|
||||
|
||||
if (isTop && !showSymbol) return
|
||||
if (!isTop && !showHint) return
|
||||
|
||||
val textColor = k.getSymbolColor()
|
||||
val textSize = if (k.symbolTextSize > 0) sp(k.symbolTextSize) else sp(12f)
|
||||
|
||||
if (text.isIconFont) {
|
||||
drawIcon(canvas, text, textSize, textColor)
|
||||
} else {
|
||||
symbolPaint.apply {
|
||||
color = textColor
|
||||
this.textSize = textSize
|
||||
typeface = FontManager.getTypeface("symbol_font")
|
||||
}
|
||||
|
||||
val fontMetrics = symbolPaint.fontMetrics
|
||||
val offsetX = if (isTop) k.keySymbolOffsetX else k.keyHintOffsetX
|
||||
val offsetY = if (isTop) k.keySymbolOffsetY else k.keyHintOffsetY
|
||||
val centerX = (width - paddingLeft - paddingRight) / 2f + paddingLeft + sp(offsetX)
|
||||
val centerY = if (isTop) {
|
||||
paddingTop - fontMetrics.top + sp(offsetY)
|
||||
} else {
|
||||
height - paddingBottom - fontMetrics.bottom + sp(offsetY)
|
||||
}
|
||||
|
||||
canvas.drawText(text, centerX, centerY, symbolPaint)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -17,7 +17,6 @@ import com.osfans.trime.util.appContext
|
||||
import splitties.bitflags.hasFlag
|
||||
import splitties.dimensions.dp
|
||||
import splitties.systemservices.windowManager
|
||||
import timber.log.Timber
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.pow
|
||||
|
||||
@ -27,18 +26,18 @@ class Keyboard(
|
||||
private val theme: Theme,
|
||||
selfConfig: TextKeyboard? = null,
|
||||
) {
|
||||
// TODO: Apply proper dp unit conversion for keyHeight, verticalGap, and horizontalGap in a future breaking change
|
||||
|
||||
/** 按鍵默認水平間距 */
|
||||
private val horizontalGap: Int =
|
||||
internal val horizontalGap: Int =
|
||||
intArrayOf(
|
||||
selfConfig?.horizontalGap ?: 0,
|
||||
theme.generalStyle.horizontalGap,
|
||||
).firstOrNull { it > 0 } ?: 0
|
||||
).firstOrNull { it > 0 }?.let { appContext.dp(it) } ?: 0
|
||||
|
||||
/** 默認鍵寬 */
|
||||
private val keyWidth: Int = (allowedWidth * theme.generalStyle.keyWidth / 100).toInt()
|
||||
|
||||
/** 默認鍵高 */
|
||||
/** 默認鍵高 (NOTE: 无需 dp 转换,会被 keyboardHeight 等比例缩放) */
|
||||
private val keyHeight: Int =
|
||||
intArrayOf(
|
||||
selfConfig?.height?.toInt() ?: 0,
|
||||
@ -46,11 +45,11 @@ class Keyboard(
|
||||
).firstOrNull { it > 0 } ?: 0
|
||||
|
||||
/** 默認行距 */
|
||||
private val verticalGap: Int =
|
||||
internal val verticalGap: Int =
|
||||
intArrayOf(
|
||||
selfConfig?.verticalGap ?: 0,
|
||||
theme.generalStyle.verticalGap,
|
||||
).firstOrNull { it > 0 } ?: 0
|
||||
).firstOrNull { it > 0 }?.let { appContext.dp(it) } ?: 0
|
||||
|
||||
/** 默認按鍵圓角半徑 */
|
||||
val roundCorner: Float =
|
||||
@ -88,6 +87,8 @@ class Keyboard(
|
||||
var modifier = 0
|
||||
private set
|
||||
|
||||
var firstPressedKeyIndex: Int = -1
|
||||
|
||||
/** Width of the screen available to fit the keyboard */
|
||||
private val allowedWidth: Int
|
||||
get() {
|
||||
@ -135,7 +136,6 @@ class Keyboard(
|
||||
val isLock = selfConfig?.lock ?: false // 切換程序時記憶鍵盤
|
||||
val asciiKeyboard: String? = selfConfig?.asciiKeyboard // 英文鍵盤
|
||||
|
||||
// todo 把按下按键弹出的内容改为单独设计的view,而不是keyboard
|
||||
val keyboardHeight: Int =
|
||||
intArrayOf(
|
||||
selfConfig?.let { getKeyboardHeightFromKeyboardConfig(it) } ?: 0,
|
||||
@ -143,167 +143,187 @@ class Keyboard(
|
||||
).firstOrNull { it > 0 } ?: 0
|
||||
|
||||
init {
|
||||
|
||||
if (selfConfig != null) {
|
||||
val columns = selfConfig.columns
|
||||
// 按键高度取值顺序: keys > keyboard/height > style/key_height
|
||||
// 考虑到key设置height_land需要对皮肤做大量修改,而当部分key设置height而部分没有设时会造成按键高度异常,故取消普通按键的height_land参数
|
||||
var rowHeight = keyHeight
|
||||
// 定义 新的键盘尺寸计算方式, 避免尺寸计算不恰当,导致切换键盘时键盘高度发生变化,UI闪烁的问题。同时可以快速调整整个键盘的尺寸
|
||||
// 1. default键盘的高度 = 其他键盘的高度
|
||||
// 2. 当键盘高度(不含padding)与keyboard_height不一致时,每行按键等比例缩放按键高度高度,行之间的间距向上取整数、padding不缩放;
|
||||
// 3. 由于高度只能取整数,缩放后仍然存在余数的,由 auto_height_index 指定的行吸收(遵循四舍五入)
|
||||
// 特别的,当值为负数时,为倒序序号(-1即倒数第一个);当值大于按键行数时,为最后一行
|
||||
val autoHeightIndex = selfConfig.autoHeightIndex
|
||||
|
||||
fun firstNonZero(a: Float, b: Float, c: Float): Float = if (a != 0f) {
|
||||
a
|
||||
} else if (b != 0f) {
|
||||
b
|
||||
} else {
|
||||
c
|
||||
}
|
||||
|
||||
val keys = selfConfig.keys
|
||||
val keyboardKeyWidth = selfConfig.width
|
||||
val maxColumns = if (columns == -1) Int.MAX_VALUE else columns
|
||||
val isSplit = appContext.isLandscapeMode() && landscapePercent > 0
|
||||
val (rowWidthTotalWeight, oneWeightWidthPx, multiplier, scaledHeight, scaledVerticalGap) =
|
||||
KeyboardSizeCalculator(
|
||||
isSplit,
|
||||
landscapePercent,
|
||||
maxColumns,
|
||||
allowedWidth,
|
||||
keyboardHeight,
|
||||
keyboardKeyWidth,
|
||||
keyHeight,
|
||||
horizontalGap,
|
||||
verticalGap,
|
||||
autoHeightIndex,
|
||||
).calc(keys)
|
||||
|
||||
var x = this.horizontalGap / 2
|
||||
var y = scaledVerticalGap
|
||||
var row = 0
|
||||
val maxColumns = if (selfConfig.columns == -1) Int.MAX_VALUE else selfConfig.columns
|
||||
|
||||
val isSplit = appContext.isLandscapeMode() && landscapePercent > 0
|
||||
val splitRatio = if (isSplit) landscapePercent / 100f else 0f
|
||||
|
||||
val oneWeightWidthPx =
|
||||
allowedWidth.toFloat() / (MAX_TOTAL_WEIGHT * (1 + splitRatio))
|
||||
|
||||
// total width weight for each row.
|
||||
val rowWidthTotalWeight = mutableListOf<Float>()
|
||||
|
||||
// raw height of each row before scaling
|
||||
val rowRawHeight = mutableListOf<Int>()
|
||||
|
||||
var x = 0
|
||||
var column = 0
|
||||
var rowHeight = keyHeight
|
||||
var totalKeyWidth = 0f
|
||||
|
||||
// determine row count, row heights, total row weights; does not create Key objects
|
||||
for (key in keys) {
|
||||
|
||||
// determine the width weight of this key
|
||||
val keyWidthWeight =
|
||||
if (key.width == 0f && key.click.isNotEmpty()) keyboardKeyWidth else key.width
|
||||
|
||||
val widthPx = (keyWidthWeight * allowedWidth / MAX_TOTAL_WEIGHT).toInt()
|
||||
|
||||
// wrap to next row if column or width limit is reached
|
||||
if (column >= maxColumns || x + widthPx > allowedWidth) {
|
||||
rowWidthTotalWeight.add(totalKeyWidth)
|
||||
rowRawHeight.add(rowHeight)
|
||||
x = 0
|
||||
column = 0
|
||||
totalKeyWidth = 0f
|
||||
}
|
||||
|
||||
// first key of a row defines the row height
|
||||
if (column == 0) {
|
||||
rowHeight = if (key.height > 0) key.height.toInt() else keyHeight
|
||||
}
|
||||
|
||||
totalKeyWidth += keyWidthWeight
|
||||
|
||||
// only clickable keys count toward column count
|
||||
if (key.click.isNotEmpty()) {
|
||||
column++
|
||||
}
|
||||
|
||||
x += widthPx
|
||||
}
|
||||
|
||||
rowWidthTotalWeight.add(totalKeyWidth)
|
||||
rowRawHeight.add(rowHeight)
|
||||
|
||||
val rows = rowRawHeight.size
|
||||
val rawHeightSum = rowRawHeight.sum()
|
||||
|
||||
// scaled row heights after fitting into keyboardHeight
|
||||
val rowHeightScaled = MutableList(rows) { 0 }
|
||||
|
||||
var remainHeight = keyboardHeight
|
||||
val scale = keyboardHeight.toFloat() / rawHeightSum
|
||||
|
||||
// scale row heights to keyboardHeight; last row absorbs rounding errors
|
||||
for (i in 0 until rows - 1) {
|
||||
val h = (rowRawHeight[i] * scale).toInt()
|
||||
rowHeightScaled[i] = h
|
||||
remainHeight -= h
|
||||
}
|
||||
|
||||
rowHeightScaled[rows - 1] = remainHeight
|
||||
|
||||
var xPos = 0
|
||||
var yPos = 0
|
||||
|
||||
var row = 0
|
||||
column = 0
|
||||
|
||||
var rowWeightAccumulo = 0f
|
||||
var currentRowHeight = rowHeightScaled[0]
|
||||
|
||||
// indicates whether the split gap has been inserted in the current row
|
||||
var splitInserted = false
|
||||
|
||||
minWidth = 0
|
||||
|
||||
try {
|
||||
var rowWidthWeight = 0f
|
||||
for (textKey in keys) {
|
||||
val gap = this.horizontalGap
|
||||
val keyWidth =
|
||||
if (textKey.width == 0f && textKey.click.isNotEmpty()) {
|
||||
keyboardKeyWidth
|
||||
} else {
|
||||
textKey.width
|
||||
}
|
||||
var widthPx = (keyWidth * oneWeightWidthPx).toInt()
|
||||
widthPx -= gap
|
||||
if (column >= maxColumns || x + widthPx > allowedWidth) {
|
||||
// new row
|
||||
rowWidthWeight = 0f
|
||||
x = gap / 2
|
||||
y += scaledVerticalGap + rowHeight
|
||||
column = 0
|
||||
row++
|
||||
if (mKeys.isNotEmpty()) {
|
||||
mKeys[mKeys.size - 1].edgeFlags =
|
||||
mKeys[mKeys.size - 1].edgeFlags or EDGE_RIGHT
|
||||
}
|
||||
}
|
||||
rowWidthWeight += keyWidth
|
||||
val totalWeightOfThisRow = rowWidthTotalWeight[row] ?: 0f
|
||||
if (isSplit && rowWidthWeight >= totalWeightOfThisRow / 2 + 1) {
|
||||
rowWidthWeight = Int.MIN_VALUE.toFloat()
|
||||
val weight = (totalWeightOfThisRow * multiplier * oneWeightWidthPx).toInt()
|
||||
if (keyWidth > 20) {
|
||||
// enlarge the key if this key is a long key
|
||||
widthPx += weight
|
||||
} else {
|
||||
x += weight // (10 * (defaultWidth));
|
||||
}
|
||||
}
|
||||
if (column == 0) {
|
||||
rowHeight =
|
||||
if (keyboardHeight > 0) {
|
||||
scaledHeight[row]
|
||||
} else {
|
||||
if (textKey.height > 0) textKey.height.toInt() else keyHeight
|
||||
}
|
||||
}
|
||||
if (textKey.click.isEmpty()) { // 無按鍵事件
|
||||
x += widthPx + gap
|
||||
continue // 縮進
|
||||
}
|
||||
val key = Key(this, textKey)
|
||||
key.keyTextOffsetX =
|
||||
floatArrayOf(
|
||||
textKey.keyTextOffsetX,
|
||||
selfConfig.keyTextOffsetX,
|
||||
theme.generalStyle.keyTextOffsetX,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keyTextOffsetY =
|
||||
floatArrayOf(
|
||||
textKey.keyTextOffsetY,
|
||||
selfConfig.keyTextOffsetY,
|
||||
theme.generalStyle.keyTextOffsetY,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keySymbolOffsetX =
|
||||
floatArrayOf(
|
||||
textKey.keySymbolOffsetX,
|
||||
selfConfig.keySymbolOffsetX,
|
||||
theme.generalStyle.keySymbolOffsetX,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keySymbolOffsetY =
|
||||
floatArrayOf(
|
||||
textKey.keySymbolOffsetY,
|
||||
selfConfig.keySymbolOffsetY,
|
||||
theme.generalStyle.keySymbolOffsetY,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keyHintOffsetX =
|
||||
floatArrayOf(
|
||||
textKey.keyHintOffsetX,
|
||||
selfConfig.keyHintOffsetX,
|
||||
theme.generalStyle.keyHintOffsetX,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keyHintOffsetY =
|
||||
floatArrayOf(
|
||||
textKey.keyHintOffsetY,
|
||||
selfConfig.keyHintOffsetY,
|
||||
theme.generalStyle.keyHintOffsetY,
|
||||
).firstOrNull { it != 0f } ?: 0f
|
||||
key.keyPressOffsetX =
|
||||
intArrayOf(
|
||||
textKey.keyPressOffsetX,
|
||||
selfConfig.keyPressOffsetX,
|
||||
theme.generalStyle.keyPressOffsetX,
|
||||
).firstOrNull { it != 0 } ?: 0
|
||||
key.keyPressOffsetY =
|
||||
intArrayOf(
|
||||
textKey.keyPressOffsetY,
|
||||
selfConfig.keyPressOffsetY,
|
||||
theme.generalStyle.keyPressOffsetY,
|
||||
).firstOrNull { it != 0 } ?: 0
|
||||
key.x = x
|
||||
key.y = y
|
||||
val rightGap = abs(allowedWidth - x - widthPx - gap / 2)
|
||||
// 右側不留白
|
||||
key.width =
|
||||
if (rightGap <= allowedWidth / 100) allowedWidth - x - gap / 2 else widthPx
|
||||
key.height = rowHeight
|
||||
key.gap = gap
|
||||
key.row = row
|
||||
key.column = column
|
||||
column++
|
||||
x += key.width + key.gap
|
||||
mKeys.add(key)
|
||||
if (x > minWidth) {
|
||||
minWidth = x
|
||||
// create Key objects, assign position, size, offsets
|
||||
for (textKey in keys) {
|
||||
|
||||
val keyWidthWeight =
|
||||
if (textKey.width == 0f && textKey.click.isNotEmpty()) keyboardKeyWidth else textKey.width
|
||||
|
||||
var widthPx = (keyWidthWeight * oneWeightWidthPx).toInt()
|
||||
|
||||
// wrap to next row if limits are exceeded
|
||||
if (column >= maxColumns || xPos + widthPx > allowedWidth) {
|
||||
xPos = 0
|
||||
yPos += currentRowHeight
|
||||
row++
|
||||
column = 0
|
||||
rowWeightAccumulo = 0f
|
||||
splitInserted = false
|
||||
currentRowHeight = rowHeightScaled[row]
|
||||
}
|
||||
|
||||
rowWeightAccumulo += keyWidthWeight
|
||||
|
||||
val totalWeight = rowWidthTotalWeight[row]
|
||||
|
||||
// if split keyboard layout is enabled, insert split gap at row middle when cumulative width ≥ 50%
|
||||
if (isSplit && !splitInserted && rowWeightAccumulo >= totalWeight * 0.5f) {
|
||||
splitInserted = true
|
||||
val gap = (totalWeight * splitRatio * oneWeightWidthPx).toInt()
|
||||
|
||||
// large keys absorb the gap; small keys shift right
|
||||
if (keyWidthWeight > 20f) {
|
||||
widthPx += gap
|
||||
} else {
|
||||
xPos += gap
|
||||
}
|
||||
}
|
||||
if (mKeys.isNotEmpty()) {
|
||||
mKeys[mKeys.size - 1].edgeFlags =
|
||||
mKeys[mKeys.size - 1].edgeFlags or EDGE_RIGHT
|
||||
|
||||
// spacer keys only move the cursor; no Key object is created
|
||||
if (textKey.click.isEmpty()) {
|
||||
xPos += widthPx
|
||||
continue
|
||||
}
|
||||
this.height = y + rowHeight + scaledVerticalGap
|
||||
for (key in mKeys) {
|
||||
if (key.column == 0) key.edgeFlags = key.edgeFlags or EDGE_LEFT
|
||||
if (key.row == 0) key.edgeFlags = key.edgeFlags or EDGE_TOP
|
||||
if (key.row == row) key.edgeFlags = key.edgeFlags or EDGE_BOTTOM
|
||||
|
||||
val key = Key(this, textKey)
|
||||
|
||||
key.keyTextOffsetX = firstNonZero(textKey.keyTextOffsetX, selfConfig.keyTextOffsetX, theme.generalStyle.keyTextOffsetX)
|
||||
key.keyTextOffsetY = firstNonZero(textKey.keyTextOffsetY, selfConfig.keyTextOffsetY, theme.generalStyle.keyTextOffsetY)
|
||||
key.keySymbolOffsetX = firstNonZero(textKey.keySymbolOffsetX, selfConfig.keySymbolOffsetX, theme.generalStyle.keySymbolOffsetX)
|
||||
key.keySymbolOffsetY = firstNonZero(textKey.keySymbolOffsetY, selfConfig.keySymbolOffsetY, theme.generalStyle.keySymbolOffsetY)
|
||||
key.keyHintOffsetX = firstNonZero(textKey.keyHintOffsetX, selfConfig.keyHintOffsetX, theme.generalStyle.keyHintOffsetX)
|
||||
key.keyHintOffsetY = firstNonZero(textKey.keyHintOffsetY, selfConfig.keyHintOffsetY, theme.generalStyle.keyHintOffsetY)
|
||||
|
||||
key.x = xPos
|
||||
key.y = yPos
|
||||
|
||||
// correct minor rounding errors on the right edge
|
||||
val rightGap = abs(allowedWidth - xPos - widthPx)
|
||||
key.width = if (rightGap <= allowedWidth / 100) allowedWidth - xPos else widthPx
|
||||
|
||||
key.height = currentRowHeight
|
||||
key.row = row
|
||||
key.column = column
|
||||
|
||||
column++
|
||||
xPos += key.width
|
||||
|
||||
mKeys.add(key)
|
||||
|
||||
if (xPos > minWidth) {
|
||||
minWidth = xPos
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "Failed to create keyboard")
|
||||
}
|
||||
|
||||
mKeys.lastOrNull()?.edgeFlags = mKeys.lastOrNull()?.edgeFlags?.or(EDGE_RIGHT) ?: 0
|
||||
|
||||
height = yPos + currentRowHeight
|
||||
|
||||
for (key in mKeys) {
|
||||
if (key.column == 0) key.edgeFlags = key.edgeFlags or EDGE_LEFT
|
||||
if (key.row == 0) key.edgeFlags = key.edgeFlags or EDGE_TOP
|
||||
if (key.row == row) key.edgeFlags = key.edgeFlags or EDGE_BOTTOM
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -492,6 +512,7 @@ class Keyboard(
|
||||
private const val GRID_WIDTH = 10
|
||||
private const val GRID_HEIGHT = 5
|
||||
private const val GRID_SIZE = GRID_WIDTH * GRID_HEIGHT
|
||||
private const val MAX_TOTAL_WEIGHT = 100
|
||||
|
||||
/** Number of key widths from current touch point to search for nearest keys. */
|
||||
const val SEARCH_DISTANCE = 1.4f
|
||||
|
||||
@ -13,7 +13,7 @@ interface KeyboardActionListener {
|
||||
* @param keyEventCode the unicode of the key being pressed. If the touch is not on a valid key,
|
||||
* the value will be zero.
|
||||
*/
|
||||
fun onPress(keyEventCode: Int, isSound: Boolean = true)
|
||||
fun onPress(keyEventCode: Int)
|
||||
|
||||
/**
|
||||
* Called when the user releases a key. This is sent after the [.onKey] is called. For
|
||||
|
||||
@ -1,380 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2015 - 2025 Rime community
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
package com.osfans.trime.ime.keyboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.os.SystemClock
|
||||
import android.util.SparseArray
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.util.isNotEmpty
|
||||
import androidx.core.util.size
|
||||
import androidx.lifecycle.findViewTreeLifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.osfans.trime.data.prefs.AppPrefs
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.max
|
||||
|
||||
open class KeyboardGestureFrame(context: Context) : FrameLayout(context) {
|
||||
|
||||
private data class PointerState(
|
||||
var keyIndex: Int = -1,
|
||||
var startX: Float = 0f,
|
||||
var startY: Float = 0f,
|
||||
var lastX: Float = 0f,
|
||||
var startTime: Long = 0L,
|
||||
var isLongPressed: Boolean = false,
|
||||
var shouldPerformSwipe: Boolean = false,
|
||||
var slideActivated: Boolean = false,
|
||||
var longPressJob: Job? = null,
|
||||
var repeatJob: Job? = null,
|
||||
var lastSwipeBehavior: KeyBehavior? = null,
|
||||
var currentX: Float = 0f,
|
||||
var currentY: Float = 0f,
|
||||
)
|
||||
|
||||
private val lifecycleScope by lazy {
|
||||
findViewTreeLifecycleOwner()?.lifecycleScope!!
|
||||
}
|
||||
|
||||
private val activePointers = SparseArray<PointerState>()
|
||||
private val lastTapTimes = SparseArray<Long>()
|
||||
|
||||
var onKeyActionListener: ((keyIndex: Int, behavior: KeyBehavior) -> Boolean)? = null
|
||||
var onKeySlideListener: ((keyIndex: Int, deltaX: Int, x: Float, y: Float) -> Boolean)? = null
|
||||
var onKeyStateListener: ((keyIndex: Int, behavior: KeyBehavior, isVisible: Boolean, isPressed: Boolean, isRepeating: Boolean) -> Unit)? = null
|
||||
var onKeyReleaseListener: ((keyIndex: Int) -> Unit)? = null
|
||||
var onPopupChangeFocus: ((keyIndex: Int, x: Float, y: Float) -> Unit)? = null
|
||||
var onPopupSelected: ((keyIndex: Int) -> Unit)? = null
|
||||
|
||||
open fun getKeyIndex(x: Float, y: Float): Int = -1
|
||||
open fun isKeyRepeatable(keyIndex: Int): Boolean = false
|
||||
open fun isKeySlideCursor(keyIndex: Int): Boolean = false
|
||||
open fun isKeySlideDelete(keyIndex: Int): Boolean = false
|
||||
open fun hasAction(keyIndex: Int, behavior: KeyBehavior): Boolean = false
|
||||
open fun hasPopupKeys(keyIndex: Int): Boolean = false
|
||||
open val verticalCorrection: Int = 0
|
||||
|
||||
init {
|
||||
setWillNotDraw(false)
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
val x = event.getX(pointerIndex)
|
||||
val y = event.getY(pointerIndex)
|
||||
val adjustedY = if (y >= -verticalCorrection) y + verticalCorrection else y
|
||||
val keyIndex = getKeyIndex(x, adjustedY)
|
||||
if (keyIndex == -1) return true
|
||||
|
||||
val state = PointerState(
|
||||
keyIndex = keyIndex,
|
||||
startX = x,
|
||||
startY = y,
|
||||
lastX = x,
|
||||
currentX = x,
|
||||
currentY = y,
|
||||
startTime = SystemClock.elapsedRealtime(),
|
||||
)
|
||||
activePointers.put(pointerId, state)
|
||||
|
||||
onKeyStateListener?.invoke(keyIndex, KeyBehavior.CLICK, true, true, false)
|
||||
val hasLongClick = hasAction(keyIndex, KeyBehavior.LONG_CLICK)
|
||||
val hasPopup = hasPopupKeys(keyIndex)
|
||||
if (hasLongClick || hasPopup || isKeyRepeatable(keyIndex)) launchLongPressJob(pointerId, state)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
for (i in 0 until event.pointerCount) {
|
||||
val pointerId = event.getPointerId(i)
|
||||
val state = activePointers.get(pointerId) ?: continue
|
||||
val keyIndex = state.keyIndex
|
||||
if (keyIndex == -1) continue
|
||||
val x = event.getX(i)
|
||||
val y = event.getY(i)
|
||||
val dx = x - state.startX
|
||||
val dy = y - state.startY
|
||||
state.currentX = x
|
||||
state.currentY = y
|
||||
|
||||
if (!swipeEnabled) {
|
||||
val currentKeyIndex = getKeyIndex(x, y)
|
||||
if (currentKeyIndex != -1 && currentKeyIndex != state.keyIndex) {
|
||||
activateKeyFeedback(currentKeyIndex)
|
||||
deactivateKeyFeedback(state.keyIndex)
|
||||
state.keyIndex = currentKeyIndex
|
||||
}
|
||||
state.longPressJob?.cancel()
|
||||
continue
|
||||
}
|
||||
|
||||
if (!state.isLongPressed && swipeEnabled) {
|
||||
val behavior = getSwipeBehavior(state, dx, dy)
|
||||
if (state.lastSwipeBehavior != behavior) {
|
||||
state.lastSwipeBehavior = behavior
|
||||
activateKeyFeedback(keyIndex, behavior)
|
||||
}
|
||||
}
|
||||
|
||||
if ((isKeySlideCursor(keyIndex) || isKeySlideDelete(keyIndex)) && onKeySlideListener != null && !state.isLongPressed) {
|
||||
if (!state.slideActivated) {
|
||||
if (abs(dx) >= swipeTravel) {
|
||||
state.slideActivated = true
|
||||
state.lastX = state.startX + (if (dx > 0) swipeTravel else -swipeTravel)
|
||||
}
|
||||
}
|
||||
|
||||
if (state.slideActivated) {
|
||||
val startX = state.startX
|
||||
val lastX = state.lastX
|
||||
val totalPast = getNStep(startX, lastX, slideStepSize.toFloat())
|
||||
val totalNow = getNStep(startX, x, slideStepSize.toFloat())
|
||||
val delta = totalNow - totalPast
|
||||
if (delta != 0) {
|
||||
onKeySlideListener?.invoke(keyIndex, delta, x, y)
|
||||
state.lastX = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (state.isLongPressed) {
|
||||
onPopupChangeFocus?.invoke(keyIndex, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP, MotionEvent.ACTION_POINTER_UP -> {
|
||||
val pointerIndex = event.actionIndex
|
||||
val pointerId = event.getPointerId(pointerIndex)
|
||||
val state = activePointers.get(pointerId) ?: return true
|
||||
activePointers.delete(pointerId)
|
||||
val keyIndex = state.keyIndex
|
||||
if (keyIndex == -1) return true
|
||||
val x = event.getX(pointerIndex)
|
||||
val y = event.getY(pointerIndex)
|
||||
|
||||
if (state.slideActivated) onKeySlideListener?.invoke(keyIndex, 0, x, y)
|
||||
state.longPressJob?.cancel()
|
||||
state.repeatJob?.cancel()
|
||||
onKeyReleaseListener?.invoke(keyIndex)
|
||||
|
||||
val hasLazy = hasAction(keyIndex, KeyBehavior.LAZY_DOUBLE_CLICK)
|
||||
val hasDouble = hasAction(keyIndex, KeyBehavior.DOUBLE_CLICK)
|
||||
|
||||
if (state.isLongPressed) {
|
||||
onPopupSelected?.invoke(keyIndex)
|
||||
deactivateKeyFeedback(keyIndex, KeyBehavior.LONG_CLICK)
|
||||
return true
|
||||
}
|
||||
|
||||
if (state.shouldPerformSwipe && !state.slideActivated) {
|
||||
onKeyActionListener?.invoke(keyIndex, state.lastSwipeBehavior!!)
|
||||
deactivateKeyFeedback(keyIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
if (activePointers.isNotEmpty()) {
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.COMBO)
|
||||
deactivateKeyFeedback(keyIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
if (!hasLazy && !hasDouble) {
|
||||
if (!state.slideActivated) {
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.CLICK)
|
||||
}
|
||||
deactivateKeyFeedback(keyIndex)
|
||||
return true
|
||||
}
|
||||
|
||||
val now = SystemClock.elapsedRealtime()
|
||||
val lastTime = lastTapTimes.get(keyIndex, 0L)
|
||||
val timeDelta = now - lastTime
|
||||
if (hasLazy && !hasDouble) {
|
||||
if (timeDelta <= doubleTapTimeout) {
|
||||
lastTapTimes.delete(keyIndex)
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.LAZY_DOUBLE_CLICK)
|
||||
} else {
|
||||
val scheduledAt = now
|
||||
lastTapTimes.put(keyIndex, scheduledAt)
|
||||
lifecycleScope.launch {
|
||||
delay(doubleTapTimeout.toLong())
|
||||
if (lastTapTimes.get(keyIndex, -1L) == scheduledAt) {
|
||||
lastTapTimes.delete(keyIndex)
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.CLICK)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (timeDelta <= doubleTapTimeout) {
|
||||
lastTapTimes.delete(keyIndex)
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.DOUBLE_CLICK)
|
||||
} else if (!state.slideActivated) {
|
||||
lastTapTimes.put(keyIndex, now)
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.CLICK)
|
||||
}
|
||||
|
||||
deactivateKeyFeedback(keyIndex)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
onKeyReleaseListener?.invoke(-1)
|
||||
for (i in 0 until activePointers.size) {
|
||||
val state = activePointers.valueAt(i)
|
||||
state.longPressJob?.cancel()
|
||||
state.repeatJob?.cancel()
|
||||
deactivateKeyFeedback(state.keyIndex)
|
||||
}
|
||||
activePointers.clear()
|
||||
lastTapTimes.clear()
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun launchLongPressJob(pointerId: Int, state: PointerState) {
|
||||
val keyIndex = state.keyIndex
|
||||
state.longPressJob = lifecycleScope.launch {
|
||||
delay(longPressTimeout.toLong())
|
||||
if (activePointers.get(pointerId) != state || state.shouldPerformSwipe || state.slideActivated) return@launch
|
||||
state.isLongPressed = true
|
||||
if (isKeyRepeatable(keyIndex)) {
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.CLICK)
|
||||
launchRepeatClickJob(pointerId, state)
|
||||
} else {
|
||||
if (hasPopupKeys(keyIndex)) onKeyStateListener?.invoke(keyIndex, KeyBehavior.LONG_CLICK, true, true, false)
|
||||
if (onKeyActionListener?.invoke(keyIndex, KeyBehavior.LONG_CLICK) == true) {
|
||||
deactivateKeyFeedback(keyIndex, KeyBehavior.LONG_CLICK)
|
||||
activePointers.delete(pointerId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchRepeatClickJob(pointerId: Int, state: PointerState) {
|
||||
val keyIndex = state.keyIndex
|
||||
state.repeatJob = lifecycleScope.launch {
|
||||
try {
|
||||
delay(repeatInterval.toLong())
|
||||
while (activePointers.get(pointerId) == state) {
|
||||
onKeyActionListener?.invoke(keyIndex, KeyBehavior.CLICK)
|
||||
onKeyStateListener?.invoke(keyIndex, KeyBehavior.CLICK, false, false, true)
|
||||
delay(repeatInterval.toLong())
|
||||
}
|
||||
} finally {
|
||||
deactivateKeyFeedback(keyIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getSwipeBehavior(state: PointerState, dx: Float, dy: Float): KeyBehavior {
|
||||
val absDx = abs(dx)
|
||||
val absDy = abs(dy)
|
||||
val distance = max(absDx, absDy)
|
||||
val elapsed = SystemClock.elapsedRealtime() - state.startTime
|
||||
|
||||
val velocity = if (elapsed > 0) (distance / elapsed) * 1000f else 0f
|
||||
val isSwipe = distance >= swipeTravel || velocity >= swipeVelocity
|
||||
|
||||
val behavior = if (isSwipe) {
|
||||
if (absDx > absDy) {
|
||||
if (dx > 0) KeyBehavior.SWIPE_RIGHT else KeyBehavior.SWIPE_LEFT
|
||||
} else {
|
||||
if (dy > 0) KeyBehavior.SWIPE_DOWN else KeyBehavior.SWIPE_UP
|
||||
}
|
||||
} else {
|
||||
KeyBehavior.CLICK
|
||||
}
|
||||
|
||||
state.shouldPerformSwipe = isSwipe
|
||||
|
||||
return behavior
|
||||
}
|
||||
|
||||
fun getNStep(start: Float, end: Float, step: Float): Int = (if (start < end) 1 else -1) * floor(abs(end - start) / step).toInt()
|
||||
|
||||
private fun activateKeyFeedback(keyIndex: Int, behavior: KeyBehavior = KeyBehavior.CLICK) {
|
||||
onKeyStateListener?.invoke(keyIndex, behavior, true, false, false)
|
||||
}
|
||||
|
||||
private fun deactivateKeyFeedback(keyIndex: Int, behavior: KeyBehavior = KeyBehavior.CLICK) {
|
||||
onKeyStateListener?.invoke(keyIndex, behavior, false, false, false)
|
||||
}
|
||||
|
||||
private val touchPaint by lazy {
|
||||
if (DEBUG_SHOW_TOUCH_PATH) {
|
||||
Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
style = Paint.Style.FILL
|
||||
strokeWidth = 2f
|
||||
strokeCap = Paint.Cap.ROUND
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawForeground(canvas: Canvas) {
|
||||
super.onDrawForeground(canvas)
|
||||
if (touchPaint == null) return
|
||||
|
||||
for (i in 0 until activePointers.size) {
|
||||
val state = activePointers.valueAt(i)
|
||||
drawTouchPath(canvas, state.startX, state.startY, state.currentX, state.currentY)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawTouchPath(canvas: Canvas, startX: Float, startY: Float, endX: Float, endY: Float) {
|
||||
val p = touchPaint ?: return
|
||||
|
||||
p.color = 0xFF90CAF9.toInt()
|
||||
p.style = Paint.Style.FILL
|
||||
canvas.drawCircle(startX, startY, 6f, p)
|
||||
|
||||
p.color = 0xFF000000.toInt()
|
||||
p.style = Paint.Style.STROKE
|
||||
canvas.drawLine(startX, startY, endX, endY, p)
|
||||
|
||||
p.color = 0xFFFFCC80.toInt()
|
||||
p.style = Paint.Style.FILL
|
||||
canvas.drawCircle(endX, endY, 6f, p)
|
||||
|
||||
p.textSize = 28f
|
||||
p.style = Paint.Style.FILL
|
||||
p.alpha = 255
|
||||
p.color = 0xFF000000.toInt()
|
||||
|
||||
val startText = "x:${startX.toInt()} y:${startY.toInt()}"
|
||||
val endText = "x:${endX.toInt()} y:${endY.toInt()}"
|
||||
|
||||
canvas.drawText(startText, startX + 10f, startY + 30f, p)
|
||||
canvas.drawText(endText, endX + 10f, endY + 30f, p)
|
||||
|
||||
invalidate()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEBUG_SHOW_TOUCH_PATH = false
|
||||
|
||||
private val swipeEnabled by AppPrefs.defaultInstance().keyboard.swipeEnabled
|
||||
private val swipeTravel by AppPrefs.defaultInstance().keyboard.swipeTravel
|
||||
private val swipeVelocity by AppPrefs.defaultInstance().keyboard.swipeVelocity
|
||||
private val longPressTimeout by AppPrefs.defaultInstance().keyboard.longPressTimeout
|
||||
private val repeatInterval by AppPrefs.defaultInstance().keyboard.repeatInterval
|
||||
private val doubleTapTimeout by AppPrefs.defaultInstance().keyboard.doubleTapTimeout
|
||||
private val slideStepSize by AppPrefs.defaultInstance().keyboard.slideStepSize
|
||||
}
|
||||
}
|
||||
@ -1,13 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package com.osfans.trime.ime.keyboard
|
||||
|
||||
data class KeyboardSize(
|
||||
val rowWidthTotalWeight: Map<Int, Float>,
|
||||
val defaultWidth: Float,
|
||||
val multiplier: Float,
|
||||
val scaledHeight: List<Int>,
|
||||
val scaledVerticalGap: Int,
|
||||
)
|
||||
@ -1,145 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2015 - 2024 Rime community
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package com.osfans.trime.ime.keyboard
|
||||
|
||||
import com.osfans.trime.data.theme.model.TextKeyboard
|
||||
import com.osfans.trime.util.appContext
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
|
||||
class KeyboardSizeCalculator(
|
||||
isSplit: Boolean,
|
||||
splitPercent: Int,
|
||||
private val maxColumns: Int,
|
||||
private val mAllowedWidth: Int,
|
||||
private val keyboardHeight: Int,
|
||||
private val keyboardKeyWidth: Float,
|
||||
private val keyHeight: Int,
|
||||
private val mDefaultHorizontalGap: Int,
|
||||
private val mDefaultVerticalGap: Int,
|
||||
private val autoHeightIndex: Int,
|
||||
) {
|
||||
private val splitSpaceRatio: Float = if (isSplit) (splitPercent / 100f) else 0f
|
||||
|
||||
fun calc(keys: List<TextKeyboard.TextKey>): KeyboardSize {
|
||||
var x = mDefaultHorizontalGap / 2
|
||||
var y = 0
|
||||
var column = 0
|
||||
var row = 0
|
||||
|
||||
var rowHeight = keyHeight
|
||||
|
||||
var rawSumHeight = 0
|
||||
val rawHeight = ArrayList<Int>()
|
||||
|
||||
var totalKeyWidth = 0f
|
||||
var maxColumn = 0
|
||||
val rowTotalWeight = HashMap<Int, Float>()
|
||||
|
||||
for (key in keys) {
|
||||
val keyWidthWeight =
|
||||
if (key.width == 0f && key.click.isNotEmpty()) {
|
||||
keyboardKeyWidth
|
||||
} else {
|
||||
key.width
|
||||
}
|
||||
val widthPx =
|
||||
(keyWidthWeight * mAllowedWidth / MAX_TOTAL_WEIGHT).toInt() - mDefaultHorizontalGap
|
||||
if (column >= maxColumns || x + widthPx > mAllowedWidth) {
|
||||
maxColumn = maxOf(maxColumn, column)
|
||||
rowTotalWeight[row] = totalKeyWidth
|
||||
|
||||
x = mDefaultHorizontalGap / 2
|
||||
y += mDefaultVerticalGap + rowHeight
|
||||
totalKeyWidth = 0f
|
||||
column = 0
|
||||
row++
|
||||
rawSumHeight += rowHeight
|
||||
rawHeight.add(rowHeight)
|
||||
}
|
||||
|
||||
if (column == 0) {
|
||||
rowHeight = if (key.height > 0) key.height.toInt() else keyHeight
|
||||
}
|
||||
totalKeyWidth += keyWidthWeight
|
||||
if (key.click.isEmpty()) { // 無按鍵事件
|
||||
x += widthPx + mDefaultHorizontalGap
|
||||
continue // 縮進
|
||||
}
|
||||
column++
|
||||
val rightGap = abs(mAllowedWidth - x - widthPx - mDefaultHorizontalGap / 2)
|
||||
x += (
|
||||
if (rightGap <= mAllowedWidth / MAX_TOTAL_WEIGHT) {
|
||||
mAllowedWidth - x -
|
||||
mDefaultHorizontalGap / 2
|
||||
} else {
|
||||
widthPx
|
||||
}
|
||||
) + mDefaultHorizontalGap
|
||||
}
|
||||
rowTotalWeight[row] = totalKeyWidth
|
||||
|
||||
rawSumHeight += rowHeight
|
||||
rawHeight.add(rowHeight)
|
||||
|
||||
val scaledVerticalGap = calculateScaledVerticalGap(rawSumHeight, rawHeight)
|
||||
return KeyboardSize(
|
||||
rowTotalWeight,
|
||||
calculateOneWeightWidthPx(),
|
||||
splitSpaceRatio,
|
||||
calculateAdjustedHeight(rawSumHeight, rawHeight, scaledVerticalGap),
|
||||
scaledVerticalGap.toInt(),
|
||||
)
|
||||
}
|
||||
|
||||
private fun calculateOneWeightWidthPx(): Float = (mAllowedWidth / (MAX_TOTAL_WEIGHT * (1 + splitSpaceRatio)))
|
||||
|
||||
private fun calculateScaledVerticalGap(
|
||||
rawSumHeight: Int,
|
||||
rawHeight: List<Int>,
|
||||
): Double {
|
||||
val scale: Double =
|
||||
keyboardHeight.toDouble() / (rawSumHeight + mDefaultVerticalGap * (rawHeight.size + 1))
|
||||
|
||||
return ceil((mDefaultVerticalGap * scale))
|
||||
}
|
||||
|
||||
private fun calculateAdjustedHeight(
|
||||
rawSumHeight: Int,
|
||||
rawHeight: List<Int>,
|
||||
scaledVerticalGap: Double,
|
||||
): List<Int> {
|
||||
var remainHeight = keyboardHeight - scaledVerticalGap * (rawHeight.size + 1)
|
||||
|
||||
val scale = remainHeight / rawSumHeight
|
||||
|
||||
var finalAutoHeightIndex = -100
|
||||
if (autoHeightIndex < 0) {
|
||||
finalAutoHeightIndex = rawHeight.size + autoHeightIndex
|
||||
if (finalAutoHeightIndex < 0) finalAutoHeightIndex = 0
|
||||
} else if (autoHeightIndex >= rawHeight.size) {
|
||||
finalAutoHeightIndex = rawHeight.size - 1
|
||||
}
|
||||
|
||||
val newHeight = rawHeight.toMutableList()
|
||||
for (i in rawHeight.indices) {
|
||||
if (i != finalAutoHeightIndex) {
|
||||
val h: Int = (rawHeight[i] * scale).toInt()
|
||||
newHeight[i] = h
|
||||
remainHeight -= h
|
||||
}
|
||||
}
|
||||
if (remainHeight < 1) {
|
||||
if (rawHeight[finalAutoHeightIndex] > 0) remainHeight = 1.0
|
||||
}
|
||||
newHeight[finalAutoHeightIndex] = remainHeight.toInt()
|
||||
|
||||
return newHeight
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_TOTAL_WEIGHT = 100
|
||||
}
|
||||
}
|
||||
@ -7,626 +7,89 @@ package com.osfans.trime.ime.keyboard
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withClip
|
||||
import com.mikepenz.iconics.IconicsDrawable
|
||||
import com.mikepenz.iconics.utils.sizeDp
|
||||
import com.osfans.trime.daemon.RimeDaemon
|
||||
import com.osfans.trime.data.prefs.AppPrefs
|
||||
import com.osfans.trime.data.theme.ColorManager
|
||||
import com.osfans.trime.data.theme.FontManager
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.children
|
||||
import com.osfans.trime.data.theme.Theme
|
||||
import com.osfans.trime.ime.core.TrimeInputMethodService
|
||||
import com.osfans.trime.ime.popup.PopupAction
|
||||
import com.osfans.trime.ime.popup.PopupActionListener
|
||||
import com.osfans.trime.ime.popup.PopupDelegate
|
||||
import com.osfans.trime.util.sp
|
||||
import splitties.dimensions.dp
|
||||
import timber.log.Timber
|
||||
import kotlin.math.min
|
||||
import kotlin.math.pow
|
||||
|
||||
/** 顯示[鍵盤][Keyboard]及[按鍵][Key] */
|
||||
// TODO: move layout calculation responsibilities from Keyboard to KeyboardView using ConstraintLayout
|
||||
@SuppressLint("ViewConstructor")
|
||||
class KeyboardView(
|
||||
context: Context,
|
||||
private val theme: Theme,
|
||||
private val keyboard: Keyboard,
|
||||
private val popup: PopupDelegate,
|
||||
private val service: TrimeInputMethodService,
|
||||
) : KeyboardGestureFrame(context) {
|
||||
val popup: PopupDelegate,
|
||||
val service: TrimeInputMethodService,
|
||||
private val keyboardActionListener: KeyboardActionListener,
|
||||
) : FrameLayout(context) {
|
||||
|
||||
private val rime get() = RimeDaemon.getFirstSessionOrNull()!!
|
||||
private val keyTextSize = theme.generalStyle.keyTextSize
|
||||
private val labelTextSize =
|
||||
theme.generalStyle.keyLongTextSize
|
||||
.takeIf { it > 0 } ?: keyTextSize
|
||||
private val keys get() = keyboard.keys
|
||||
|
||||
private val symbolTextSize = theme.generalStyle.symbolTextSize
|
||||
private val mShadowRadius = theme.generalStyle.shadowRadius
|
||||
private val mShadowColor = ColorManager.getColor("shadow_color")
|
||||
private val keyBorderColor = ColorManager.getColor("key_border_color")
|
||||
internal var labelEnter: String = theme.generalStyle.enterLabel.default
|
||||
internal val keyTextSize = theme.generalStyle.keyTextSize
|
||||
internal val labelTextSize = theme.generalStyle.keyLongTextSize.takeIf { it > 0 } ?: keyTextSize
|
||||
|
||||
// Working variable
|
||||
private val originCoords = intArrayOf(0, 0)
|
||||
private val mKeys get() = keyboard.keys
|
||||
init {
|
||||
setWillNotDraw(false)
|
||||
buildKeyViews()
|
||||
}
|
||||
|
||||
var keyboardActionListener: KeyboardActionListener? = null
|
||||
override val verticalCorrection: Int
|
||||
get() = theme.generalStyle.verticalCorrection
|
||||
private var mProximityThreshold = 0
|
||||
private fun buildKeyViews() {
|
||||
removeAllViews()
|
||||
|
||||
/**
|
||||
* Enables or disables the key feedback popup. This is a popup that shows a magnified version of
|
||||
* the depressed key. By default the preview is enabled.
|
||||
*/
|
||||
private val showPreview by AppPrefs.defaultInstance().keyboard.popupOnKeyPress
|
||||
private val vibrateOnKeyRelease by AppPrefs.defaultInstance().keyboard.vibrateOnKeyRelease
|
||||
private val vibrateOnKeyRepeat by AppPrefs.defaultInstance().keyboard.vibrateOnKeyRepeat
|
||||
|
||||
private val deletedTextBuffer = ArrayDeque<String>()
|
||||
|
||||
/**
|
||||
* 是否允許距離校正 When enabled, calls to [KeyboardActionListener.onKey] will include key codes for
|
||||
* adjacent keys. When disabled, only the primary key code will be reported.
|
||||
*/
|
||||
private val enableProximityCorrection = theme.generalStyle.proximityCorrection
|
||||
|
||||
/** True if all keys should be drawn */
|
||||
private var invalidateAllKeys = false
|
||||
|
||||
/** The keys that should be drawn */
|
||||
private val invalidatedKeys = hashSetOf<Key>()
|
||||
|
||||
/** The dirty region in the keyboard bitmap */
|
||||
private val dirtyRect = Rect()
|
||||
|
||||
/** The keyboard bitmap for faster updates */
|
||||
private var drawingBuffer: Bitmap? = null
|
||||
|
||||
/** The canvas for the above mutable keyboard bitmap */
|
||||
private val drawingCanvas = Canvas()
|
||||
|
||||
private val basePaint =
|
||||
Paint().apply {
|
||||
isAntiAlias = true
|
||||
textAlign = Paint.Align.CENTER
|
||||
keys.forEachIndexed { index, key ->
|
||||
val keyView = createKeyView(index, key)
|
||||
addView(keyView)
|
||||
}
|
||||
}
|
||||
|
||||
private var labelEnter: String = theme.generalStyle.enterLabel.default
|
||||
private fun createKeyView(index: Int, key: Key): KeyView = KeyView(context, key = key, keyboard = keyboard, keyboardView = this, keyboardActionListener = keyboardActionListener).apply {
|
||||
id = index
|
||||
|
||||
layoutParams = LayoutParams(key.width, key.height)
|
||||
|
||||
translationX = key.x.toFloat()
|
||||
translationY = key.y.toFloat()
|
||||
|
||||
setPadding(
|
||||
keyboard.horizontalGap / 2,
|
||||
keyboard.verticalGap / 2,
|
||||
keyboard.horizontalGap / 2,
|
||||
keyboard.verticalGap / 2,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
val fullWidth = keyboard.minWidth + paddingLeft + paddingRight
|
||||
val fullHeight = keyboard.height + paddingTop + paddingBottom
|
||||
|
||||
val measuredWidth = minOf(
|
||||
MeasureSpec.getSize(widthMeasureSpec),
|
||||
fullWidth,
|
||||
)
|
||||
|
||||
measureChildren(widthMeasureSpec, heightMeasureSpec)
|
||||
setMeasuredDimension(measuredWidth, fullHeight)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
fun invalidateAllKeys() {
|
||||
children.forEach { it.invalidate() }
|
||||
}
|
||||
|
||||
fun onEnterKeyLabelUpdate(label: String) {
|
||||
labelEnter = label
|
||||
}
|
||||
|
||||
private val popupActionListener: PopupActionListener by lazy { popup.listener }
|
||||
|
||||
init {
|
||||
computeProximityThreshold(keyboard)
|
||||
invalidateAllKeys()
|
||||
|
||||
onKeyActionListener = { keyIndex, behavior ->
|
||||
if (behavior == KeyBehavior.LONG_CLICK && hasPopupKeys(keyIndex)) {
|
||||
val popupKeys = mKeys.get(keyIndex).popup
|
||||
val bounds = getKeyBounds(keyIndex)
|
||||
popupActionListener.onPopupAction(
|
||||
PopupAction.ShowKeyboardAction(keyIndex, popupKeys, bounds),
|
||||
)
|
||||
false
|
||||
} else {
|
||||
detectAndSendKey(keyIndex, behavior)
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
onKeySlideListener = { keyIndex, deltaX, x, y ->
|
||||
val key = mKeys.getOrNull(keyIndex)
|
||||
val ic = service.currentInputConnection
|
||||
|
||||
when {
|
||||
key?.click?.isSlideCursor == true -> {
|
||||
when {
|
||||
deltaX > 0 -> keyboardActionListener?.onAction(KeyAction("Right"))
|
||||
deltaX < 0 -> keyboardActionListener?.onAction(KeyAction("Left"))
|
||||
}
|
||||
true
|
||||
}
|
||||
key?.click?.isSlideDelete == true -> {
|
||||
when {
|
||||
deltaX < 0 -> {
|
||||
val beforeText = ic.getTextBeforeCursor(1, 0) ?: ""
|
||||
if (beforeText.isNotEmpty()) {
|
||||
deletedTextBuffer.addFirst(beforeText.toString())
|
||||
ic?.deleteSurroundingText(1, 0)
|
||||
}
|
||||
true
|
||||
}
|
||||
deltaX > 0 -> {
|
||||
if (deletedTextBuffer.isNotEmpty()) {
|
||||
ic?.commitText(deletedTextBuffer.removeFirst(), 1)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
onKeyStateListener = { keyIndex, behavior, isVisible, isPressed, isRepeating ->
|
||||
val key = mKeys.getOrNull(keyIndex)
|
||||
if (isPressed || (isRepeating && vibrateOnKeyRepeat)) keyboardActionListener?.onPress(key?.getCode(behavior) ?: 0, !isRepeating)
|
||||
if (!isRepeating) {
|
||||
if (isVisible) {
|
||||
key?.onPressed()
|
||||
invalidateKey(key)
|
||||
if (showPreview) showPopup(keyIndex, behavior)
|
||||
} else {
|
||||
key?.onReleased()
|
||||
invalidateKey(key)
|
||||
hidePopup(keyIndex)
|
||||
if (behavior == KeyBehavior.LONG_CLICK || vibrateOnKeyRelease) keyboardActionListener?.onPress(key?.getCode(behavior) ?: 0, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onKeyReleaseListener = { keyIndex ->
|
||||
deletedTextBuffer.clear()
|
||||
}
|
||||
|
||||
onPopupSelected = { keyIndex ->
|
||||
val triggerAction = PopupAction.TriggerAction(keyIndex)
|
||||
popupActionListener.onPopupAction(triggerAction)
|
||||
triggerAction.outAction?.let { action ->
|
||||
keyboardActionListener?.onAction(KeyAction(action))
|
||||
}
|
||||
}
|
||||
|
||||
onPopupChangeFocus = { keyIndex, x, y ->
|
||||
val key = mKeys.getOrNull(keyIndex)
|
||||
if (key != null) {
|
||||
val relativeX = x - key.x.toFloat()
|
||||
val relativeY = y - key.y.toFloat()
|
||||
|
||||
val changeFocusAction = PopupAction.ChangeFocusAction(keyIndex, relativeX, relativeY)
|
||||
popupActionListener.onPopupAction(changeFocusAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getKeyIndex(x: Float, y: Float): Int = getKeyIndices(x.toInt(), y.toInt())
|
||||
override fun isKeyRepeatable(keyIndex: Int): Boolean = mKeys.getOrNull(keyIndex)?.click?.isRepeatable ?: false
|
||||
override fun isKeySlideCursor(keyIndex: Int): Boolean = mKeys.getOrNull(keyIndex)?.click?.isSlideCursor ?: false
|
||||
override fun isKeySlideDelete(keyIndex: Int): Boolean = mKeys.getOrNull(keyIndex)?.click?.isSlideDelete ?: false
|
||||
override fun hasAction(keyIndex: Int, behavior: KeyBehavior): Boolean = mKeys.getOrNull(keyIndex)?.hasAction(behavior) ?: false
|
||||
override fun hasPopupKeys(keyIndex: Int): Boolean = mKeys.getOrNull(keyIndex)?.popup?.isNotEmpty() == true
|
||||
|
||||
private fun showPopup(keyIndex: Int, behavior: KeyBehavior = KeyBehavior.CLICK) {
|
||||
val key = mKeys.getOrNull(keyIndex) ?: return
|
||||
val bounds = getKeyBounds(keyIndex)
|
||||
val previewText = key.getPreviewText(behavior)
|
||||
|
||||
// 当preview显式为空时,不显示气泡提示
|
||||
if (previewText.isEmpty()) return
|
||||
|
||||
val context = if (previewText.isIconFont) previewText else String(Character.toChars(previewText.codePointAt(0)))
|
||||
popupActionListener.onPopupAction(
|
||||
PopupAction.PreviewAction(keyIndex, context, bounds),
|
||||
)
|
||||
}
|
||||
|
||||
private fun hidePopup(keyIndex: Int) {
|
||||
popupActionListener.onPopupAction(PopupAction.DismissAction(keyIndex))
|
||||
}
|
||||
|
||||
private fun getKeyBounds(keyIndex: Int): Rect {
|
||||
val key = mKeys.getOrNull(keyIndex) ?: return Rect()
|
||||
val location = intArrayOf(0, 0)
|
||||
getLocationInWindow(location)
|
||||
return Rect(
|
||||
key.x + location[0],
|
||||
key.y + location[1],
|
||||
key.x + key.width + location[0],
|
||||
key.y + key.height + location[1],
|
||||
)
|
||||
}
|
||||
|
||||
private fun setModifier(
|
||||
action: KeyAction,
|
||||
behavior: KeyBehavior,
|
||||
): Boolean = setModifier(action.isShiftLock xor (behavior == KeyBehavior.LONG_CLICK), action.modifierKeyOnMask)
|
||||
|
||||
private fun setModifier(
|
||||
on: Boolean,
|
||||
code: Int,
|
||||
): Boolean = keyboard.clickModifierKey(on, code).also { if (it) invalidateAllKeys() }
|
||||
|
||||
// 重置全部修饰键的状态(如果有锁定则不重置)
|
||||
private fun refreshModifier() {
|
||||
if (keyboard.refreshModifier()) {
|
||||
invalidateAllKeys()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回鍵盤是否爲大寫狀態
|
||||
*/
|
||||
val isCapsOn: Boolean
|
||||
get() = keyboard.mShiftKey?.isOn ?: false
|
||||
|
||||
public override fun onMeasure(
|
||||
widthMeasureSpec: Int,
|
||||
heightMeasureSpec: Int,
|
||||
) {
|
||||
// Round up a little
|
||||
val fullWidth = keyboard.minWidth + paddingLeft + paddingRight
|
||||
val fullHeight = keyboard.height + paddingTop + paddingBottom
|
||||
val measuredWidth =
|
||||
if (MeasureSpec.getSize(widthMeasureSpec) < fullWidth + 10) {
|
||||
MeasureSpec.getSize(widthMeasureSpec)
|
||||
} else {
|
||||
fullWidth
|
||||
}
|
||||
setMeasuredDimension(measuredWidth, fullHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算水平和豎直方向的相鄰按鍵中心的平均距離的平方,這樣不需要做開方運算
|
||||
*
|
||||
* @param keyboard 鍵盤
|
||||
*/
|
||||
private fun computeProximityThreshold(keyboard: Keyboard?) {
|
||||
if (keyboard == null && mKeys.isEmpty()) return
|
||||
val dimensionSum = mKeys.sumOf { key -> min(key.width, key.height) + key.gap }
|
||||
if (dimensionSum < 0) return
|
||||
mProximityThreshold = (dimensionSum * Keyboard.SEARCH_DISTANCE / mKeys.size).pow(2).toInt() // Square it
|
||||
}
|
||||
|
||||
public override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
if (canvas.isHardwareAccelerated) {
|
||||
onDrawKeyboard(canvas)
|
||||
return
|
||||
}
|
||||
val bufferNeedsUpdates = invalidateAllKeys || invalidatedKeys.isNotEmpty()
|
||||
if (bufferNeedsUpdates || drawingBuffer == null) {
|
||||
if (maybeAllocateDrawingBuffer()) {
|
||||
invalidateAllKeys = true
|
||||
drawingCanvas.setBitmap(drawingBuffer)
|
||||
}
|
||||
onDrawKeyboard(drawingCanvas)
|
||||
}
|
||||
canvas.drawBitmap(drawingBuffer!!, 0.0f, 0.0f, null)
|
||||
}
|
||||
|
||||
private fun maybeAllocateDrawingBuffer(): Boolean {
|
||||
if (width == 0 || height == 0) {
|
||||
return false
|
||||
}
|
||||
if (drawingBuffer != null && drawingBuffer!!.width == width && drawingBuffer!!.height == height) {
|
||||
return false
|
||||
}
|
||||
freeDrawingBuffer()
|
||||
drawingBuffer = createBitmap(width, height)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun freeDrawingBuffer() {
|
||||
drawingCanvas.setBitmap(null)
|
||||
drawingCanvas.setMatrix(null)
|
||||
if (drawingBuffer != null) {
|
||||
drawingBuffer!!.recycle()
|
||||
drawingBuffer = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDrawKeyboard(canvas: Canvas) {
|
||||
val paint = basePaint
|
||||
val drawAllKeys = invalidateAllKeys || invalidatedKeys.isEmpty()
|
||||
val isHardwareAccelerated = canvas.isHardwareAccelerated
|
||||
if (drawAllKeys || isHardwareAccelerated) {
|
||||
if (!isHardwareAccelerated && background != null) {
|
||||
canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
|
||||
background.draw(canvas)
|
||||
}
|
||||
for (key in mKeys) {
|
||||
onDrawKey(key, canvas, paint)
|
||||
}
|
||||
} else {
|
||||
for (key in invalidatedKeys) {
|
||||
if (!mKeys.contains(key)) continue
|
||||
if (background != null) {
|
||||
val x = key.x + paddingLeft
|
||||
val y = key.y + paddingTop
|
||||
dirtyRect.set(x, y, x + key.width, y + key.height)
|
||||
canvas.withClip(dirtyRect) {
|
||||
drawColor(Color.BLACK, PorterDuff.Mode.CLEAR)
|
||||
background.draw(this)
|
||||
}
|
||||
}
|
||||
onDrawKey(key, canvas, paint)
|
||||
}
|
||||
}
|
||||
|
||||
invalidatedKeys.clear()
|
||||
invalidateAllKeys = false
|
||||
}
|
||||
|
||||
private fun onDrawKey(
|
||||
key: Key,
|
||||
canvas: Canvas,
|
||||
paint: Paint,
|
||||
) {
|
||||
val keyDrawX = (key.x + paddingLeft).toFloat()
|
||||
val keyDrawY = (key.y + paddingTop).toFloat()
|
||||
canvas.translate(keyDrawX, keyDrawY)
|
||||
|
||||
drawKeyBackground(key, canvas)
|
||||
drawKeyLabel(key, canvas, paint)
|
||||
drawKeySymbol(key, canvas, paint)
|
||||
|
||||
canvas.translate(-keyDrawX, -keyDrawY)
|
||||
}
|
||||
|
||||
private fun drawKeyBackground(
|
||||
key: Key,
|
||||
canvas: Canvas,
|
||||
) {
|
||||
val keyBackground = key.getBackgroundDrawable()
|
||||
if (keyBackground != null) {
|
||||
if (keyBackground is GradientDrawable) {
|
||||
(key.roundCorner ?: keyboard.roundCorner).takeIf { it > 0f }?.let {
|
||||
keyBackground.cornerRadius = dp(it)
|
||||
}
|
||||
(key.keyBorder ?: keyboard.keyBorder).takeIf { it > 0 }?.let {
|
||||
keyBackground.setStroke(dp(it), keyBorderColor)
|
||||
}
|
||||
}
|
||||
onDrawKeyBackground(key, canvas, keyBackground)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawKeyLabel(
|
||||
key: Key,
|
||||
canvas: Canvas,
|
||||
paint: Paint,
|
||||
) {
|
||||
val keyLabel = key.getLabel().let {
|
||||
if (it == "enter_labels") labelEnter else it
|
||||
}
|
||||
if (keyLabel.isEmpty()) return
|
||||
|
||||
if (keyLabel.isIconFont) {
|
||||
val size = if (key.keyTextSize > 0) sp(key.keyTextSize) else sp(if (keyLabel.length > 1) labelTextSize else keyTextSize)
|
||||
drawIconLabel(key, keyLabel, canvas, size, centered = true)
|
||||
} else {
|
||||
drawTextLabel(key, keyLabel, canvas, paint)
|
||||
}
|
||||
}
|
||||
|
||||
private fun drawIconLabel(
|
||||
key: Key,
|
||||
label: String,
|
||||
canvas: Canvas,
|
||||
size: Float,
|
||||
color: Int = key.getTextColor(),
|
||||
offsetX: Float = key.keyTextOffsetX,
|
||||
offsetY: Float = key.keyTextOffsetY,
|
||||
centered: Boolean = true,
|
||||
) {
|
||||
val iconSize = size / resources.displayMetrics.density
|
||||
val icon = IconicsDrawable(context, label.toIconName()).apply {
|
||||
sizeDp = iconSize.toInt()
|
||||
colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN)
|
||||
}
|
||||
|
||||
val centerX = key.width / 2f - icon.intrinsicWidth / 2f
|
||||
val centerY = if (centered) key.height / 2f else key.height.toFloat()
|
||||
val x = (centerX + sp(offsetX)).toInt()
|
||||
val y = (centerY - icon.intrinsicHeight / 2f + sp(offsetY)).toInt()
|
||||
|
||||
icon.setBounds(x, y, x + icon.intrinsicWidth, y + icon.intrinsicHeight)
|
||||
icon.draw(canvas)
|
||||
}
|
||||
|
||||
private fun drawTextLabel(
|
||||
key: Key,
|
||||
label: String,
|
||||
canvas: Canvas,
|
||||
paint: Paint,
|
||||
) {
|
||||
paint.typeface = FontManager.getTypeface("key_font")
|
||||
paint.textSize = if (key.keyTextSize > 0) sp(key.keyTextSize) else sp(if (label.length > 1) labelTextSize else keyTextSize)
|
||||
paint.color = key.getTextColor()
|
||||
|
||||
val centerX = key.width * 0.5f
|
||||
val centerY = key.height * 0.5f
|
||||
val labelX = centerX + sp(key.keyTextOffsetX)
|
||||
val labelBaseline = centerY + sp(key.keyTextOffsetY)
|
||||
|
||||
if (mShadowRadius > 0f) {
|
||||
paint.setShadowLayer(mShadowRadius, 0f, 0f, mShadowColor)
|
||||
}
|
||||
|
||||
val adjustmentY = (paint.textSize - paint.descent()) / 2f
|
||||
canvas.drawText(label, labelX, labelBaseline + adjustmentY, paint)
|
||||
|
||||
paint.clearShadowLayer()
|
||||
}
|
||||
|
||||
private fun drawKeySymbol(
|
||||
key: Key,
|
||||
canvas: Canvas,
|
||||
paint: Paint,
|
||||
) {
|
||||
val showSymbol = rime.run { !getRuntimeOption("_hide_key_symbol") }
|
||||
val showHint = rime.run { !getRuntimeOption("_hide_key_hint") }
|
||||
if (!showSymbol && !showHint) return
|
||||
|
||||
val centerX = key.width / 2f
|
||||
val symbolSize = listOf(key.symbolTextSize, symbolTextSize).firstOrNull { it > 0f }?.let { sp(it) } ?: sp(symbolTextSize)
|
||||
|
||||
fun drawSymbolText(label: String, offsetX: Float, offsetY: Float, isTop: Boolean) {
|
||||
paint.apply {
|
||||
typeface = FontManager.getTypeface("symbol_font")
|
||||
textSize = symbolSize
|
||||
color = key.getSymbolColor()
|
||||
}
|
||||
val fontMetrics = paint.fontMetrics
|
||||
val y = if (isTop) -fontMetrics.top + sp(offsetY) else key.height - fontMetrics.bottom + sp(offsetY)
|
||||
canvas.drawText(label, centerX + sp(offsetX), y, paint)
|
||||
}
|
||||
|
||||
if (showSymbol && key.symbolLabel.isNotEmpty()) {
|
||||
val label = key.symbolLabel
|
||||
if (label.isIconFont) {
|
||||
drawIconLabel(key, label, canvas, symbolSize, key.getSymbolColor(), key.keySymbolOffsetX, key.keySymbolOffsetY, centered = false)
|
||||
} else {
|
||||
drawSymbolText(label, key.keySymbolOffsetX, key.keySymbolOffsetY, isTop = true)
|
||||
}
|
||||
}
|
||||
|
||||
if (showHint && key.hint.isNotEmpty()) {
|
||||
val label = key.hint
|
||||
if (label.isIconFont) {
|
||||
drawIconLabel(key, label, canvas, symbolSize, key.getSymbolColor(), key.keyHintOffsetX, key.keyHintOffsetY, centered = false)
|
||||
} else {
|
||||
drawSymbolText(label, key.keyHintOffsetX, key.keyHintOffsetY, isTop = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onDrawKeyBackground(
|
||||
key: Key,
|
||||
canvas: Canvas,
|
||||
background: Drawable,
|
||||
) {
|
||||
val padding = Rect().also { background.getPadding(it) }
|
||||
val bgWidth = key.width + padding.left + padding.right
|
||||
val bgHeight = key.height + padding.top + padding.bottom
|
||||
val bgX = -padding.left.toFloat()
|
||||
val bgY = -padding.top.toFloat()
|
||||
background.setBounds(0, 0, bgWidth, bgHeight)
|
||||
canvas.translate(bgX, bgY)
|
||||
background.draw(canvas)
|
||||
canvas.translate(-bgX, -bgY)
|
||||
}
|
||||
|
||||
private fun getKeyIndices(
|
||||
x: Int,
|
||||
y: Int,
|
||||
): Int {
|
||||
var primaryIndex = -1
|
||||
var closestKey = -1
|
||||
var closestKeyDist = mProximityThreshold + 1
|
||||
IntArray(MAX_NEARBY_KEYS).fill(Int.MAX_VALUE)
|
||||
val nearestKeyIndices = keyboard.getNearestKeys(x, y)
|
||||
for (nearestKeyIndex in nearestKeyIndices!!) {
|
||||
val key = mKeys[nearestKeyIndex]
|
||||
val isInside = key.isInside(x, y)
|
||||
if (isInside) {
|
||||
primaryIndex = nearestKeyIndex
|
||||
}
|
||||
val dist = key.squaredDistanceFrom(x, y)
|
||||
if (enableProximityCorrection && dist < mProximityThreshold || isInside) {
|
||||
// Find insertion point
|
||||
if (dist < closestKeyDist) {
|
||||
closestKeyDist = dist
|
||||
closestKey = nearestKeyIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
if (primaryIndex == -1) {
|
||||
primaryIndex = closestKey
|
||||
}
|
||||
return primaryIndex
|
||||
}
|
||||
|
||||
private fun releaseKey(code: Int) {
|
||||
Timber.d("releaseKey: keyCode=$code")
|
||||
keyboardActionListener?.onRelease(code)
|
||||
}
|
||||
|
||||
private val hookShiftArrow by AppPrefs.defaultInstance().keyboard.hookShiftArrow
|
||||
|
||||
fun isHookShiftArrow(keyCode: Int): Boolean {
|
||||
if (!hookShiftArrow) return false
|
||||
|
||||
return when (keyCode) {
|
||||
in KeyEvent.KEYCODE_DPAD_UP..KeyEvent.KEYCODE_DPAD_RIGHT -> true
|
||||
KeyEvent.KEYCODE_MOVE_HOME, KeyEvent.KEYCODE_MOVE_END -> true
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectAndSendKey(
|
||||
index: Int,
|
||||
behavior: KeyBehavior = KeyBehavior.CLICK,
|
||||
) {
|
||||
val key = mKeys.getOrNull(index) ?: return
|
||||
val action = key.getAction(behavior) ?: return
|
||||
Timber.d("detectAndSendKey: label=${key.getLabel()}, code=${action.code}, type=$behavior, modifier=${action.isModifierKey}")
|
||||
|
||||
if (action.isModifierKey) {
|
||||
setModifier(action, behavior)
|
||||
return
|
||||
}
|
||||
|
||||
keyboardActionListener?.onAction(action)
|
||||
releaseKey(action.code)
|
||||
if (!isHookShiftArrow(action.code)) refreshModifier()
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests a redraw of the entire keyboard. Calling [invalidate] is not sufficient because
|
||||
* the keyboard renders the keys to an off-screen buffer and an invalidate() only draws the cached
|
||||
* buffer.
|
||||
*
|
||||
* @see invalidateKey
|
||||
*/
|
||||
fun invalidateAllKeys() {
|
||||
Timber.d("invalidateAllKeys")
|
||||
invalidatedKeys.clear()
|
||||
invalidateAllKeys = true
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidates a key so that it will be redrawn on the next repaint. Use this method if only one
|
||||
* key is changing it's content. Any changes that affect the position or size of the key may not
|
||||
* be honored.
|
||||
*
|
||||
* @param key the key in the attached [Keyboard].
|
||||
* @see invalidateAllKeys
|
||||
*/
|
||||
private fun invalidateKey(key: Key?) {
|
||||
if (invalidateAllKeys || key == null) return
|
||||
invalidatedKeys.add(key)
|
||||
invalidate()
|
||||
}
|
||||
get() = keyboard.mShiftKey?.isOn == true
|
||||
|
||||
fun onDetach() {
|
||||
popup.dismissAll()
|
||||
freeDrawingBuffer()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
freeDrawingBuffer()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_NEARBY_KEYS = 12
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,7 +100,6 @@ class KeyboardWindow :
|
||||
currentKeyboardView?.also {
|
||||
it.onDetach()
|
||||
keyboardView.removeView(it)
|
||||
it.keyboardActionListener = null
|
||||
}
|
||||
currentKeyboard?.lastAsciiMode = rime.run { statusCached }.isAsciiMode
|
||||
}
|
||||
@ -120,7 +119,7 @@ class KeyboardWindow :
|
||||
|
||||
val config = selectKeyboardConfig(target)
|
||||
val keyboard = currentKeyboard ?: Keyboard(theme, config)
|
||||
val view = currentKeyboardView ?: KeyboardView(context, theme, keyboard, popup, service)
|
||||
val view = currentKeyboardView ?: KeyboardView(context, theme, keyboard, popup, service, keyboardActionListener)
|
||||
|
||||
if (currentKeyboard == null) {
|
||||
cachedKeyboards[target] = keyboard to view
|
||||
@ -147,7 +146,6 @@ class KeyboardWindow :
|
||||
}
|
||||
|
||||
view.let {
|
||||
it.keyboardActionListener = keyboardActionListener
|
||||
keyboardView.apply {
|
||||
(it.parent as? android.view.ViewGroup)?.removeView(it)
|
||||
add(it, lParams(matchParent, matchParent))
|
||||
@ -322,13 +320,9 @@ class KeyboardWindow :
|
||||
}
|
||||
|
||||
override fun onAttached() {
|
||||
currentKeyboardView?.keyboardActionListener = keyboardActionListener
|
||||
}
|
||||
|
||||
override fun onDetached() {
|
||||
currentKeyboardView?.let {
|
||||
it.onDetach()
|
||||
it.keyboardActionListener = null
|
||||
}
|
||||
currentKeyboardView?.onDetach()
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user