Merge branch 'develop' into feature-oled-black
This commit is contained in:
commit
d0dcbab750
1505 changed files with 14143 additions and 9545 deletions
|
|
@ -8,14 +8,19 @@
|
|||
|
||||
package io.element.android.libraries.designsystem.atomic.atoms
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.components.Badge
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
|
||||
/**
|
||||
* https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=1960-491
|
||||
*/
|
||||
object MatrixBadgeAtom {
|
||||
data class MatrixBadgeData(
|
||||
val text: String,
|
||||
|
|
@ -40,6 +45,12 @@ object MatrixBadgeAtom {
|
|||
Type.Negative -> ElementTheme.colors.bgCriticalSubtle
|
||||
Type.Info -> ElementTheme.colors.bgBadgeInfo
|
||||
}
|
||||
val borderStroke = when (data.type) {
|
||||
Type.Positive -> null
|
||||
Type.Neutral -> BorderStroke(1.dp, ElementTheme.colors.borderInteractiveSecondary)
|
||||
Type.Negative -> null
|
||||
Type.Info -> null
|
||||
}
|
||||
val textColor = when (data.type) {
|
||||
Type.Positive -> ElementTheme.colors.textBadgeAccent
|
||||
Type.Neutral -> ElementTheme.colors.textPrimary
|
||||
|
|
@ -58,6 +69,7 @@ object MatrixBadgeAtom {
|
|||
backgroundColor = backgroundColor,
|
||||
iconColor = iconColor,
|
||||
textColor = textColor,
|
||||
borderStroke = borderStroke,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.libraries.designsystem.colors.gradientCriticalColors
|
||||
import io.element.android.libraries.designsystem.colors.gradientInfoColors
|
||||
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.AvatarType
|
||||
|
|
@ -38,13 +40,16 @@ import io.element.android.libraries.designsystem.theme.components.Icon
|
|||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=2392-6721
|
||||
*/
|
||||
@Composable
|
||||
fun ComposerAlertMolecule(
|
||||
avatar: AvatarData?,
|
||||
content: AnnotatedString,
|
||||
onSubmitClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
level: ComposerAlertLevel = ComposerAlertLevel.Default,
|
||||
level: ComposerAlertLevel = ComposerAlertLevel.Info,
|
||||
showIcon: Boolean = false,
|
||||
submitText: String = stringResource(CommonStrings.action_ok),
|
||||
) {
|
||||
|
|
@ -52,20 +57,12 @@ fun ComposerAlertMolecule(
|
|||
modifier.fillMaxWidth()
|
||||
) {
|
||||
val lineColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.borderInfoSubtle
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.borderInfoSubtle
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.borderCriticalSubtle
|
||||
}
|
||||
|
||||
val startColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.bgInfoSubtle
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.bgInfoSubtle
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.bgCriticalSubtle
|
||||
}
|
||||
|
||||
val textColor = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.textPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.textInfoPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.textPrimary
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.textCriticalPrimary
|
||||
}
|
||||
|
||||
|
|
@ -75,12 +72,13 @@ fun ComposerAlertMolecule(
|
|||
.height(1.dp)
|
||||
.background(lineColor)
|
||||
)
|
||||
val brush = Brush.verticalGradient(
|
||||
listOf(startColor, ElementTheme.colors.bgCanvasDefault),
|
||||
)
|
||||
val gradientColors = when (level) {
|
||||
ComposerAlertLevel.Info -> gradientInfoColors()
|
||||
ComposerAlertLevel.Critical -> gradientCriticalColors()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(brush)
|
||||
.background(Brush.verticalGradient(gradientColors))
|
||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
|
||||
) {
|
||||
Column(
|
||||
|
|
@ -96,12 +94,10 @@ fun ComposerAlertMolecule(
|
|||
)
|
||||
} else if (showIcon) {
|
||||
val icon = when (level) {
|
||||
ComposerAlertLevel.Default -> CompoundIcons.Info()
|
||||
ComposerAlertLevel.Info -> CompoundIcons.Info()
|
||||
ComposerAlertLevel.Critical -> CompoundIcons.Error()
|
||||
}
|
||||
val iconTint = when (level) {
|
||||
ComposerAlertLevel.Default -> ElementTheme.colors.iconPrimary
|
||||
ComposerAlertLevel.Info -> ElementTheme.colors.iconInfoPrimary
|
||||
ComposerAlertLevel.Critical -> ElementTheme.colors.iconCriticalPrimary
|
||||
}
|
||||
|
|
@ -131,7 +127,6 @@ fun ComposerAlertMolecule(
|
|||
}
|
||||
|
||||
enum class ComposerAlertLevel {
|
||||
Default,
|
||||
Info,
|
||||
Critical
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ internal data class ComposerAlertMoleculeParams(
|
|||
|
||||
internal class ComposerAlertMoleculeParamsProvider : PreviewParameterProvider<ComposerAlertMoleculeParams> {
|
||||
private val allLevels = sequenceOf(
|
||||
ComposerAlertLevel.Default,
|
||||
ComposerAlertLevel.Info,
|
||||
ComposerAlertLevel.Critical
|
||||
)
|
||||
|
|
|
|||
|
|
@ -38,8 +38,11 @@ fun gradientSubtleColors(): List<Color> = listOf(
|
|||
fun gradientInfoColors(): List<Color> = listOf(
|
||||
ElementTheme.colors.gradientInfoStop1,
|
||||
ElementTheme.colors.gradientInfoStop2,
|
||||
ElementTheme.colors.gradientInfoStop3,
|
||||
ElementTheme.colors.gradientInfoStop4,
|
||||
ElementTheme.colors.gradientInfoStop5,
|
||||
ElementTheme.colors.gradientInfoStop6,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun gradientCriticalColors(): List<Color> = listOf(
|
||||
ElementTheme.colors.gradientCriticalStop1,
|
||||
ElementTheme.colors.gradientCriticalStop2,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,419 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.produceState
|
||||
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.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.graphics.createBitmap
|
||||
import androidx.core.graphics.withSave
|
||||
import coil3.Image
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.asImage
|
||||
import coil3.memory.MemoryCache
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.allowHardware
|
||||
import coil3.toBitmap
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.colors.AvatarColorsProvider
|
||||
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
|
||||
|
||||
private val PIN_WIDTH = 42.dp
|
||||
private val PIN_HEIGHT = PIN_WIDTH * 1.2f
|
||||
private val AVATAR_SIZE = PIN_WIDTH - 10.dp
|
||||
private val CONTENT_OFFSET = 5.dp
|
||||
private val DOT_RADIUS = 6.dp
|
||||
private val STROKE_WIDTH = 1.dp
|
||||
|
||||
/**
|
||||
* Variants of location pin markers.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface PinVariant {
|
||||
data class UserLocation(
|
||||
val avatarData: AvatarData,
|
||||
val isLive: Boolean,
|
||||
) : PinVariant
|
||||
|
||||
data object PinnedLocation : PinVariant
|
||||
data object StaleLocation : PinVariant
|
||||
}
|
||||
|
||||
/**
|
||||
* A location pin composable that supports multiple variants.
|
||||
*
|
||||
* Based on Figma design: https://www.figma.com/design/G1xy0HDZKJf5TCRFmKb5d5/Compound-Android-Components?node-id=4665-2890&m=dev
|
||||
*/
|
||||
@Composable
|
||||
fun LocationPin(
|
||||
variant: PinVariant,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val image = rememberLocationPinBitmap(variant)
|
||||
Canvas(modifier = modifier.size(PIN_WIDTH, PIN_HEIGHT)) {
|
||||
if (image != null) {
|
||||
drawImage(image)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a location pin to an [ImageBitmap] using Canvas operations.
|
||||
* @param variant The pin variant to render
|
||||
* @return The rendered [ImageBitmap], or null if still loading
|
||||
*/
|
||||
@Composable
|
||||
fun rememberLocationPinBitmap(variant: PinVariant): ImageBitmap? {
|
||||
val context = LocalContext.current
|
||||
val density = LocalDensity.current
|
||||
val imageLoader = SingletonImageLoader.get(context)
|
||||
val colors = pinColors(variant)
|
||||
val cacheKey = rememberCacheKey(variant)
|
||||
return produceState<ImageBitmap?>(initialValue = null, cacheKey) {
|
||||
val memoryCacheKey = MemoryCache.Key(cacheKey)
|
||||
val cached = imageLoader.memoryCache?.get(memoryCacheKey)
|
||||
if (cached != null) {
|
||||
value = cached.image.toBitmap().asImageBitmap()
|
||||
} else {
|
||||
val dimensions = PinDimensions(density)
|
||||
val bitmap = LocationPinRenderer.renderPin(variant, colors, dimensions, context, imageLoader)
|
||||
imageLoader.memoryCache?.set(memoryCacheKey, MemoryCache.Value(bitmap.asImage()))
|
||||
value = bitmap.asImageBitmap()
|
||||
}
|
||||
}.value
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun pinColors(variant: PinVariant): PinColors {
|
||||
return when (variant) {
|
||||
is PinVariant.UserLocation -> {
|
||||
val avatarColors = AvatarColorsProvider.provide(variant.avatarData.id)
|
||||
if (variant.isLive) {
|
||||
PinColors(
|
||||
fill = ElementTheme.colors.iconAccentPrimary,
|
||||
stroke = Color.Transparent,
|
||||
dot = Color.Transparent,
|
||||
avatarStroke = ElementTheme.colors.bgCanvasDefault,
|
||||
avatarBackground = avatarColors.background,
|
||||
avatarForeground = avatarColors.foreground,
|
||||
)
|
||||
} else {
|
||||
PinColors(
|
||||
fill = ElementTheme.colors.bgCanvasDefault,
|
||||
stroke = ElementTheme.colors.iconQuaternaryAlpha,
|
||||
dot = Color.Transparent,
|
||||
avatarStroke = ElementTheme.colors.iconQuaternaryAlpha,
|
||||
avatarBackground = avatarColors.background,
|
||||
avatarForeground = avatarColors.foreground,
|
||||
)
|
||||
}
|
||||
}
|
||||
PinVariant.PinnedLocation -> PinColors(
|
||||
fill = ElementTheme.colors.bgCanvasDefault,
|
||||
stroke = ElementTheme.colors.iconSecondaryAlpha,
|
||||
avatarStroke = Color.Transparent,
|
||||
avatarBackground = Color.Transparent,
|
||||
avatarForeground = Color.Transparent,
|
||||
dot = ElementTheme.colors.iconPrimary,
|
||||
)
|
||||
PinVariant.StaleLocation -> PinColors(
|
||||
fill = ElementTheme.colors.bgSubtleSecondary,
|
||||
stroke = ElementTheme.colors.iconDisabled,
|
||||
avatarStroke = Color.Transparent,
|
||||
avatarBackground = Color.Transparent,
|
||||
avatarForeground = Color.Transparent,
|
||||
dot = ElementTheme.colors.iconDisabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Color configuration for rendering a location pin.
|
||||
*/
|
||||
private data class PinColors(
|
||||
val fill: Color,
|
||||
val stroke: Color,
|
||||
val dot: Color,
|
||||
val avatarStroke: Color,
|
||||
val avatarBackground: Color,
|
||||
val avatarForeground: Color,
|
||||
)
|
||||
|
||||
/**
|
||||
* Pre-calculated pixel dimensions for rendering a location pin.
|
||||
*/
|
||||
private class PinDimensions(density: Density) {
|
||||
val pinWidth = with(density) { PIN_WIDTH.toPx() }
|
||||
val pinHeight = with(density) { PIN_HEIGHT.toPx() }
|
||||
val avatarSize: Float = with(density) { AVATAR_SIZE.toPx() }
|
||||
val avatarOffset: Float = with(density) { CONTENT_OFFSET.toPx() }
|
||||
val dotRadius: Float = with(density) { DOT_RADIUS.toPx() }
|
||||
val strokeWidth: Float = with(density) { STROKE_WIDTH.toPx() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders location pins to bitmaps using Canvas operations.
|
||||
* Uses Coil for avatar loading.
|
||||
* Paint objects are shared across all renders.
|
||||
*/
|
||||
private object LocationPinRenderer {
|
||||
// Shared Paint objects to avoid allocations
|
||||
private val fillPaint = Paint().apply {
|
||||
style = Paint.Style.FILL
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val strokePaint = Paint().apply {
|
||||
style = Paint.Style.STROKE
|
||||
isAntiAlias = true
|
||||
}
|
||||
private val textPaint = Paint().apply {
|
||||
textAlign = Paint.Align.CENTER
|
||||
isAntiAlias = true
|
||||
isFakeBoldText = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a pin variant to bitmap. Suspending for async avatar loading.
|
||||
*/
|
||||
suspend fun renderPin(
|
||||
variant: PinVariant,
|
||||
colors: PinColors,
|
||||
dimensions: PinDimensions,
|
||||
context: Context,
|
||||
imageLoader: ImageLoader,
|
||||
): Bitmap {
|
||||
val bitmap = createBitmap(dimensions.pinWidth.toInt(), dimensions.pinHeight.toInt())
|
||||
val canvas = Canvas(bitmap)
|
||||
canvas.drawPinShape(colors.fill, colors.stroke, dimensions)
|
||||
when (variant) {
|
||||
is PinVariant.UserLocation -> {
|
||||
val avatarImage = loadAvatarImage(variant.avatarData, context, imageLoader)
|
||||
canvas.drawAvatar(
|
||||
avatarImage = avatarImage,
|
||||
avatarData = variant.avatarData,
|
||||
borderColor = colors.avatarStroke,
|
||||
backgroundColor = colors.avatarBackground,
|
||||
foregroundColor = colors.avatarForeground,
|
||||
dimensions = dimensions,
|
||||
)
|
||||
}
|
||||
PinVariant.PinnedLocation,
|
||||
PinVariant.StaleLocation -> canvas.drawDot(colors.dot, dimensions)
|
||||
}
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun Canvas.drawPinShape(fillColor: Color, strokeColor: Color, dimensions: PinDimensions) {
|
||||
val path = createPinPath(dimensions)
|
||||
fillPaint.color = fillColor.toArgb()
|
||||
drawPath(path, fillPaint)
|
||||
strokePaint.color = strokeColor.toArgb()
|
||||
strokePaint.strokeWidth = dimensions.strokeWidth
|
||||
drawPath(path, strokePaint)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the teardrop-shaped pin path to match dimensions.
|
||||
* Based on SVG path with dimensions 40x48 (ratio 1:1.2).
|
||||
*/
|
||||
private fun createPinPath(dimensions: PinDimensions): Path {
|
||||
val svgWidth = 40f
|
||||
val svgHeight = 48f
|
||||
val inset = dimensions.strokeWidth / 2
|
||||
val scaleX = (dimensions.pinWidth - dimensions.strokeWidth) / svgWidth
|
||||
val scaleY = (dimensions.pinHeight - dimensions.strokeWidth) / svgHeight
|
||||
|
||||
val path = Path().apply {
|
||||
moveTo(20f, 48f)
|
||||
cubicTo(19.4167f, 48f, 18.8333f, 47.8965f, 18.25f, 47.6895f)
|
||||
cubicTo(17.6667f, 47.4825f, 17.1458f, 47.1721f, 16.6875f, 46.7581f)
|
||||
cubicTo(13.9792f, 44.2743f, 11.5833f, 41.8525f, 9.5f, 39.4929f)
|
||||
cubicTo(7.41667f, 37.1332f, 5.67708f, 34.8461f, 4.28125f, 32.6313f)
|
||||
cubicTo(2.88542f, 30.4166f, 1.82292f, 28.2846f, 1.09375f, 26.2354f)
|
||||
cubicTo(0.364583f, 24.1863f, 0f, 22.2303f, 0f, 20.3674f)
|
||||
cubicTo(0f, 14.1578f, 2.01042f, 9.21087f, 6.03125f, 5.52652f)
|
||||
cubicTo(10.0521f, 1.84217f, 14.7083f, 0f, 20f, 0f)
|
||||
cubicTo(25.2917f, 0f, 29.9479f, 1.84217f, 33.9688f, 5.52652f)
|
||||
cubicTo(37.9896f, 9.21087f, 40f, 14.1578f, 40f, 20.3674f)
|
||||
cubicTo(40f, 22.2303f, 39.6354f, 24.1863f, 38.9062f, 26.2354f)
|
||||
cubicTo(38.1771f, 28.2846f, 37.1146f, 30.4166f, 35.7188f, 32.6313f)
|
||||
cubicTo(34.3229f, 34.8461f, 32.5833f, 37.1332f, 30.5f, 39.4929f)
|
||||
cubicTo(28.4167f, 41.8525f, 26.0208f, 44.2743f, 23.3125f, 46.7581f)
|
||||
cubicTo(22.8542f, 47.1721f, 22.3333f, 47.4825f, 21.75f, 47.6895f)
|
||||
cubicTo(21.1667f, 47.8965f, 20.5833f, 48f, 20f, 48f)
|
||||
close()
|
||||
}
|
||||
val matrix = Matrix().apply {
|
||||
setScale(scaleX, scaleY)
|
||||
postTranslate(inset, inset)
|
||||
}
|
||||
path.transform(matrix)
|
||||
return path
|
||||
}
|
||||
|
||||
private suspend fun loadAvatarImage(
|
||||
avatarData: AvatarData,
|
||||
context: Context,
|
||||
imageLoader: ImageLoader,
|
||||
): Image? {
|
||||
val request = ImageRequest.Builder(context)
|
||||
.data(avatarData)
|
||||
// Disable hardware rendering for Canvas
|
||||
.allowHardware(false)
|
||||
.build()
|
||||
return imageLoader.execute(request).image
|
||||
}
|
||||
|
||||
private fun Canvas.drawAvatar(
|
||||
avatarImage: Image?,
|
||||
avatarData: AvatarData,
|
||||
borderColor: Color,
|
||||
backgroundColor: Color,
|
||||
foregroundColor: Color,
|
||||
dimensions: PinDimensions,
|
||||
) {
|
||||
val centerX = dimensions.pinWidth / 2
|
||||
val avatarY = dimensions.avatarOffset
|
||||
val avatarRadius = dimensions.avatarSize / 2
|
||||
|
||||
withSave {
|
||||
if (avatarImage != null) {
|
||||
val bitmap = avatarImage.toBitmap()
|
||||
// Calculate centered square crop (ContentScale.Crop behavior)
|
||||
val srcSize = minOf(bitmap.width, bitmap.height)
|
||||
val srcX = (bitmap.width - srcSize) / 2
|
||||
val srcY = (bitmap.height - srcSize) / 2
|
||||
val srcRect = Rect(srcX, srcY, srcX + srcSize, srcY + srcSize)
|
||||
val destRect = RectF(
|
||||
centerX - avatarRadius,
|
||||
avatarY,
|
||||
centerX + avatarRadius,
|
||||
avatarY + dimensions.avatarSize
|
||||
)
|
||||
val clipPath = Path().apply {
|
||||
addCircle(centerX, avatarY + avatarRadius, avatarRadius, Path.Direction.CW)
|
||||
}
|
||||
clipPath(clipPath)
|
||||
drawBitmap(bitmap, srcRect, destRect, null)
|
||||
} else {
|
||||
drawInitialLetterAvatar(
|
||||
initialLetter = avatarData.initialLetter,
|
||||
centerX = centerX,
|
||||
centerY = avatarY + avatarRadius,
|
||||
radius = avatarRadius,
|
||||
foreground = foregroundColor.toArgb(),
|
||||
background = backgroundColor.toArgb()
|
||||
)
|
||||
}
|
||||
}
|
||||
strokePaint.color = borderColor.toArgb()
|
||||
strokePaint.strokeWidth = dimensions.strokeWidth
|
||||
drawCircle(centerX, avatarY + avatarRadius, avatarRadius, strokePaint)
|
||||
}
|
||||
|
||||
private fun Canvas.drawInitialLetterAvatar(
|
||||
initialLetter: String,
|
||||
centerX: Float,
|
||||
centerY: Float,
|
||||
radius: Float,
|
||||
foreground: Int,
|
||||
background: Int,
|
||||
) {
|
||||
fillPaint.color = background
|
||||
drawCircle(centerX, centerY, radius, fillPaint)
|
||||
textPaint.color = foreground
|
||||
textPaint.textSize = radius * 1.2f
|
||||
val textBounds = Rect()
|
||||
textPaint.getTextBounds(initialLetter, 0, 1, textBounds)
|
||||
val textY = centerY + textBounds.height() / 2f
|
||||
drawText(initialLetter, centerX, textY, textPaint)
|
||||
}
|
||||
|
||||
private fun Canvas.drawDot(dotColor: Color, dimensions: PinDimensions) {
|
||||
if (dotColor == Color.Transparent) return
|
||||
val centerX = dimensions.pinWidth / 2
|
||||
val centerY = dimensions.avatarOffset + dimensions.avatarSize / 2
|
||||
fillPaint.color = dotColor.toArgb()
|
||||
drawCircle(centerX, centerY, dimensions.dotRadius, fillPaint)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun LocationPinPreview() = ElementPreview {
|
||||
val sampleAvatarData = AvatarData(
|
||||
id = "@alice:matrix.org",
|
||||
name = "Alice",
|
||||
url = null,
|
||||
size = AvatarSize.SelectedUser
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = false),
|
||||
)
|
||||
LocationPin(
|
||||
variant = PinVariant.UserLocation(avatarData = sampleAvatarData, isLive = true),
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
LocationPin(
|
||||
variant = PinVariant.PinnedLocation,
|
||||
)
|
||||
LocationPin(
|
||||
variant = PinVariant.StaleLocation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rememberCacheKey(variant: PinVariant): String {
|
||||
val isLightTheme = ElementTheme.isLightTheme
|
||||
val density = LocalDensity.current.density
|
||||
return remember(isLightTheme, density, variant) {
|
||||
val pinVariant = when (variant) {
|
||||
PinVariant.PinnedLocation -> "pin_pinned"
|
||||
PinVariant.StaleLocation -> "pin_stale"
|
||||
is PinVariant.UserLocation -> "pin_user_${variant.avatarData.id}_${variant.isLive}"
|
||||
}
|
||||
"${pinVariant}_{$isLightTheme}_{$density}"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2023-2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.libraries.designsystem.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.R
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.theme.components.Icon
|
||||
|
||||
@Composable
|
||||
fun PinIcon(
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(ElementTheme.colors.bgSubtlePrimary)
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.width(22.dp),
|
||||
resourceId = R.drawable.pin,
|
||||
contentDescription = null,
|
||||
tint = Color.Unspecified,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun PinIconPreview() = ElementPreview {
|
||||
PinIcon()
|
||||
}
|
||||
|
|
@ -46,7 +46,8 @@ enum class AvatarSize(val dp: Dp) {
|
|||
RoomInviteItem(52.dp),
|
||||
InviteSender(16.dp),
|
||||
|
||||
EditRoomDetails(68.dp),
|
||||
EditRoomDetails(64.dp),
|
||||
EditSpaceDetails(96.dp),
|
||||
RoomListManageUser(96.dp),
|
||||
|
||||
NotificationsOptIn(32.dp),
|
||||
|
|
@ -75,6 +76,6 @@ enum class AvatarSize(val dp: Dp) {
|
|||
SpaceMember(24.dp),
|
||||
LeaveSpaceRoom(32.dp),
|
||||
SelectParentSpace(32.dp),
|
||||
|
||||
AccountItem(32.dp),
|
||||
LocationPin(32.dp)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ fun ListDialog(
|
|||
enabled: Boolean = true,
|
||||
applyPaddingToContents: Boolean = true,
|
||||
destructiveSubmit: Boolean = false,
|
||||
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(16.dp),
|
||||
listItems: LazyListScope.() -> Unit,
|
||||
) {
|
||||
val decoratedSubtitle: @Composable (() -> Unit)? = subtitle?.let {
|
||||
|
|
@ -67,6 +68,7 @@ fun ListDialog(
|
|||
listItems = listItems,
|
||||
applyPaddingToContents = applyPaddingToContents,
|
||||
destructiveSubmit = destructiveSubmit,
|
||||
verticalArrangement = verticalArrangement,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -82,6 +84,7 @@ private fun ListDialogContent(
|
|||
enabled: Boolean,
|
||||
applyPaddingToContents: Boolean,
|
||||
destructiveSubmit: Boolean,
|
||||
verticalArrangement: Arrangement.Vertical,
|
||||
subtitle: @Composable (() -> Unit)? = null,
|
||||
) {
|
||||
SimpleAlertDialogContent(
|
||||
|
|
@ -99,7 +102,7 @@ private fun ListDialogContent(
|
|||
val horizontalPadding = if (applyPaddingToContents) 0.dp else 8.dp
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = horizontalPadding),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = verticalArrangement,
|
||||
) { listItems() }
|
||||
}
|
||||
}
|
||||
|
|
@ -126,6 +129,7 @@ internal fun ListDialogContentPreview() {
|
|||
enabled = true,
|
||||
destructiveSubmit = false,
|
||||
applyPaddingToContents = true,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import io.element.android.libraries.designsystem.R
|
|||
// All the icons should be defined in Compound.
|
||||
internal val iconsOther = listOf(
|
||||
R.drawable.ic_notification,
|
||||
R.drawable.ic_stop,
|
||||
R.drawable.pin,
|
||||
R.drawable.ic_winner,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,9 @@ import androidx.compose.ui.geometry.CornerRadius
|
|||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.graphics.AndroidPaint
|
||||
import androidx.compose.ui.graphics.ClipOp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Paint
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||
|
|
@ -55,10 +55,10 @@ fun Modifier.blurredShapeShadow(
|
|||
addRoundRect(RoundRect(Rect(Offset.Zero, size), CornerRadius(cornerRadius.toPx())))
|
||||
}
|
||||
|
||||
val frameworkPaint = android.graphics.Paint()
|
||||
// Draw the blurred shadow, then cut out the shape from it
|
||||
clipPath(path, ClipOp.Difference) {
|
||||
val paint = Paint()
|
||||
val frameworkPaint = paint.asFrameworkPaint()
|
||||
val paint = AndroidPaint(frameworkPaint)
|
||||
if (blurRadius != 0.dp) {
|
||||
frameworkPaint.maskFilter = BlurMaskFilter(blurRadius.toPx(), BlurMaskFilter.Blur.NORMAL)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ fun ModalBottomSheet(
|
|||
tonalElevation: Dp = if (ElementTheme.isLightTheme) 0.dp else BottomSheetDefaults.Elevation,
|
||||
scrimColor: Color = BottomSheetDefaults.ScrimColor,
|
||||
dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() },
|
||||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
|
||||
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.modalWindowInsets },
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val safeSheetState = if (LocalInspectionMode.current) sheetStateForPreview() else sheetState
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,16V8C6,7.45 6.196,6.979 6.588,6.588C6.979,6.196 7.45,6 8,6H16C16.55,6 17.021,6.196 17.413,6.588C17.804,6.979 18,7.45 18,8V16C18,16.55 17.804,17.021 17.413,17.413C17.021,17.804 16.55,18 16,18H8C7.45,18 6.979,17.804 6.588,17.413C6.196,17.021 6,16.55 6,16Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue