From 315392d8ef7ccceb2a0e8861798c755a929d30cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 24 Jun 2024 11:04:01 +0200 Subject: [PATCH] Improve the way we cut the bubble layout to give space for the sender Avatar. Instead of drawing a circle with the same color of the background behind the avatar, properly clear the top start corner of the bubble using `CompositingStrategy.Offscreen` and `BlendMode.Clear`. --- .../timeline/components/MessageEventBubble.kt | 37 ++++++++++++-- .../components/TimelineItemEventRow.kt | 50 ++++++------------- .../impl/timeline/model/bubble/BubbleState.kt | 5 +- 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt index 9e1095e02a..f30f6cac55 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/MessageEventBubble.kt @@ -32,7 +32,13 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.BlendMode +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme @@ -40,8 +46,10 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItemGrou import io.element.android.features.messages.impl.timeline.model.bubble.BubbleState import io.element.android.features.messages.impl.timeline.model.bubble.BubbleStateProvider import io.element.android.libraries.core.extensions.to01 +import io.element.android.libraries.designsystem.components.avatar.AvatarSize import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight +import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.theme.components.Surface import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.messageFromMeBackground @@ -51,6 +59,7 @@ import io.element.android.libraries.testtags.testTag private val BUBBLE_RADIUS = 12.dp internal val BUBBLE_INCOMING_OFFSET = 16.dp +private val avatarRadius = AvatarSize.TimelineSender.dp / 2 // Design says: The maximum width of a bubble is still 3/4 of the screen width. But try with 85% now. private const val BUBBLE_WIDTH_RATIO = 0.85f @@ -66,11 +75,12 @@ fun MessageEventBubble( content: @Composable () -> Unit = {}, ) { fun bubbleShape(): Shape { + val topLeftCorner = if (state.cutTopStart) 0.dp else BUBBLE_RADIUS return when (state.groupPosition) { TimelineItemGroupPosition.First -> if (state.isMine) { RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp, BUBBLE_RADIUS) } else { - RoundedCornerShape(BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) + RoundedCornerShape(topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, 0.dp) } TimelineItemGroupPosition.Middle -> if (state.isMine) { RoundedCornerShape(BUBBLE_RADIUS, 0.dp, 0.dp, BUBBLE_RADIUS) @@ -84,7 +94,7 @@ fun MessageEventBubble( } TimelineItemGroupPosition.None -> RoundedCornerShape( - BUBBLE_RADIUS, + topLeftCorner, BUBBLE_RADIUS, BUBBLE_RADIUS, BUBBLE_RADIUS @@ -106,11 +116,30 @@ fun MessageEventBubble( else -> ElementTheme.colors.messageFromOtherBackground } val bubbleShape = bubbleShape() + val radiusPx = (avatarRadius + SENDER_AVATAR_BORDER_WIDTH).toPx() + val yOffsetPx = -(NEGATIVE_MARGIN_FOR_BUBBLE + avatarRadius).toPx() Box( modifier = modifier .fillMaxWidth(BUBBLE_WIDTH_RATIO) - .padding(horizontal = 16.dp) - .offsetForItem(), + .padding(start = avatarRadius, end = 16.dp) + .offsetForItem() + .graphicsLayer { + compositingStrategy = CompositingStrategy.Offscreen + } + .drawWithContent { + drawContent() + if (state.cutTopStart) { + drawCircle( + color = Color.Black, + center = Offset( + x = 0f, + y = yOffsetPx, + ), + radius = radiusPx, + blendMode = BlendMode.Clear, + ) + } + }, // Need to set the contentAlignment again (it's already set in TimelineItemEventRow), for the case // when content width is low. contentAlignment = if (state.isMine) Alignment.CenterEnd else Alignment.CenterStart diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 6f81c9c55f..735e2e6a82 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -17,7 +17,6 @@ package io.element.android.features.messages.impl.timeline.components import android.annotation.SuppressLint -import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation @@ -46,8 +45,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.clipToBounds -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalViewConfiguration import androidx.compose.ui.platform.ViewConfiguration import androidx.compose.ui.res.stringResource @@ -59,7 +56,6 @@ import androidx.compose.ui.semantics.testTag import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -118,6 +114,12 @@ import kotlinx.coroutines.launch import kotlin.math.abs import kotlin.math.roundToInt +// The bubble has a negative margin to be placed a bit upper regarding the sender +// information and overlap the avatar. +val NEGATIVE_MARGIN_FOR_BUBBLE = (-8).dp +// Width of the transparent border around the sender avatar +val SENDER_AVATAR_BORDER_WIDTH = 3.dp + @Composable fun TimelineItemEventRow( event: TimelineItem.Event, @@ -289,13 +291,11 @@ private fun TimelineItemEventRowContent( ) = createRefs() // Sender - val avatarStrokeSize = 3.dp if (event.showSenderInformation && !timelineRoomInfo.isDm) { MessageSenderInformation( event.senderId, event.senderProfile, event.senderAvatar, - avatarStrokeSize, Modifier .constrainAs(sender) { top.linkTo(parent.top) @@ -321,7 +321,7 @@ private fun TimelineItemEventRowContent( MessageEventBubble( modifier = Modifier .constrainAs(message) { - top.linkTo(sender.bottom, margin = -avatarStrokeSize - 8.dp) + top.linkTo(sender.bottom, margin = NEGATIVE_MARGIN_FOR_BUBBLE) this.linkStartOrEnd(event) }, state = bubbleState, @@ -373,37 +373,17 @@ private fun MessageSenderInformation( senderId: UserId, senderProfile: ProfileTimelineDetails, senderAvatar: AvatarData, - avatarStrokeSize: Dp, modifier: Modifier = Modifier ) { - val avatarStrokeColor = MaterialTheme.colorScheme.background - val avatarSize = senderAvatar.size.dp val avatarColors = AvatarColorsProvider.provide(senderAvatar.id, ElementTheme.isLightTheme) - Box( - modifier = modifier - ) { - // Background of Avatar, to erase the corner of the message content - Canvas( - modifier = Modifier - .size(size = avatarSize + avatarStrokeSize) - .clipToBounds() - ) { - drawCircle( - color = avatarStrokeColor, - center = Offset(x = (avatarSize / 2).toPx(), y = (avatarSize / 2).toPx()), - radius = (avatarSize / 2 + avatarStrokeSize).toPx() - ) - } - // Content - Row { - Avatar(senderAvatar) - Spacer(modifier = Modifier.width(4.dp)) - SenderName( - senderId = senderId, - senderProfile = senderProfile, - senderNameMode = SenderNameMode.Timeline(avatarColors.foreground), - ) - } + Row(modifier = modifier) { + Avatar(senderAvatar) + Spacer(modifier = Modifier.width(4.dp)) + SenderName( + senderId = senderId, + senderProfile = senderProfile, + senderNameMode = SenderNameMode.Timeline(avatarColors.foreground), + ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt index 4f1fc3c546..3d66e7c1e9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/bubble/BubbleState.kt @@ -24,4 +24,7 @@ data class BubbleState( val isMine: Boolean, val isHighlighted: Boolean, val timelineRoomInfo: TimelineRoomInfo, -) +) { + /** True to cut out the top start corner of the bubble, to give margin for the sender avatar. */ + val cutTopStart: Boolean = groupPosition.isNew() && !isMine && !timelineRoomInfo.isDm +}