Merge branch 'develop' into feature-oled-black

This commit is contained in:
Timur Gilfanov 2026-04-05 12:06:20 +04:00 committed by GitHub
commit f19295d63d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
291 changed files with 4973 additions and 1595 deletions

View file

@ -16,7 +16,10 @@ import android.widget.EditText
import androidx.appcompat.app.ActionBar.LayoutParams
import androidx.compose.animation.core.Animatable
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectVerticalDragGestures
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.awaitVerticalPointerSlopOrCancellation
import androidx.compose.foundation.gestures.verticalDrag
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
@ -41,10 +44,14 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.positionChange
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
@ -94,7 +101,7 @@ fun ExpandableBottomSheetLayout(
.run {
if (isSwipeGestureEnabled) {
pointerInput(maxBottomSheetContentHeight) {
detectVerticalDragGestures(
customDetectVerticalDragGestures(
onVerticalDrag = { _, dragAmount ->
val calculatedHeight = max(minBottomContentHeightPx, currentBottomContentHeightPx - dragAmount.roundToInt())
val newHeight = min(calculatedMaxBottomContentHeightPx, calculatedHeight)
@ -120,7 +127,11 @@ fun ExpandableBottomSheetLayout(
animatable.animateTo(destination)
}
}
},
canScroll = {
// We only consider we can scroll in the contents if the min size matches the max size so it's maximized
minBottomContentHeightPx == calculatedMaxBottomContentHeightPx
},
)
}
} else {
@ -189,6 +200,45 @@ fun ExpandableBottomSheetLayout(
)
}
// The original detectVerticalDragGestures doesn't allow us to conditionally consume the initial slop event that triggers the drag,
// which is necessary in our case to allow inner scrollables to work when the sheet is not fully expanded, so we need to re-implement it here
private suspend fun PointerInputScope.customDetectVerticalDragGestures(
onDragStart: (Offset) -> Unit = {},
onDragEnd: () -> Unit = {},
onDragCancel: () -> Unit = {},
canScroll: () -> Boolean = { false },
onVerticalDrag: (change: PointerInputChange, dragAmount: Float) -> Unit,
) {
awaitEachGesture {
val down = awaitFirstDown(requireUnconsumed = false)
var overSlop = 0f
val drag =
awaitVerticalPointerSlopOrCancellation(down.id, down.type) { change, over ->
// Consuming this event is what triggers the dragging instead of the inner content scrolling
// We should only consume it if we can't scroll in the inner content so we drag the bottom sheet instead, otherwise we let it pass through
// This is the only change compared to the original detectVerticalDragGestures implementation
if (!canScroll()) {
change.consume()
}
overSlop = over
}
if (drag != null) {
onDragStart.invoke(drag.position)
onVerticalDrag.invoke(drag, overSlop)
if (
verticalDrag(drag.id) {
onVerticalDrag(it, it.positionChange().y)
it.consume()
}
) {
onDragEnd()
} else {
onDragCancel()
}
}
}
}
@Preview(showBackground = true)
@Composable
@Suppress("UnusedPrivateMember")

View file

@ -32,9 +32,13 @@ 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.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.createBitmap
import androidx.core.graphics.drawable.toBitmap
import androidx.core.graphics.withSave
import coil3.Image
import coil3.ImageLoader
@ -50,6 +54,7 @@ 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.utils.CommonDrawables
private val PIN_WIDTH = 42.dp
private val PIN_HEIGHT = PIN_WIDTH * 1.2f
@ -99,21 +104,33 @@ fun LocationPin(
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
val resources = LocalResources.current
return if (LocalInspectionMode.current) {
// In preview mode, skip async loading and return a simple placeholder image instead to avoid using ImageLoader
val dimensions = PinDimensions(density)
val avatarImage = ResourcesCompat.getDrawable(resources, CommonDrawables.sample_avatar, context.theme)?.toBitmap()?.asImage()
LocationPinRenderer.renderPin(variant, colors, dimensions, avatarImage).asImageBitmap()
} else {
produceState<ImageBitmap?>(initialValue = null, cacheKey) {
val imageLoader = SingletonImageLoader.get(context)
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 = with(LocationPinRenderer) {
val avatarImage = loadAvatarImage(variant, context, imageLoader)
renderPin(variant, colors, dimensions, avatarImage)
}
imageLoader.memoryCache?.set(memoryCacheKey, MemoryCache.Value(bitmap.asImage()))
value = bitmap.asImageBitmap()
}
}.value
}
}
@Composable
@ -208,19 +225,17 @@ private object LocationPinRenderer {
/**
* Renders a pin variant to bitmap. Suspending for async avatar loading.
*/
suspend fun renderPin(
fun renderPin(
variant: PinVariant,
colors: PinColors,
dimensions: PinDimensions,
context: Context,
imageLoader: ImageLoader,
avatarImage: Image?,
): 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,
@ -284,11 +299,15 @@ private object LocationPinRenderer {
return path
}
private suspend fun loadAvatarImage(
avatarData: AvatarData,
suspend fun loadAvatarImage(
variant: PinVariant,
context: Context,
imageLoader: ImageLoader,
): Image? {
val avatarData = when (variant) {
is PinVariant.UserLocation -> variant.avatarData
else -> return null
}
val request = ImageRequest.Builder(context)
.data(avatarData)
// Disable hardware rendering for Canvas

View file

@ -75,6 +75,9 @@ val SemanticColors.pinnedMessageBannerIndicator
val SemanticColors.pinnedMessageBannerBorder
get() = if (isLight) LightColorTokens.colorAlphaGray400 else DarkColorTokens.colorAlphaGray400
val SemanticColors.floatingDateBadgeBackground
get() = if (isLight) bgCanvasDefault else bgSubtlePrimary
@PreviewsDayNight
@Composable
internal fun ColorAliasesPreview() = ElementPreview {