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 5d1937a9a9..7715097cfb 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 @@ -25,6 +25,7 @@ import android.graphics.RenderNode import android.os.Build import android.util.Log import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path @@ -52,7 +53,9 @@ abstract class BaseBackdropNode( ) : Modifier.Node(), DrawModifierNode { - abstract fun resolveRenderEffect(density: Density): RenderEffect? + open fun resolveRenderEffect(density: Density, size: Size): RenderEffect? = resolveRenderEffect(density) + + open fun resolveRenderEffect(density: Density): RenderEffect? = null private var renderNode: RenderNode? = null private val androidOutline = AndroidOutline() @@ -86,7 +89,7 @@ abstract class BaseBackdropNode( // checks as satisfying minor SDK 37.2 requirements. @SuppressLint("NewApi") override fun ContentDrawScope.draw() { - val effect = resolveRenderEffect(this) + val effect = resolveRenderEffect(this, size) if (Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN && effect != null) { val widthPx = size.width.roundToInt() val heightPx = size.height.roundToInt() diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierModifier.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierModifier.kt new file mode 100644 index 0000000000..2795d7a78b --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierModifier.kt @@ -0,0 +1,235 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.jetchat.blur + +import android.graphics.RenderEffect +import android.graphics.Shader +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.isSpecified +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * Specification for an optical magnifier backdrop effect using a chained hardware [RenderEffect]. + * + * @param zoom The magnification factor applied to content behind this element (e.g. 1.35f = 135% scale). + * @param blurRadius Optional blur radius applied to the backdrop before magnification (0.dp for crystal clear glass). + * @param lensCurvature Curvature falloff factor simulating a convex spherical lens (higher values curve more towards 1.0 at edges). + * @param chromaticAberration Radial color fringe offset at the lens perimeter simulating optical dispersion. + * @param rimIntensity Intensity of the Fresnel inner edge rim highlight / glass bevel. + * @param specularIntensity Intensity of the 3D convex glass dome specular sheen. + * @param tileMode Edge handling mode if blur is applied. + */ +data class MagnifierSpec( + val zoom: Float = 1.35f, + val blurRadius: Dp = 0.dp, + val lensCurvature: Float = 0.35f, + val chromaticAberration: Dp = 1.5.dp, + val rimIntensity: Float = 0.18f, + val specularIntensity: Float = 0.15f, + val tileMode: Shader.TileMode = Shader.TileMode.CLAMP, +) { + /** + * Creates a chained hardware [RenderEffect] configured with this specification. + */ + fun createRenderEffect(density: Density, size: Size): RenderEffect? { + val blurPx = with(density) { blurRadius.toPx() } + val chromaticPx = with(density) { chromaticAberration.toPx() } + return createMagnifierEffect( + size = size, + zoom = zoom, + blurRadiusPx = blurPx, + lensCurvature = lensCurvature, + chromaticAberrationPx = chromaticPx, + rimIntensity = rimIntensity, + specularIntensity = specularIntensity, + tileMode = tileMode, + ) + } +} + +/** + * Draws the content behind this composable magnified with an optical lens effect using + * a chained [RenderEffect] (refraction + chromatic dispersion + Fresnel rim + specular sheen), + * clipped to [shape], beneath this composable's content. + * + * @param zoom The magnification factor (e.g., 1.35f = 135% scale). + * @param blurRadius Optional blur applied to the backdrop before magnification (0.dp for crystal clear glass). + * @param lensCurvature Convex lens curvature factor (tapers magnification towards edges). + * @param chromaticAberration Radial color dispersion at the lens rim. + * @param rimIntensity Intensity of the inner rim bevel highlight. + * @param specularIntensity Intensity of the convex glass specular sheen. + * @param shape The shape of the magnifier lens region. + * @param tint An optional translucent color wash drawn over the magnified 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.backdropMagnifier( + zoom: Float = 1.35f, + blurRadius: Dp = 0.dp, + lensCurvature: Float = 0.35f, + chromaticAberration: Dp = 1.5.dp, + rimIntensity: Float = 0.18f, + specularIntensity: Float = 0.15f, + shape: Shape = RectangleShape, + tint: Color = Color.Unspecified, + elevation: Dp = 0.dp, + outerShadowOnly: Boolean = true, + fallbackColor: Color = if (tint.isSpecified) tint else Color.Transparent, +): Modifier = backdropMagnifier( + spec = MagnifierSpec( + zoom = zoom, + blurRadius = blurRadius, + lensCurvature = lensCurvature, + chromaticAberration = chromaticAberration, + rimIntensity = rimIntensity, + specularIntensity = specularIntensity, + ), + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +/** + * Overload of [backdropMagnifier] configured via a [MagnifierSpec]. + */ +fun Modifier.backdropMagnifier( + spec: MagnifierSpec, + 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 BackdropMagnifierElement( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, +) + +private data class BackdropMagnifierElement( + val spec: MagnifierSpec, + val shape: Shape, + val tint: Color, + val elevation: Dp, + val outerShadowOnly: Boolean, + val fallbackColor: Color, +) : ModifierNodeElement() { + override fun create(): BackdropMagnifierNode = BackdropMagnifierNode( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + + override fun update(node: BackdropMagnifierNode) { + node.update( + spec = spec, + shape = shape, + tint = tint, + elevation = elevation, + outerShadowOnly = outerShadowOnly, + fallbackColor = fallbackColor, + ) + } + + override fun InspectorInfo.inspectableProperties() { + name = "backdropMagnifier" + properties["spec"] = spec + properties["shape"] = shape + properties["tint"] = tint + properties["elevation"] = elevation + properties["outerShadowOnly"] = outerShadowOnly + properties["fallbackColor"] = fallbackColor + } +} + +private class BackdropMagnifierNode( + var spec: MagnifierSpec, + 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 cachedSpec: MagnifierSpec? = null + private var cachedSize: Size = Size.Unspecified + + override fun resolveRenderEffect(density: Density, size: Size): RenderEffect? { + val currentDensity = density.density + if (cachedEffect == null || + cachedDensity != currentDensity || + cachedSpec != spec || + cachedSize != size + ) { + cachedEffect = spec.createRenderEffect(density, size) + cachedDensity = currentDensity + cachedSpec = spec + cachedSize = size + } + return cachedEffect + } + + fun update(spec: MagnifierSpec, shape: Shape, tint: Color, elevation: Dp, outerShadowOnly: Boolean, fallbackColor: Color) { + var changed = false + if (this.spec != spec) { + this.spec = spec + 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/MagnifierShader.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt new file mode 100644 index 0000000000..5867aa6e59 --- /dev/null +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/blur/MagnifierShader.kt @@ -0,0 +1,190 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.compose.jetchat.blur + +import android.graphics.RenderEffect +import android.graphics.RuntimeShader +import android.graphics.Shader +import android.os.Build +import android.util.Log +import androidx.compose.ui.geometry.Size + +/** + * AGSL shader simulating an optical magnifier lens. + * + * Performs: + * 1. Geometric coordinate scaling relative to the element center (magnification). + * 2. Convex spherical lens profile: zoom is highest across the central axis and tapers smoothly towards + * the outer perimeter, mimicking a physical glass loupe or reading bar magnifier. + * 3. Chromatic dispersion (aberration): separates R and B channels radially towards the lens boundary + * to recreate natural optical glass prism dispersion. + */ +const val MAGNIFIER_LENS_SHADER = """ + uniform shader content; + uniform float2 size; + uniform float zoom; + uniform float lensCurvature; + uniform float chromaticAberration; + + half4 main(float2 fragCoord) { + float2 center = size * 0.5; + float2 delta = fragCoord - center; + float dist = length(delta); + + // Compute normalized distance to boundary for pill, circle, or rounded rectangle + float r = min(size.x, size.y) * 0.5; + float halfSpanX = max(size.x * 0.5 - r, 0.0); + float closestX = clamp(delta.x, -halfSpanX, halfSpanX); + float distToAxis = length(delta - float2(closestX, 0.0)); + float normDist = clamp(distToAxis / max(r, 0.001), 0.0, 1.0); + + // Convex spherical lens profile: + // Center has full zoom; curvature gently tapers zoom towards the edge + float effectiveZoom = mix(zoom, 1.0, normDist * normDist * lensCurvature); + effectiveZoom = max(effectiveZoom, 0.1); + + // Sample coordinate scaled relative to center + float2 sampleCoord = center + delta / effectiveZoom; + + // Chromatic dispersion direction and offset (strongest near lens rim) + float2 dir = dist > 0.001 ? delta / dist : float2(0.0); + float dispersion = normDist * normDist * chromaticAberration; + + half rColor = content.eval(sampleCoord + dir * dispersion).r; + half gColor = content.eval(sampleCoord).g; + half bColor = content.eval(sampleCoord - dir * dispersion).b; + half aColor = content.eval(sampleCoord).a; + + half4 result = half4(rColor, gColor, bColor, aColor); + result.rgb = clamp(result.rgb, 0.0, result.a); + return result; + } +""" + +/** + * AGSL shader simulating glass surface finish, Fresnel rim reflection, and 3D convex specular sheen. + */ +const val GLASS_FINISH_SHADER = """ + uniform shader content; + uniform float2 size; + uniform float rimIntensity; + uniform float specularIntensity; + + half4 main(float2 fragCoord) { + half4 color = content.eval(fragCoord); + + float2 center = size * 0.5; + float2 delta = fragCoord - center; + + float r = min(size.x, size.y) * 0.5; + float halfSpanX = max(size.x * 0.5 - r, 0.0); + float closestX = clamp(delta.x, -halfSpanX, halfSpanX); + float2 axisOffset = delta - float2(closestX, 0.0); + float distToAxis = length(axisOffset); + float normDist = clamp(distToAxis / max(r, 0.001), 0.0, 1.0); + + // 1. Fresnel edge rim highlight (inner edge glow / bevel) + float rim = smoothstep(0.72, 0.98, normDist) * rimIntensity; + + // 2. 3D convex glass dome surface normal & specular highlight + float2 normalXY = distToAxis > 0.001 ? (axisOffset / distToAxis) * normDist : float2(0.0); + float normalZ = sqrt(max(1.0 - normDist * normDist, 0.0)); + float3 normal = normalize(float3(normalXY, normalZ)); + float3 lightDir = normalize(float3(-0.5, -0.7, 0.8)); + + float NdotL = max(dot(normal, lightDir), 0.0); + float specular = pow(NdotL, 6.0) * specularIntensity; + + // Apply additive glass illumination modulated by content alpha + half3 highlight = half3(rim + specular) * color.a; + color.rgb = clamp(color.rgb + highlight, 0.0, color.a); + + return color; + } +""" + +/** + * Creates a chained hardware [RenderEffect] implementing an optical glass magnifier. + * + * Chaining pipeline: + * 1. Optional inner hardware blur filter: [RenderEffect.createBlurEffect] (for frosted/soft-focus magnifier). + * 2. Lens magnification shader: [MAGNIFIER_LENS_SHADER] (scales backdrop coords, applies spherical curvature + * and chromatic dispersion). + * 3. Glass surface finish shader: [GLASS_FINISH_SHADER] (adds Fresnel rim highlight and 3D convex specular sheen). + * + * Uses [RenderEffect.createChainEffect] to chain these stages together into a single hardware pass. + */ +fun createMagnifierEffect( + size: Size, + zoom: Float = 1.35f, + blurRadiusPx: Float = 0f, + lensCurvature: Float = 0.35f, + chromaticAberrationPx: Float = 3f, + rimIntensity: Float = 0.18f, + specularIntensity: Float = 0.15f, + tileMode: Shader.TileMode = Shader.TileMode.CLAMP, +): RenderEffect? { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) return null + if (size.width <= 0f || size.height <= 0f) return null + + val blurEffect = if (blurRadiusPx > 0f) { + RenderEffect.createBlurEffect( + blurRadiusPx.coerceAtLeast(0.01f), + blurRadiusPx.coerceAtLeast(0.01f), + tileMode, + ) + } 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 + } + + 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) + } + + 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/conversation/Conversation.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/Conversation.kt index 91593e0d29..6dff34079f 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 @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.exclude import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -47,7 +48,9 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.paddingFrom import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.sizeIn import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState @@ -419,25 +422,18 @@ fun Message( } val spaceBetweenAuthors = if (isLastMessageByAuthor) Modifier.padding(top = 8.dp) else Modifier - Row(modifier = spaceBetweenAuthors) { - if (isLastMessageByAuthor) { - // Avatar - Image( - modifier = Modifier - .clickable(onClick = { onAuthorClick(msg.author) }) - .padding(horizontal = 16.dp) - .size(42.dp) - .border(1.5.dp, borderColor, CircleShape) - .border(3.dp, MaterialTheme.colorScheme.surface, CircleShape) - .clip(CircleShape) - .align(Alignment.Top), - painter = painterResource(id = msg.authorImage), - contentScale = ContentScale.Crop, - contentDescription = null, - ) - } else { - // Space under avatar - Spacer(modifier = Modifier.width(74.dp)) + Row(modifier = spaceBetweenAuthors.fillMaxWidth()) { + if (!isUserMe) { + if (isLastMessageByAuthor) { + AuthorAvatar( + authorImage = msg.authorImage, + authorName = msg.author, + borderColor = borderColor, + onAuthorClick = onAuthorClick, + ) + } else { + Spacer(modifier = Modifier.width(74.dp)) + } } AuthorAndTextMessage( msg = msg, @@ -447,12 +443,44 @@ fun Message( authorClicked = onAuthorClick, onVideoClick = onVideoClick, modifier = Modifier - .padding(end = 16.dp) + .padding( + start = if (isUserMe) 16.dp else 0.dp, + end = if (isUserMe) 0.dp else 16.dp, + ) .weight(1f), ) + if (isUserMe) { + if (isLastMessageByAuthor) { + AuthorAvatar( + authorImage = msg.authorImage, + authorName = msg.author, + borderColor = borderColor, + onAuthorClick = onAuthorClick, + ) + } else { + Spacer(modifier = Modifier.width(74.dp)) + } + } } } +@Composable +private fun RowScope.AuthorAvatar(authorImage: Int, authorName: String, borderColor: Color, onAuthorClick: (String) -> Unit) { + Image( + modifier = Modifier + .clickable(onClick = { onAuthorClick(authorName) }) + .padding(horizontal = 16.dp) + .size(42.dp) + .border(1.5.dp, borderColor, CircleShape) + .border(3.dp, MaterialTheme.colorScheme.surface, CircleShape) + .clip(CircleShape) + .align(Alignment.Top), + painter = painterResource(id = authorImage), + contentScale = ContentScale.Crop, + contentDescription = null, + ) +} + @Composable fun AuthorAndTextMessage( msg: Message, @@ -463,7 +491,10 @@ fun AuthorAndTextMessage( modifier: Modifier = Modifier, onVideoClick: (String) -> Unit = {}, ) { - Column(modifier = modifier) { + Column( + modifier = modifier, + horizontalAlignment = if (isUserMe) Alignment.End else Alignment.Start, + ) { if (isLastMessageByAuthor) { AuthorNameTimestamp(msg) } @@ -504,7 +535,8 @@ private fun AuthorNameTimestamp(msg: Message) { } } -private val ChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp) +private val ChatBubbleShapeOthers = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp) +private val ChatBubbleShapeMe = RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp) @Composable fun DayHeader(dayString: String) { @@ -536,19 +568,19 @@ private fun RowScope.DayHeaderLine() { @Composable fun ChatItemBubble(message: Message, isUserMe: Boolean, authorClicked: (String) -> Unit, onVideoClick: (String) -> Unit = {}) { - val backgroundBubbleColor = if (isUserMe) { MaterialTheme.colorScheme.primary } else { MaterialTheme.colorScheme.surfaceVariant } + val bubbleShape = if (isUserMe) ChatBubbleShapeMe else ChatBubbleShapeOthers - Column { + Column(horizontalAlignment = if (isUserMe) Alignment.End else Alignment.Start) { val hasText = message.content.isNotBlank() || (message.image == null && message.videoUri == null) if (hasText) { Surface( color = backgroundBubbleColor, - shape = ChatBubbleShape, + shape = bubbleShape, ) { ClickableMessage( message = message, @@ -558,18 +590,28 @@ fun ChatItemBubble(message: Message, isUserMe: Boolean, authorClicked: (String) } } - message.image?.let { + message.image?.let { imageRes -> if (hasText) { Spacer(modifier = Modifier.height(4.dp)) } + val painter = painterResource(imageRes) + val intrinsicSize = painter.intrinsicSize + val aspectRatio = if (intrinsicSize.width > 0f && intrinsicSize.height > 0f) { + intrinsicSize.width / intrinsicSize.height + } else { + 1f + } Surface( color = backgroundBubbleColor, - shape = ChatBubbleShape, + shape = bubbleShape, ) { Image( - painter = painterResource(it), - contentScale = ContentScale.Fit, - modifier = Modifier.size(160.dp), + painter = painter, + contentScale = ContentScale.Crop, + modifier = Modifier + .sizeIn(maxWidth = 240.dp, maxHeight = 260.dp) + .aspectRatio(aspectRatio, matchHeightConstraintsFirst = aspectRatio < 1f) + .clip(bubbleShape), contentDescription = stringResource(id = R.string.attached_image), ) } @@ -581,16 +623,17 @@ fun ChatItemBubble(message: Message, isUserMe: Boolean, authorClicked: (String) } Surface( color = backgroundBubbleColor, - shape = ChatBubbleShape, + shape = bubbleShape, ) { VideoThumbnail( videoUri = videoUri, onClick = { onVideoClick(videoUri) }, - shape = ChatBubbleShape, + shape = bubbleShape, modifier = Modifier + .widthIn(max = 260.dp) .fillMaxWidth() .height(200.dp) - .clip(ChatBubbleShape), + .clip(bubbleShape), ) } } diff --git a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt index 49ceb985fd..70096af84d 100644 --- a/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt +++ b/Jetchat/app/src/main/java/com/example/compose/jetchat/conversation/JumpToBottom.kt @@ -20,19 +20,23 @@ import androidx.compose.animation.core.animateDp import androidx.compose.animation.core.updateTransition import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import com.example.compose.jetchat.R +import com.example.compose.jetchat.blur.backdropMagnifier private enum class Visibility { VISIBLE, @@ -57,6 +61,7 @@ fun JumpToBottom(enabled: Boolean, onClicked: () -> Unit, modifier: Modifier = M } } if (bottomOffset > 0.dp) { + val fabShape = CircleShape ExtendedFloatingActionButton( icon = { Icon( @@ -68,12 +73,28 @@ fun JumpToBottom(enabled: Boolean, onClicked: () -> Unit, modifier: Modifier = M text = { Text(text = stringResource(id = R.string.jumpBottom)) }, + shape = fabShape, onClick = onClicked, - containerColor = MaterialTheme.colorScheme.surface, + containerColor = Color.Transparent, contentColor = MaterialTheme.colorScheme.primary, + elevation = FloatingActionButtonDefaults.elevation( + defaultElevation = 0.dp, + pressedElevation = 0.dp, + focusedElevation = 0.dp, + hoveredElevation = 0.dp, + ), modifier = modifier .offset { IntOffset(x = 0, y = -bottomOffset.roundToPx()) } - .height(36.dp), + .height(36.dp) + .backdropMagnifier( + zoom = 1.35f, + blurRadius = 2.dp, + lensCurvature = 0.35f, + chromaticAberration = 1.5.dp, + shape = fabShape, + tint = MaterialTheme.colorScheme.surface.copy(alpha = 0.45f), + elevation = 3.dp, + ), ) } }