Fix long messages not being clickable (#6356)
* Fix long messages not being clickable As @bmarty found out, `clip = true` causes the click event to be ignored in some cases. Since we have the shape we want to draw and we're using a custom `onDraw` modifier anyway to cut-out part of the path, we can just draw everything using the modifier and avoid using `clip = true`. This seems to fix the issue. * Fix clipping of images or other items that cover the bubble * Fix borders being displayed for contents * Extract the layer drawing logic into `drawInLayer` to simplify the inlined code. Remove redundant code, those changes are now in the `drawInLayer` block * Workaround for lint issue: it seems like detekt can't properly detect usages in content receivers * Update screenshots --------- Co-authored-by: ElementBot <android@element.io>
This commit is contained in:
committed by
GitHub
parent
13bbd24df1
commit
a2d9f241dd
@@ -22,14 +22,12 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.CompositingStrategy
|
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -49,6 +47,7 @@ import io.element.android.libraries.designsystem.theme.messageFromMeBackground
|
|||||||
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
|
import io.element.android.libraries.designsystem.theme.messageFromOtherBackground
|
||||||
import io.element.android.libraries.testtags.TestTags
|
import io.element.android.libraries.testtags.TestTags
|
||||||
import io.element.android.libraries.testtags.testTag
|
import io.element.android.libraries.testtags.testTag
|
||||||
|
import io.element.android.libraries.ui.utils.graphics.drawInLayer
|
||||||
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
import io.element.android.libraries.ui.utils.time.isTalkbackActive
|
||||||
|
|
||||||
private val BUBBLE_RADIUS = 12.dp
|
private val BUBBLE_RADIUS = 12.dp
|
||||||
@@ -78,33 +77,46 @@ fun MessageEventBubble(
|
|||||||
.onKeyboardContextMenuAction(onLongClick)
|
.onKeyboardContextMenuAction(onLongClick)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cutTopStart = state.cutTopStart
|
||||||
// Ignore state.isHighlighted for now, we need a design decision on it.
|
// Ignore state.isHighlighted for now, we need a design decision on it.
|
||||||
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
|
val backgroundBubbleColor = MessageEventBubbleDefaults.backgroundBubbleColor(state.isMine)
|
||||||
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
|
val bubbleShape = remember(state) { MessageEventBubbleDefaults.shape(state.cutTopStart, state.groupPosition, state.isMine) }
|
||||||
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx()
|
||||||
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx()
|
||||||
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
|
||||||
BoxWithConstraints(
|
BoxWithConstraints(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.graphicsLayer {
|
.drawWithCache {
|
||||||
shape = bubbleShape
|
// Calculate the outline of the background and cache it
|
||||||
clip = true
|
val outline = bubbleShape.createOutline(size, layoutDirection, this)
|
||||||
compositingStrategy = CompositingStrategy.Offscreen
|
|
||||||
}
|
onDrawWithContent {
|
||||||
.drawWithContent {
|
// Draw the contents in a layer to be able to clip them with the same outline
|
||||||
|
// For some reason, doing this clipping outside a layer messes up with the touch events
|
||||||
|
drawInLayer(
|
||||||
|
composingStrategy = CompositingStrategy.Offscreen,
|
||||||
|
outline = outline,
|
||||||
|
clip = true,
|
||||||
|
) {
|
||||||
|
// Draw the background first, so that it's behind the content
|
||||||
drawRect(backgroundBubbleColor)
|
drawRect(backgroundBubbleColor)
|
||||||
|
|
||||||
|
// Then draw the content on top of it
|
||||||
drawContent()
|
drawContent()
|
||||||
if (state.cutTopStart) {
|
|
||||||
|
// And then clip the top start corner if needed to make room for the avatar
|
||||||
|
if (cutTopStart) {
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.Black,
|
color = Color.Black,
|
||||||
center = Offset(
|
center = Offset(
|
||||||
x = if (isRtl) size.width else 0f,
|
x = if (layoutDirection == LayoutDirection.Rtl) size.width else 0f,
|
||||||
y = yOffsetPx,
|
y = yOffsetPx,
|
||||||
),
|
),
|
||||||
radius = radiusPx,
|
radius = radiusPx,
|
||||||
blendMode = BlendMode.Clear,
|
blendMode = BlendMode.Clear,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
// Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case
|
||||||
// when content width is low.
|
// when content width is low.
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.ui.utils.graphics
|
||||||
|
|
||||||
|
import androidx.compose.ui.draw.CacheDrawScope
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
|
import androidx.compose.ui.graphics.layer.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.layer.drawLayer
|
||||||
|
import androidx.compose.ui.graphics.layer.setOutline
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the content of [recordBlock] in a separate layer, which can be customized using [composingStrategy], [outline] and [clip].
|
||||||
|
*/
|
||||||
|
context(scope: androidx.compose.ui.graphics.drawscope.DrawScope)
|
||||||
|
fun CacheDrawScope.drawInLayer(
|
||||||
|
composingStrategy: CompositingStrategy = CompositingStrategy.Auto,
|
||||||
|
outline: Outline? = null,
|
||||||
|
clip: Boolean = false,
|
||||||
|
recordBlock: ContentDrawScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val layer = obtainGraphicsLayer().apply {
|
||||||
|
this.compositingStrategy = composingStrategy
|
||||||
|
this.clip = clip
|
||||||
|
outline?.let { this.setOutline(it) }
|
||||||
|
|
||||||
|
record(block = recordBlock)
|
||||||
|
}
|
||||||
|
scope.drawLayer(layer)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user