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.View
|
||||
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.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
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(
|
||||
val layout: Layout,
|
||||
@@ -34,6 +48,17 @@ class TextSelectionHelper {
|
||||
private set
|
||||
var handleViewProgress by mutableFloatStateOf(0f)
|
||||
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
|
||||
|
||||
@@ -89,6 +114,26 @@ class TextSelectionHelper {
|
||||
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 {
|
||||
val info = layoutInfo ?: return -1
|
||||
val localX = x - info.windowX
|
||||
@@ -128,5 +173,154 @@ class TextSelectionHelper {
|
||||
layoutInfo = null
|
||||
isActive = false
|
||||
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