• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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 package com.android.compose.animation
18 
19 import androidx.compose.foundation.gestures.Orientation
20 import androidx.compose.runtime.Stable
21 import androidx.compose.ui.Modifier
22 import androidx.compose.ui.layout.Measurable
23 import androidx.compose.ui.layout.MeasureResult
24 import androidx.compose.ui.layout.MeasureScope
25 import androidx.compose.ui.layout.layout
26 import androidx.compose.ui.node.LayoutModifierNode
27 import androidx.compose.ui.node.ModifierNodeElement
28 import androidx.compose.ui.unit.Constraints
29 import androidx.compose.ui.unit.Dp
30 import kotlin.math.roundToInt
31 
32 /** A component that can bounce in one dimension, for instance when it is tapped. */
33 @Stable
34 interface Bounceable {
35     val bounce: Dp
36 }
37 
38 /**
39  * Bounce a composable in the given [orientation] when this [bounceable], the [previousBounceable]
40  * or [nextBounceable] is bouncing.
41  *
42  * Important: This modifier should be used on composables that have a fixed size in [orientation],
43  * i.e. they should be placed *after* modifiers like Modifier.fillMaxWidth() or Modifier.height().
44  *
45  * @param bounceable the [Bounceable] associated to the current composable that will make this
46  *   composable size grow when bouncing.
47  * @param previousBounceable the [Bounceable] associated to the previous composable in [orientation]
48  *   that will make this composable shrink when bouncing.
49  * @param nextBounceable the [Bounceable] associated to the next composable in [orientation] that
50  *   will make this composable shrink when bouncing.
51  * @param orientation the orientation in which this bounceable should grow/shrink.
52  * @param bounceEnd whether this bounceable should bounce on the end (right in LTR layouts, left in
53  *   RTL layouts) side. This can be used for grids for which the last item does not align perfectly
54  *   with the end of the grid.
55  */
56 @Stable
Modifiernull57 fun Modifier.bounceable(
58     bounceable: Bounceable,
59     previousBounceable: Bounceable?,
60     nextBounceable: Bounceable?,
61     orientation: Orientation,
62     bounceEnd: Boolean = nextBounceable != null,
63 ): Modifier {
64     return this then
65         BounceableElement(bounceable, previousBounceable, nextBounceable, orientation, bounceEnd)
66 }
67 
68 private data class BounceableElement(
69     private val bounceable: Bounceable,
70     private val previousBounceable: Bounceable?,
71     private val nextBounceable: Bounceable?,
72     private val orientation: Orientation,
73     private val bounceEnd: Boolean,
74 ) : ModifierNodeElement<BounceableNode>() {
createnull75     override fun create(): BounceableNode {
76         return BounceableNode(
77             bounceable,
78             previousBounceable,
79             nextBounceable,
80             orientation,
81             bounceEnd,
82         )
83     }
84 
updatenull85     override fun update(node: BounceableNode) {
86         node.bounceable = bounceable
87         node.previousBounceable = previousBounceable
88         node.nextBounceable = nextBounceable
89         node.orientation = orientation
90         node.bounceEnd = bounceEnd
91     }
92 }
93 
94 private class BounceableNode(
95     var bounceable: Bounceable,
96     var previousBounceable: Bounceable?,
97     var nextBounceable: Bounceable?,
98     var orientation: Orientation,
99     var bounceEnd: Boolean = nextBounceable != null,
100 ) : Modifier.Node(), LayoutModifierNode {
measurenull101     override fun MeasureScope.measure(
102         measurable: Measurable,
103         constraints: Constraints,
104     ): MeasureResult {
105         // The constraints in the orientation should be fixed, otherwise there is no way to know
106         // what the size of our child node will be without this animation code.
107         checkFixedSize(constraints, orientation)
108 
109         var sizePrevious = 0f
110         var sizeNext = 0f
111 
112         val previousBounceable = previousBounceable
113         if (previousBounceable != null) {
114             sizePrevious += bounceable.bounce.toPx() - previousBounceable.bounce.toPx()
115         }
116 
117         val nextBounceable = nextBounceable
118         if (nextBounceable != null) {
119             sizeNext += bounceable.bounce.toPx() - nextBounceable.bounce.toPx()
120         } else if (bounceEnd) {
121             sizeNext += bounceable.bounce.toPx()
122         }
123 
124         when (orientation) {
125             Orientation.Horizontal -> {
126                 val idleWidth = constraints.maxWidth
127                 val animatedWidth = (idleWidth + sizePrevious + sizeNext).roundToInt()
128                 val animatedConstraints =
129                     constraints.copy(minWidth = animatedWidth, maxWidth = animatedWidth)
130 
131                 val placeable = measurable.measure(animatedConstraints)
132 
133                 // Important: we still place the element using the idle size coming from the
134                 // constraints, otherwise the parent will automatically center this node given the
135                 // size that it expects us to be. This allows us to then place the element where we
136                 // want it to be.
137                 return layout(idleWidth, placeable.height) {
138                     placeable.placeRelative(-sizePrevious.roundToInt(), 0)
139                 }
140             }
141             Orientation.Vertical -> {
142                 val idleHeight = constraints.maxHeight
143                 val animatedHeight = (idleHeight + sizePrevious + sizeNext).roundToInt()
144                 val animatedConstraints =
145                     constraints.copy(minHeight = animatedHeight, maxHeight = animatedHeight)
146 
147                 val placeable = measurable.measure(animatedConstraints)
148                 return layout(placeable.width, idleHeight) {
149                     placeable.placeRelative(0, -sizePrevious.roundToInt())
150                 }
151             }
152         }
153     }
154 }
155 
checkFixedSizenull156 private fun checkFixedSize(constraints: Constraints, orientation: Orientation) {
157     when (orientation) {
158         Orientation.Horizontal -> {
159             check(constraints.hasFixedWidth) {
160                 "Modifier.bounceable() should receive a fixed width from its parent. Make sure " +
161                     "that it is used *after* a fixed-width Modifier in the horizontal axis (like" +
162                     " Modifier.fillMaxWidth() or Modifier.width())."
163             }
164         }
165         Orientation.Vertical -> {
166             check(constraints.hasFixedHeight) {
167                 "Modifier.bounceable() should receive a fixed height from its parent. Make sure " +
168                     "that it is used *after* a fixed-height Modifier in the vertical axis (like" +
169                     " Modifier.fillMaxHeight() or Modifier.height())."
170             }
171         }
172     }
173 }
174