Merge pull request #4103 from element-hq/feature/bma/fixDmAvatarRtl

Fix dm avatar rtl
This commit is contained in:
Benoit Marty 2025-01-02 16:15:11 +01:00 committed by GitHub
commit 1272ee43d6
26 changed files with 245 additions and 49 deletions

View file

@ -0,0 +1,127 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.knockrequests.impl.banner
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
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.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
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 kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
/**
* Draw a row of avatars (they must all have the same size), from start to end.
* @param avatarDataList the avatars to render. Note: they will all be rendered, the caller may
* want to limit the list size
* @param modifier Jetpack Compose modifier
* @param overlapRatio the overlap ration. When 0f, avatars will render without overlap, when 1f
* only the first avatar will be visible
*/
@Composable
fun AvatarRow(
avatarDataList: ImmutableList<AvatarData>,
modifier: Modifier = Modifier,
overlapRatio: Float = 0.5f,
) {
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box(
modifier = modifier,
) {
val lastItemIndex = avatarDataList.size - 1
val avatarSize = avatarDataList.firstOrNull()?.size?.dp ?: return
val avatarSizePx = avatarSize.toPx()
avatarDataList
.reversed()
.forEachIndexed { index, avatarData ->
Avatar(
modifier = Modifier
.padding(start = avatarSize * (1 - overlapRatio) * (lastItemIndex - index))
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
// Draw content and clear the pixels for the avatar on the left (right in RTL).
drawContent()
val xOffset = if (isRtl) {
size.width - avatarSizePx * (overlapRatio - 0.5f)
} else {
0f + avatarSizePx * (overlapRatio - 0.5f)
}
if (index < lastItemIndex) {
drawCircle(
color = Color.Black,
center = Offset(
x = xOffset,
y = size.height / 2,
),
radius = avatarSizePx / 2,
blendMode = BlendMode.Clear,
)
}
}
.size(size = avatarSize)
// Keep internal padding, it has the advantage to not reduce the size of the Avatar image,
// which is already small in our use case.
.padding(2.dp),
avatarData = avatarData,
)
}
}
}
@Composable
@PreviewsDayNight
internal fun AvatarRowPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
ElementPreview {
ContentToPreview(overlapRatio)
}
}
@Composable
@PreviewsDayNight
internal fun AvatarRowRtlPreview(@PreviewParameter(OverlapRatioProvider::class) overlapRatio: Float) {
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl,
) {
ElementPreview {
ContentToPreview(overlapRatio)
}
}
}
@Composable
private fun ContentToPreview(overlapRatio: Float) {
AvatarRow(
avatarDataList = listOf("A", "B", "C").map {
AvatarData(
id = it,
name = it,
size = AvatarSize.RoomListItem,
)
}.toImmutableList(),
overlapRatio = overlapRatio,
)
}

View file

@ -19,19 +19,12 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.graphicsLayer
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -56,6 +49,7 @@ 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.components.Text
import io.element.android.libraries.ui.strings.CommonStrings import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
private const val MAX_AVATAR_COUNT = 3 private const val MAX_AVATAR_COUNT = 3
@ -120,9 +114,9 @@ private fun KnockRequestsBannerContent(
} }
Column( Column(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 16.dp) .padding(all = 16.dp)
) { ) {
Row { Row {
KnockRequestAvatarView( KnockRequestAvatarView(
@ -212,44 +206,16 @@ private fun KnockRequestAvatarListView(
knockRequests: ImmutableList<KnockRequestPresentable>, knockRequests: ImmutableList<KnockRequestPresentable>,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val avatarSize = AvatarSize.KnockRequestBanner.dp val avatars = knockRequests
Box( .take(MAX_AVATAR_COUNT)
.map { knockRequest ->
knockRequest.getAvatarData(AvatarSize.KnockRequestBanner)
}
.toImmutableList()
AvatarRow(
avatarDataList = avatars,
modifier = modifier, modifier = modifier,
) { )
knockRequests
.take(MAX_AVATAR_COUNT)
.reversed()
.let { smallReversedList ->
val lastItemIndex = smallReversedList.size - 1
smallReversedList.forEachIndexed { index, knockRequest ->
Avatar(
modifier = Modifier
.padding(start = avatarSize / 2 * (lastItemIndex - index))
.graphicsLayer {
compositingStrategy = CompositingStrategy.Offscreen
}
.drawWithContent {
// Draw content and clear the pixels for the avatar on the left.
drawContent()
if (index < lastItemIndex) {
drawCircle(
color = Color.Black,
center = Offset(
x = 0f,
y = size.height / 2,
),
radius = avatarSize.toPx() / 2,
blendMode = BlendMode.Clear,
)
}
}
.size(size = avatarSize)
.padding(2.dp),
avatarData = knockRequest.getAvatarData(AvatarSize.KnockRequestBanner),
)
}
}
}
} }
@Composable @Composable

View file

@ -0,0 +1,20 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.knockrequests.impl.banner
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
class OverlapRatioProvider : PreviewParameterProvider<Float> {
override val values: Sequence<Float> = sequenceOf(
0f,
0.25f,
0.5f,
0.75f,
1f
)
}

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
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.clip import androidx.compose.ui.draw.clip
@ -21,7 +22,9 @@ 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.CompositingStrategy
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.ElementThemedPreview
import io.element.android.libraries.designsystem.preview.PreviewGroup import io.element.android.libraries.designsystem.preview.PreviewGroup
import io.element.android.libraries.designsystem.text.toPx import io.element.android.libraries.designsystem.text.toPx
@ -45,10 +48,11 @@ fun DmAvatars(
val boxSize = userAvatarData.size.dp * SIZE_RATIO val boxSize = userAvatarData.size.dp * SIZE_RATIO
val boxSizePx = boxSize.toPx() val boxSizePx = boxSize.toPx()
val otherAvatarRadius = otherUserAvatarData.size.dp.toPx() / 2 val otherAvatarRadius = otherUserAvatarData.size.dp.toPx() / 2
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
Box( Box(
modifier = modifier.size(boxSize), modifier = modifier.size(boxSize),
) { ) {
// Draw user avatar and cut top right corner // Draw user avatar and cut top end corner
Avatar( Avatar(
avatarData = userAvatarData, avatarData = userAvatarData,
modifier = Modifier modifier = Modifier
@ -58,10 +62,15 @@ fun DmAvatars(
} }
.drawWithContent { .drawWithContent {
drawContent() drawContent()
val xOffset = if (isRtl) {
size.width - boxSizePx + otherAvatarRadius
} else {
boxSizePx - otherAvatarRadius
}
drawCircle( drawCircle(
color = Color.Black, color = Color.Black,
center = Offset( center = Offset(
x = boxSizePx - otherAvatarRadius, x = xOffset,
y = size.height - (boxSizePx - otherAvatarRadius), y = size.height - (boxSizePx - otherAvatarRadius),
), ),
radius = otherAvatarRadius / 0.9f, radius = otherAvatarRadius / 0.9f,
@ -106,3 +115,13 @@ internal fun DmAvatarsPreview() = ElementThemedPreview {
openOtherAvatarPreview = {}, openOtherAvatarPreview = {},
) )
} }
@Preview(group = PreviewGroup.Avatars)
@Composable
internal fun DmAvatarsRtlPreview() {
CompositionLocalProvider(
LocalLayoutDirection provides LayoutDirection.Rtl,
) {
DmAvatarsPreview()
}
}

View file

@ -50,6 +50,7 @@ class KonsistClassNameTest {
.withAllParentsOf(PreviewParameterProvider::class) .withAllParentsOf(PreviewParameterProvider::class)
.withoutName( .withoutName(
"AspectRatioProvider", "AspectRatioProvider",
"OverlapRatioProvider",
) )
.also { .also {
// Check that classes are actually found // Check that classes are actually found

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d318e0e4fa1bd8a123057dca6efc9c92c5dc96614dad3514813559274288e115
size 8520

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0bee0c2e2c712621bbc92c430807c758429ef1179394b107c393f9236295bbb
size 8447

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4a92d34b2ac73b7d3341f39f46bbf0a4dcf9862e9cccdfc0111b64c562930f03
size 7720

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:79a021e58c28d0e0cbb50f99304dae5c8c2ec95ca99a02d7876a167adc911e7f
size 6948

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea4febe911c963e720535c576a65842d2616ffb61103153a8a8738886c26eca7
size 5251

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:33ad034f7be86da7a2399d11c0991b5d6997ddaffa9f26b20a456f1b22666b70
size 8825

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5784d917b0176cfa6b584804b28d8592ed97fe6e1f60a805619cce3170acb802
size 8722

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:06a0cef40417aa586e85b5896e763279e2c3df6121ffd65b9b85129e2a5ea4f4
size 8023

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ac5300b143c12ebf39fb6b0074677b5f16760217cca3823229304838e322e424
size 7375

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:690a1313ac8dd2cb84d7d560a16bdcd3eb0b65ad16d502da37efccecc4198b27
size 5467

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:836e6d27c46a85d5a8d6fd8b612ba4b2d79c2ffddd5660c00bf0a4302871734c
size 8503

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c2606e39c21d12059e70c1bf95a80828c415ad1dc5fe27d2533dad58cec6e948
size 8445

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fe8810842b717b145cfd850221a0b24608b7693d60735c15d2a2ea349449f781
size 8015

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dff87afdeca9e6de4a5053947270d93a61a23faf4a605e52b6b01e814b5d5e3e
size 6927

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ea4febe911c963e720535c576a65842d2616ffb61103153a8a8738886c26eca7
size 5251

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4cfea11d355bbe92346b49438ef59e4f7fe694039cdec8fdfc6ca097edbcdcea
size 8837

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d4de82de886361037273b9a06fd29bff1b24a20a329d8590d97d03515ca21b43
size 8733

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cecf626b261bfbe72dfd9692a467d7205ee773db2afcb7adeabecfd9e390daf0
size 8331

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6e05d4bb9772b9535617ae9da1f461ed399fc9665dc2adf55a9d647ff640319b
size 7331

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:690a1313ac8dd2cb84d7d560a16bdcd3eb0b65ad16d502da37efccecc4198b27
size 5467

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b0007ee6df39ec6b8f180cab21a99f97adef39deb1055ba8d179e5fee5ca1203
size 13856