1
+ package ru.tech.imageresizershrinker.core.ui.widget.modifier
2
+
3
+ import androidx.compose.animation.core.Animatable
4
+ import androidx.compose.animation.core.AnimationEndReason
5
+ import androidx.compose.animation.core.AnimationSpec
6
+ import androidx.compose.animation.core.AnimationVector2D
7
+ import androidx.compose.animation.core.FiniteAnimationSpec
8
+ import androidx.compose.animation.core.Spring
9
+ import androidx.compose.animation.core.VectorConverter
10
+ import androidx.compose.animation.core.spring
11
+ import androidx.compose.runtime.getValue
12
+ import androidx.compose.runtime.mutableStateOf
13
+ import androidx.compose.runtime.setValue
14
+ import androidx.compose.ui.Modifier
15
+ import androidx.compose.ui.layout.IntrinsicMeasurable
16
+ import androidx.compose.ui.layout.IntrinsicMeasureScope
17
+ import androidx.compose.ui.layout.LayoutModifier
18
+ import androidx.compose.ui.layout.Measurable
19
+ import androidx.compose.ui.layout.MeasureResult
20
+ import androidx.compose.ui.layout.MeasureScope
21
+ import androidx.compose.ui.node.LayoutModifierNode
22
+ import androidx.compose.ui.node.ModifierNodeElement
23
+ import androidx.compose.ui.platform.InspectorInfo
24
+ import androidx.compose.ui.unit.Constraints
25
+ import androidx.compose.ui.unit.IntSize
26
+ import androidx.compose.ui.unit.constrain
27
+ import kotlinx.coroutines.launch
28
+
29
+ /* *
30
+ * This modifier animates its own size when its child modifier (or the child composable if it
31
+ * is already at the tail of the chain) changes size. This allows the parent modifier to observe
32
+ * a smooth size change, resulting in an overall continuous visual change.
33
+ *
34
+ * A [FiniteAnimationSpec] can be optionally specified for the size change animation. By default,
35
+ * [spring] will be used.
36
+ *
37
+ * An optional [finishedListener] can be supplied to get notified when the size change animation is
38
+ * finished. Since the content size change can be dynamic in many cases, both initial value and
39
+ * target value (i.e. final size) will be passed to the [finishedListener]. __Note:__ if the
40
+ * animation is interrupted, the initial value will be the size at the point of interruption. This
41
+ * is intended to help determine the direction of the size change (i.e. expand or collapse in x and
42
+ * y dimensions).
43
+ *
44
+ * @sample androidx.compose.animation.samples.AnimateContent
45
+ *
46
+ * @param animationSpec a finite animation that will be used to animate size change, [spring] by
47
+ * default
48
+ * @param finishedListener an optional listener to be called when the content change animation is
49
+ * completed.
50
+ */
51
+ fun Modifier.animateContentSizeNoClip (
52
+ animationSpec : FiniteAnimationSpec <IntSize > = spring(
53
+ stiffness = Spring .StiffnessMediumLow
54
+ ),
55
+ finishedListener : ((initialValue: IntSize , targetValue: IntSize ) -> Unit )? = null
56
+ ): Modifier =
57
+ this then SizeAnimationModifierElement (animationSpec, finishedListener)
58
+
59
+ private data class SizeAnimationModifierElement (
60
+ val animationSpec : FiniteAnimationSpec <IntSize >,
61
+ val finishedListener : ((initialValue: IntSize , targetValue: IntSize ) -> Unit )?
62
+ ) : ModifierNodeElement<SizeAnimationModifierNode>() {
63
+ override fun create (): SizeAnimationModifierNode =
64
+ SizeAnimationModifierNode (animationSpec, finishedListener)
65
+
66
+ override fun update (node : SizeAnimationModifierNode ) {
67
+ node.animationSpec = animationSpec
68
+ node.listener = finishedListener
69
+ }
70
+
71
+ override fun InspectorInfo.inspectableProperties () {
72
+ name = " animateContentSize"
73
+ properties[" animationSpec" ] = animationSpec
74
+ properties[" finishedListener" ] = finishedListener
75
+ }
76
+ }
77
+
78
+ internal val InvalidSize = IntSize (Int .MIN_VALUE , Int .MIN_VALUE )
79
+ internal val IntSize .isValid: Boolean
80
+ get() = this != InvalidSize
81
+
82
+ /* *
83
+ * This class creates a [LayoutModifier] that measures children, and responds to children's size
84
+ * change by animating to that size. The size reported to parents will be the animated size.
85
+ */
86
+ private class SizeAnimationModifierNode (
87
+ var animationSpec : AnimationSpec <IntSize >,
88
+ var listener : ((startSize: IntSize , endSize: IntSize ) -> Unit )? = null
89
+ ) : LayoutModifierNodeWithPassThroughIntrinsics() {
90
+ private var lookaheadSize: IntSize = InvalidSize
91
+ private var lookaheadConstraints: Constraints = Constraints ()
92
+ set(value) {
93
+ field = value
94
+ lookaheadConstraintsAvailable = true
95
+ }
96
+ private var lookaheadConstraintsAvailable: Boolean = false
97
+
98
+ private fun targetConstraints (default : Constraints ) =
99
+ if (lookaheadConstraintsAvailable) {
100
+ lookaheadConstraints
101
+ } else {
102
+ default
103
+ }
104
+
105
+ data class AnimData (
106
+ val anim : Animatable <IntSize , AnimationVector2D >,
107
+ var startSize : IntSize
108
+ )
109
+
110
+ var animData: AnimData ? by mutableStateOf(null )
111
+
112
+ override fun onReset () {
113
+ super .onReset()
114
+ // Reset is an indication that the node may be re-used, in such case, animData becomes stale
115
+ animData = null
116
+ }
117
+
118
+ override fun onAttach () {
119
+ super .onAttach()
120
+ // When re-attached, we may be attached to a tree without lookahead scope.
121
+ lookaheadSize = InvalidSize
122
+ lookaheadConstraintsAvailable = false
123
+ }
124
+
125
+ override fun MeasureScope.measure (
126
+ measurable : Measurable ,
127
+ constraints : Constraints
128
+ ): MeasureResult {
129
+ val placeable = if (isLookingAhead) {
130
+ lookaheadConstraints = constraints
131
+ measurable.measure(constraints)
132
+ } else {
133
+ // Measure with lookahead constraints when available, to avoid unnecessary relayout
134
+ // in child during the lookahead animation.
135
+ measurable.measure(targetConstraints(constraints))
136
+ }
137
+ val measuredSize = IntSize (placeable.width, placeable.height)
138
+ val (width, height) = if (isLookingAhead) {
139
+ lookaheadSize = measuredSize
140
+ measuredSize
141
+ } else {
142
+ animateTo(if (lookaheadSize.isValid) lookaheadSize else measuredSize).let {
143
+ // Constrain the measure result to incoming constraints, so that parent doesn't
144
+ // force center this layout.
145
+ constraints.constrain(it)
146
+ }
147
+ }
148
+ return layout(width, height) {
149
+ placeable.placeRelative(0 , 0 )
150
+ }
151
+ }
152
+
153
+ fun animateTo (targetSize : IntSize ): IntSize {
154
+ val data = animData?.apply {
155
+ if (targetSize != anim.targetValue) {
156
+ startSize = anim.value
157
+ coroutineScope.launch {
158
+ val result = anim.animateTo(targetSize, animationSpec)
159
+ if (result.endReason == AnimationEndReason .Finished ) {
160
+ listener?.invoke(startSize, result.endState.value)
161
+ }
162
+ }
163
+ }
164
+ } ? : AnimData (
165
+ Animatable (
166
+ targetSize, IntSize .VectorConverter , IntSize (1 , 1 )
167
+ ),
168
+ targetSize
169
+ )
170
+
171
+ animData = data
172
+ return data.anim.value
173
+ }
174
+ }
175
+
176
+ internal abstract class LayoutModifierNodeWithPassThroughIntrinsics :
177
+ LayoutModifierNode , Modifier .Node () {
178
+ override fun IntrinsicMeasureScope.minIntrinsicWidth (
179
+ measurable : IntrinsicMeasurable ,
180
+ height : Int
181
+ ) = measurable.minIntrinsicWidth(height)
182
+
183
+ override fun IntrinsicMeasureScope.minIntrinsicHeight (
184
+ measurable : IntrinsicMeasurable ,
185
+ width : Int
186
+ ) = measurable.minIntrinsicHeight(width)
187
+
188
+ override fun IntrinsicMeasureScope.maxIntrinsicWidth (
189
+ measurable : IntrinsicMeasurable ,
190
+ height : Int
191
+ ) = measurable.maxIntrinsicWidth(height)
192
+
193
+ override fun IntrinsicMeasureScope.maxIntrinsicHeight (
194
+ measurable : IntrinsicMeasurable ,
195
+ width : Int
196
+ ) = measurable.maxIntrinsicHeight(width)
197
+ }
198
+
199
+ internal abstract class LayoutModifierWithPassThroughIntrinsics : LayoutModifier {
200
+ final override fun IntrinsicMeasureScope.minIntrinsicWidth (
201
+ measurable : IntrinsicMeasurable ,
202
+ height : Int
203
+ ) = measurable.minIntrinsicWidth(height)
204
+
205
+ final override fun IntrinsicMeasureScope.minIntrinsicHeight (
206
+ measurable : IntrinsicMeasurable ,
207
+ width : Int
208
+ ) = measurable.minIntrinsicHeight(width)
209
+
210
+ final override fun IntrinsicMeasureScope.maxIntrinsicWidth (
211
+ measurable : IntrinsicMeasurable ,
212
+ height : Int
213
+ ) = measurable.maxIntrinsicWidth(height)
214
+
215
+ final override fun IntrinsicMeasureScope.maxIntrinsicHeight (
216
+ measurable : IntrinsicMeasurable ,
217
+ width : Int
218
+ ) = measurable.maxIntrinsicHeight(width)
219
+ }
0 commit comments