Merge pull request #4103 from element-hq/feature/bma/fixDmAvatarRtl
Fix dm avatar rtl
This commit is contained in:
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ class KonsistClassNameTest {
|
||||
.withAllParentsOf(PreviewParameterProvider::class)
|
||||
.withoutName(
|
||||
"AspectRatioProvider",
|
||||
"OverlapRatioProvider",
|
||||
)
|
||||
.also {
|
||||
// Check that classes are actually found
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user