Skip to content

Commit e34f258

Browse files
feat: Programmatically expand/collapse ExpansionTile (#5738)
* initial commit * docs + example * fix: set default value for `CupertinoSlidingSegmentedControl.selected_index` * apply copilot review suggestion * add tests * add integration test golden for CupertinoSlidingSegmentedButton on macOS * refactor: replace `initially_expanded` with `expanded` in examples * refactor: update `initially_expanded` to `expanded` in integration tests and examples * refactor: update image display examples and test cases * add logo SVG and enhance image source tests * docs: update image example to include base64 strings and byte data support * refactor: enhance `ExpansionTile` examples and tests * Increase pump times and duration in SVG image test Updated the test_src_svg_string test to use 5 pump times and a pump duration of 1000ms, likely to improve reliability or accommodate rendering timing. * Refactor image integration tests and update workflow Limits macOS integration tests to test_image.py in the workflow for focused testing. Refactors test_image.py to use a shared base64 string, reduces pump_times for faster execution, and updates the golden image for src_bytes.png. * Enable full test suite and skip failing image test Restores the full integration test suite in the macOS workflow matrix. Skips the 'test_src_bytes' test in test_image.py due to rendering issues in the CI environment. --------- Co-authored-by: Feodor Fitsner <[email protected]>
1 parent a8cef8f commit e34f258

File tree

65 files changed

+759
-417
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+759
-417
lines changed

packages/flet/lib/src/controls/cupertino_sliding_segmented_button.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,13 @@ class CupertinoSlidingSegmentedButtonControl extends StatelessWidget {
1919
debugPrint("CupertinoSlidingSegmentedButtonControl build: ${control.id}");
2020

2121
var controls = control.buildWidgets("controls");
22-
2322
if (controls.length < 2) {
2423
return const ErrorControl(
2524
"CupertinoSlidingSegmentedButton must have at minimum two visible controls");
2625
}
2726

2827
var button = CupertinoSlidingSegmentedControl(
29-
groupValue: control.getInt("selected_index"),
28+
groupValue: control.getInt("selected_index", 0)!,
3029
proportionalWidth: control.getBool("proportional_width", false)!,
3130
backgroundColor: control.getColor(
3231
"bgcolor", context, CupertinoColors.tertiarySystemFill)!,
@@ -36,15 +35,13 @@ class CupertinoSlidingSegmentedButtonControl extends StatelessWidget {
3635
"thumb_color",
3736
context,
3837
const CupertinoDynamicColor.withBrightness(
39-
color: Color(0xFFFFFFFF),
40-
darkColor: Color(0xFF636366),
41-
))!,
38+
color: Color(0xFFFFFFFF), darkColor: Color(0xFF636366)))!,
4239
children: controls.asMap().map((i, c) => MapEntry(i, c)),
4340
onValueChanged: (int? index) {
4441
if (!control.disabled) {
45-
control
46-
.updateProperties({"selected_index": index ?? 0}, notify: true);
47-
control.triggerEvent("change", index ?? 0);
42+
index = index ?? 0;
43+
control.updateProperties({"selected_index": index}, notify: true);
44+
control.triggerEvent("change", index);
4845
}
4946
},
5047
);
Lines changed: 87 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import 'package:flet/src/utils/animations.dart';
12
import 'package:flutter/material.dart';
3+
import 'package:flutter/scheduler.dart';
24

35
import '../extensions/control.dart';
46
import '../models/control.dart';
@@ -11,97 +13,114 @@ import '../utils/numbers.dart';
1113
import '../utils/theme.dart';
1214
import '../widgets/error.dart';
1315
import 'base_controls.dart';
14-
import 'control_widget.dart';
1516

16-
class ExpansionTileControl extends StatelessWidget {
17+
class ExpansionTileControl extends StatefulWidget {
1718
final Control control;
1819

19-
const ExpansionTileControl({
20-
super.key,
21-
required this.control,
22-
});
20+
const ExpansionTileControl({super.key, required this.control});
2321

2422
@override
25-
Widget build(BuildContext context) {
26-
debugPrint("ExpansionTile build: ${control.id}");
23+
State<ExpansionTileControl> createState() => _ExpansionTileControlState();
24+
}
25+
26+
class _ExpansionTileControlState extends State<ExpansionTileControl> {
27+
late final ExpansibleController _controller;
28+
bool _expanded = false;
29+
30+
@override
31+
void initState() {
32+
super.initState();
33+
_controller = ExpansibleController();
34+
_expanded = widget.control.getBool("expanded", false)!;
35+
}
36+
37+
@override
38+
void dispose() {
39+
_controller.dispose();
40+
super.dispose();
41+
}
2742

28-
var controls = control
29-
.children("controls")
30-
.map((child) => ControlWidget(control: child, key: ValueKey(child.id)))
31-
.toList();
43+
// Schedules an update to the controller after the current frame.
44+
// This ensures the expansion/collapse animation is triggered safely.
45+
void _scheduleControllerUpdate(bool expanded) {
46+
SchedulerBinding.instance.addPostFrameCallback((_) {
47+
if (!mounted) return; // Prevents updates if the widget is disposed.
3248

33-
var leading = control.buildIconOrWidget("leading");
34-
var title = control.buildTextOrWidget("title");
35-
var subtitle = control.buildTextOrWidget("subtitle");
36-
var trailing = control.buildIconOrWidget("trailing");
49+
if (expanded) {
50+
_controller.expand();
51+
} else {
52+
_controller.collapse();
53+
}
54+
});
55+
}
3756

57+
@override
58+
Widget build(BuildContext context) {
59+
debugPrint("ExpansionTile build: ${widget.control.id}");
60+
61+
final title = widget.control.buildTextOrWidget("title");
3862
if (title == null) {
3963
return const ErrorControl(
4064
"ExpansionTile.title must be provided and visible");
4165
}
4266

43-
bool maintainState = control.getBool("maintain_state", false)!;
44-
bool initiallyExpanded = control.getBool("initially_expanded", false)!;
45-
46-
var iconColor = control.getColor("icon_color", context);
47-
var textColor = control.getColor("text_color", context);
48-
var bgColor = control.getColor("bgcolor", context);
49-
var collapsedBgColor = control.getColor("collapsed_bgcolor", context);
50-
var collapsedIconColor = control.getColor("collapsed_icon_color", context);
51-
var collapsedTextColor = control.getColor("collapsed_text_color", context);
52-
53-
var affinity = control.getListTileControlAffinity(
54-
"affinity", ListTileControlAffinity.platform)!;
55-
var clipBehavior = parseClip(control.getString("clip_behavior"));
67+
var expanded = widget.control.getBool("expanded", false)!;
68+
if (_expanded != expanded) {
69+
_expanded = expanded;
70+
_scheduleControllerUpdate(expanded);
71+
}
5672

57-
var expandedCrossAxisAlignment = control.getCrossAxisAlignment(
73+
var expandedCrossAxisAlignment = widget.control.getCrossAxisAlignment(
5874
"expanded_cross_axis_alignment", CrossAxisAlignment.center)!;
59-
6075
if (expandedCrossAxisAlignment == CrossAxisAlignment.baseline) {
6176
return const ErrorControl(
62-
'CrossAxisAlignment.baseline is not supported since the expanded '
77+
'CrossAxisAlignment.BASELINE is not supported since the expanded '
6378
'controls are aligned in a column, not a row. '
6479
'Try aligning the controls differently.');
6580
}
6681

67-
Function(bool)? onChange = !control.disabled
68-
? (expanded) {
69-
control.triggerEvent("change", expanded);
70-
}
71-
: null;
72-
73-
Widget tile = ExpansionTile(
74-
controlAffinity: affinity,
75-
childrenPadding: control.getPadding("controls_padding"),
76-
tilePadding: control.getEdgeInsets("tile_padding"),
77-
expandedAlignment: control.getAlignment("expanded_alignment"),
78-
expandedCrossAxisAlignment:
79-
control.getCrossAxisAlignment("expanded_cross_axis_alignment"),
80-
backgroundColor: bgColor,
81-
iconColor: iconColor,
82-
textColor: textColor,
83-
collapsedBackgroundColor: collapsedBgColor,
84-
collapsedIconColor: collapsedIconColor,
85-
collapsedTextColor: collapsedTextColor,
86-
maintainState: maintainState,
87-
initiallyExpanded: initiallyExpanded,
88-
clipBehavior: clipBehavior,
89-
shape: control.getShape("shape", Theme.of(context)),
90-
collapsedShape: control.getShape("collapsed_shape", Theme.of(context)),
91-
onExpansionChanged: onChange,
92-
visualDensity: control.getVisualDensity("visual_density"),
93-
enableFeedback: control.getBool("enable_feedback"),
94-
showTrailingIcon: control.getBool("show_trailing_icon", true)!,
95-
enabled: !control.disabled,
96-
minTileHeight: control.getDouble("min_tile_height"),
97-
dense: control.getBool("dense"),
98-
leading: leading,
82+
final tile = ExpansionTile(
83+
controller: _controller,
84+
controlAffinity: widget.control.getListTileControlAffinity("affinity"),
85+
childrenPadding: widget.control.getPadding("controls_padding"),
86+
tilePadding: widget.control.getEdgeInsets("tile_padding"),
87+
expandedAlignment: widget.control.getAlignment("expanded_alignment"),
88+
expandedCrossAxisAlignment: expandedCrossAxisAlignment,
89+
backgroundColor: widget.control.getColor("bgcolor", context),
90+
iconColor: widget.control.getColor("icon_color", context),
91+
textColor: widget.control.getColor("text_color", context),
92+
collapsedBackgroundColor:
93+
widget.control.getColor("collapsed_bgcolor", context),
94+
collapsedIconColor:
95+
widget.control.getColor("collapsed_icon_color", context),
96+
collapsedTextColor:
97+
widget.control.getColor("collapsed_text_color", context),
98+
maintainState: widget.control.getBool("maintain_state", false)!,
99+
initiallyExpanded: expanded,
100+
clipBehavior: widget.control.getClipBehavior("clip_behavior"),
101+
shape: widget.control.getShape("shape", Theme.of(context)),
102+
collapsedShape:
103+
widget.control.getShape("collapsed_shape", Theme.of(context)),
104+
onExpansionChanged: (bool expanded) {
105+
_expanded = expanded;
106+
widget.control.updateProperties({"expanded": expanded});
107+
widget.control.triggerEvent("change", expanded);
108+
},
109+
visualDensity: widget.control.getVisualDensity("visual_density"),
110+
enableFeedback: widget.control.getBool("enable_feedback"),
111+
showTrailingIcon: widget.control.getBool("show_trailing_icon", true)!,
112+
enabled: !widget.control.disabled,
113+
minTileHeight: widget.control.getDouble("min_tile_height"),
114+
dense: widget.control.getBool("dense"),
115+
expansionAnimationStyle:
116+
widget.control.getAnimationStyle("animation_style"),
117+
leading: widget.control.buildIconOrWidget("leading"),
99118
title: title,
100-
subtitle: subtitle,
101-
trailing: trailing,
102-
children: controls,
119+
subtitle: widget.control.buildTextOrWidget("subtitle"),
120+
trailing: widget.control.buildIconOrWidget("trailing"),
121+
children: widget.control.buildWidgets("controls"),
103122
);
104123

105-
return LayoutControl(control: control, child: tile);
124+
return LayoutControl(control: widget.control, child: tile);
106125
}
107126
}

packages/flet/lib/src/utils/theme.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import '../flet_backend.dart';
88
import '../models/control.dart';
99
import '../utils/transforms.dart';
1010
import 'alignment.dart';
11+
import 'animations.dart';
1112
import 'borders.dart';
1213
import 'box.dart';
1314
import 'buttons.dart';
@@ -850,6 +851,9 @@ ListTileThemeData? parseListTileTheme(
850851
parseTextStyle(value["leading_and_trailing_text_style"], theme),
851852
mouseCursor: parseWidgetStateMouseCursor(value["mouse_cursor"]),
852853
minTileHeight: parseDouble(value["min_height"]),
854+
controlAffinity: parseListTileControlAffinity(value["affinity"]),
855+
style: parseListTileStyle(value["style"]),
856+
titleAlignment: parseListTileTitleAlignment(value["title_alignment"]),
853857
);
854858
}
855859

@@ -894,6 +898,10 @@ ExpansionTileThemeData? parseExpansionTileTheme(
894898
tilePadding: parsePadding(value["tile_padding"]),
895899
expandedAlignment: parseAlignment(value["expanded_alignment"]),
896900
childrenPadding: parsePadding(value["controls_padding"]),
901+
shape: parseShape(value["shape"], theme),
902+
collapsedShape: parseShape(value["collapsed_shape"], theme),
903+
expansionAnimationStyle:
904+
parseAnimationStyle(value["expansion_animation_style"]),
897905
);
898906
}
899907

sdk/python/README.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ Package relationships:
44

55
```mermaid
66
graph TD;
7-
flet-core-->flet-runtime;
8-
flet-core-->flet-pyodide;
9-
flet-runtime-->flet-embed;
10-
flet-runtime-->flet;
11-
```
7+
flet --> flet-cli;
8+
flet --> flet-desktop;
9+
flet --> flet-web;
10+
```

sdk/python/examples/apps/controls-gallery/examples/layout/expansiontile/01_expansiontile_example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def example():
2727
title=ft.Text("ExpansionTile 3"),
2828
subtitle=ft.Text("Leading expansion arrow icon"),
2929
affinity=ft.TileAffinity.LEADING,
30-
initially_expanded=True,
30+
expanded=True,
3131
collapsed_text_color=ft.Colors.BLUE,
3232
text_color=ft.Colors.BLUE,
3333
controls=[

sdk/python/examples/controls/expansion_tile/__init__.py

Whitespace-only changes.

sdk/python/examples/controls/expansion_tile/basic.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ def main(page: ft.Page):
66
page.spacing = 0
77
page.padding = 0
88

9-
def handle_expansion_tile_change(e: ft.Event[ft.ExpansionTile]):
9+
def handle_tile_change(e: ft.Event[ft.ExpansionTile]):
1010
page.show_dialog(
1111
ft.SnackBar(
1212
duration=1000,
@@ -25,44 +25,45 @@ def handle_expansion_tile_change(e: ft.Event[ft.ExpansionTile]):
2525

2626
page.add(
2727
ft.ExpansionTile(
28+
expanded=True,
2829
title=ft.Text("ExpansionTile 1"),
2930
subtitle=ft.Text("Trailing expansion arrow icon"),
30-
bgcolor=ft.Colors.BLUE_GREY_200,
31-
collapsed_bgcolor=ft.Colors.BLUE_GREY_200,
3231
affinity=ft.TileAffinity.PLATFORM,
3332
maintain_state=True,
3433
collapsed_text_color=ft.Colors.RED,
3534
text_color=ft.Colors.RED,
3635
controls=[
37-
ft.ListTile(
38-
title=ft.Text("This is sub-tile number 1"),
39-
bgcolor=ft.Colors.BLUE_GREY_200,
40-
)
36+
ft.ListTile(title=ft.Text("This is sub-tile number 1.1")),
37+
ft.ListTile(title=ft.Text("This is sub-tile number 1.2")),
4138
],
4239
),
4340
ft.ExpansionTile(
41+
expanded=True,
4442
title=ft.Text("ExpansionTile 2"),
4543
subtitle=ft.Text("Custom expansion arrow icon"),
4644
trailing=ft.Icon(ft.Icons.ARROW_DROP_DOWN),
4745
collapsed_text_color=ft.Colors.GREEN,
4846
text_color=ft.Colors.GREEN,
49-
on_change=handle_expansion_tile_change,
50-
controls=[ft.ListTile(title=ft.Text("This is sub-tile number 2"))],
47+
on_change=handle_tile_change,
48+
controls=[
49+
ft.ListTile(title=ft.Text("This is sub-tile number 2.1")),
50+
ft.ListTile(title=ft.Text("This is sub-tile number 2.2")),
51+
],
5152
),
5253
ft.ExpansionTile(
54+
expanded=True,
5355
title=ft.Text("ExpansionTile 3"),
5456
subtitle=ft.Text("Leading expansion arrow icon"),
5557
affinity=ft.TileAffinity.LEADING,
56-
initially_expanded=True,
5758
collapsed_text_color=ft.Colors.BLUE_800,
5859
text_color=ft.Colors.BLUE_200,
5960
controls=[
60-
ft.ListTile(title=ft.Text("This is sub-tile number 3")),
61-
ft.ListTile(title=ft.Text("This is sub-tile number 4")),
62-
ft.ListTile(title=ft.Text("This is sub-tile number 5")),
61+
ft.ListTile(title=ft.Text("This is sub-tile number 3.1")),
62+
ft.ListTile(title=ft.Text("This is sub-tile number 3.2")),
6363
],
6464
),
6565
)
6666

6767

68-
ft.run(main)
68+
if __name__ == "__main__":
69+
ft.run(main)

sdk/python/examples/controls/expansion_tile/borders.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,5 @@ def main(page: ft.Page):
4141
)
4242

4343

44-
ft.run(main)
44+
if __name__ == "__main__":
45+
ft.run(main)
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import flet as ft
2+
3+
4+
def main(page: ft.Page):
5+
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
6+
page.spacing = 20
7+
8+
def switch_animation(e: ft.Event[ft.CupertinoSlidingSegmentedButton]):
9+
if e.control.selected_index == 0:
10+
tile.animation_style = None
11+
elif e.control.selected_index == 1:
12+
tile.animation_style = ft.AnimationStyle(
13+
curve=ft.AnimationCurve.BOUNCE_OUT,
14+
duration=ft.Duration(seconds=5),
15+
)
16+
else:
17+
tile.animation_style = ft.AnimationStyle.no_animation()
18+
19+
page.add(
20+
ft.CupertinoSlidingSegmentedButton(
21+
selected_index=0,
22+
thumb_color=ft.Colors.BLUE_400,
23+
on_change=switch_animation,
24+
controls=[
25+
ft.Text("Default animation"),
26+
ft.Text("Custom animation"),
27+
ft.Text("No animation"),
28+
],
29+
),
30+
tile := ft.ExpansionTile(
31+
expanded=True,
32+
title=ft.Text(
33+
"Expand/Collapse me while being attentive to the animations!"
34+
),
35+
controls=[
36+
ft.ListTile(title=ft.Text("Sub-item 1")),
37+
ft.ListTile(title=ft.Text("Sub-item 2")),
38+
ft.ListTile(title=ft.Text("Sub-item 3")),
39+
],
40+
),
41+
)
42+
43+
44+
if __name__ == "__main__":
45+
ft.run(main)
-67.6 KB
Binary file not shown.

0 commit comments

Comments
 (0)