mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-11-02 21:32:34 +00:00
Fix selection behaviour when long pressing emojis with no skin tone variants
Also improve the threshold calculation to dismiss the displayed skin tone picker a bit
This commit is contained in:
+81
-68
@@ -30,6 +30,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
import androidx.compose.ui.graphics.drawscope.Fill
|
import androidx.compose.ui.graphics.drawscope.Fill
|
||||||
@@ -48,7 +49,7 @@ import io.element.android.compound.theme.ElementTheme
|
|||||||
import io.element.android.emojibasebindings.Emoji
|
import io.element.android.emojibasebindings.Emoji
|
||||||
import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction
|
import io.element.android.features.messages.impl.timeline.a11y.a11yReactionAction
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePadding
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePadding
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePickerContent
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinTonePicker
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSize
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSize
|
||||||
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSpacing
|
import io.element.android.features.messages.impl.timeline.components.customreaction.picker.SkinToneSlotSpacing
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
@@ -83,6 +84,7 @@ fun EmojiItem(
|
|||||||
emoji = item.unicode,
|
emoji = item.unicode,
|
||||||
userAlreadyReacted = isSelected,
|
userAlreadyReacted = isSelected,
|
||||||
)
|
)
|
||||||
|
val hasSkinTones = !item.skins.isNullOrEmpty()
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
|
.sizeIn(minWidth = 40.dp, minHeight = 40.dp)
|
||||||
@@ -90,12 +92,88 @@ fun EmojiItem(
|
|||||||
.background(backgroundColor, CircleShape)
|
.background(backgroundColor, CircleShape)
|
||||||
.indication(interactionSource, ripple())
|
.indication(interactionSource, ripple())
|
||||||
.pointerInput(item) {
|
.pointerInput(item) {
|
||||||
|
// Only detect drag after long press for those items which have a skin tone picker
|
||||||
|
if (hasSkinTones) {
|
||||||
|
var startOffset = Offset.Zero
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = { position ->
|
||||||
|
if (onLongPress != null) {
|
||||||
|
startOffset = position
|
||||||
|
dismissed = false
|
||||||
|
onLongPress(item)
|
||||||
|
hoveredIndex = -1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDrag = { change, _ ->
|
||||||
|
if (!dismissed) {
|
||||||
|
change.consume()
|
||||||
|
// It's a valid drag event if it's within ~2 items height above, so, on top of the current one or the skin tone selector,
|
||||||
|
// or 1 item below, which should be far enough to not be triggered by mistake
|
||||||
|
val isValidDrag = if (change.position.y < startOffset.y) {
|
||||||
|
startOffset.y - change.position.y <= itemSize.height * 2f
|
||||||
|
} else {
|
||||||
|
change.position.y - startOffset.y <= itemSize.height
|
||||||
|
}
|
||||||
|
if (!isValidDrag) {
|
||||||
|
dismissed = true
|
||||||
|
hoveredIndex = -1
|
||||||
|
onDismissSkinPicker?.invoke()
|
||||||
|
} else {
|
||||||
|
val skinCount = item.skins!!.size
|
||||||
|
val slotWidthPx = with(density) { SkinToneSlotSize.toPx() }
|
||||||
|
val spacingPx = with(density) { SkinToneSlotSpacing.toPx() }
|
||||||
|
val paddingPx = with(density) { SkinTonePadding.toPx() }
|
||||||
|
val totalSlots = 1 + skinCount
|
||||||
|
val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx
|
||||||
|
val pickerHalfWidthPx = pickerWidthPx / 2f
|
||||||
|
val centerXPx = itemSize.width / 2f
|
||||||
|
val pickerLeftPx = centerXPx - pickerHalfWidthPx
|
||||||
|
val xInPicker = change.position.x - pickerLeftPx
|
||||||
|
val index = if (xInPicker in 0f..pickerWidthPx) {
|
||||||
|
(xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1)
|
||||||
|
} else {
|
||||||
|
-1
|
||||||
|
}
|
||||||
|
hoveredIndex = index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragEnd = {
|
||||||
|
if (!dismissed) {
|
||||||
|
val idx = hoveredIndex
|
||||||
|
val skinCount = item.skins?.size ?: 0
|
||||||
|
if (idx in 0..skinCount) {
|
||||||
|
if (idx == 0) {
|
||||||
|
onSelectEmoji(item)
|
||||||
|
} else {
|
||||||
|
val skin = item.skins?.getOrNull(idx - 1)
|
||||||
|
if (skin != null) {
|
||||||
|
onSelectEmoji(item.copy(unicode = skin.unicode))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (idx < 0) {
|
||||||
|
onSelectEmoji(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismissed = false
|
||||||
|
hoveredIndex = -1
|
||||||
|
onDismissSkinPicker?.invoke()
|
||||||
|
},
|
||||||
|
onDragCancel = {
|
||||||
|
dismissed = false
|
||||||
|
hoveredIndex = -1
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always detect long press and tap gestures
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
onPress = { pressOffset ->
|
onPress = { pressOffset ->
|
||||||
val press = PressInteraction.Press(pressOffset)
|
val press = PressInteraction.Press(pressOffset)
|
||||||
interactionSource.emit(press)
|
interactionSource.emit(press)
|
||||||
if (tryAwaitRelease()) {
|
if (tryAwaitRelease()) {
|
||||||
interactionSource.emit(PressInteraction.Release(press))
|
interactionSource.emit(PressInteraction.Release(press))
|
||||||
|
onSelectEmoji(item)
|
||||||
} else {
|
} else {
|
||||||
interactionSource.emit(PressInteraction.Cancel(press))
|
interactionSource.emit(PressInteraction.Cancel(press))
|
||||||
}
|
}
|
||||||
@@ -103,71 +181,6 @@ fun EmojiItem(
|
|||||||
onTap = { onSelectEmoji(item) },
|
onTap = { onSelectEmoji(item) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.pointerInput(item) {
|
|
||||||
detectDragGesturesAfterLongPress(
|
|
||||||
onDragStart = { _ ->
|
|
||||||
if (item.skins != null && onLongPress != null) {
|
|
||||||
dismissed = false
|
|
||||||
onLongPress(item)
|
|
||||||
hoveredIndex = -1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDrag = { change, _ ->
|
|
||||||
if (item.skins != null && !dismissed) {
|
|
||||||
change.consume()
|
|
||||||
val yThresholdPx = with(density) { 100.dp.toPx() }
|
|
||||||
if (change.position.y > yThresholdPx) {
|
|
||||||
dismissed = true
|
|
||||||
hoveredIndex = -1
|
|
||||||
onDismissSkinPicker?.invoke()
|
|
||||||
} else {
|
|
||||||
val skinCount = item.skins!!.size
|
|
||||||
val slotWidthPx = with(density) { SkinToneSlotSize.toPx() }
|
|
||||||
val spacingPx = with(density) { SkinToneSlotSpacing.toPx() }
|
|
||||||
val paddingPx = with(density) { SkinTonePadding.toPx() }
|
|
||||||
val totalSlots = 1 + skinCount
|
|
||||||
val pickerWidthPx = 2 * paddingPx + totalSlots * slotWidthPx + (totalSlots - 1) * spacingPx
|
|
||||||
val pickerHalfWidthPx = pickerWidthPx / 2f
|
|
||||||
val centerXPx = itemSize.width / 2f
|
|
||||||
val pickerLeftPx = centerXPx - pickerHalfWidthPx
|
|
||||||
val xInPicker = change.position.x - pickerLeftPx
|
|
||||||
val index = if (xInPicker >= 0f && xInPicker <= pickerWidthPx) {
|
|
||||||
(xInPicker / slotWidthPx).toInt().coerceIn(0, totalSlots - 1)
|
|
||||||
} else {
|
|
||||||
-1
|
|
||||||
}
|
|
||||||
hoveredIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragEnd = {
|
|
||||||
if (!dismissed) {
|
|
||||||
val idx = hoveredIndex
|
|
||||||
val skinCount = item.skins?.size ?: 0
|
|
||||||
if (idx in 0..skinCount) {
|
|
||||||
if (idx == 0) {
|
|
||||||
onSelectEmoji(item)
|
|
||||||
} else {
|
|
||||||
val skin = item.skins?.getOrNull(idx - 1)
|
|
||||||
if (skin != null) {
|
|
||||||
onSelectEmoji(item.copy(unicode = skin.unicode))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (idx < 0) {
|
|
||||||
onSelectEmoji(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dismissed = false
|
|
||||||
hoveredIndex = -1
|
|
||||||
onDismissSkinPicker?.invoke()
|
|
||||||
},
|
|
||||||
onDragCancel = {
|
|
||||||
dismissed = false
|
|
||||||
hoveredIndex = -1
|
|
||||||
onDismissSkinPicker?.invoke()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.clearAndSetSemantics {
|
.clearAndSetSemantics {
|
||||||
contentDescription = description
|
contentDescription = description
|
||||||
},
|
},
|
||||||
@@ -177,7 +190,7 @@ fun EmojiItem(
|
|||||||
text = item.unicode,
|
text = item.unicode,
|
||||||
style = LocalTextStyle.current.copy(fontSize = emojiSize),
|
style = LocalTextStyle.current.copy(fontSize = emojiSize),
|
||||||
)
|
)
|
||||||
if (item.skins != null) {
|
if (hasSkinTones) {
|
||||||
Canvas(
|
Canvas(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomEnd)
|
.align(Alignment.BottomEnd)
|
||||||
@@ -201,7 +214,7 @@ fun EmojiItem(
|
|||||||
alignment = Alignment.BottomCenter,
|
alignment = Alignment.BottomCenter,
|
||||||
offset = IntOffset(0, popupOffsetPx),
|
offset = IntOffset(0, popupOffsetPx),
|
||||||
) {
|
) {
|
||||||
SkinTonePickerContent(
|
SkinTonePicker(
|
||||||
emoji = item,
|
emoji = item,
|
||||||
onSelect = { selectedEmoji ->
|
onSelect = { selectedEmoji ->
|
||||||
onSelectEmoji(selectedEmoji)
|
onSelectEmoji(selectedEmoji)
|
||||||
|
|||||||
Reference in New Issue
Block a user