Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 189 additions & 0 deletions android/src/main/java/com/swmansion/enriched/EnrichedDragHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package com.swmansion.enriched

import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.util.TypedValue
import android.view.DragEvent
import android.view.Gravity
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import com.swmansion.enriched.utils.dp

class EnrichedDragHandler(
private val view: EnrichedTextInputView,
private val clipboardManager: EnrichedClipboardManager,
) {
private data class EnrichedDragLocalState(
val sourceView: EnrichedTextInputView,
val start: Int,
val end: Int,
)

private var lastTouchDownX = 0f
private var lastTouchDownY = 0f
private var blockNextActionUpAfterDrag = false

fun onDragEvent(event: DragEvent): Boolean? {
if (event.action != DragEvent.ACTION_DROP) {
return null
}

return handleDrop(event)
}

fun performLongClick(): Boolean {
if (!isLastTouchWithinSelection()) {
return false
}

if (!startEnrichedSelectionDrag()) {
return false
}

blockNextActionUpAfterDrag = true
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
return true
}

fun onTouchEvent(event: MotionEvent): Boolean {
if (
blockNextActionUpAfterDrag &&
(
event.actionMasked == MotionEvent.ACTION_UP ||
event.actionMasked == MotionEvent.ACTION_CANCEL
)
) {
blockNextActionUpAfterDrag = false
return true
}

if (event.actionMasked == MotionEvent.ACTION_DOWN) {
lastTouchDownX = event.x
lastTouchDownY = event.y
}

return false
}

private fun handleDrop(event: DragEvent): Boolean? {
val localState = event.localState
val enrichedLocalState = localState as? EnrichedDragLocalState ?: return null

val clip = event.clipData ?: return null
val dropOffset = view.getOffsetForPosition(event.x, event.y)
val sameViewLocalState = enrichedLocalState.takeIf { it.sourceView == view }
if (
sameViewLocalState != null &&
dropOffset >= sameViewLocalState.start &&
dropOffset < sameViewLocalState.end
) {
return true
}

val lengthBeforeDrop = view.text?.length ?: 0
val didInsert = clipboardManager.insertClipData(clip, dropOffset, dropOffset)
if (!didInsert) {
return null
}

if (sameViewLocalState != null) {
deleteSourceAfterLocalDrop(sameViewLocalState, dropOffset, lengthBeforeDrop)
}

return true
}

private fun isLastTouchWithinSelection(): Boolean {
val start = minOf(view.selectionStart, view.selectionEnd)
val end = maxOf(view.selectionStart, view.selectionEnd)
if (start >= end) return false

val touchOffset = view.getOffsetForPosition(lastTouchDownX, lastTouchDownY)
return touchOffset >= start && touchOffset < end
}

private fun startEnrichedSelectionDrag(): Boolean {
val start = minOf(view.selectionStart, view.selectionEnd)
val end = maxOf(view.selectionStart, view.selectionEnd)
if (start >= end) return false

val clip = clipboardManager.createSelectedTextClipData(start, end) ?: return false
val started =
view.startDragAndDrop(
clip,
createTextDragShadow(start, end),
EnrichedDragLocalState(view, start, end),
View.DRAG_FLAG_GLOBAL,
)
Comment thread
IvanIhnatsiuk marked this conversation as resolved.

if (started) {
view.setSelection(end)
}

return started
}

private fun createTextDragBackgroundDrawable(): GradientDrawable =
GradientDrawable().apply {
shape = GradientDrawable.RECTANGLE
cornerRadius = 10.dp.toFloat()
setColor(Color.LTGRAY)
}

private fun createTextDragShadow(
start: Int,
end: Int,
): View.DragShadowBuilder {
val shadowView = TextView(view.context)

val padding = 10.dp

shadowView.apply {
text = view.editableText?.subSequence(start, end) ?: ""
setTextColor(view.textColors)
setTextSize(TypedValue.COMPLEX_UNIT_PX, view.textSize)
gravity = Gravity.CENTER
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
maxWidth = view.width
background = createTextDragBackgroundDrawable()
setPadding(padding, padding / 2, padding, padding / 2)
}

val size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
shadowView.measure(size, size)
shadowView.layout(0, 0, shadowView.measuredWidth, shadowView.measuredHeight)

return View.DragShadowBuilder(shadowView)
}

private fun deleteSourceAfterLocalDrop(
localState: EnrichedDragLocalState,
dropOffset: Int,
lengthBeforeDrop: Int,
) {
val editable = view.editableText
var dragSourceStart = localState.start
var dragSourceEnd = localState.end

if (dropOffset <= dragSourceStart) {
val shift = editable.length - lengthBeforeDrop
dragSourceStart += shift
dragSourceEnd += shift
}

val safeStart = dragSourceStart.coerceIn(0, editable.length)
val safeEnd = dragSourceEnd.coerceIn(safeStart, editable.length)
if (safeStart >= safeEnd) return

view.transactionManager.runSilently {
editable.replace(safeStart, safeEnd, "")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import android.util.AttributeSet
import android.util.Log
import android.util.TypedValue
import android.view.ActionMode
import android.view.DragEvent
import android.view.Gravity
import android.view.MotionEvent
import android.view.View
Expand Down Expand Up @@ -155,6 +156,14 @@ class EnrichedTextInputView : AppCompatEditText {
CheckListClickHandler(this)
}

private val dragHandler by lazy {
EnrichedDragHandler(this, clipboardManager)
}

override fun onDragEvent(event: DragEvent): Boolean = dragHandler.onDragEvent(event) ?: super.onDragEvent(event)

override fun performLongClick(): Boolean = dragHandler.performLongClick() || super.performLongClick()

var spanWatcher: EnrichedSpanWatcher? = null

constructor(context: Context) : super(context) {
Expand Down Expand Up @@ -272,7 +281,9 @@ class EnrichedTextInputView : AppCompatEditText {
// https://github.com/facebook/react-native/blob/36df97f500aa0aa8031098caf7526db358b6ddc1/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactEditText.kt#L295C1-L296C1
override fun onTouchEvent(event: MotionEvent): Boolean {
if (checkboxClickHandler.handleTouch(event)) return true
when (event.action) {

if (dragHandler.onTouchEvent(event)) return true
when (event.actionMasked) {
MotionEvent.ACTION_DOWN -> {
detectScrollMovement = true
// Disallow parent views to intercept touch events, until we can detect if we should be
Expand Down Expand Up @@ -380,7 +391,7 @@ class EnrichedTextInputView : AppCompatEditText {
end: Int? = null,
) {
transactionManager.runWithIgnoredSpanWatcher {
val currentText = (text as? SpannableStringBuilder) ?: return@runWithIgnoredSpanWatcher
val currentText = editableText
val length = currentText.length

val insertionStart = selection.start
Expand Down
4 changes: 2 additions & 2 deletions android/src/main/java/com/swmansion/enriched/utils/Numbers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ object Dimens {
val Float.dp: Float
get() = this * Dimens.density

val Int.dp: Float
get() = this * Dimens.density
val Int.dp: Int
get() = (this * Dimens.density).toInt()
Comment thread
IvanIhnatsiuk marked this conversation as resolved.
4 changes: 2 additions & 2 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2621,7 +2621,7 @@ PODS:
- React-perflogger (= 0.83.0)
- React-utils (= 0.83.0)
- SocketRocket
- ReactNativeEnriched (0.0.76):
- ReactNativeEnriched (0.0.77):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -3240,7 +3240,7 @@ SPEC CHECKSUMS:
ReactAppDependencyProvider: 23e2bca1661f8781e55fcc05a151fc1df97bc1fb
ReactCodegen: 10a61330b137caaad6f7fbe7f5d0e7a40d621700
ReactCommon: c6cd81778336e767e27fe63c6707dd6b735fff5c
ReactNativeEnriched: 42dbc84d8a782db27e0645a354e51bdfd80cf68d
ReactNativeEnriched: a3ec2fb837120152fda350c02a1a530c9c93741f
RNReanimated: 361d5b8e20a77cd1f7907ad301f6d51ef79c1545
RNScreens: 7179cc1ba31b4e18ed29f10abf20c24a7961cf4c
RNWorklets: 6c4441bf4c01fd1598099789264481d199d18968
Expand Down