mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Rewrite fallbackphoto system.
This commit is contained in:
committed by
Greyson Parrelli
parent
d698f74d0b
commit
11557e4815
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user