Introduce CompositeAvatar to render heroes when main AvatarData does not have URL.
This commit is contained in:
parent
3aed09ef4d
commit
f886bf7105
2 changed files with 170 additions and 4 deletions
|
|
@ -32,6 +32,7 @@ import androidx.compose.ui.platform.LocalInspectionMode
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
|
@ -52,18 +53,22 @@ fun Avatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
|
// If not null, will be used instead of the size from avatarData
|
||||||
|
forcedAvatarSize: Dp? = null,
|
||||||
) {
|
) {
|
||||||
val commonModifier = modifier
|
val commonModifier = modifier
|
||||||
.size(avatarData.size.dp)
|
.size(forcedAvatarSize ?: avatarData.size.dp)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
if (avatarData.url.isNullOrBlank()) {
|
if (avatarData.url.isNullOrBlank()) {
|
||||||
InitialsAvatar(
|
InitialsAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
ImageAvatar(
|
ImageAvatar(
|
||||||
avatarData = avatarData,
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
modifier = commonModifier,
|
modifier = commonModifier,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
)
|
)
|
||||||
|
|
@ -73,6 +78,7 @@ fun Avatar(
|
||||||
@Composable
|
@Composable
|
||||||
private fun ImageAvatar(
|
private fun ImageAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
|
forcedAvatarSize: Dp?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentDescription: String? = null,
|
contentDescription: String? = null,
|
||||||
) {
|
) {
|
||||||
|
|
@ -98,9 +104,15 @@ private fun ImageAvatar(
|
||||||
SideEffect {
|
SideEffect {
|
||||||
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
Timber.e(state.result.throwable, "Error loading avatar $state\n${state.result}")
|
||||||
}
|
}
|
||||||
InitialsAvatar(avatarData = avatarData)
|
InitialsAvatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
else -> InitialsAvatar(avatarData = avatarData)
|
else -> InitialsAvatar(
|
||||||
|
avatarData = avatarData,
|
||||||
|
forcedAvatarSize = forcedAvatarSize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,13 +121,14 @@ private fun ImageAvatar(
|
||||||
@Composable
|
@Composable
|
||||||
private fun InitialsAvatar(
|
private fun InitialsAvatar(
|
||||||
avatarData: AvatarData,
|
avatarData: AvatarData,
|
||||||
|
forcedAvatarSize: Dp?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
val avatarColors = AvatarColorsProvider.provide(avatarData.id, ElementTheme.isLightTheme)
|
||||||
Box(
|
Box(
|
||||||
modifier.background(color = avatarColors.background)
|
modifier.background(color = avatarColors.background)
|
||||||
) {
|
) {
|
||||||
val fontSize = avatarData.size.dp.toSp() / 2
|
val fontSize = (forcedAvatarSize ?: avatarData.size.dp).toSp() / 2
|
||||||
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
val originalFont = ElementTheme.typography.fontHeadingMdBold
|
||||||
val ratio = fontSize.value / originalFont.fontSize.value
|
val ratio = fontSize.value / originalFont.fontSize.value
|
||||||
val lineHeight = originalFont.lineHeight * ratio
|
val lineHeight = originalFont.lineHeight * ratio
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
* Copyright (c) 2024 New Vector Ltd
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.designsystem.components.avatar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.libraries.designsystem.preview.ElementThemedPreview
|
||||||
|
import io.element.android.libraries.designsystem.preview.PreviewGroup
|
||||||
|
import java.util.Collections
|
||||||
|
import kotlin.math.PI
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CompositeAvatar(
|
||||||
|
avatarData: AvatarData,
|
||||||
|
heroes: List<AvatarData>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: String? = null,
|
||||||
|
) {
|
||||||
|
if (avatarData.url != null || heroes.isEmpty()) {
|
||||||
|
Avatar(avatarData, modifier, contentDescription)
|
||||||
|
} else {
|
||||||
|
val limitedHeroes = heroes.take(4)
|
||||||
|
val numberOfHeroes = limitedHeroes.size
|
||||||
|
if (numberOfHeroes == 4) {
|
||||||
|
// Swap 2 and 3 so that the 4th hero is at the bottom right
|
||||||
|
Collections.swap(limitedHeroes, 2, 3)
|
||||||
|
}
|
||||||
|
when (numberOfHeroes) {
|
||||||
|
0 -> {
|
||||||
|
// Cannot happen
|
||||||
|
}
|
||||||
|
1 -> {
|
||||||
|
Avatar(heroes[0], modifier, contentDescription)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val angle = 2 * Math.PI / numberOfHeroes
|
||||||
|
val offsetRadius = when (numberOfHeroes) {
|
||||||
|
2 -> avatarData.size.dp.value / 4.2
|
||||||
|
3 -> avatarData.size.dp.value / 4.0
|
||||||
|
4 -> avatarData.size.dp.value / 3.1
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
val heroAvatarSize = when (numberOfHeroes) {
|
||||||
|
2 -> avatarData.size.dp / 2.2f
|
||||||
|
3 -> avatarData.size.dp / 2.4f
|
||||||
|
4 -> avatarData.size.dp / 2.2f
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
val angleOffset = when (numberOfHeroes) {
|
||||||
|
2 -> PI
|
||||||
|
3 -> 7 * PI / 6
|
||||||
|
4 -> 13 * PI / 4
|
||||||
|
else -> error("Unsupported number of heroes: $numberOfHeroes")
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(avatarData.size.dp)
|
||||||
|
.semantics {
|
||||||
|
this.contentDescription = contentDescription.orEmpty()
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
limitedHeroes.forEachIndexed { index, heroAvatar ->
|
||||||
|
val xOffset = (offsetRadius * cos(angle * index.toDouble() + angleOffset)).dp
|
||||||
|
val yOffset = (offsetRadius * sin(angle * index.toDouble() + angleOffset)).dp
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(heroAvatarSize)
|
||||||
|
.offset(
|
||||||
|
x = xOffset,
|
||||||
|
y = yOffset,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Avatar(
|
||||||
|
heroAvatar,
|
||||||
|
forcedAvatarSize = heroAvatarSize,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(group = PreviewGroup.Avatars)
|
||||||
|
@Composable
|
||||||
|
internal fun CompositeAvatarPreview() = ElementThemedPreview {
|
||||||
|
val mainAvatar = anAvatarData(
|
||||||
|
id = "Zac",
|
||||||
|
name = "Zac",
|
||||||
|
size = AvatarSize.RoomListItem,
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(0) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(1) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(2) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(3) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(4) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
CompositeAvatar(
|
||||||
|
avatarData = mainAvatar,
|
||||||
|
heroes = List(5) { aHeroAvatarData(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun aHeroAvatarData(i: Int) = anAvatarData(
|
||||||
|
id = ('A' + i).toString(),
|
||||||
|
name = ('A' + i).toString()
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue