• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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