Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@ class CupertinoSlidingSegmentedButtonControl extends StatelessWidget {
debugPrint("CupertinoSlidingSegmentedButtonControl build: ${control.id}");

var controls = control.buildWidgets("controls");

if (controls.length < 2) {
return const ErrorControl(
"CupertinoSlidingSegmentedButton must have at minimum two visible controls");
}

var button = CupertinoSlidingSegmentedControl(
groupValue: control.getInt("selected_index"),
groupValue: control.getInt("selected_index", 0)!,
proportionalWidth: control.getBool("proportional_width", false)!,
backgroundColor: control.getColor(
"bgcolor", context, CupertinoColors.tertiarySystemFill)!,
Expand All @@ -36,15 +35,13 @@ class CupertinoSlidingSegmentedButtonControl extends StatelessWidget {
"thumb_color",
context,
const CupertinoDynamicColor.withBrightness(
color: Color(0xFFFFFFFF),
darkColor: Color(0xFF636366),
))!,
color: Color(0xFFFFFFFF), darkColor: Color(0xFF636366)))!,
children: controls.asMap().map((i, c) => MapEntry(i, c)),
onValueChanged: (int? index) {
if (!control.disabled) {
control
.updateProperties({"selected_index": index ?? 0}, notify: true);
control.triggerEvent("change", index ?? 0);
index = index ?? 0;
control.updateProperties({"selected_index": index}, notify: true);
control.triggerEvent("change", index);
}
},
);
Expand Down
155 changes: 87 additions & 68 deletions packages/flet/lib/src/controls/expansion_tile.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:flet/src/utils/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';

import '../extensions/control.dart';
import '../models/control.dart';
Expand All @@ -11,97 +13,114 @@ import '../utils/numbers.dart';
import '../utils/theme.dart';
import '../widgets/error.dart';
import 'base_controls.dart';
import 'control_widget.dart';

class ExpansionTileControl extends StatelessWidget {
class ExpansionTileControl extends StatefulWidget {
final Control control;

const ExpansionTileControl({
super.key,
required this.control,
});
const ExpansionTileControl({super.key, required this.control});

@override
Widget build(BuildContext context) {
debugPrint("ExpansionTile build: ${control.id}");
State<ExpansionTileControl> createState() => _ExpansionTileControlState();
}

class _ExpansionTileControlState extends State<ExpansionTileControl> {
late final ExpansibleController _controller;
bool _expanded = false;

@override
void initState() {
super.initState();
_controller = ExpansibleController();
_expanded = widget.control.getBool("expanded", false)!;
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}

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

var leading = control.buildIconOrWidget("leading");
var title = control.buildTextOrWidget("title");
var subtitle = control.buildTextOrWidget("subtitle");
var trailing = control.buildIconOrWidget("trailing");
if (expanded) {
_controller.expand();
} else {
_controller.collapse();
}
});
}

@override
Widget build(BuildContext context) {
debugPrint("ExpansionTile build: ${widget.control.id}");

final title = widget.control.buildTextOrWidget("title");
if (title == null) {
return const ErrorControl(
"ExpansionTile.title must be provided and visible");
}

bool maintainState = control.getBool("maintain_state", false)!;
bool initiallyExpanded = control.getBool("initially_expanded", false)!;

var iconColor = control.getColor("icon_color", context);
var textColor = control.getColor("text_color", context);
var bgColor = control.getColor("bgcolor", context);
var collapsedBgColor = control.getColor("collapsed_bgcolor", context);
var collapsedIconColor = control.getColor("collapsed_icon_color", context);
var collapsedTextColor = control.getColor("collapsed_text_color", context);

var affinity = control.getListTileControlAffinity(
"affinity", ListTileControlAffinity.platform)!;
var clipBehavior = parseClip(control.getString("clip_behavior"));
var expanded = widget.control.getBool("expanded", false)!;
if (_expanded != expanded) {
_expanded = expanded;
_scheduleControllerUpdate(expanded);
}

var expandedCrossAxisAlignment = control.getCrossAxisAlignment(
var expandedCrossAxisAlignment = widget.control.getCrossAxisAlignment(
"expanded_cross_axis_alignment", CrossAxisAlignment.center)!;

if (expandedCrossAxisAlignment == CrossAxisAlignment.baseline) {
return const ErrorControl(
'CrossAxisAlignment.baseline is not supported since the expanded '
'CrossAxisAlignment.BASELINE is not supported since the expanded '
'controls are aligned in a column, not a row. '
'Try aligning the controls differently.');
}

Function(bool)? onChange = !control.disabled
? (expanded) {
control.triggerEvent("change", expanded);
}
: null;

Widget tile = ExpansionTile(
controlAffinity: affinity,
childrenPadding: control.getPadding("controls_padding"),
tilePadding: control.getEdgeInsets("tile_padding"),
expandedAlignment: control.getAlignment("expanded_alignment"),
expandedCrossAxisAlignment:
control.getCrossAxisAlignment("expanded_cross_axis_alignment"),
backgroundColor: bgColor,
iconColor: iconColor,
textColor: textColor,
collapsedBackgroundColor: collapsedBgColor,
collapsedIconColor: collapsedIconColor,
collapsedTextColor: collapsedTextColor,
maintainState: maintainState,
initiallyExpanded: initiallyExpanded,
clipBehavior: clipBehavior,
shape: control.getShape("shape", Theme.of(context)),
collapsedShape: control.getShape("collapsed_shape", Theme.of(context)),
onExpansionChanged: onChange,
visualDensity: control.getVisualDensity("visual_density"),
enableFeedback: control.getBool("enable_feedback"),
showTrailingIcon: control.getBool("show_trailing_icon", true)!,
enabled: !control.disabled,
minTileHeight: control.getDouble("min_tile_height"),
dense: control.getBool("dense"),
leading: leading,
final tile = ExpansionTile(
controller: _controller,
controlAffinity: widget.control.getListTileControlAffinity("affinity"),
childrenPadding: widget.control.getPadding("controls_padding"),
tilePadding: widget.control.getEdgeInsets("tile_padding"),
expandedAlignment: widget.control.getAlignment("expanded_alignment"),
expandedCrossAxisAlignment: expandedCrossAxisAlignment,
backgroundColor: widget.control.getColor("bgcolor", context),
iconColor: widget.control.getColor("icon_color", context),
textColor: widget.control.getColor("text_color", context),
collapsedBackgroundColor:
widget.control.getColor("collapsed_bgcolor", context),
collapsedIconColor:
widget.control.getColor("collapsed_icon_color", context),
collapsedTextColor:
widget.control.getColor("collapsed_text_color", context),
maintainState: widget.control.getBool("maintain_state", false)!,
initiallyExpanded: expanded,
clipBehavior: widget.control.getClipBehavior("clip_behavior"),
shape: widget.control.getShape("shape", Theme.of(context)),
collapsedShape:
widget.control.getShape("collapsed_shape", Theme.of(context)),
onExpansionChanged: (bool expanded) {
_expanded = expanded;
widget.control.updateProperties({"expanded": expanded});
widget.control.triggerEvent("change", expanded);
},
visualDensity: widget.control.getVisualDensity("visual_density"),
enableFeedback: widget.control.getBool("enable_feedback"),
showTrailingIcon: widget.control.getBool("show_trailing_icon", true)!,
enabled: !widget.control.disabled,
minTileHeight: widget.control.getDouble("min_tile_height"),
dense: widget.control.getBool("dense"),
expansionAnimationStyle:
widget.control.getAnimationStyle("animation_style"),
leading: widget.control.buildIconOrWidget("leading"),
title: title,
subtitle: subtitle,
trailing: trailing,
children: controls,
subtitle: widget.control.buildTextOrWidget("subtitle"),
trailing: widget.control.buildIconOrWidget("trailing"),
children: widget.control.buildWidgets("controls"),
);

return LayoutControl(control: control, child: tile);
return LayoutControl(control: widget.control, child: tile);
}
}
8 changes: 8 additions & 0 deletions packages/flet/lib/src/utils/theme.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../flet_backend.dart';
import '../models/control.dart';
import '../utils/transforms.dart';
import 'alignment.dart';
import 'animations.dart';
import 'borders.dart';
import 'box.dart';
import 'buttons.dart';
Expand Down Expand Up @@ -850,6 +851,9 @@ ListTileThemeData? parseListTileTheme(
parseTextStyle(value["leading_and_trailing_text_style"], theme),
mouseCursor: parseWidgetStateMouseCursor(value["mouse_cursor"]),
minTileHeight: parseDouble(value["min_height"]),
controlAffinity: parseListTileControlAffinity(value["affinity"]),
style: parseListTileStyle(value["style"]),
titleAlignment: parseListTileTitleAlignment(value["title_alignment"]),
);
}

Expand Down Expand Up @@ -894,6 +898,10 @@ ExpansionTileThemeData? parseExpansionTileTheme(
tilePadding: parsePadding(value["tile_padding"]),
expandedAlignment: parseAlignment(value["expanded_alignment"]),
childrenPadding: parsePadding(value["controls_padding"]),
shape: parseShape(value["shape"], theme),
collapsedShape: parseShape(value["collapsed_shape"], theme),
expansionAnimationStyle:
parseAnimationStyle(value["expansion_animation_style"]),
);
}

Expand Down
9 changes: 4 additions & 5 deletions sdk/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ Package relationships:

```mermaid
graph TD;
flet-core-->flet-runtime;
flet-core-->flet-pyodide;
flet-runtime-->flet-embed;
flet-runtime-->flet;
```
flet --> flet-cli;
flet --> flet-desktop;
flet --> flet-web;
```
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def example():
title=ft.Text("ExpansionTile 3"),
subtitle=ft.Text("Leading expansion arrow icon"),
affinity=ft.TileAffinity.LEADING,
initially_expanded=True,
expanded=True,
collapsed_text_color=ft.Colors.BLUE,
text_color=ft.Colors.BLUE,
controls=[
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/examples/controls/expansion_tile/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def handle_expansion_tile_change(e: ft.Event[ft.ExpansionTile]):
title=ft.Text("ExpansionTile 3"),
subtitle=ft.Text("Leading expansion arrow icon"),
affinity=ft.TileAffinity.LEADING,
initially_expanded=True,
expanded=True,
collapsed_text_color=ft.Colors.BLUE_800,
text_color=ft.Colors.BLUE_200,
controls=[
Expand Down
44 changes: 44 additions & 0 deletions sdk/python/examples/controls/expansion_tile/custom_animations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import flet as ft


def main(page: ft.Page):
page.horizontal_alignment = ft.CrossAxisAlignment.CENTER
page.spacing = 20

def switch_animation(e: ft.Event[ft.CupertinoSlidingSegmentedButton]):
if e.control.selected_index == 0:
tile.animation_style = None
elif e.control.selected_index == 1:
tile.animation_style = ft.AnimationStyle(
curve=ft.AnimationCurve.BOUNCE_OUT,
duration=ft.Duration(seconds=5),
)
else:
tile.animation_style = ft.AnimationStyle.no_animation()

page.add(
ft.CupertinoSlidingSegmentedButton(
selected_index=0,
thumb_color=ft.Colors.BLUE_400,
on_change=switch_animation,
controls=[
ft.Text("Default animation"),
ft.Text("Custom animation"),
ft.Text("No animation"),
],
),
tile := ft.ExpansionTile(
expanded=True,
title=ft.Text(
"Expand/Collapse me while being attentive to the animations!"
),
controls=[
ft.ListTile(title=ft.Text("Sub-item 1")),
ft.ListTile(title=ft.Text("Sub-item 2")),
ft.ListTile(title=ft.Text("Sub-item 3")),
],
),
)


ft.run(main)
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import flet as ft


def main(page: ft.Page):
page.spacing = 20

def expand_tile(e: ft.Event[ft.FilledButton]):
tile.expanded = True

def collapse_tile(e: ft.Event[ft.OutlinedButton]):
tile.expanded = False

page.add(
ft.Row(
alignment=ft.MainAxisAlignment.CENTER,
controls=[
ft.FilledButton("Expand Tile", on_click=expand_tile),
ft.OutlinedButton("Collapse Tile", on_click=collapse_tile),
],
),
tile := ft.ExpansionTile(
title=ft.Text("I am the title of this tile.", weight=ft.FontWeight.BOLD),
subtitle=ft.Text("This is the subtitle."),
affinity=ft.TileAffinity.LEADING,
controls=[ft.Text("👻", size=80)],
expanded=True,
on_change=lambda e: print(
f"Tile was {'expanded' if e.data else 'collapsed'}"
),
),
)


ft.run(main)
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def handle_expansion_tile_change(e: ft.Event[ft.ExpansionTile]):
title=ft.Text("ExpansionTile 3"),
subtitle=ft.Text("Leading expansion arrow icon"),
affinity=ft.TileAffinity.LEADING,
initially_expanded=True,
expanded=True,
collapsed_text_color=ft.Colors.BLUE_800,
text_color=ft.Colors.BLUE_200,
controls=[
Expand Down
11 changes: 11 additions & 0 deletions sdk/python/packages/flet/docs/controls/expansiontile.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ example_media: ../examples/controls/expansion_tile/media

{{ image(example_media + "/basic.png", alt="basic", width="80%") }}

## Programmatic expansion/collapse

```python
--8<-- "{{ examples }}/programmatic_expansion.py"
```

## Custom animations

```python
--8<-- "{{ examples }}/custom_animations.py"
```

### Theme mode toggle

Expand Down
Loading
Loading