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 a411ac87ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.text.style.TextAlign
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.ui.strings.CommonStrings
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
private const val MAX_AVATAR_COUNT = 3
@ -120,9 +114,9 @@ private fun KnockRequestsBannerContent(
}
Column(
modifier
.fillMaxWidth()
.padding(all = 16.dp)
modifier
.fillMaxWidth()
.padding(all = 16.dp)
) {
Row {
KnockRequestAvatarView(
@ -212,44 +206,16 @@ private fun KnockRequestAvatarListView(
knockRequests: ImmutableList<KnockRequestPresentable>,
modifier: Modifier = Modifier,
) {
val avatarSize = AvatarSize.KnockRequestBanner.dp
Box(
val avatars = knockRequests
.take(MAX_AVATAR_COUNT)
.map { knockRequest ->
knockRequest.getAvatarData(AvatarSize.KnockRequestBanner)
}
.toImmutableList()
AvatarRow(
avatarDataList = avatars,
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

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