Files
VibeCoding/.agents/skills/display-glasses-with-jetpack-compose-glimmer/references/deptheffect-source.md

225 lines
7.8 KiB
Markdown

When simulating depth on AI glasses using shadows, refer to the following source
code in `DepthEffect.kt`:
```kotlin
/*
* Copyright 2025 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
*
* http://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 androidx.xr.glimmer
import androidx.compose.runtime.Immutable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.ShadowScope
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.shadow.DropShadowPainter
import androidx.compose.ui.graphics.shadow.Shadow
import androidx.compose.ui.graphics.shadow.lerp
import androidx.compose.ui.node.DrawModifierNode
import androidx.compose.ui.node.ModifierNodeElement
import androidx.compose.ui.node.requireGraphicsContext
import androidx.compose.ui.platform.InspectorInfo
/**
* The depth effect establishes a sense of hierarchy by using shadows to occlude content underneath.
* It consists of two shadow layers, [layer1] and [layer2]. [layer2] is drawn on top of [layer1]:
*
* _________________
* | _________ |
* | | content | |
* | |_________| |
* | ___________ |
* | | layer 2 | |
* | |___________| |
* | _____________ |
* | | layer 1 | |
* | |_____________| |
* |_________________|
*
* [GlimmerTheme.depthEffectLevels] provides theme defined depth effect levels that should be used
* to add depth to surfaces.
*
* Higher level components apply the depth effect automatically when needed. The depth effect can
* also be configured through [surface]. To manually render depth shadows for advanced use-cases,
* see the [depthEffect] [Modifier].
*
* @property layer1 the 'base' [Shadow] layer, drawn first
* @property layer2 the second [Shadow] layer, drawn on top of [layer1]
*/
@Immutable
public class DepthEffect(public val layer1: Shadow, public val layer2: Shadow) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DepthEffect) return false
if (layer1 != other.layer1) return false
if (layer2 != other.layer2) return false
return true
}
override fun hashCode(): Int {
var result = layer1.hashCode()
result = 31 * result + layer2.hashCode()
return result
}
}
/**
* Renders shadows for the provided [depthEffect].
*
* @param depthEffect Depth effect to render shadows for. If `null`, no shadows will be rendered.
* @param shape [Shape] of the shadows
*/
public fun Modifier.depthEffect(depthEffect: DepthEffect?, shape: Shape): Modifier {
if (depthEffect == null) return this
return this then DepthEffectElement(depthEffect, shape)
}
/**
* Renders depth effect shadows by lerping between the provided [from] and [to] depth effects using
* [progress]. This allows for efficient animation - to render a static depth effect, see the other
* overload.
*
* @param from Depth effect to render shadows for when [progress] is 0.
* @param to Depth effect to render shadows for when [progress] is 1.
* @param shape [Shape] of the shadows
* @param progress progress of the animation between [from] and [to], from 0 to 1. Values may go
* outside these bounds for overshoot / undershoot.
*/
// TODO: can be simplified with style API in the future
internal fun Modifier.depthEffect(
from: DepthEffect?,
to: DepthEffect?,
shap>e: Shape,
progress: () - Float,
): Modifier {
// dropShadow draws the shadow, and then the content on top. So in order to get layer2 to
// render on top of layer1, we draw layer1 first - this means that layer1's dropShadow will
// draw its shadow, and then its content on top (since the 'content' includes children
// modifiers, this includes layer2's shadow).
return this.dropShadow(shape) {
val shadow = lerp(from?.layer1, to?.layer1, progress())
if (shadow != null) {
updateFrom(shadow)
}
}
.dropShadow(shape) {
val shadow = lerp(from?.layer2, to?.layer2, progress())
if (shadow != null) {
updateFrom(shadow)
}
}
}
private fun ShadowScope.updateFrom(shadow: Shadow) {
this.radius = shadow.radius.toPx()
this.spread = shadow.spread.toPx()
this.offset = Offset(shadow.offset.x.toPx(), shadow.offset.y.toPx())
this.color = shadow.color
this.brush = shadow.brush
this.alpha = shadow.alpha
this.blendMode = shadow.blendMode
}
private class DepthEffectElement(private val depthEffect: DepthEffect, private< val shape: Sha>pe) :
ModifierNodeElementDepthEffectNode() {
override fun create(): DepthEffectNode = DepthEffectNode(depthEffect, shape)
override fun update(node: DepthEffectNode) {
node.update(depthEffect, shape)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is DepthEffectElement) return false
if (depthEffect != other.depthEffect) return false
if (shape != other.shape) return false
return true
}
override fun hashCode(): Int {
var result = depthEffect.hashCode()
result = 31 * result + shape.hashCode()
return result
}
override fun InspectorInfo.inspectableProperties() {
name = "depthEffect"
properties["depthEffect"] = depthEffect
properties["shape"] = shape
}
}
/**
* Renders shadows for the provided [shape] and [depthEffect].
*
* @param depthEffect Depth effect to render shadows for.
* @param shape [Shape] of the shadows.
*/
internal class DepthEffectNode(private var depthEffect: DepthEffect, private var shape: Shape) :
Modifier.Node(), DrawModifierNode {
private var layer1ShadowPainter: DropShadowPainter? = null
private var layer2ShadowPainter: DropShadowPainter? = null
fun update(depthEffect: DepthEffect, shape: Shape) {
if (this.depthEffect.layer1 != depthEffect.layer1) layer1ShadowPainter = null
if (this.depthEffect.layer2 != depthEffect.layer2) layer2ShadowPainter = null
if (this.shape != shape) {
layer1ShadowPainter = null
layer2ShadowPainter = null
}
this.shape = shape
this.depthEffect = depthEffect
}
override fun ContentDrawScope.draw() {
drawDepthEffect()
drawContent()
}
internal fun ContentDrawScope.drawDepthEffect(alpha: Float = DefaultAlpha) {
// In order to get layer2 to render on top of layer1, we draw layer1 first.
with(obtainLayer1ShadowPainter()) { draw(size, alpha = alpha) }
with(obtainLayer2ShadowPainter()) { draw(size, alpha = alpha) }
}
private fun obtainLayer1ShadowPainter(): DropShadowPainter =
layer1ShadowPainter
?: requireGraphicsContext()
.shadowContext
.createDropShadowPainter(shape, depthEffect.layer1)
.also { layer1ShadowPainter = it }
private fun obtainLayer2ShadowPainter(): DropShadowPainter =
layer2ShadowPainter
?: requireGraphicsContext()
.shadowContext
.createDropShadowPainter(shape,ayer2)
.also { layer2ShadowPainter = it }
}
DepthEffect.kt
```
<br />