1 /* <lambda>null2 * Copyright (C) 2025 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 @file:OptIn(ExperimentalMaterial3ExpressiveApi::class) 18 19 package com.android.mechanics.behavior 20 21 import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi 22 import androidx.compose.material3.MotionScheme 23 import androidx.compose.ui.unit.Density 24 import androidx.compose.ui.unit.Dp 25 import androidx.compose.ui.unit.dp 26 import androidx.compose.ui.util.fastCoerceIn 27 import androidx.compose.ui.util.lerp 28 import com.android.mechanics.spec.Breakpoint 29 import com.android.mechanics.spec.BreakpointKey 30 import com.android.mechanics.spec.DirectionalMotionSpec 31 import com.android.mechanics.spec.InputDirection 32 import com.android.mechanics.spec.Mapping 33 import com.android.mechanics.spec.MotionSpec 34 import com.android.mechanics.spec.OnChangeSegmentHandler 35 import com.android.mechanics.spec.SegmentData 36 import com.android.mechanics.spec.SegmentKey 37 import com.android.mechanics.spec.buildDirectionalMotionSpec 38 import com.android.mechanics.spec.builder 39 import com.android.mechanics.spec.reverseBuilder 40 import com.android.mechanics.spring.SpringParameters 41 42 /** Motion spec for a vertically expandable container. */ 43 class VerticalExpandContainerSpec( 44 val isFloating: Boolean, 45 val minRadius: Dp = Defaults.MinRadius, 46 val radius: Dp = Defaults.Radius, 47 val visibleHeight: Dp = Defaults.VisibleHeight, 48 val preDetachRatio: Float = Defaults.PreDetachRatio, 49 val detachHeight: Dp = if (isFloating) radius * 3 else Defaults.DetachHeight, 50 val attachHeight: Dp = if (isFloating) radius * 2 else Defaults.AttachHeight, 51 val widthOffset: Dp = Defaults.WidthOffset, 52 val attachSpring: SpringParameters = Defaults.AttachSpring, 53 val detachSpring: SpringParameters = Defaults.DetachSpring, 54 val opacitySpring: SpringParameters = Defaults.OpacitySpring, 55 ) { 56 fun createHeightSpec(motionScheme: MotionScheme, density: Density): MotionSpec { 57 return with(density) { 58 val spatialSpring = SpringParameters(motionScheme.defaultSpatialSpec()) 59 60 val detachSpec = 61 DirectionalMotionSpec.builder( 62 initialMapping = Mapping.Zero, 63 defaultSpring = spatialSpring, 64 ) 65 .toBreakpoint(0f, key = Breakpoints.Attach) 66 .continueWith(Mapping.Linear(preDetachRatio)) 67 .toBreakpoint(detachHeight.toPx(), key = Breakpoints.Detach) 68 .completeWith(Mapping.Identity, detachSpring) 69 70 val attachSpec = 71 DirectionalMotionSpec.reverseBuilder(defaultSpring = spatialSpring) 72 .toBreakpoint(attachHeight.toPx(), key = Breakpoints.Detach) 73 .completeWith(mapping = Mapping.Zero, attachSpring) 74 75 val segmentHandlers = 76 mapOf<SegmentKey, OnChangeSegmentHandler>( 77 SegmentKey(Breakpoints.Detach, Breakpoint.maxLimit.key, InputDirection.Min) to 78 { currentSegment, _, newDirection -> 79 if (newDirection != currentSegment.direction) currentSegment else null 80 }, 81 SegmentKey(Breakpoints.Attach, Breakpoints.Detach, InputDirection.Max) to 82 { currentSegment: SegmentData, newInput: Float, newDirection: InputDirection 83 -> 84 if (newDirection != currentSegment.direction && newInput >= 0) 85 currentSegment 86 else null 87 }, 88 ) 89 90 MotionSpec( 91 maxDirection = detachSpec, 92 minDirection = attachSpec, 93 segmentHandlers = segmentHandlers, 94 ) 95 } 96 } 97 98 fun createWidthSpec( 99 intrinsicWidth: Float, 100 motionScheme: MotionScheme, 101 density: Density, 102 ): MotionSpec { 103 return with(density) { 104 if (isFloating) { 105 MotionSpec(buildDirectionalMotionSpec(Mapping.Fixed(intrinsicWidth))) 106 } else { 107 MotionSpec( 108 buildDirectionalMotionSpec({ input -> 109 val fraction = (input / detachHeight.toPx()).fastCoerceIn(0f, 1f) 110 intrinsicWidth - lerp(widthOffset.toPx(), 0f, fraction) 111 }) 112 ) 113 } 114 } 115 } 116 117 fun createAlphaSpec(motionScheme: MotionScheme, density: Density): MotionSpec { 118 return with(density) { 119 val detachSpec = 120 DirectionalMotionSpec.builder( 121 SpringParameters(motionScheme.defaultEffectsSpec()), 122 initialMapping = Mapping.Zero, 123 ) 124 .toBreakpoint(visibleHeight.toPx()) 125 .completeWith(Mapping.One, opacitySpring) 126 127 val attachSpec = 128 DirectionalMotionSpec.builder( 129 SpringParameters(motionScheme.defaultEffectsSpec()), 130 initialMapping = Mapping.Zero, 131 ) 132 .toBreakpoint(visibleHeight.toPx()) 133 .completeWith(Mapping.One, opacitySpring) 134 135 MotionSpec(maxDirection = detachSpec, minDirection = attachSpec) 136 } 137 } 138 139 companion object { 140 object Breakpoints { 141 val Attach = BreakpointKey("EdgeContainerExpansion::Attach") 142 val Detach = BreakpointKey("EdgeContainerExpansion::Detach") 143 } 144 145 object Defaults { 146 val VisibleHeight = 24.dp 147 val PreDetachRatio = .25f 148 val DetachHeight = 80.dp 149 val AttachHeight = 40.dp 150 151 val WidthOffset = 28.dp 152 153 val MinRadius = 28.dp 154 val Radius = 46.dp 155 156 val AttachSpring = SpringParameters(stiffness = 380f, dampingRatio = 0.9f) 157 val DetachSpring = SpringParameters(stiffness = 380f, dampingRatio = 0.9f) 158 val OpacitySpring = SpringParameters(stiffness = 1200f, dampingRatio = 0.99f) 159 } 160 } 161 } 162