feat: добавить TextSelectionOverlay — highlight, handles, drag interaction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,11 +7,25 @@ import android.text.Layout
|
|||||||
import android.view.HapticFeedbackConstants
|
import android.view.HapticFeedbackConstants
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
|
||||||
|
|
||||||
data class LayoutInfo(
|
data class LayoutInfo(
|
||||||
val layout: Layout,
|
val layout: Layout,
|
||||||
@@ -34,6 +48,17 @@ class TextSelectionHelper {
|
|||||||
private set
|
private set
|
||||||
var handleViewProgress by mutableFloatStateOf(0f)
|
var handleViewProgress by mutableFloatStateOf(0f)
|
||||||
private set
|
private set
|
||||||
|
var movingHandle by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var movingHandleStart by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
private var movingOffsetX = 0f
|
||||||
|
private var movingOffsetY = 0f
|
||||||
|
|
||||||
|
var startHandleX by mutableFloatStateOf(0f)
|
||||||
|
var startHandleY by mutableFloatStateOf(0f)
|
||||||
|
var endHandleX by mutableFloatStateOf(0f)
|
||||||
|
var endHandleY by mutableFloatStateOf(0f)
|
||||||
|
|
||||||
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
|
val isInSelectionMode: Boolean get() = isActive && selectionStart >= 0 && selectionEnd > selectionStart
|
||||||
|
|
||||||
@@ -89,6 +114,26 @@ class TextSelectionHelper {
|
|||||||
selectionEnd = newEnd
|
selectionEnd = newEnd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun beginHandleDrag(isStart: Boolean, touchX: Float, touchY: Float) {
|
||||||
|
movingHandle = true
|
||||||
|
movingHandleStart = isStart
|
||||||
|
movingOffsetX = if (isStart) startHandleX - touchX else endHandleX - touchX
|
||||||
|
movingOffsetY = if (isStart) startHandleY - touchY else endHandleY - touchY
|
||||||
|
}
|
||||||
|
|
||||||
|
fun moveHandle(touchX: Float, touchY: Float) {
|
||||||
|
if (!movingHandle) return
|
||||||
|
val x = (touchX + movingOffsetX).toInt()
|
||||||
|
val y = (touchY + movingOffsetY).toInt()
|
||||||
|
val offset = getCharOffsetFromCoords(x, y)
|
||||||
|
if (offset < 0) return
|
||||||
|
if (movingHandleStart) updateSelectionStart(offset) else updateSelectionEnd(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun endHandleDrag() {
|
||||||
|
movingHandle = false
|
||||||
|
}
|
||||||
|
|
||||||
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
|
fun getCharOffsetFromCoords(x: Int, y: Int): Int {
|
||||||
val info = layoutInfo ?: return -1
|
val info = layoutInfo ?: return -1
|
||||||
val localX = x - info.windowX
|
val localX = x - info.windowX
|
||||||
@@ -128,5 +173,154 @@ class TextSelectionHelper {
|
|||||||
layoutInfo = null
|
layoutInfo = null
|
||||||
isActive = false
|
isActive = false
|
||||||
handleViewProgress = 0f
|
handleViewProgress = 0f
|
||||||
|
movingHandle = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val HandleSize = 22.dp
|
||||||
|
private val HandleInset = 8.dp
|
||||||
|
private val HighlightCorner = 6.dp
|
||||||
|
private val HighlightColor = PrimaryBlue.copy(alpha = 0.3f)
|
||||||
|
private val HandleColor = PrimaryBlue
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextSelectionOverlay(
|
||||||
|
helper: TextSelectionHelper,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (!helper.isInSelectionMode) return
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val handleSizePx = with(density) { HandleSize.toPx() }
|
||||||
|
val handleInsetPx = with(density) { HandleInset.toPx() }
|
||||||
|
val highlightCornerPx = with(density) { HighlightCorner.toPx() }
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.pointerInput(helper.isActive) {
|
||||||
|
if (!helper.isActive) return@pointerInput
|
||||||
|
awaitPointerEventScope {
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent()
|
||||||
|
val change = event.changes.firstOrNull() ?: continue
|
||||||
|
|
||||||
|
when {
|
||||||
|
change.pressed && !helper.movingHandle -> {
|
||||||
|
val x = change.position.x
|
||||||
|
val y = change.position.y
|
||||||
|
val startRect = Rect(
|
||||||
|
helper.startHandleX - handleSizePx / 2 - handleInsetPx,
|
||||||
|
helper.startHandleY - handleInsetPx,
|
||||||
|
helper.startHandleX + handleSizePx / 2 + handleInsetPx,
|
||||||
|
helper.startHandleY + handleSizePx + handleInsetPx
|
||||||
|
)
|
||||||
|
val endRect = Rect(
|
||||||
|
helper.endHandleX - handleSizePx / 2 - handleInsetPx,
|
||||||
|
helper.endHandleY - handleInsetPx,
|
||||||
|
helper.endHandleX + handleSizePx / 2 + handleInsetPx,
|
||||||
|
helper.endHandleY + handleSizePx + handleInsetPx
|
||||||
|
)
|
||||||
|
when {
|
||||||
|
startRect.contains(Offset(x, y)) -> {
|
||||||
|
helper.beginHandleDrag(isStart = true, x, y)
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
endRect.contains(Offset(x, y)) -> {
|
||||||
|
helper.beginHandleDrag(isStart = false, x, y)
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
helper.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
change.pressed && helper.movingHandle -> {
|
||||||
|
helper.moveHandle(change.position.x, change.position.y)
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
!change.pressed && helper.movingHandle -> {
|
||||||
|
helper.endHandleDrag()
|
||||||
|
change.consume()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
val info = helper.layoutInfo ?: return@Canvas
|
||||||
|
val layout = info.layout
|
||||||
|
val text = info.text
|
||||||
|
|
||||||
|
val startOffset = helper.selectionStart.coerceIn(0, text.length)
|
||||||
|
val endOffset = helper.selectionEnd.coerceIn(0, text.length)
|
||||||
|
if (startOffset >= endOffset) return@Canvas
|
||||||
|
|
||||||
|
val startLine = layout.getLineForOffset(startOffset)
|
||||||
|
val endLine = layout.getLineForOffset(endOffset)
|
||||||
|
|
||||||
|
for (line in startLine..endLine) {
|
||||||
|
val lineTop = layout.getLineTop(line).toFloat() + info.windowY
|
||||||
|
val lineBottom = layout.getLineBottom(line).toFloat() + info.windowY
|
||||||
|
val left = if (line == startLine) {
|
||||||
|
layout.getPrimaryHorizontal(startOffset) + info.windowX
|
||||||
|
} else {
|
||||||
|
layout.getLineLeft(line) + info.windowX
|
||||||
|
}
|
||||||
|
val right = if (line == endLine) {
|
||||||
|
layout.getPrimaryHorizontal(endOffset) + info.windowX
|
||||||
|
} else {
|
||||||
|
layout.getLineRight(line) + info.windowX
|
||||||
|
}
|
||||||
|
drawRoundRect(
|
||||||
|
color = HighlightColor,
|
||||||
|
topLeft = Offset(left, lineTop),
|
||||||
|
size = Size(right - left, lineBottom - lineTop),
|
||||||
|
cornerRadius = CornerRadius(highlightCornerPx)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val startHx = layout.getPrimaryHorizontal(startOffset) + info.windowX
|
||||||
|
val startHy = layout.getLineBottom(startLine).toFloat() + info.windowY
|
||||||
|
val endHx = layout.getPrimaryHorizontal(endOffset) + info.windowX
|
||||||
|
val endHy = layout.getLineBottom(endLine).toFloat() + info.windowY
|
||||||
|
|
||||||
|
helper.startHandleX = startHx
|
||||||
|
helper.startHandleY = startHy
|
||||||
|
helper.endHandleX = endHx
|
||||||
|
helper.endHandleY = endHy
|
||||||
|
|
||||||
|
drawStartHandle(startHx, startHy, handleSizePx)
|
||||||
|
drawEndHandle(endHx, endHy, handleSizePx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawStartHandle(x: Float, y: Float, size: Float) {
|
||||||
|
val half = size / 2f
|
||||||
|
drawCircle(
|
||||||
|
color = HandleColor,
|
||||||
|
radius = half,
|
||||||
|
center = Offset(x, y + half)
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
color = HandleColor,
|
||||||
|
topLeft = Offset(x, y),
|
||||||
|
size = Size(half, half)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawEndHandle(x: Float, y: Float, size: Float) {
|
||||||
|
val half = size / 2f
|
||||||
|
drawCircle(
|
||||||
|
color = HandleColor,
|
||||||
|
radius = half,
|
||||||
|
center = Offset(x, y + half)
|
||||||
|
)
|
||||||
|
drawRect(
|
||||||
|
color = HandleColor,
|
||||||
|
topLeft = Offset(x - half, y),
|
||||||
|
size = Size(half, half)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user