mirror of
https://github.com/florisboard/florisboard.git
synced 2024-09-19 19:42:20 +02:00
Merge pull request #288 from florisboard/feat-improve-moving-gestures
Improve moving gestures detection and UX
This commit is contained in:
commit
3e8a227320
@ -63,36 +63,59 @@ abstract class SwipeGesture {
|
||||
* @property listener The listener to report detected swipes to.
|
||||
*/
|
||||
class Detector(private val context: Context, private val listener: Listener) {
|
||||
private val eventList: MutableList<MotionEvent> = mutableListOf()
|
||||
private var indexFirst: Int = 0
|
||||
private var indexLastMoveRecognized: Int = 0
|
||||
private var firstMotionEvent: MotionEvent? = null
|
||||
private var lastMotionEvent: MotionEvent? = null
|
||||
private var absUnitCountX: Int = 0
|
||||
private var absUnitCountY: Int = 0
|
||||
private var unitWidth: Double = numericValue(context, DistanceThreshold.NORMAL) / 4.0
|
||||
|
||||
var distanceThreshold: DistanceThreshold = DistanceThreshold.NORMAL
|
||||
set(value) {
|
||||
field = value
|
||||
unitWidth = numericValue(context, value) / 4.0
|
||||
}
|
||||
var velocityThreshold: VelocityThreshold = VelocityThreshold.NORMAL
|
||||
|
||||
fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
/**
|
||||
* Method which evaluates if a given [event] is a gesture.
|
||||
*
|
||||
* @param event The MotionEvent which should be checked for a gesture.
|
||||
* @param alwaysTriggerOnMove Set to true if the moving detection algorithm should always
|
||||
* trigger, regardless of the distance from the previous event. Defaults to false.
|
||||
* @return True if the given [event] is a gesture, false otherwise.
|
||||
*/
|
||||
fun onTouchEvent(event: MotionEvent, alwaysTriggerOnMove: Boolean = false): Boolean {
|
||||
try {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN,
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
clearEventList()
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
resetState()
|
||||
firstMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
lastMotionEvent = firstMotionEvent
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
eventList.add(MotionEvent.obtainNoHistory(event))
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val lastEvent = eventList[indexLastMoveRecognized]
|
||||
val diffX = event.x - lastEvent.x
|
||||
val diffY = event.y - lastEvent.y
|
||||
val distanceThresholdNV = numericValue(context, distanceThreshold) / 4.0f
|
||||
return if (abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) {
|
||||
indexLastMoveRecognized = eventList.size - 1
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
val firstEvent = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
val lastEvent = lastMotionEvent ?: return false
|
||||
val relDiffX = event.x - lastEvent.x
|
||||
val relDiffY = event.y - lastEvent.y
|
||||
return if (alwaysTriggerOnMove || abs(relDiffX) > unitWidth || abs(relDiffY) > unitWidth) {
|
||||
lastMotionEvent = MotionEvent.obtainNoHistory(event)
|
||||
val direction = detectDirection(relDiffX.toDouble(), relDiffY.toDouble())
|
||||
val newAbsUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
val newAbsUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
val relUnitCountX = newAbsUnitCountX - absUnitCountX
|
||||
val relUnitCountY = newAbsUnitCountY - absUnitCountY
|
||||
absUnitCountX = newAbsUnitCountX
|
||||
absUnitCountY = newAbsUnitCountY
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_MOVE,
|
||||
diffX = event.x - firstEvent.x,
|
||||
diffY = event.y - firstEvent.y
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
relUnitCountX,
|
||||
relUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
@ -100,32 +123,36 @@ abstract class SwipeGesture {
|
||||
}
|
||||
MotionEvent.ACTION_UP,
|
||||
MotionEvent.ACTION_POINTER_UP -> {
|
||||
val firstEvent = eventList[indexFirst]
|
||||
val diffX = event.x - firstEvent.x
|
||||
val diffY = event.y - firstEvent.y
|
||||
val distanceThresholdNV = numericValue(context, distanceThreshold)
|
||||
val firstEvent = firstMotionEvent ?: return false
|
||||
val absDiffX = event.x - firstEvent.x
|
||||
val absDiffY = event.y - firstEvent.y
|
||||
/*val velocityThresholdNV = numericValue(velocityThreshold)
|
||||
val velocity =
|
||||
((convertPixelsToDp(
|
||||
sqrt(diffX.pow(2) + diffY.pow(2)),
|
||||
context
|
||||
) / event.downTime) * 10.0f.pow(8)).toInt()*/
|
||||
clearEventList()
|
||||
// return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV) && velocity >= velocityThresholdNV) {
|
||||
return if ((abs(diffX) > distanceThresholdNV || abs(diffY) > distanceThresholdNV)) {
|
||||
val direction = detectDirection(diffX.toDouble(), diffY.toDouble())
|
||||
val ret = if ((abs(absDiffX) > (unitWidth * 4.0) || abs(absDiffY) > (unitWidth * 4.0))) {
|
||||
val direction = detectDirection(absDiffX.toDouble(), absDiffY.toDouble())
|
||||
absUnitCountX = (absDiffX / unitWidth).toInt()
|
||||
absUnitCountY = (absDiffY / unitWidth).toInt()
|
||||
listener.onSwipe(Event(
|
||||
direction = direction,
|
||||
type = Type.TOUCH_UP,
|
||||
diffX = diffX,
|
||||
diffY = diffY
|
||||
absUnitCountX,
|
||||
absUnitCountY,
|
||||
absUnitCountX,
|
||||
absUnitCountY
|
||||
))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
resetState()
|
||||
return ret
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
clearEventList()
|
||||
resetState()
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
@ -175,22 +202,45 @@ abstract class SwipeGesture {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up and clears the event list.
|
||||
* Resets the state.
|
||||
*/
|
||||
private fun clearEventList() {
|
||||
for (event in eventList) {
|
||||
event.recycle()
|
||||
}
|
||||
eventList.clear()
|
||||
indexFirst = 0
|
||||
indexLastMoveRecognized = 0
|
||||
private fun resetState() {
|
||||
firstMotionEvent = null
|
||||
lastMotionEvent = null
|
||||
absUnitCountX = 0
|
||||
absUnitCountY = 0
|
||||
}
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
/**
|
||||
* An interface which provides an abstract callback function, which will be called for any
|
||||
* detected swipe event.
|
||||
*/
|
||||
fun interface Listener {
|
||||
fun onSwipe(event: Event): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class which describes a single gesture event.
|
||||
*/
|
||||
data class Event(
|
||||
/** The direction of the swipe. */
|
||||
val direction: Direction,
|
||||
/** The type of the swipe. */
|
||||
val type: Type,
|
||||
/** The unit count on the x-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the first event (ACTION_DOWN). */
|
||||
val absUnitCountY: Int,
|
||||
/** The unit count on the x-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountX: Int,
|
||||
/** The unit count on the y-axis, measured from the last event (ACTION_MOVE). */
|
||||
val relUnitCountY: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* ENum which defines the direction of the detected swipe.
|
||||
*/
|
||||
enum class Direction {
|
||||
UP_LEFT,
|
||||
UP,
|
||||
@ -202,13 +252,9 @@ abstract class SwipeGesture {
|
||||
LEFT,
|
||||
}
|
||||
|
||||
data class Event(
|
||||
val direction: Direction,
|
||||
val type: Type,
|
||||
val diffX: Float,
|
||||
val diffY: Float
|
||||
)
|
||||
|
||||
/**
|
||||
* Enum which defines the type of the gesture.
|
||||
*/
|
||||
enum class Type {
|
||||
TOUCH_UP,
|
||||
TOUCH_MOVE;
|
||||
|
@ -42,6 +42,7 @@ import dev.patrickgold.florisboard.ime.theme.ThemeManager
|
||||
import dev.patrickgold.florisboard.ime.theme.ThemeValue
|
||||
import dev.patrickgold.florisboard.util.*
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* View class for managing the rendering and the events of a single keyboard key.
|
||||
@ -61,6 +62,8 @@ class KeyView(
|
||||
field = value
|
||||
updateKeyPressedBackground()
|
||||
}
|
||||
private var initSelectionStart: Int = 0
|
||||
private var initSelectionEnd: Int = 0
|
||||
private var hasTriggeredGestureMove: Boolean = false
|
||||
private var keyHintMode: KeyHintMode = KeyHintMode.DISABLED
|
||||
private val longKeyPressHandler: Handler = Handler(context.mainLooper)
|
||||
@ -204,7 +207,11 @@ class KeyView(
|
||||
*/
|
||||
fun onFlorisTouchEvent(event: MotionEvent?): Boolean {
|
||||
if (event == null || !isEnabled) return false
|
||||
if (swipeGestureDetector.onTouchEvent(event)) {
|
||||
val alwaysTriggerOnMove = (hasTriggeredGestureMove
|
||||
&& florisboard?.activeEditorInstance?.isRawInputEditor == false
|
||||
&& (data.code == KeyCode.DELETE && prefs.gestures.deleteKeySwipeLeft == SwipeAction.DELETE_CHARACTERS_PRECISELY
|
||||
|| data.code == KeyCode.SPACE))
|
||||
if (swipeGestureDetector.onTouchEvent(event, alwaysTriggerOnMove)) {
|
||||
isKeyPressed = false
|
||||
longKeyPressHandler.cancelAll()
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
@ -246,6 +253,8 @@ class KeyView(
|
||||
}
|
||||
}
|
||||
if (data.code == KeyCode.SPACE) {
|
||||
initSelectionStart = florisboard?.activeEditorInstance?.selection?.start ?: 0
|
||||
initSelectionEnd = florisboard?.activeEditorInstance?.selection?.end ?: 0
|
||||
longKeyPressHandler.postDelayed((delayMillis * 2.5f).toLong()) {
|
||||
when (prefs.gestures.spaceBarLongPress) {
|
||||
SwipeAction.NO_ACTION,
|
||||
@ -290,7 +299,6 @@ class KeyView(
|
||||
repeatedKeyPressHandler.cancelAll()
|
||||
if (data.code != KeyCode.SHIFT) {
|
||||
if (hasTriggeredGestureMove && data.code == KeyCode.DELETE) {
|
||||
hasTriggeredGestureMove = false
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
if (selection.isSelectionMode) {
|
||||
deleteBackwards()
|
||||
@ -306,6 +314,7 @@ class KeyView(
|
||||
keyboardView.popupManager.hide()
|
||||
}
|
||||
}
|
||||
hasTriggeredGestureMove = false
|
||||
isKeyPressed = false
|
||||
}
|
||||
else -> return false
|
||||
@ -318,14 +327,14 @@ class KeyView(
|
||||
* defined in the prefs.
|
||||
*/
|
||||
override fun onSwipe(event: SwipeGesture.Event): Boolean {
|
||||
val florisboard = florisboard ?: return false
|
||||
return when (data.code) {
|
||||
KeyCode.DELETE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (prefs.gestures.deleteKeySwipeLeft) {
|
||||
SwipeAction.DELETE_CHARACTERS_PRECISELY -> {
|
||||
val charWidth = SwipeGesture.numericValue(context, swipeGestureDetector.distanceThreshold) / 4.0f
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
setSelection(
|
||||
(selection.end - (event.diffX.times(-1) / charWidth).toInt()).coerceIn(0, selection.end),
|
||||
(selection.end + event.absUnitCountX).coerceIn(0, selection.end),
|
||||
selection.end
|
||||
)
|
||||
}
|
||||
@ -335,7 +344,7 @@ class KeyView(
|
||||
}
|
||||
SwipeAction.DELETE_WORDS_PRECISELY -> when (event.direction) {
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftAppendWordToSelection()
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
@ -343,7 +352,7 @@ class KeyView(
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.activeEditorInstance?.apply {
|
||||
florisboard.activeEditorInstance.apply {
|
||||
leftPopWordFromSelection()
|
||||
}
|
||||
shouldBlockNextKeyCode = true
|
||||
@ -358,17 +367,46 @@ class KeyView(
|
||||
KeyCode.SPACE -> when (event.type) {
|
||||
SwipeGesture.Type.TOUCH_MOVE -> when (event.direction) {
|
||||
SwipeGesture.Direction.UP -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
if (event.absUnitCountY.times(-1) >= 6) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeUp)
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
SwipeGesture.Direction.LEFT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
if (prefs.gestures.spaceBarSwipeLeft == SwipeAction.MOVE_CURSOR_LEFT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedText.length)
|
||||
florisboard.activeEditorInstance.setSelection(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeLeft)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
SwipeGesture.Direction.RIGHT -> {
|
||||
florisboard?.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
if (prefs.gestures.spaceBarSwipeRight == SwipeAction.MOVE_CURSOR_RIGHT) {
|
||||
if (!florisboard.activeEditorInstance.isRawInputEditor) {
|
||||
val s = (initSelectionEnd + event.absUnitCountX).coerceIn(0, florisboard.activeEditorInstance.cachedText.length)
|
||||
florisboard.activeEditorInstance.setSelection(s, s)
|
||||
} else {
|
||||
for (n in 0 until abs(event.relUnitCountX)) {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
florisboard.executeSwipeAction(prefs.gestures.spaceBarSwipeRight)
|
||||
}
|
||||
hasTriggeredGestureMove = true
|
||||
shouldBlockNextKeyCode = true
|
||||
true
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user