Skip to content

Commit 1c0fdff

Browse files
committed
some fixes for release
1 parent 0f60605 commit 1c0fdff

File tree

2 files changed

+227
-8
lines changed

2 files changed

+227
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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+
}

core/ui/src/main/kotlin/ru/tech/imageresizershrinker/core/ui/widget/other/ExpandableItem.kt

+8-8
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
package ru.tech.imageresizershrinker.core.ui.widget.other
1919

20-
import androidx.compose.animation.animateContentSize
2120
import androidx.compose.animation.core.FiniteAnimationSpec
2221
import androidx.compose.animation.core.animateFloatAsState
2322
import androidx.compose.animation.core.tween
@@ -55,6 +54,7 @@ import androidx.compose.ui.unit.dp
5554
import ru.tech.imageresizershrinker.core.ui.utils.animation.FancyTransitionEasing
5655
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.EnhancedIconButton
5756
import ru.tech.imageresizershrinker.core.ui.widget.enhanced.hapticsCombinedClickable
57+
import ru.tech.imageresizershrinker.core.ui.widget.modifier.animateContentSizeNoClip
5858
import ru.tech.imageresizershrinker.core.ui.widget.modifier.container
5959
import ru.tech.imageresizershrinker.core.ui.widget.modifier.shapeByInteraction
6060

@@ -74,29 +74,29 @@ fun ExpandableItem(
7474
onLongClick: (() -> Unit)? = null,
7575
expansionIconContainerColor: Color = Color.Transparent
7676
) {
77-
val shape = shapeByInteraction(
77+
val animatedShape = shapeByInteraction(
7878
shape = shape,
7979
pressedShape = pressedShape,
8080
interactionSource = interactionSource
8181
)
8282

8383
Column(
84-
Modifier
85-
.animateContentSize(
86-
animationSpec = spec(10)
87-
)
84+
modifier = Modifier
8885
.then(modifier)
8986
.container(
9087
color = color,
9188
resultPadding = 0.dp,
92-
shape = shape
89+
shape = animatedShape
90+
)
91+
.animateContentSizeNoClip(
92+
animationSpec = spec(10)
9393
)
9494
) {
9595
var expanded by rememberSaveable(initialState) { mutableStateOf(initialState) }
9696
val rotation by animateFloatAsState(if (expanded) 180f else 0f)
9797
Row(
9898
modifier = Modifier
99-
.clip(shape)
99+
.clip(animatedShape)
100100
.hapticsCombinedClickable(
101101
interactionSource = interactionSource,
102102
indication = LocalIndication.current,

0 commit comments

Comments
 (0)