skill: add Android skills
This commit is contained in:
@@ -0,0 +1,749 @@
|
||||
When using surfaces to build a custom component, refer to the following source
|
||||
code in `Surface.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 android.graphics.RuntimeShader
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.AnimationSpec
|
||||
import androidx.compose.animation.core.AnimationVector1D
|
||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.interaction.FocusInteraction
|
||||
import androidx.compose.foundation.interaction.Interaction
|
||||
import androidx.compose.foundation.interaction.InteractionSource
|
||||
import androidx.compose.foundation.interaction.PressInteraction
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shader
|
||||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||
import androidx.compose.ui.graphics.layer.GraphicsLayer
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.node.DrawModifierNode
|
||||
import androidx.compose.ui.node.ModifierNodeElement
|
||||
import androidx.compose.ui.node.invalidateDraw
|
||||
import androidx.compose.ui.node.requireGraphicsContext
|
||||
import androidx.compose.ui.platform.InspectorInfo
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.lerp
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import org.intellij.lang.annotations.Language
|
||||
|
||||
/**
|
||||
* A surface is a fundamental building block in Glimmer. A surface represents a distinct visual area
|
||||
* or 'physical' boundary for components such as buttons and cards. A [surface] implements shared
|
||||
* visual decoration for Jetpack Compose Glimmer components:
|
||||
* 1) Clipping: a surface clips its children to the shape specified by [shape]
|
||||
* 2) Border: a surface draws an inner [border] to emphasize the boundary of the component. When
|
||||
* focused, a surface draws a wider border with a focused highlight on top to indicate the focus
|
||||
* state.
|
||||
* 3) Background: a surface has a background color of [color].
|
||||
* 4) Depth effect: a surface can have different [DepthEffect] shadows for different states, as
|
||||
* specified by [depthEffect].
|
||||
* 5) Content color: a surface provides a [contentColor] for text and icons inside the surface. By
|
||||
* default this is calculated from the provided background color.
|
||||
* 6) Interaction states: when focused, a surface displays draws a wider border with a focused
|
||||
* highlight on top. When pressed, a surface draws a pressed overlay. This happens for
|
||||
* interactions emitted from [interactionSource].
|
||||
*
|
||||
* Use surface on its own for decorative elements that cannot be interacted with by a user:
|
||||
*
|
||||
* @sample androidx.xr.glimmer.samples.SurfaceSample
|
||||
*
|
||||
* In most cases surfaces should be interactive, to allow users to consistently move focus and
|
||||
* navigate between components. You can use [androidx.compose.foundation.focusable] for focus-only
|
||||
* surfaces, or [androidx.compose.foundation.clickable] and other modifiers for surfaces with
|
||||
* actions. To ensure the surface correctly reflects the interaction states, provide the same
|
||||
* [InteractionSource] to all modifiers.
|
||||
*
|
||||
* For example, to create a clickable surface:
|
||||
*
|
||||
* @sample androidx.xr.glimmer.samples.ClickableSurfaceSample
|
||||
*
|
||||
* Similarly, to create a focusable surface:
|
||||
*
|
||||
* @sample androidx.xr.glimmer.samples.FocusableSurfaceSample
|
||||
* @param enabled controls the enabled state of this surface. When `false`, a disabled overlay
|
||||
* visual will be drawn on top of the surface. Note that this only affects the visual decoration;
|
||||
* it does not intercept input or block interaction states (such as focus or press) from the
|
||||
* [interactionSource].
|
||||
* @param shape the [Shape] used to clip this surface, and also used to draw the background and
|
||||
* border
|
||||
* @param color the background [Color] for this surface
|
||||
* @param contentColor the [Color] for content inside this surface
|
||||
* @param depthEffect the [SurfaceDepthEffect] for this surface, representing the [DepthEffect]
|
||||
* shadows rendered in different states.
|
||||
* @param border an optional inner border for this surface
|
||||
* @param interactionSource the [InteractionSource] that emits [Interaction]s for this surface. For
|
||||
* interactive surfaces, the [InteractionSource] instance provided to this surface must be shared
|
||||
* with the modifier responsible for emitting [Interaction]s, such as
|
||||
* [androidx.compose.foundation.focusable] or [androidx.compose.foundation.clickable].
|
||||
*/
|
||||
@Composable
|
||||
public fun Modifier.surface(
|
||||
enabled: Boolean = true,
|
||||
shape: Shape = GlimmerTheme.shapes.medium,
|
||||
color: Color = GlimmerTheme.colors.surface,
|
||||
contentColor: Color = calculateContentColor(color),
|
||||
depthEffect: SurfaceDepthEffect? = null,
|
||||
border: BorderStroke? = SurfaceDefaults.border(),
|
||||
interactionSource: InteractionSource? = null,
|
||||
): Modifier {
|
||||
return this.surfaceDepthEffect(depthEffect, shape, interactionSource)
|
||||
.clip(shape)
|
||||
.contentColorProvider(contentColor)
|
||||
.then(
|
||||
SurfaceNodeElement(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
border = border,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
)
|
||||
.background(color = color, shape = shape)
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the [DepthEffect] used by a [surface] in different states.
|
||||
*
|
||||
* Focused [surface]s with a [focusedDepthEffect] will have a higher zIndex set so they can draw
|
||||
* their focused depth effect over siblings.
|
||||
*
|
||||
* @property [depthEffect] the [DepthEffect] used when the [surface] is in its default state (no
|
||||
* other interactions are ongoing)
|
||||
* @property [focusedDepthEffect] the [DepthEffect] used when the [surface] is focused
|
||||
*/
|
||||
@Immutable
|
||||
public class SurfaceDepthEffect(
|
||||
public val depthEffect: DepthEffect?,
|
||||
public val focusedDepthEffect: DepthEffect?,
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SurfaceDepthEffect) return false
|
||||
|
||||
if (depthEffect != other.depthEffect) return false
|
||||
if (focusedDepthEffect != other.focusedDepthEffect) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = depthEffect?.hashCode() ?: 0
|
||||
result = 31 * result + (focusedDepthEffect?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/** Default values used for [surface]. */
|
||||
public object SurfaceDefaults {
|
||||
/**
|
||||
* Create the default [BorderStroke] used for a [surface]. Use the other overload in order to
|
||||
* change the width or color.
|
||||
*/
|
||||
@Composable
|
||||
public fun border(): BorderStroke {
|
||||
return GlimmerTheme.LocalGlimmerTheme.current.defaultSurfaceBorder
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the default [BorderStroke] used for a [surface], with optional overrides for [width]
|
||||
* and [color].
|
||||
*
|
||||
* @param width width of the border in [Dp]. Use [Dp.Hairline] for one-pixel border.
|
||||
* @param color color to paint the border with
|
||||
*/
|
||||
@Composable
|
||||
public fun border(
|
||||
width: Dp = DefaultSurfaceBorderWidth,
|
||||
color: Color = GlimmerTheme.colors.outline,
|
||||
): BorderStroke {
|
||||
return BorderStroke(width, color)
|
||||
}
|
||||
|
||||
/** Returns the default (cached) border for a [surface]. */
|
||||
internal val GlimmerTheme.defaultSurfaceBorder: BorderStroke
|
||||
get() {
|
||||
return defaultSurfaceBorderCached
|
||||
?: BorderStroke(DefaultSurfaceBorderWidth, colors.outline).also {
|
||||
defaultSurfaceBorderCached = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Surface node responsible for drawing the border, focused border and highlight, and pressed
|
||||
* overlay.
|
||||
*/
|
||||
private class SurfaceNodeElement(
|
||||
private val enabled: Boolean,
|
||||
private val shape: Shape,
|
||||
private val border: BorderStroke?,
|
||||
private val interactionSource: InteractionSource?,
|
||||
) : ModifierNodeElement<SurfaceNode>() {
|
||||
override fun create(): SurfaceNode =
|
||||
SurfaceNode(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
border = border,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
override fun update(node: SurfaceNode) =
|
||||
node.update(
|
||||
enabled = enabled,
|
||||
shape = shape,
|
||||
border = border,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other !is SurfaceNodeElement) return false
|
||||
|
||||
if (enabled != other.enabled) return false
|
||||
if (shape != other.shape) return false
|
||||
if (border != other.border) return false
|
||||
if (interactionSource != other.interactionSource) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = enabled.hashCode()
|
||||
result = 31 * result + shape.hashCode()
|
||||
result = 31 * result + (border?.hashCode() ?: 0)
|
||||
result = 31 * result + (interactionSource?.hashCode() ?: 0)
|
||||
return result
|
||||
}
|
||||
|
||||
override fun InspectorInfo.inspectableProperties() {
|
||||
name = "surface"
|
||||
properties["enabled"] = enabled
|
||||
properties["shape"] = shape
|
||||
properties["border"] = border
|
||||
properties["interactionSource"] = interactionSource
|
||||
}
|
||||
}
|
||||
|
||||
private class SurfaceNode(
|
||||
private var enabled: Boolean,
|
||||
private var shape: Shape,
|
||||
private var border: BorderStroke?,
|
||||
private var interactionSource: InteractionSource?,
|
||||
) : DrawModifierNode, Modifier.Node() {
|
||||
|
||||
override val shouldAutoInvalidate = false
|
||||
|
||||
// Cache borders and highlight for unfocused and focused states. This means we
|
||||
// can avoid recreating these for a given surface, if the border and shape never
|
||||
// change. Changing between unfocused and focus states only requires a draw
|
||||
// invalidation, as the borders are already cached.
|
||||
|
||||
// Unfocused border
|
||||
private val unfocusedBorderLogic: BorderLogic = BorderLogic()
|
||||
private val unfocusedBorderWidth: () -> Dp = { border?.width ?: 0.dp }
|
||||
// Focused border - this consists of two layers. A 'base' layer (which is the
|
||||
// unfocused border with a different size) and the highlight we draw on top of
|
||||
// this base layer. We need to increase the size of the underlying border to
|
||||
// make sure that the highlight area fully matches the underlying border, to
|
||||
// avoid inconsistent areas of coverage due to the transparency of the
|
||||
// highlight.
|
||||
private var focusedBorderLogic: BorderLogic? = null
|
||||
private var focusedHighlightBorderLogic: BorderLogic? = null
|
||||
private var focusedBorderWidth: (() -> Dp)? = null
|
||||
|
||||
// Highlight shader / brush
|
||||
var shader: Shader? = null
|
||||
var shaderBrush: Brush? = null
|
||||
|
||||
// Graphics Layer
|
||||
private var unfocusedBorderLayer: GraphicsLayer? = null
|
||||
private var unfocusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null
|
||||
|
||||
private var focusedBorderLayer: GraphicsLayer? = null
|
||||
private var focusedGraphicsLayerProvider: (() -> GraphicsLayer)? = null
|
||||
|
||||
private var focusedHighlightBorderLayer: GraphicsLayer? = null
|
||||
private var focusedHighlightGraphicsLayerProvider: (() -> GraphicsLayer)? = null
|
||||
|
||||
private var interactionCollectionJob: Job? = null
|
||||
|
||||
// Enter / exit animation progress for the width and fade effect applied to the highlight
|
||||
private var _focusedHighlightProgress: Animatable<Float, AnimationVector1D>? = null
|
||||
private val focusedHighlightProgress
|
||||
get() = _focusedHighlightProgress?.value ?: 0f
|
||||
|
||||
// Rotation progress applied to the highlight
|
||||
private var _focusedHighlightRotationProgress: Animatable<Float, AnimationVector1D>? = null
|
||||
private val focusedHighlightRotationProgress
|
||||
get() = _focusedHighlightRotationProgress?.value ?: 0f
|
||||
|
||||
private var pressedOverlayAlpha: Animatable<Float, AnimationVector1D>? = null
|
||||
// Job that runs for a minimum duration to make sure quick presses are still visible
|
||||
private var minimumPressDuration: Job? = null
|
||||
private var pressReleaseAnimation: Job? = null
|
||||
|
||||
fun update(
|
||||
enabled: Boolean,
|
||||
shape: Shape,
|
||||
border: BorderStroke?,
|
||||
interactionSource: InteractionSource?,
|
||||
) {
|
||||
if (this.enabled != enabled) {
|
||||
this.enabled = enabled
|
||||
invalidateDraw()
|
||||
}
|
||||
if (this.shape != shape) {
|
||||
this.shape = shape
|
||||
invalidateDraw()
|
||||
}
|
||||
if (this.border != border) {
|
||||
this.border = border
|
||||
invalidateDraw()
|
||||
}
|
||||
if (this.interactionSource != interactionSource) {
|
||||
this.interactionSource = interactionSource
|
||||
observeInteractions()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttach() {
|
||||
observeInteractions()
|
||||
}
|
||||
|
||||
var isFocused = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (value) {
|
||||
startFocusAnimation()
|
||||
} else {
|
||||
stopFocusAnimation()
|
||||
}
|
||||
// No need to invalidate the border cache - we build it ahead of time to account for
|
||||
// focus changes. Just invalidate draw so we can switch to drawing the correct
|
||||
// border.
|
||||
invalidateDraw()
|
||||
}
|
||||
}
|
||||
|
||||
var isPressed = false
|
||||
set(value) {
|
||||
if (field != value) {
|
||||
field = value
|
||||
if (value) {
|
||||
pressedOverlayAlpha = pressedOverlayAlpha ?: Animatable(0f)
|
||||
pressReleaseAnimation?.cancel()
|
||||
minimumPressDuration?.cancel()
|
||||
|
||||
minimumPressDuration =
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
delay(PressedOverlayMinimumDurationMillis)
|
||||
}
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
pressedOverlayAlpha?.animateTo(
|
||||
PressedOverlayAlpha,
|
||||
PressedOverlayEnterAnimationSpec,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
pressedOverlayAlpha?.let { progress ->
|
||||
pressReleaseAnimation =
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
minimumPressDuration?.join()
|
||||
progress.animateTo(0f, PressedOverlayExitAnimationSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
invalidateDraw()
|
||||
}
|
||||
}
|
||||
|
||||
private fun observeInteractions() {
|
||||
interactionCollectionJob?.cancel()
|
||||
interactionCollectionJob = null
|
||||
isFocused = false
|
||||
isPressed = false
|
||||
interactionSource?.let { source ->
|
||||
interactionCollectionJob =
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
var focusCount = 0
|
||||
var pressCount = 0
|
||||
source.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is FocusInteraction.Focus -> focusCount++
|
||||
is FocusInteraction.Unfocus -> focusCount--
|
||||
is PressInteraction.Press -> pressCount++
|
||||
is PressInteraction.Release -> pressCount--
|
||||
is PressInteraction.Cancel -> pressCount--
|
||||
}
|
||||
isFocused = focusCount > 0
|
||||
isPressed = pressCount > 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFocusAnimation() {
|
||||
_focusedHighlightProgress = _focusedHighlightProgress ?: Animatable(0f)
|
||||
_focusedHighlightRotationProgress = _focusedHighlightRotationProgress ?: Animatable(0f)
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
_focusedHighlightProgress?.snapTo(0f)
|
||||
_focusedHighlightProgress?.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = FocusedEnterAnimationSpec,
|
||||
)
|
||||
}
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
_focusedHighlightRotationProgress?.snapTo(0f)
|
||||
_focusedHighlightRotationProgress?.animateTo(
|
||||
targetValue = 1f,
|
||||
animationSpec = FocusedHighlightRotationAnimationSpec,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopFocusAnimation() {
|
||||
coroutineScope.launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
_focusedHighlightProgress?.animateTo(
|
||||
targetValue = 0f,
|
||||
animationSpec = FocusedExitAnimationSpec,
|
||||
)
|
||||
if (isActive) {
|
||||
_focusedHighlightRotationProgress?.snapTo(0f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun ContentDrawScope.draw() {
|
||||
val outline = shape.createOutline(size, layoutDirection, this)
|
||||
drawContent()
|
||||
val pressedOverlayColor = PressedOverlayColor.copy(alpha = pressedOverlayAlpha?.value ?: 0f)
|
||||
drawRect(color = pressedOverlayColor)
|
||||
if (border != null) {
|
||||
val progress = focusedHighlightProgress
|
||||
val gContext = requireGraphicsContext()
|
||||
if (progress > 0f) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
val rotationProgressRadians =
|
||||
(FocusedHighlightRotationEndAngleRadians -
|
||||
FocusedHighlightRotationStartAngleRadians) *
|
||||
focusedHighlightRotationProgress
|
||||
val rotationRadians =
|
||||
FocusedHighlightRotationStartAngleRadians + rotationProgressRadians
|
||||
shader =
|
||||
HighlightShaderHelper.configureShader(
|
||||
shader = shader,
|
||||
size = size,
|
||||
rotationRadians = rotationRadians.toFloat(),
|
||||
progress = progress,
|
||||
)
|
||||
shaderBrush = shaderBrush ?: ShaderBrush(shader!!)
|
||||
}
|
||||
focusedBorderLogic = focusedBorderLogic ?: BorderLogic()
|
||||
focusedHighlightBorderLogic = focusedHighlightBorderLogic ?: BorderLogic()
|
||||
focusedBorderWidth =
|
||||
focusedBorderWidth
|
||||
?: {
|
||||
val b = border
|
||||
if (b != null) {
|
||||
lerp(
|
||||
b.width,
|
||||
FocusedSurfaceBorderWidth,
|
||||
// Capture class property instead of function-local progress to
|
||||
// make sure this will read the animation state when the lambda
|
||||
// is invoked and not capture a stale variable
|
||||
focusedHighlightProgress,
|
||||
)
|
||||
} else {
|
||||
0.dp
|
||||
}
|
||||
}
|
||||
focusedBorderLogic!!.drawBorder(
|
||||
this,
|
||||
focusedBorderWidth!!,
|
||||
border!!.brush,
|
||||
focusedGraphicsLayerProvider
|
||||
?: {
|
||||
focusedBorderLayer
|
||||
?: gContext.createGraphicsLayer().also {
|
||||
focusedBorderLayer = it
|
||||
}
|
||||
}
|
||||
.also { focusedGraphicsLayerProvider = it },
|
||||
outline,
|
||||
)
|
||||
|
||||
shaderBrush?.let {
|
||||
focusedHighlightBorderLogic!!.drawBorder(
|
||||
this,
|
||||
focusedBorderWidth!!,
|
||||
it,
|
||||
focusedHighlightGraphicsLayerProvider
|
||||
?: {
|
||||
focusedHighlightBorderLayer
|
||||
?: gContext.createGraphicsLayer().also {
|
||||
focusedHighlightBorderLayer = it
|
||||
}
|
||||
}
|
||||
.also { focusedHighlightGraphicsLayerProvider = it },
|
||||
outline,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
unfocusedBorderLogic.drawBorder(
|
||||
this,
|
||||
unfocusedBorderWidth,
|
||||
border!!.brush,
|
||||
unfocusedGraphicsLayerProvider
|
||||
?: {
|
||||
unfocusedBorderLayer
|
||||
?: gContext.createGraphicsLayer().also {
|
||||
unfocusedBorderLayer = it
|
||||
}
|
||||
}
|
||||
.also { unfocusedGraphicsLayerProvider = it },
|
||||
outline,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!enabled) {
|
||||
drawRect(DisabledOverlayColor)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
_focusedHighlightProgress = null
|
||||
_focusedHighlightRotationProgress = null
|
||||
pressedOverlayAlpha = null
|
||||
|
||||
unfocusedGraphicsLayerProvider = null
|
||||
val gContext = requireGraphicsContext()
|
||||
unfocusedBorderLayer?.let {
|
||||
gContext.releaseGraphicsLayer(it)
|
||||
unfocusedBorderLayer = null
|
||||
}
|
||||
|
||||
focusedGraphicsLayerProvider = null
|
||||
focusedBorderLayer?.let {
|
||||
gContext.releaseGraphicsLayer(it)
|
||||
focusedBorderLayer = null
|
||||
}
|
||||
|
||||
focusedHighlightGraphicsLayerProvider = null
|
||||
focusedHighlightBorderLayer?.let {
|
||||
gContext.releaseGraphicsLayer(it)
|
||||
focusedHighlightBorderLayer = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders and animates a [surface]'s [depthEffect] for a given [shape], by observing
|
||||
* [interactionSource].
|
||||
*/
|
||||
@Composable
|
||||
private fun Modifier.surfaceDepthEffect(
|
||||
depthEffect: SurfaceDepthEffect?,
|
||||
shape: Shape,
|
||||
interactionSource: InteractionSource?,
|
||||
): Modifier {
|
||||
if (depthEffect == null) return this
|
||||
val focusedProgress = remember { Animatable(0f) }
|
||||
// If focused and there is focused depth effect, we need to draw the surface on top of
|
||||
// other siblings to make sure the depth effect occludes siblings.
|
||||
val zIndex by remember {
|
||||
// Derived to avoid invalidating layout each frame of the animation
|
||||
derivedStateOf {
|
||||
if (depthEffect.focusedDepthEffect != null && focusedProgress.value >= 0.5f) 1f else 0f
|
||||
}
|
||||
}
|
||||
if (interactionSource != null) {
|
||||
LaunchedEffect(interactionSource) {
|
||||
interactionSource.interactions.collect { interaction ->
|
||||
when (interaction) {
|
||||
is FocusInteraction.Focus ->
|
||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
focusedProgress.animateTo(1f, FocusedEnterAnimationSpec)
|
||||
}
|
||||
|
||||
is FocusInteraction.Unfocus ->
|
||||
launch(start = CoroutineStart.UNDISPATCHED) {
|
||||
focusedProgress.animateTo(0f, FocusedExitAnimationSpec)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.width, placeable.height) { placeable.place(0, 0, zIndex = zIndex) }
|
||||
}
|
||||
.depthEffect(
|
||||
from = depthEffect.depthEffect,
|
||||
to = depthEffect.focusedDepthEffect,
|
||||
shape = shape,
|
||||
progress = { focusedProgress.value },
|
||||
)
|
||||
}
|
||||
|
||||
/** Default border width for a [surface]. */
|
||||
private val DefaultSurfaceBorderWidth = 2.dp
|
||||
|
||||
/** Focused border width for a [surface]. */
|
||||
private val FocusedSurfaceBorderWidth = 5.dp
|
||||
|
||||
/** Enter animation for focus highlight and depth effect */
|
||||
private val FocusedEnterAnimationSpec: AnimationSpec<Float> =
|
||||
spring(dampingRatio = 1f, stiffness = 600f)
|
||||
|
||||
/** Exit animation for focus highlight and depth effect */
|
||||
private val FocusedExitAnimationSpec: AnimationSpec<Float> =
|
||||
spring(dampingRatio = 1f, stiffness = 100f)
|
||||
|
||||
private val FocusedHighlightRotationAnimationSpec: AnimationSpec<Float> =
|
||||
tween(durationMillis = 700, easing = LinearOutSlowInEasing, delayMillis = 40)
|
||||
|
||||
private val FocusedHighlightRotationStartAngleRadians: Double = Math.toRadians(-100.0)
|
||||
|
||||
private val FocusedHighlightRotationEndAngleRadians: Double = Math.toRadians(35.0)
|
||||
|
||||
private val PressedOverlayColor = Color.White
|
||||
|
||||
internal val DisabledOverlayColor = Color(0x8F191919)
|
||||
|
||||
private const val PressedOverlayAlpha = 0.16f
|
||||
|
||||
private val PressedOverlayEnterAnimationSpec: AnimationSpec<Float> =
|
||||
spring(dampingRatio = 0.84f, stiffness = 8000f)
|
||||
|
||||
private val PressedOverlayExitAnimationSpec: AnimationSpec<Float> =
|
||||
spring(dampingRatio = 0.85f, stiffness = 50f)
|
||||
|
||||
private const val PressedOverlayMinimumDurationMillis = 300L
|
||||
|
||||
@Language(value = "AGSL")
|
||||
private const val FocusedHighlightShader =
|
||||
"""
|
||||
/**
|
||||
* Rotating linear gradient shader, where rotation is controlled by iRotation uniform.
|
||||
* This is essentially the same as:
|
||||
* LinearGradientShader(
|
||||
* colors = FocusedHighlightColors,
|
||||
* colorStops = FocusedHighlightColorStops,
|
||||
* from = Offset.Zero,
|
||||
* to = Offset(size.width, size.height),
|
||||
* )
|
||||
* But allowing for efficient rotation, instead of needing to create a new shader / brush every
|
||||
* frame with new coordinates.
|
||||
*/
|
||||
// Width / height
|
||||
uniform float2 iResolution;
|
||||
// Rotation in radians. 0 radians means a horizontal gradient.
|
||||
// Positive values will have the effect of rotating the gradient clockwise.
|
||||
uniform float iRotation;
|
||||
// Alpha animation progress from 0 to 1. This will be applied to the color stops so that each
|
||||
// color stop will fade in.
|
||||
uniform float iAlphaProgress;
|
||||
|
||||
half4 main(float2 fragCoord) {
|
||||
// Horizontal gradient
|
||||
half4 colors[4];
|
||||
colors[0] = half4(1.0, 1.0, 1.0, 1.0 * iAlphaProgress); // White with 100% alpha
|
||||
colors[1] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha
|
||||
colors[2] = half4(1.0, 1.0, 1.0, 0.2 * iAlphaProgress); // White with 20% alpha
|
||||
colors[3] = half4(1.0, 1.0, 1.0, 0.8 * iAlphaProgress); // White with 80% alpha
|
||||
|
||||
// Stops for the horizontal gradient
|
||||
float stops[4];
|
||||
stops[0] = 0.0;
|
||||
stops[1] = 0.3;
|
||||
stops[2] = 0.66;
|
||||
stops[3] = 1.0;
|
||||
|
||||
// Normalize
|
||||
half2 uv = fragCoord.xy / iResolution.xy;
|
||||
|
||||
// Offset around a rotational center
|
||||
half2 rotationCenter = half2(0.5, 0.5);
|
||||
uv -= rotationCenter;
|
||||
|
||||
// Rotate
|
||||
// We rotate in the opposite direction as we are rotating the coordinate we sample the gradient
|
||||
// from. To create the effect of a gradient 'moving' clockwise, we need to move the
|
||||
// coordinate in the opposite direction (counter-clockwise).
|
||||
float2x2 matrix = float2x2(cos(-iRotation),-sin(-iRotation),sin(-iRotation),cos(-iRotation));
|
||||
uv *= matrix;
|
||||
|
||||
// Translate back into [0,1] space
|
||||
uv += rotationCenter;
|
||||
|
||||
// Blend through stops using the x coordinate, since we have a horizontal gradient
|
||||
half4 color = mix(colors[0], colors[1], smoothstep(stops[0], stops[1], uv.x));
|
||||
color = mix(color, colors[2], smoothstep(stops[1], stops[2], uv.x));
|
||||
color = mix(color, colors[3], smoothstep(stops[2], stops[3], uv.x));
|
||||
|
||||
return color;
|
||||
}
|
||||
"""
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
|
||||
private object HighlightShaderHelper {
|
||||
@JvmStatic
|
||||
fun configureShader(
|
||||
shader: Shader?,
|
||||
size: Size,
|
||||
rotationRadians: Float,
|
||||
progress: Float,
|
||||
): Shader {
|
||||
val shader = shader as? RuntimeShader ?: RuntimeShader(FocusedHighlightShader)
|
||||
shader.setFloatUniform("iResolution", size.width, size.height)
|
||||
shader.setFloatUniform("iRotation", rotationRadians)
|
||||
shader.setFloatUniform("iAlphaProgress", progress)
|
||||
return shader
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<br />
|
||||
Reference in New Issue
Block a user