diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt index c6560d6028..6d17f0eeed 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BackdropBlurModifier.kt @@ -21,9 +21,11 @@ import android.graphics.RenderNode import android.graphics.Shader import android.os.Build import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.blur.BlurRadiusSpec import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.node.ModifierNodeElement import androidx.compose.ui.platform.InspectorInfo @@ -31,57 +33,10 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -/** - * Specification for blur radius parameters, capable of creating a hardware [RenderEffect] - * for use with [RenderNode.setBackdropRenderEffect]. - * - * @param radiusX The horizontal blur radius. - * @param radiusY The vertical blur radius (defaults to [radiusX]). - * @param tileMode The tile mode for handling edges (defaults to [Shader.TileMode.CLAMP]). - */ -data class BlurRadiusSpec(val radiusX: Dp, val radiusY: Dp = radiusX, val tileMode: Shader.TileMode = Shader.TileMode.CLAMP) { - /** - * Creates an Android [RenderEffect] configured with this specification. - */ - fun createRenderEffect(density: Density): RenderEffect? { - val rxPx = with(density) { radiusX.toPx() } - val ryPx = with(density) { radiusY.toPx() } - return createRenderEffect(rxPx, ryPx, tileMode) - } - - companion object { - /** - * Creates a hardware [RenderEffect] blur effect from pixel radii. - */ - fun createRenderEffect( - radiusXPx: Float, - radiusYPx: Float = radiusXPx, - tileMode: Shader.TileMode = Shader.TileMode.CLAMP, - ): RenderEffect? { - if (Build.VERSION.SDK_INT >= 31 && (radiusXPx > 0f || radiusYPx > 0f)) { - return RenderEffect.createBlurEffect( - radiusXPx.coerceAtLeast(0.01f), - radiusYPx.coerceAtLeast(0.01f), - tileMode, - ) - } - return null - } - - /** - * Creates a hardware [RenderEffect] blur effect from a [Dp] radius. - */ - fun createRenderEffect(radius: Dp, density: Density, tileMode: Shader.TileMode = Shader.TileMode.CLAMP): RenderEffect? { - val px = with(density) { radius.toPx() } - return createRenderEffect(px, px, tileMode) - } - } -} - /** * Applies an in-window backdrop [RenderEffect] to content drawn behind this composable in the window. * - * On supported platforms (Android 17 / SDK 37+), this leverages [RenderNode.setBackdropRenderEffect] + * On supported platforms (Android 17 / SDK 37.2+), this leverages [RenderNode.setBackdropRenderEffect] * to apply hardware-accelerated visual effects (like blur) to the backdrop before this composable * is drawn, enabling translucent floating navigation bars, top app bars, and frosted-glass cards. * @@ -91,7 +46,7 @@ data class BlurRadiusSpec(val radiusX: Dp, val radiusY: Dp = radiusX, val tileMo * @param elevation Optional elevation shadow cast by this component. * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area so the shadow * does not darken the translucent frosted glass interior. - * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17 (37.2). */ fun Modifier.backdropRenderEffect( renderEffect: RenderEffect?, @@ -110,10 +65,36 @@ fun Modifier.backdropRenderEffect( ) /** - * Draws the content behind this composable blurred with the specified [radius], + * Overload of [backdropRenderEffect] accepting Compose's [androidx.compose.ui.graphics.RenderEffect]. + */ +fun Modifier.backdropRenderEffect( + renderEffect: androidx.compose.ui.graphics.RenderEffect?, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = backdropRenderEffect( + renderEffect = if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN) { + renderEffect?.asAndroidRenderEffect() + } else { + null + }, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Draws the content behind this composable blurred according to the provided [spec], * clipped to [shape], beneath this composable's own content. * - * @param radius The blur radius to apply to the backdrop. + * Uses Compose's built-in [BlurRadiusSpec] to configure uniform or spatially-varying blur + * radii (such as [BlurRadiusSpec.uniform], [BlurRadiusSpec.verticalGradient], etc.). + * + * @param spec The [BlurRadiusSpec] defining the blur radius or gradient. * @param shape The shape of the frosted-glass region. * @param tint An optional translucent color overlay drawn over the blurred backdrop. * @param elevation Optional elevation shadow cast by this component. @@ -121,15 +102,14 @@ fun Modifier.backdropRenderEffect( * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. */ fun Modifier.backdropBlur( - radius: Dp, + spec: BlurRadiusSpec, shape: Shape = RectangleShape, tint: Color = Color.Unspecified, elevation: Dp = 0.dp, outerShadowOnly: Boolean = true, fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, -): Modifier = backdropBlur( - radiusX = radius, - radiusY = radius, +): Modifier = this then BackdropBlurElement( + spec = spec, shape = shape, tint = tint, elevation = elevation, @@ -138,10 +118,10 @@ fun Modifier.backdropBlur( ) /** - * Overload of [backdropBlur] allowing independent horizontal and vertical blur radii. + * Draws the content behind this composable blurred with the specified uniform [radius], + * clipped to [shape], beneath this composable's own content. * - * @param radiusX The horizontal blur radius. - * @param radiusY The vertical blur radius. + * @param radius The blur radius to apply to the backdrop. * @param shape The shape of the frosted-glass region. * @param tint An optional translucent color overlay drawn over the blurred backdrop. * @param elevation Optional elevation shadow cast by this component. @@ -149,15 +129,14 @@ fun Modifier.backdropBlur( * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. */ fun Modifier.backdropBlur( - radiusX: Dp, - radiusY: Dp, + radius: Dp, shape: Shape = RectangleShape, tint: Color = Color.Unspecified, elevation: Dp = 0.dp, outerShadowOnly: Boolean = true, fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, ): Modifier = backdropBlur( - spec = BlurRadiusSpec(radiusX, radiusY), + spec = BlurRadiusSpec.uniform(radius), shape = shape, tint = tint, elevation = elevation, @@ -166,23 +145,44 @@ fun Modifier.backdropBlur( ) /** - * Overload of [backdropBlur] configured via a [BlurRadiusSpec]. + * Overload of [backdropBlur] allowing independent horizontal and vertical blur radii. + * + * @param radiusX The horizontal blur radius. + * @param radiusY The vertical blur radius. + * @param shape The shape of the frosted-glass region. + * @param tint An optional translucent color overlay drawn over the blurred backdrop. + * @param elevation Optional elevation shadow cast by this component. + * @param outerShadowOnly If true, clips out the shadow cast beneath the outline area. + * @param fallbackColor An optional fallback background color for platforms earlier than Android 17. */ fun Modifier.backdropBlur( - spec: BlurRadiusSpec, + radiusX: Dp, + radiusY: Dp, shape: Shape = RectangleShape, tint: Color = Color.Unspecified, elevation: Dp = 0.dp, outerShadowOnly: Boolean = true, fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, -): Modifier = this then BackdropBlurElement( - spec = spec, - shape = shape, - tint = tint, - elevation = elevation, - outerShadowOnly = outerShadowOnly, - fallbackColor = fallbackColor, -) +): Modifier = if (radiusX == radiusY) { + backdropBlur( + spec = BlurRadiusSpec.uniform(radiusX), + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) +} else { + this then BackdropEllipticalBlurElement( + radiusX = radiusX, + radiusY = radiusY, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) +} private data class BackdropRenderEffectElement( val renderEffect: RenderEffect?, @@ -262,6 +262,49 @@ private data class BackdropBlurElement( } } +private data class BackdropEllipticalBlurElement( + val radiusX: Dp, + val radiusY: Dp, + val shape: Shape, + val tint: Color, + val elevation: Dp, + val outerShadowOnly: Boolean, + val fallbackColor: Color, +) : ModifierNodeElement() { + override fun create(): BackdropEllipticalBlurNode = BackdropEllipticalBlurNode( + radiusX = radiusX, + radiusY = radiusY, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + + override fun update(node: BackdropEllipticalBlurNode) { + node.update( + radiusX = radiusX, + radiusY = radiusY, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "backdropBlur" + properties["radiusX"] = radiusX + properties["radiusY"] = radiusY + properties["shape"] = shape + properties["tint"] = tint + properties["elevation"] = elevation + properties["outerShadowOnly"] = outerShadowOnly + properties["fallbackColor"] = fallbackColor + } +} + private class BackdropRenderEffectNode( var renderEffect: RenderEffect?, shape: Shape, @@ -317,13 +360,24 @@ private class BackdropBlurNode( private var cachedEffect: RenderEffect? = null private var cachedDensity: Float = -1f + private var cachedSize: Size = Size.Unspecified private var cachedSpec: BlurRadiusSpec? = null - override fun resolveRenderEffect(density: Density): RenderEffect? { + override fun resolveRenderEffect(density: Density, size: Size): RenderEffect? { + if (Build.VERSION.SDK_INT_FULL < Build.VERSION_CODES_FULL.CINNAMON_BUN) return null val currentDensity = density.density - if (cachedEffect == null || cachedDensity != currentDensity || cachedSpec != spec) { - cachedEffect = spec.createRenderEffect(density) + if (cachedEffect == null || cachedDensity != currentDensity || cachedSize != size || cachedSpec != spec) { + cachedEffect = if (size.width > 0f && size.height > 0f) { + try { + spec.createRenderEffect(size, density).asAndroidRenderEffect() + } catch (_: Throwable) { + null + } + } else { + null + } cachedDensity = currentDensity + cachedSize = size cachedSpec = spec } return cachedEffect @@ -361,3 +415,78 @@ private class BackdropBlurNode( } } } + +private class BackdropEllipticalBlurNode( + var radiusX: Dp, + var radiusY: Dp, + shape: Shape, + tint: Color, + elevation: Dp, + outerShadowOnly: Boolean, + fallbackColor: Color, +) : BaseBackdropNode(shape, tint, elevation, outerShadowOnly, fallbackColor) { + + private var cachedEffect: RenderEffect? = null + private var cachedDensity: Float = -1f + private var cachedRadiusX: Dp = 0.dp + private var cachedRadiusY: Dp = 0.dp + + override fun resolveRenderEffect(density: Density): RenderEffect? { + if (Build.VERSION.SDK_INT_FULL < Build.VERSION_CODES_FULL.CINNAMON_BUN) return null + val currentDensity = density.density + if (cachedEffect == null || cachedDensity != currentDensity || cachedRadiusX != radiusX || cachedRadiusY != radiusY) { + val rxPx = with(density) { radiusX.toPx() } + val ryPx = with(density) { radiusY.toPx() } + cachedEffect = if (rxPx > 0f || ryPx > 0f) { + RenderEffect.createBlurEffect( + rxPx.coerceAtLeast(0.01f), + ryPx.coerceAtLeast(0.01f), + Shader.TileMode.CLAMP, + ) + } else { + null + } + cachedDensity = currentDensity + cachedRadiusX = radiusX + cachedRadiusY = radiusY + } + return cachedEffect + } + + fun update(radiusX: Dp, radiusY: Dp, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) { + var changed = false + if (this.radiusX != radiusX) { + this.radiusX = radiusX + cachedEffect = null + changed = true + } + if (this.radiusY != radiusY) { + this.radiusY = radiusY + cachedEffect = null + changed = true + } + if (this.shape != shape) { + this.shape = shape + changed = true + } + if (this.tint != tint) { + this.tint = tint + changed = true + } + if (this.elevation != elevation) { + this.elevation = elevation + changed = true + } + if (this.outerShadowOnly != outerShadowOnly) { + this.outerShadowOnly = outerShadowOnly + changed = true + } + if (this.fallbackColor != fallbackColor) { + this.fallbackColor = fallbackColor + changed = true + } + if (changed) { + markDirty() + } + } +} diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt index 7715097cfb..42dcaf6323 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/BaseBackdropNode.kt @@ -24,6 +24,7 @@ import android.graphics.RenderEffect import android.graphics.RenderNode import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color @@ -85,11 +86,15 @@ abstract class BaseBackdropNode( isDirty = true } - // Suppress NewApi: Android Lint does not yet recognize Build.VERSION.SDK_INT_FULL - // checks as satisfying minor SDK 37.2 requirements. + // Suppress NewApi: Emulator/device images may report CINNAMON_BUN (37.0) even when + // setBackdropRenderEffect is annotated with CINNAMON_BUN_2 (37.2). @SuppressLint("NewApi") override fun ContentDrawScope.draw() { - val effect = resolveRenderEffect(this, size) + val effect = if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN) { + resolveRenderEffect(this, size) + } else { + null + } if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN && effect != null) { val widthPx = size.width.roundToInt() val heightPx = size.height.roundToInt() @@ -189,6 +194,7 @@ abstract class BaseBackdropNode( /** * Helper to populate an [AndroidOutline] from a Compose [Outline]. */ +@RequiresApi(Build.VERSION_CODES_FULL.CINNAMON_BUN) private fun updateAndroidOutline(androidOutline: AndroidOutline, composeOutline: Outline, width: Int, height: Int) { androidOutline.alpha = 1.0f when (composeOutline) { @@ -228,22 +234,12 @@ private fun updateAndroidOutline(androidOutline: AndroidOutline, composeOutline: AndroidPath.Direction.CW, ) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - androidOutline.setPath(path) - } else { - @Suppress("DEPRECATION") - androidOutline.setConvexPath(path) - } + androidOutline.setPath(path) } } is Outline.Generic -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - androidOutline.setPath(composeOutline.path.asAndroidPath()) - } else { - @Suppress("DEPRECATION") - androidOutline.setConvexPath(composeOutline.path.asAndroidPath()) - } + androidOutline.setPath(composeOutline.path.asAndroidPath()) } } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt index 5867aa6e59..017b4ca0ad 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt @@ -138,7 +138,7 @@ fun createMagnifierEffect( specularIntensity: Float = 0.15f, tileMode: Shader.TileMode = Shader.TileMode.CLAMP, ): RenderEffect? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return null + if (Build.VERSION.SDK_INT_FULL < Build.VERSION_CODES_FULL.CINNAMON_BUN) return null if (size.width <= 0f || size.height <= 0f) return null val blurEffect = if (blurRadiusPx > 0f) { @@ -149,41 +149,39 @@ fun createMagnifierEffect( ) } else null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - try { - val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply { - setFloatUniform("size", size.width, size.height) - setFloatUniform("zoom", zoom) - setFloatUniform("lensCurvature", lensCurvature) - setFloatUniform("chromaticAberration", chromaticAberrationPx) - } - val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content") - - val stage1 = if (blurEffect != null) { - // inner = blurEffect (blurs backdrop first) - // outer = lensEffect (magnifies the blurred backdrop) - RenderEffect.createChainEffect(lensEffect, blurEffect) - } else { - lensEffect - } + try { + val lensShader = RuntimeShader(MAGNIFIER_LENS_SHADER).apply { + setFloatUniform("size", size.width, size.height) + setFloatUniform("zoom", zoom) + setFloatUniform("lensCurvature", lensCurvature) + setFloatUniform("chromaticAberration", chromaticAberrationPx) + } + val lensEffect = RenderEffect.createRuntimeShaderEffect(lensShader, "content") + + val stage1 = if (blurEffect != null) { + // inner = blurEffect (blurs backdrop first) + // outer = lensEffect (magnifies the blurred backdrop) + RenderEffect.createChainEffect(lensEffect, blurEffect) + } else { + lensEffect + } - if (rimIntensity > 0f || specularIntensity > 0f) { - val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply { - setFloatUniform("size", size.width, size.height) - setFloatUniform("rimIntensity", rimIntensity) - setFloatUniform("specularIntensity", specularIntensity) - } - val glassEffect = RenderEffect.createRuntimeShaderEffect(glassShader, "content") - - // inner = stage1 (magnified backdrop) - // outer = glassEffect (adds lens rim highlight & specular sheen) - return RenderEffect.createChainEffect(glassEffect, stage1) + if (rimIntensity > 0f || specularIntensity > 0f) { + val glassShader = RuntimeShader(GLASS_FINISH_SHADER).apply { + setFloatUniform("size", size.width, size.height) + setFloatUniform("rimIntensity", rimIntensity) + setFloatUniform("specularIntensity", specularIntensity) } + val glassEffect = RenderEffect.createRuntimeShaderEffect(glassShader, "content") - return stage1 - } catch (t: Throwable) { - Log.w("BackdropMagnifier", "Failed to create chained magnifier effect: ${t.message}") + // inner = stage1 (magnified backdrop) + // outer = glassEffect (adds lens rim highlight & specular sheen) + return RenderEffect.createChainEffect(glassEffect, stage1) } + + return stage1 + } catch (t: Throwable) { + Log.w("BackdropMagnifier", "Failed to create chained magnifier effect: ${t.message}") } return blurEffect diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt index f76636b0b3..15d8f36d2d 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/components/JetchatAppBar.kt @@ -22,9 +22,9 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable @@ -44,8 +44,17 @@ fun JetchatAppBar( onNavIconPressed: () -> Unit = { }, title: @Composable () -> Unit, actions: @Composable RowScope.() -> Unit = {}, + navigationIcon: @Composable () -> Unit = { + JetchatIcon( + contentDescription = stringResource(id = R.string.navigation_drawer_open), + modifier = Modifier + .size(64.dp) + .clickable(onClick = onNavIconPressed) + .padding(16.dp), + ) + }, ) { - CenterAlignedTopAppBar( + TopAppBar( modifier = modifier, actions = actions, title = title, @@ -54,15 +63,7 @@ fun JetchatAppBar( containerColor = Color.Transparent, scrolledContainerColor = Color.Transparent, ), - navigationIcon = { - JetchatIcon( - contentDescription = stringResource(id = R.string.navigation_drawer_open), - modifier = Modifier - .size(64.dp) - .clickable(onClick = onNavIconPressed) - .padding(16.dp), - ) - }, + navigationIcon = navigationIcon, ) } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt index ce17c68d63..4d203dca80 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt @@ -41,6 +41,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -69,8 +70,11 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.ScaffoldDefaults @@ -98,6 +102,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.paint import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.blur.BlurRadiusSpec +import androidx.compose.ui.graphics.blur.BlurStop import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.input.nestedscroll.nestedScroll @@ -108,6 +114,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -212,35 +219,7 @@ fun ConversationContent( scrollBehavior = scrollBehavior, ) }, - // Exclude ime and navigation bar padding so this can be added by the UserInput composable - contentWindowInsets = ScaffoldDefaults - .contentWindowInsets - .exclude(WindowInsets.navigationBars) - .exclude(WindowInsets.ime), - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { paddingValues -> - Column( - Modifier.fillMaxSize() - .padding(bottom = paddingValues.calculateBottomPadding()) - .background(color = background) - .border(width = 2.dp, color = borderStroke) - .dragAndDropTarget(shouldStartDragAndDrop = { event -> - event - .mimeTypes() - .contains( - ClipDescription.MIMETYPE_TEXT_PLAIN, - ) - }, target = dragAndDropCallback), - ) { - Messages( - messages = uiState.messages, - navigateToProfile = navigateToProfile, - modifier = Modifier.weight(1f), - scrollState = scrollState, - contentPadding = PaddingValues(top = paddingValues.calculateTopPadding()), - onVideoClick = { videoUri -> activeVideoUri = videoUri }, - onMessageLikeToggled = onMessageLikeToggled, - ) + bottomBar = { UserInput( onMessageSent = { content -> uiState.addMessage( @@ -264,7 +243,52 @@ fun ConversationContent( }, // let this element handle the padding so that the elevation is shown behind the // navigation bar - modifier = Modifier.navigationBarsPadding().imePadding(), + modifier = Modifier + .imePadding() + .backdropBlur( + fallbackColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + elevation = 0.dp, + spec = BlurRadiusSpec.verticalGradient( + listOf( + BlurStop(0f, 0.dp), + BlurStop(0.5f, 32.dp), + ), + ), + ) + .navigationBarsPadding(), + ) + }, + // Exclude ime and navigation bar padding so this can be added by the UserInput composable + contentWindowInsets = ScaffoldDefaults + .contentWindowInsets + .exclude(WindowInsets.navigationBars) + .exclude(WindowInsets.ime), + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + Column( + Modifier + .fillMaxSize() + .background(color = background) + .border(width = 2.dp, color = borderStroke) + .dragAndDropTarget( + shouldStartDragAndDrop = { event -> + event + .mimeTypes() + .contains( + ClipDescription.MIMETYPE_TEXT_PLAIN, + ) + }, + target = dragAndDropCallback, + ), + ) { + Messages( + messages = uiState.messages, + navigateToProfile = navigateToProfile, + modifier = Modifier.weight(1f), + scrollState = scrollState, + contentPadding = paddingValues, + onVideoClick = { videoUri -> activeVideoUri = videoUri }, + onMessageLikeToggled = onMessageLikeToggled, ) } } @@ -300,49 +324,104 @@ fun ChannelNameBar( JetchatAppBar( modifier = modifier .backdropBlur( - tint = MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f), + fallbackColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), elevation = 0.dp, - radius = 12.dp, + spec = BlurRadiusSpec.verticalGradient( + listOf( + BlurStop(0.5f, 32.dp), + BlurStop(1f, 0.dp), + ), + ), ), scrollBehavior = scrollBehavior, onNavIconPressed = onNavIconPressed, + navigationIcon = {}, title = { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - // Channel name - Text( - text = channelName, - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurface, - ) - // Number of members - Text( - text = stringResource(R.string.members, channelMembers), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + val navDrawerDescription = stringResource(R.string.navigation_drawer_open) + Surface( + onClick = onNavIconPressed, + modifier = Modifier.semantics { + contentDescription = navDrawerDescription + }, + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.65f), + ) { + Column( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) { + Text( + text = channelName.removePrefix("#"), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + Spacer(modifier = Modifier.height(4.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy((-4).dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Image( + painter = painterResource(id = R.drawable.ali), + contentDescription = "Ali Conors", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .border(1.dp, MaterialTheme.colorScheme.surface, CircleShape), + ) + Image( + painter = painterResource(id = R.drawable.someone_else), + contentDescription = "Taylor Brooks", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .border(1.dp, MaterialTheme.colorScheme.surface, CircleShape), + ) + Image( + painter = painterResource(id = R.drawable.placeholder), + contentDescription = "John Glenn", + contentScale = ContentScale.Crop, + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .border(1.dp, MaterialTheme.colorScheme.surface, CircleShape), + ) + } + } } }, actions = { - // Search icon - Icon( - painterResource(id = R.drawable.ic_search), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .clickable(onClick = { functionalityNotAvailablePopupShown = true }) - .padding(horizontal = 12.dp, vertical = 16.dp) - .height(24.dp), - contentDescription = stringResource(id = R.string.search), - ) - // Info icon - Icon( - painterResource(id = R.drawable.ic_info), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .clickable(onClick = { functionalityNotAvailablePopupShown = true }) - .padding(horizontal = 12.dp, vertical = 16.dp) - .height(24.dp), - contentDescription = stringResource(id = R.string.info), - ) + FilledIconButton( + onClick = { functionalityNotAvailablePopupShown = true }, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + modifier = Modifier.size(40.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_search), + contentDescription = stringResource(id = R.string.search), + modifier = Modifier.size(20.dp), + ) + } + Spacer(modifier = Modifier.width(8.dp)) + FilledIconButton( + onClick = { functionalityNotAvailablePopupShown = true }, + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + modifier = Modifier.size(40.dp), + ) { + Icon( + painter = painterResource(id = R.drawable.ic_info), + contentDescription = stringResource(id = R.string.info), + modifier = Modifier.size(20.dp), + ) + } + Spacer(modifier = Modifier.width(8.dp)) }, ) } @@ -425,7 +504,9 @@ fun Messages( scrollState.animateScrollToItem(0) } }, - modifier = Modifier.align(Alignment.BottomCenter), + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = contentPadding.calculateBottomPadding()), ) } } diff --git a/Jetchat/gradle.properties b/Jetchat/gradle.properties index 9299bc6d0f..95df5e3b8e 100644 --- a/Jetchat/gradle.properties +++ b/Jetchat/gradle.properties @@ -37,3 +37,6 @@ android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official + +# Enabled parallel sync for Gradle 9.4+ +org.gradle.tooling.parallel=true