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
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