1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3561 Editor | Enhancement | Touch detection improvements - for selection and dnd (#2291)

This commit is contained in:
Evgenii Kozlov 2025-04-11 16:31:36 +02:00 committed by GitHub
parent aa0ebbdfa5
commit 8e2e1444d6
Signed by: github
GPG key ID: B5690EEEBB952194
18 changed files with 122 additions and 98 deletions

View file

@ -7,6 +7,7 @@ import android.text.Editable
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager
import androidx.lifecycle.Lifecycle
@ -910,7 +911,9 @@ class BlockAdapter(
if (holder !is SupportCustomTouchProcessor) {
when (holder) {
is RelationBlockViewHolder -> {
val touchSlop = ViewConfiguration.get(holder.itemView.context).scaledTouchSlop
val processor = EditorTouchProcessor(
touchSlop = touchSlop,
fallback = { holder.itemView.onTouchEvent(it) },
onLongClick = {
val pos = holder.bindingAdapterPosition

View file

@ -9,140 +9,128 @@ import android.text.style.ClickableSpan
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import com.anytypeio.anytype.core_ui.extensions.disable
import com.anytypeio.anytype.core_ui.widgets.text.TextInputWidget
import timber.log.Timber
import kotlin.math.abs
/**
* @property [fallback] fallback method for processing touch event.
* @property [onLongClick] long click event (replacement for [View.OnLongClickListener])
* @property [onDragAndDropTrigger] drag-and-drop triggering event
* Handles touch gestures for editor interactions.
*
* @property touchSlop threshold for detecting drag motion.
* @property fallback fallback method for processing touch event.
* @property onLongClick long click event (replacement for [View.OnLongClickListener])
* @property onDragAndDropTrigger drag-and-drop triggering event
*/
class EditorTouchProcessor(
private val touchSlop: Int,
val fallback: (event: MotionEvent?) -> Boolean,
var onLongClick: () -> Unit = {},
var onDragAndDropTrigger: (event: MotionEvent?) -> Unit = { }
) {
val moves = mutableListOf<Float>()
private val moves = mutableListOf<Float>()
private val actionHandler = Handler(Looper.getMainLooper())
private var actionUpStartInMillis: Long = 0
private var lastEvent: MotionEvent? = null
private var isDragging: Boolean = false
private val dragAndDropTimeoutRunnable = Runnable {
Timber.d("Runnable triggered")
if (moves.size > 1) {
val first = moves.first()
val last = moves.last()
val delta = abs(first - last)
if (delta == 0f) {
Timber.d("Runnable dispatched 1")
onDragAndDropTrigger(lastEvent)
} else {
Timber.d("Runnable ignored 1")
}
} else {
Timber.d("Runnable dispatched 2")
val delta = if (moves.size > 1) abs(moves.last() - moves.first()) else 0f
if (!isDragging && delta <= touchSlop) {
Timber.d("Triggering drag due to long press without movement")
onDragAndDropTrigger(lastEvent)
} else if (isDragging) {
Timber.d("Triggering drag due to long press with movement")
onDragAndDropTrigger(lastEvent)
} else {
Timber.d("Skipping drag trigger")
}
moves.clear()
}
private var actionUpStartInMillis: Long = 0
private var lastEvent: MotionEvent? = null
fun process(v: View, event: MotionEvent?): Boolean {
if (event != null) {
lastEvent = event
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Timber.d("ACTION DOWN")
actionUpStartInMillis = SystemClock.elapsedRealtime()
actionHandler.postDelayed(
dragAndDropTimeoutRunnable,
DND_TIMEOUT
)
moves.clear()
}
MotionEvent.ACTION_MOVE -> {
Timber.d("ACTION MOVE: $event")
moves.add(event.getY(0))
if (moves.size > 1) {
val first = moves.first()
val last = moves.last()
Timber.d("ACTION MOVE DELTA: ${abs(last - first)}")
}
}
MotionEvent.ACTION_CANCEL -> {
if (moves.isEmpty() && event.elapsed() > DND_TIMEOUT) {
v.emulateHapticFeedback()
}
Timber.d("ACTION CANCEL")
actionHandler.removeCallbacksAndMessages(null)
moves.clear()
}
MotionEvent.ACTION_UP -> {
moves.clear()
Timber.d("ACTION UP")
actionHandler.removeCallbacksAndMessages(null)
event ?: return fallback(null)
lastEvent = event
/**
* When clicking on mention text, the code contains two separate logics
* that handle clicks on this text: the EditorTouchProcessor,
* which is responsible for triggering drag-and-drop or long-click mode,
* and the ClickableSpan, which is applied to the mention text.
* Since it is impossible to predict which listener will execute first,
* the click on the mention is handled in two different places.
* @see fun Editable.setClickableSpan(click: ((String) -> Unit)?, mark: Markup.Mark.Mention)
*/
if (v is TextInputWidget) {
val x = (event.x - v.totalPaddingLeft + v.scrollX).toInt()
val y = (event.y - v.totalPaddingTop + v.scrollY).toInt()
when (event.action) {
MotionEvent.ACTION_DOWN -> {
Timber.d("ACTION DOWN")
actionUpStartInMillis = SystemClock.elapsedRealtime()
isDragging = false
moves.clear()
actionHandler.postDelayed(dragAndDropTimeoutRunnable, DND_TIMEOUT)
}
val layout: Layout = v.layout
val line = layout.getLineForVertical(y)
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
val link =
v.editableText.getSpans(offset, offset, ClickableSpan::class.java)
if (link.isNotEmpty()) {
v.disable()
link[0].onClick(v)
return true
}
MotionEvent.ACTION_MOVE -> {
Timber.d("ACTION MOVE: $event")
val y = event.getY(0)
moves.add(y)
if (moves.size > 1) {
val delta = abs(moves.last() - moves.first())
Timber.d("ACTION MOVE DELTA: $delta")
if (delta > touchSlop) {
isDragging = true
}
return when (actionUpStartInMillis.untilNow()) {
in LONG_PRESS_TIMEOUT..DND_TIMEOUT -> {
onLongClick()
v.performLongClickWithHaptic()
true
}
else -> {
return fallback(event)
}
}
}
else -> {
Timber.d("Ignored motion event: $event")
}
}
MotionEvent.ACTION_CANCEL -> {
Timber.d("ACTION CANCEL")
if (!isDragging && event.elapsed() > DND_TIMEOUT) {
v.emulateHapticFeedback()
}
actionHandler.removeCallbacksAndMessages(null)
moves.clear()
}
MotionEvent.ACTION_UP -> {
Timber.d("ACTION UP")
actionHandler.removeCallbacksAndMessages(null)
moves.clear()
if (v is TextInputWidget) {
val x = (event.x - v.totalPaddingLeft + v.scrollX).toInt()
val y = (event.y - v.totalPaddingTop + v.scrollY).toInt()
val layout: Layout = v.layout
val line = layout.getLineForVertical(y)
val offset = layout.getOffsetForHorizontal(line, x.toFloat())
val link = v.editableText.getSpans(offset, offset, ClickableSpan::class.java)
if (link.isNotEmpty()) {
v.disable()
link[0].onClick(v)
return true
}
}
return when {
!isDragging && actionUpStartInMillis.untilNow() >= LONG_PRESS_TIMEOUT -> {
onLongClick()
v.performLongClickWithHaptic()
true
}
else -> fallback(event)
}
}
else -> {
Timber.d("Ignored motion event: $event")
}
}
return fallback(event)
}
companion object {
val LONG_PRESS_TIMEOUT: Long = ViewConfiguration.getLongPressTimeout().toLong()
val LONG_PRESS_TIMEOUT: Long = android.view.ViewConfiguration.getLongPressTimeout().toLong()
val DND_TIMEOUT: Long = 2 * LONG_PRESS_TIMEOUT
}
private fun View.emulateHapticFeedback() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (this !is TextInputWidget) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && this !is TextInputWidget) {
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
}
}
}

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.dataview
import android.text.Spannable
import android.text.SpannableString
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.TextView
import androidx.cardview.widget.CardView
@ -204,6 +205,7 @@ sealed class DataViewBlockViewHolder(
abstract override val decoratableCard: CardView
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.error
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.core.view.marginEnd
import androidx.core.view.marginStart
@ -28,6 +29,7 @@ abstract class MediaError(
abstract fun errorClick(item: BlockView.Error, clicked: (ListenerType) -> Unit)
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.media
import android.graphics.drawable.Drawable
import android.text.Spannable
import android.view.View
import android.view.ViewConfiguration
import android.widget.TextView
import com.anytypeio.anytype.core_ui.common.SearchHighlightSpan
import com.anytypeio.anytype.core_ui.common.SearchTargetHighlightSpan
@ -24,6 +25,7 @@ import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target
import kotlin.text.get
import timber.log.Timber
class Bookmark(val binding: ItemBlockBookmarkBinding) : Media(binding.root), DecoratableCardViewHolder {
@ -43,6 +45,7 @@ class Bookmark(val binding: ItemBlockBookmarkBinding) : Media(binding.root), Dec
override val decoratableCard: View = binding.bookmarkRoot
override val editorTouchProcessor: EditorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> clickContainer.onTouchEvent(e) }
)

View file

@ -2,6 +2,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.media
import android.annotation.SuppressLint
import android.view.View
import android.view.ViewConfiguration
import com.anytypeio.anytype.core_ui.extensions.setBlockBackgroundColor
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
import com.anytypeio.anytype.core_ui.features.editor.BlockViewHolder
@ -25,6 +26,7 @@ abstract class Media(view: View) : BlockViewHolder(view),
abstract fun select(isSelected: Boolean)
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> clickContainer.onTouchEvent(e) }
)

View file

@ -6,6 +6,7 @@ import android.os.Build.VERSION_CODES.N
import android.os.Build.VERSION_CODES.N_MR1
import android.text.Editable
import android.view.View
import android.view.ViewConfiguration
import android.view.inputmethod.InputMethodManager
import android.widget.FrameLayout
import android.widget.TextView
@ -43,6 +44,7 @@ class Code(
get() = binding.snippet
val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
@ -26,6 +27,7 @@ abstract class Divider(view: View) : BlockViewHolder(view),
abstract val container: View
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.text.Spannable
import android.text.SpannableString
import android.text.style.LeadingMarginSpan
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams
@ -42,6 +43,7 @@ class LinkToObject(
private val objectType = binding.tvObjectType
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.text.Spannable
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.updateLayoutParams
@ -38,6 +39,7 @@ class LinkToObjectArchive(
val title = binding.pageTitle
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -3,6 +3,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.text.Spannable
import android.text.SpannableString
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
@ -62,6 +63,7 @@ abstract class LinkToObjectCard(
abstract override val decoratableCard: CardView
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -1,5 +1,6 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.view.ViewConfiguration
import android.widget.FrameLayout
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
@ -32,6 +33,7 @@ class LinkToObjectDelete(
get() = binding.decorationContainer
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -2,6 +2,7 @@ package com.anytypeio.anytype.core_ui.features.editor.holders.other
import android.view.MotionEvent
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.LinearLayout
import androidx.core.view.ViewCompat.generateViewId
@ -36,6 +37,7 @@ class TableOfContents(
private val defPadding = root.resources.getDimension(R.dimen.def_toc_item_padding_start).toInt()
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> root.onTouchEvent(e) }
)

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.placeholders
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import android.widget.TextView
import com.anytypeio.anytype.core_ui.databinding.ItemBlockMediaPlaceholderBinding
@ -39,6 +40,7 @@ abstract class MediaPlaceholder(
abstract fun setup()
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -1,6 +1,7 @@
package com.anytypeio.anytype.core_ui.features.editor.holders.upload
import android.view.View
import android.view.ViewConfiguration
import android.widget.FrameLayout
import com.anytypeio.anytype.core_ui.databinding.ItemBlockMediaPlaceholderBinding
import com.anytypeio.anytype.core_ui.features.editor.BlockViewDiffUtil
@ -24,6 +25,7 @@ abstract class MediaUpload(
abstract fun uploadClick(target: String, clicked: (ListenerType) -> Unit)
override val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(itemView.context).scaledTouchSlop,
fallback = { e -> itemView.onTouchEvent(e) }
)

View file

@ -10,6 +10,7 @@ import android.text.TextWatcher
import android.util.AttributeSet
import android.view.DragEvent
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.appcompat.widget.AppCompatEditText
import com.anytypeio.anytype.core_ui.features.editor.EditorTouchProcessor
import com.anytypeio.anytype.core_ui.tools.TextInputTextWatcher
@ -34,6 +35,7 @@ class CodeTextInputWidget : AppCompatEditText, SyntaxHighlighter {
val editorTouchProcessor by lazy {
EditorTouchProcessor(
touchSlop = ViewConfiguration.get(context).scaledTouchSlop,
fallback = { e -> super.onTouchEvent(e) }
)
}

View file

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.Paint
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewConfiguration
import android.widget.FrameLayout
import com.anytypeio.anytype.core_ui.R
import com.anytypeio.anytype.core_ui.databinding.TvTableOfContentsBinding
@ -19,6 +20,7 @@ class TableOfContentsItemWidget @JvmOverloads constructor(
val textView = binding.text
val editorTouchProcessor = EditorTouchProcessor(
touchSlop = ViewConfiguration.get(context).scaledTouchSlop,
fallback = { e -> this.onTouchEvent(e) }
)

View file

@ -13,6 +13,7 @@ import android.util.AttributeSet
import android.view.DragEvent
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.ViewConfiguration
import androidx.appcompat.widget.AppCompatEditText
import androidx.core.graphics.withTranslation
import com.anytypeio.anytype.core_ui.R
@ -66,6 +67,7 @@ class TextInputWidget : AppCompatEditText {
val editorTouchProcessor by lazy {
EditorTouchProcessor(
touchSlop = ViewConfiguration.get(context).scaledTouchSlop,
fallback = { e -> super.onTouchEvent(e) }
)
}