Rewrite fallbackphoto system.

This commit is contained in:
Alex Hart
2024-06-12 15:59:35 -03:00
committed by Greyson Parrelli
parent d698f74d0b
commit 11557e4815
42 changed files with 676 additions and 805 deletions

View File

@@ -0,0 +1,158 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import androidx.annotation.DrawableRes
import androidx.annotation.Px
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.util.NameUtil
/**
* Specifies what kind of avatar should be generated for a given recipient.
*/
sealed interface FallbackAvatar {
val color: AvatarColor
/**
* Transparent avatar
*/
data object Transparent : FallbackAvatar {
override val color: AvatarColor = AvatarColor.UNKNOWN
}
/**
* Generated avatars utilize the initials of the given recipient
*/
data class Text(val content: String, override val color: AvatarColor) : FallbackAvatar {
init {
check(content.isNotEmpty())
}
}
/**
* Fallback avatars that are backed by resources.
*/
sealed interface Resource : FallbackAvatar {
@DrawableRes
fun getIconBySize(size: Size): Int
/**
* Local user
*/
data class Local(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_note_compact_16
Size.MEDIUM -> R.drawable.symbol_note_24
Size.LARGE -> R.drawable.symbol_note_display_bold_40
}
}
}
/**
* Individual user without a display name.
*/
data class Person(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_person_compact_16
Size.MEDIUM -> R.drawable.symbol_person_24
Size.LARGE -> R.drawable.symbol_person_display_bold_40
}
}
}
/**
* A group
*/
data class Group(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_group_compact_16
Size.MEDIUM -> R.drawable.symbol_group_24
Size.LARGE -> R.drawable.symbol_group_display_bold_40
}
}
}
/**
* Story distribution lists
*/
data class DistributionList(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_stories_compact_16
Size.MEDIUM -> R.drawable.symbol_stories_24
Size.LARGE -> R.drawable.symbol_stories_display_bold_40
}
}
}
/**
* Call Links
*/
data class CallLink(override val color: AvatarColor) : Resource {
override fun getIconBySize(size: Size): Int {
return when (size) {
Size.SMALL -> R.drawable.symbol_video_compact_16
Size.MEDIUM -> R.drawable.symbol_video_24
Size.LARGE -> R.drawable.symbol_video_display_bold_40
}
}
}
}
enum class Size {
/**
* Smaller than 32dp
*/
SMALL,
/**
* 32dp and larger
*/
MEDIUM,
/**
* 80dp and larger
*/
LARGE
}
companion object {
const val ICON_TO_BACKGROUND_SCALE = 0.625
@JvmStatic
@JvmOverloads
fun forTextOrDefault(text: String, avatarColor: AvatarColor, default: FallbackAvatar = Resource.Person(avatarColor)): FallbackAvatar {
val abbreviation = NameUtil.getAbbreviation(text)
return if (abbreviation != null) {
Text(abbreviation, avatarColor)
} else {
default
}
}
fun getSizeByPx(@Px px: Int): Size {
return getSizeByDp(DimensionUnit.PIXELS.toDp(px.toFloat()).dp)
}
fun getSizeByDp(dp: Dp): Size {
val rawDp = dp.value
return when {
rawDp >= 80.0 -> Size.LARGE
rawDp < 32.0 -> Size.SMALL
else -> Size.MEDIUM
}
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.airbnb.lottie.SimpleColorFilter
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.RelativeCornerSize
import com.google.android.material.shape.RoundedCornerTreatment
import com.google.android.material.shape.ShapeAppearanceModel
import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.avatar.TextAvatarDrawable
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
class FallbackAvatarDrawable(
private val context: Context,
private val fallbackAvatar: FallbackAvatar
) : MaterialShapeDrawable() {
private val avatarColorPair: AvatarColorPair = AvatarColorPair.create(context, fallbackAvatar.color)
private var avatarSize: FallbackAvatar.Size = FallbackAvatar.Size.SMALL
private var icon: Drawable? = null
init {
fillColor = ColorStateList.valueOf(avatarColorPair.backgroundColor)
}
fun circleCrop(): FallbackAvatarDrawable {
shapeAppearanceModel = ShapeAppearanceModel.builder()
.setAllCorners(RoundedCornerTreatment())
.setAllCornerSizes(RelativeCornerSize(0.5f))
.build()
return this
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
avatarSize = FallbackAvatar.getSizeByPx(bounds.width())
icon = when (fallbackAvatar) {
is FallbackAvatar.Resource -> {
val resourceIcon = ContextCompat.getDrawable(context, fallbackAvatar.getIconBySize(avatarSize))!!
val iconBounds = Rect(bounds)
iconBounds.inset(
((bounds.width() - (bounds.width() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt(),
((bounds.height() - (bounds.height() * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2f).toInt()
)
resourceIcon.bounds = iconBounds
resourceIcon
}
is FallbackAvatar.Text -> TextAvatarDrawable(
context = context,
avatar = Avatar.Text(
fallbackAvatar.content,
Avatars.ColorPair(avatarColorPair.backgroundColor, avatarColorPair.foregroundColor, ""),
Avatar.DatabaseId.DoNotPersist
),
size = bounds.width()
)
FallbackAvatar.Transparent -> null
}
icon?.alpha = alpha
icon?.colorFilter = SimpleColorFilter(avatarColorPair.foregroundColor)
}
override fun draw(canvas: Canvas) {
if (icon == null) return
super.draw(canvas)
icon?.draw(canvas)
}
override fun setAlpha(alpha: Int) {
super.setAlpha(alpha)
icon?.alpha = alpha
invalidateSelf()
}
}

View File

@@ -0,0 +1,119 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.avatar.fallback
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.AvatarColorPair
@Composable
fun FallbackAvatarImage(
fallbackAvatar: FallbackAvatar,
modifier: Modifier = Modifier,
shape: Shape = CircleShape
) {
if (fallbackAvatar is FallbackAvatar.Transparent) {
Box(modifier = modifier)
return
}
val context = LocalContext.current
val colorPair = remember(fallbackAvatar) {
AvatarColorPair.create(context, fallbackAvatar.color)
}
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = modifier
.background(Color(colorPair.backgroundColor), shape)
) {
when (fallbackAvatar) {
is FallbackAvatar.Resource -> {
val size = remember(maxWidth) {
FallbackAvatar.getSizeByDp(maxWidth)
}
val padding = remember(maxWidth) {
((maxWidth.value - (maxWidth.value * FallbackAvatar.ICON_TO_BACKGROUND_SCALE)) / 2).dp
}
Icon(
painter = painterResource(fallbackAvatar.getIconBySize(size)),
contentDescription = null,
tint = Color(colorPair.foregroundColor),
modifier = Modifier
.fillMaxSize()
.padding(padding)
)
}
is FallbackAvatar.Text -> {
val size = DimensionUnit.DP.toPixels(maxWidth.value) * 0.8f
val textSize = DimensionUnit.PIXELS.toDp(Avatars.getTextSizeForLength(context, fallbackAvatar.content, size, size))
// TODO [alex] -- Handle emoji
Text(
text = fallbackAvatar.content,
color = Color(colorPair.foregroundColor),
fontSize = TextUnit(textSize, TextUnitType.Sp),
fontFamily = FontFamily(AvatarRenderer.getTypeface(context))
)
}
FallbackAvatar.Transparent -> {}
}
}
}
@SignalPreview
@Composable
fun FallbackAvatarImagePreview() {
Previews.Preview {
Column {
Text(text = "Compose - Large")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(160.dp)
)
Text(text = "Compose - Medium")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(64.dp)
)
Text(text = "Compose - Small")
FallbackAvatarImage(
fallbackAvatar = FallbackAvatar.Text("AE", AvatarColor.A100),
modifier = Modifier.size(24.dp)
)
}
}
}

View File

@@ -87,8 +87,8 @@ class AvatarView @JvmOverloads constructor(
avatar.setRecipient(recipient)
}
fun setFallbackPhotoProvider(fallbackPhotoProvider: Recipient.FallbackPhotoProvider) {
avatar.setFallbackPhotoProvider(fallbackPhotoProvider)
fun setFallbackAvatarProvider(fallbackAvatarProvider: AvatarImageView.FallbackAvatarProvider?) {
avatar.setFallbackAvatarProvider(fallbackAvatarProvider)
}
fun disableQuickContact() {