Skip to content

Commit 64b29a4

Browse files
committed
Animate bound opacity changes of nodes with a transition
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AjsYaRkTHNKszSHjd3j264
1 parent fc773f6 commit 64b29a4

3 files changed

Lines changed: 103 additions & 2 deletions

File tree

‎lib/src/transformers/widget_node_transformer_manager.dart‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,18 @@ abstract class WidgetNodeTransformerManager
5151

5252
/// Convenience method to handle widget opacity.
5353
Widget applyWidgetOpacity(BaseNode node, Widget widget) {
54-
if (node is! BlendMixin || node.opacity > 0.99) {
55-
return widget;
54+
if (node is! BlendMixin) return widget;
55+
// A node with a transition always has the animated wrapper, so a change
56+
// of its opacity (to fully opaque too) tweens instead of jumping.
57+
if (node.transition case final NodeTransition transition?) {
58+
return AnimatedOpacity(
59+
opacity: node.opacity.clamp(0.0, 1.0),
60+
duration: transition.duration,
61+
curve: transition.curve.flutterCurve,
62+
child: widget,
63+
);
5664
}
65+
if (node.opacity > 0.99) return widget;
5766

5867
final double opacity = node.opacity < 0.99 ? node.opacity : 1;
5968
return Opacity(opacity: opacity, child: widget);

‎lib/src/utils/extensions.dart‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2306,3 +2306,14 @@ extension TextInputActionCExt on TextInputActionC {
23062306
TextInputActionC.newline => TextInputAction.newline,
23072307
};
23082308
}
2309+
2310+
extension TransitionCurveExt on TransitionCurve {
2311+
/// The Flutter [Curve] for this easing.
2312+
Curve get flutterCurve => switch (this) {
2313+
TransitionCurve.linear => Curves.linear,
2314+
TransitionCurve.easeIn => Curves.easeIn,
2315+
TransitionCurve.easeOut => Curves.easeOut,
2316+
TransitionCurve.easeInOut => Curves.easeInOut,
2317+
TransitionCurve.easeOutCubic => Curves.easeOutCubic,
2318+
};
2319+
}

‎test/node_transition_test.dart‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import 'package:codelessly_api/codelessly_api.dart';
2+
import 'package:codelessly_sdk/codelessly_sdk.dart';
3+
import 'package:flutter/material.dart';
4+
import 'package:flutter_test/flutter_test.dart';
5+
6+
import 'feedback_actions_test.dart'
7+
show body, brandRegular, button, canvas, layoutOf, pumpLayouts, text;
8+
9+
final VariableData bright = VariableData(
10+
id: 'bright',
11+
name: 'bright',
12+
value: true,
13+
type: VariableType.boolean,
14+
);
15+
16+
TextNode pulse({NodeTransition? transition}) =>
17+
text('pulse', 'mainBody', 'Loading', brandRegular)
18+
..variables['opacity'] = r'${= if(bright, 1, 0.45)}'
19+
..transition = transition;
20+
21+
Future<void> pumpScreen(WidgetTester tester, {NodeTransition? transition}) =>
22+
pumpLayouts(
23+
tester,
24+
[
25+
layoutOf('layout', [
26+
canvas('main', 'mainBody'),
27+
body('mainBody', 'main', ['pulse', 'dim']),
28+
pulse(transition: transition),
29+
button(
30+
'dim',
31+
'mainBody',
32+
'Dim',
33+
SetVariableAction(variable: bright, toggled: true),
34+
),
35+
]),
36+
],
37+
variables: {
38+
'main': {bright.id: bright},
39+
},
40+
);
41+
42+
double renderedOpacity(WidgetTester tester) {
43+
final Iterable<FadeTransition> fades = tester.widgetList<FadeTransition>(
44+
find.ancestor(of: find.text('Loading'), matching: find.byType(FadeTransition)),
45+
);
46+
return fades.first.opacity.value;
47+
}
48+
49+
void main() {
50+
testWidgets('a node with a transition tweens a bound opacity', (
51+
tester,
52+
) async {
53+
await pumpScreen(
54+
tester,
55+
transition: const NodeTransition(
56+
durationMillis: 1000,
57+
curve: TransitionCurve.linear,
58+
),
59+
);
60+
expect(renderedOpacity(tester), 1);
61+
62+
await tester.tap(find.text('Dim'));
63+
await tester.pump();
64+
await tester.pump(const Duration(milliseconds: 500));
65+
expect(renderedOpacity(tester), closeTo(0.725, 0.02));
66+
67+
await tester.pump(const Duration(milliseconds: 600));
68+
expect(renderedOpacity(tester), closeTo(0.45, 0.001));
69+
});
70+
71+
testWidgets('without a transition a bound opacity jumps', (tester) async {
72+
await pumpScreen(tester);
73+
expect(find.byType(AnimatedOpacity), findsNothing);
74+
await tester.tap(find.text('Dim'));
75+
await tester.pump();
76+
final Opacity opacity = tester.widget<Opacity>(
77+
find.ancestor(of: find.text('Loading'), matching: find.byType(Opacity)).first,
78+
);
79+
expect(opacity.opacity, 0.45);
80+
});
81+
}

0 commit comments

Comments
 (0)