Выделение текста — selection mode, handles, toolbar, magnifier
This commit is contained in:
@@ -3037,6 +3037,7 @@ fun ChatDetailScreen(
|
||||
else -> {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
userScrollEnabled = !textSelectionHelper.movingHandle,
|
||||
modifier =
|
||||
Modifier.fillMaxSize()
|
||||
.nestedScroll(
|
||||
|
||||
@@ -402,6 +402,9 @@ fun MessageBubble(
|
||||
else Color(0xFF2196F3) // Стандартный Material Blue для входящих
|
||||
}
|
||||
var textViewRef by remember { mutableStateOf<com.rosetta.messenger.ui.components.AppleEmojiTextView?>(null) }
|
||||
val selectionDragEndHandler: (() -> Unit)? = if (textSelectionHelper != null) {
|
||||
{ textSelectionHelper.hideMagnifier(); textSelectionHelper.endHandleDrag() }
|
||||
} else null
|
||||
val linksEnabled = !isSelectionMode
|
||||
val textClickHandler: (() -> Unit)? = onClick
|
||||
val mentionClickHandler: ((String) -> Unit)? =
|
||||
@@ -1070,7 +1073,7 @@ fun MessageBubble(
|
||||
onLongClick =
|
||||
onLongClick, // 🔥 Long press для selection
|
||||
onViewCreated = { textViewRef = it },
|
||||
onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY ->
|
||||
onTextLongPress = if (textSelectionHelper != null && isSelected) { touchX, touchY ->
|
||||
val info = textViewRef?.getLayoutInfo()
|
||||
if (info != null) {
|
||||
textSelectionHelper.startSelection(
|
||||
@@ -1081,7 +1084,18 @@ fun MessageBubble(
|
||||
view = textViewRef
|
||||
)
|
||||
}
|
||||
} else null
|
||||
} else null,
|
||||
onSelectionDrag = if (textSelectionHelper != null) { tx, ty ->
|
||||
textSelectionHelper.moveHandle(
|
||||
(tx - textSelectionHelper.overlayWindowX),
|
||||
(ty - textSelectionHelper.overlayWindowY)
|
||||
)
|
||||
textSelectionHelper.showMagnifier(
|
||||
(tx - textSelectionHelper.overlayWindowX),
|
||||
(ty - textSelectionHelper.overlayWindowY)
|
||||
)
|
||||
} else null,
|
||||
onSelectionDragEnd = selectionDragEndHandler
|
||||
)
|
||||
},
|
||||
timeContent = {
|
||||
@@ -1287,7 +1301,7 @@ fun MessageBubble(
|
||||
onLongClick =
|
||||
onLongClick, // 🔥 Long press для selection
|
||||
onViewCreated = { textViewRef = it },
|
||||
onTextLongPress = if (textSelectionHelper != null && !isSelectionMode) { touchX, touchY ->
|
||||
onTextLongPress = if (textSelectionHelper != null && isSelected) { touchX, touchY ->
|
||||
val info = textViewRef?.getLayoutInfo()
|
||||
if (info != null) {
|
||||
textSelectionHelper.startSelection(
|
||||
@@ -1298,7 +1312,18 @@ fun MessageBubble(
|
||||
view = textViewRef
|
||||
)
|
||||
}
|
||||
} else null
|
||||
} else null,
|
||||
onSelectionDrag = if (textSelectionHelper != null) { tx, ty ->
|
||||
textSelectionHelper.moveHandle(
|
||||
(tx - textSelectionHelper.overlayWindowX),
|
||||
(ty - textSelectionHelper.overlayWindowY)
|
||||
)
|
||||
textSelectionHelper.showMagnifier(
|
||||
(tx - textSelectionHelper.overlayWindowX),
|
||||
(ty - textSelectionHelper.overlayWindowY)
|
||||
)
|
||||
} else null,
|
||||
onSelectionDragEnd = selectionDragEndHandler
|
||||
)
|
||||
},
|
||||
timeContent = {
|
||||
|
||||
@@ -72,6 +72,11 @@ class TextSelectionHelper {
|
||||
private set
|
||||
var movingHandleStart by mutableStateOf(false)
|
||||
private set
|
||||
// Telegram: isOneTouch = true during initial long-press drag (before finger lifts)
|
||||
var isOneTouch by mutableStateOf(false)
|
||||
private set
|
||||
// Telegram: direction not determined yet — first drag decides start or end handle
|
||||
private var movingDirectionSettling = false
|
||||
private var movingOffsetX = 0f
|
||||
private var movingOffsetY = 0f
|
||||
|
||||
@@ -117,6 +122,13 @@ class TextSelectionHelper {
|
||||
isActive = true
|
||||
handleViewProgress = 1f
|
||||
|
||||
// Telegram: immediately enter drag mode — user can drag without lifting finger
|
||||
movingHandle = true
|
||||
movingDirectionSettling = true
|
||||
isOneTouch = true
|
||||
movingOffsetX = 0f
|
||||
movingOffsetY = 0f
|
||||
|
||||
view?.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
showToolbar = false
|
||||
}
|
||||
@@ -161,11 +173,27 @@ class TextSelectionHelper {
|
||||
val y = (touchY + movingOffsetY).toInt()
|
||||
val offset = getCharOffsetFromCoords(x, y)
|
||||
if (offset < 0) return
|
||||
|
||||
// Telegram: first drag determines which handle to move
|
||||
if (movingDirectionSettling) {
|
||||
if (offset < selectionStart) {
|
||||
movingDirectionSettling = false
|
||||
movingHandleStart = true
|
||||
} else if (offset > selectionEnd) {
|
||||
movingDirectionSettling = false
|
||||
movingHandleStart = false
|
||||
} else {
|
||||
return // still within selected word, wait for more movement
|
||||
}
|
||||
}
|
||||
|
||||
if (movingHandleStart) updateSelectionStart(offset) else updateSelectionEnd(offset)
|
||||
}
|
||||
|
||||
fun endHandleDrag() {
|
||||
movingHandle = false
|
||||
movingDirectionSettling = false
|
||||
isOneTouch = false
|
||||
showFloatingToolbar()
|
||||
}
|
||||
|
||||
@@ -272,6 +300,8 @@ class TextSelectionHelper {
|
||||
isActive = false
|
||||
handleViewProgress = 0f
|
||||
movingHandle = false
|
||||
movingDirectionSettling = false
|
||||
isOneTouch = false
|
||||
showToolbar = false
|
||||
hideMagnifier()
|
||||
}
|
||||
@@ -301,12 +331,20 @@ private fun FloatingToolbarPopup(
|
||||
val info = helper.layoutInfo ?: return
|
||||
val layout = info.layout
|
||||
val startLine = layout.getLineForOffset(helper.selectionStart.coerceIn(0, info.text.length))
|
||||
val toolbarY = (layout.getLineTop(startLine) + info.windowY - 52).coerceAtLeast(0)
|
||||
val toolbarX = ((helper.startHandleX + helper.endHandleX) / 2f - 80f).coerceAtLeast(0f)
|
||||
|
||||
// Toolbar position: centered above selection, in overlay-local coords
|
||||
// startHandleX/endHandleX are already overlay-local
|
||||
val selectionCenterX = (helper.startHandleX + helper.endHandleX) / 2f
|
||||
val selectionTopY = layout.getLineTop(startLine).toFloat() +
|
||||
(info.windowY - helper.overlayWindowY)
|
||||
val toolbarHeight = 40f // approximate toolbar height in px
|
||||
val toolbarWidth = 160f // approximate toolbar width in px
|
||||
val toolbarX = (selectionCenterX - toolbarWidth / 2f).coerceAtLeast(8f)
|
||||
val toolbarY = (selectionTopY - toolbarHeight - 12f).coerceAtLeast(0f)
|
||||
|
||||
Popup(
|
||||
alignment = Alignment.TopStart,
|
||||
offset = IntOffset(toolbarX.toInt(), toolbarY)
|
||||
offset = IntOffset(toolbarX.toInt(), toolbarY.toInt())
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -559,6 +559,8 @@ fun AppleEmojiText(
|
||||
onClick: (() -> Unit)? = null, // 🔥 Обычный tap (selection mode в MessageBubble)
|
||||
onLongClick: (() -> Unit)? = null, // 🔥 Callback для long press (selection в MessageBubble)
|
||||
onTextLongPress: ((touchX: Int, touchY: Int) -> Unit)? = null,
|
||||
onSelectionDrag: ((touchX: Int, touchY: Int) -> Unit)? = null,
|
||||
onSelectionDragEnd: (() -> Unit)? = null,
|
||||
onViewCreated: ((com.rosetta.messenger.ui.components.AppleEmojiTextView) -> Unit)? = null,
|
||||
minHeightMultiplier: Float = 1.5f
|
||||
) {
|
||||
@@ -604,6 +606,8 @@ fun AppleEmojiText(
|
||||
setOnMentionClickListener(onMentionClick)
|
||||
setOnClickableSpanPressStartListener(onClickableSpanPressStart)
|
||||
onTextLongPressCallback = onTextLongPress
|
||||
this.onSelectionDrag = onSelectionDrag
|
||||
this.onSelectionDragEnd = onSelectionDragEnd
|
||||
onViewCreated?.invoke(this)
|
||||
// In link/mention mode, keep span clicks exclusive to avoid parent bubble menu tap.
|
||||
val canUseTextViewClick = !enableLinks
|
||||
@@ -639,6 +643,8 @@ fun AppleEmojiText(
|
||||
view.setOnMentionClickListener(onMentionClick)
|
||||
view.setOnClickableSpanPressStartListener(onClickableSpanPressStart)
|
||||
view.onTextLongPressCallback = onTextLongPress
|
||||
view.onSelectionDrag = onSelectionDrag
|
||||
view.onSelectionDragEnd = onSelectionDragEnd
|
||||
// In link/mention mode, keep span clicks exclusive to avoid parent bubble menu tap.
|
||||
val canUseTextViewClick = !enableLinks
|
||||
view.setOnClickListener(
|
||||
@@ -701,8 +707,12 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
||||
// 🔥 Long press callback для selection в MessageBubble
|
||||
var onLongClickCallback: (() -> Unit)? = null
|
||||
var onTextLongPressCallback: ((touchX: Int, touchY: Int) -> Unit)? = null
|
||||
// Telegram flow: forward drag/up events after long press fires
|
||||
var onSelectionDrag: ((touchX: Int, touchY: Int) -> Unit)? = null
|
||||
var onSelectionDragEnd: (() -> Unit)? = null
|
||||
private var downOnClickableSpan: Boolean = false
|
||||
private var suppressPerformClickOnce: Boolean = false
|
||||
private var selectionDragActive: Boolean = false
|
||||
|
||||
// 🔥 GestureDetector для обработки long press поверх LinkMovementMethod
|
||||
private val gestureDetector = GestureDetector(context, object : GestureDetector.SimpleOnGestureListener() {
|
||||
@@ -710,6 +720,8 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
||||
if (downOnClickableSpan) return
|
||||
if (onTextLongPressCallback != null) {
|
||||
onTextLongPressCallback?.invoke(e.rawX.toInt(), e.rawY.toInt())
|
||||
selectionDragActive = true
|
||||
parent?.requestDisallowInterceptTouchEvent(true) // block scroll during drag
|
||||
} else {
|
||||
onLongClickCallback?.invoke()
|
||||
}
|
||||
@@ -730,21 +742,33 @@ class AppleEmojiTextView @JvmOverloads constructor(
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
downOnClickableSpan = isTouchOnClickableSpan(event)
|
||||
suppressPerformClickOnce = downOnClickableSpan
|
||||
selectionDragActive = false
|
||||
if (downOnClickableSpan) {
|
||||
clickableSpanPressStartCallback?.invoke()
|
||||
parent?.requestDisallowInterceptTouchEvent(true)
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (selectionDragActive) {
|
||||
onSelectionDrag?.invoke(event.rawX.toInt(), event.rawY.toInt())
|
||||
return true
|
||||
}
|
||||
}
|
||||
MotionEvent.ACTION_CANCEL,
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (selectionDragActive) {
|
||||
selectionDragActive = false
|
||||
onSelectionDragEnd?.invoke()
|
||||
downOnClickableSpan = false
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
return true
|
||||
}
|
||||
downOnClickableSpan = false
|
||||
parent?.requestDisallowInterceptTouchEvent(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Позволяем GestureDetector обработать событие (для long press)
|
||||
gestureDetector.onTouchEvent(event)
|
||||
// Передаем событие дальше для обработки ссылок
|
||||
return super.dispatchTouchEvent(event)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user