Skip to content

Commit 725f1af

Browse files
Shimmer control (#5828)
* Add Shimmer control for skeleton loaders Introduced a new Shimmer control in both Dart and Python SDKs for building skeleton loaders and animated placeholders. Added implementation, documentation, and usage examples, as well as updated relevant imports, exports, and documentation navigation. The shimmer package was added as a dependency in the Dart project. * Update pick_and_upload.py * Refactor haptic and shake detector usage; add services to BasePage Refactored haptic feedback and shake detector examples to simplify control instantiation and management. Introduced a 'services' list to BasePage for managing service controls, improving code organization and extensibility. * Update shimmer placeholder and gradient examples Refined visual styles in basic_placeholder.py by adjusting colors, adding icon content, and improving opacity usage. Simplified and standardized block sizing and gradient stops in custom_gradient.py for a more consistent appearance. * Add basic Shimmer example and update docs Introduced a basic Shimmer control example with corresponding integration test and golden image. Updated the documentation to include the new example and adjusted the code sample in the Shimmer control docstring for consistency. * Set default value for 'container' in SemanticsControl Updated the 'container' property in SemanticsControl to default to false if not specified. This prevents potential null errors and ensures consistent behavior. * Fix gradient or colors error message. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fix for loop error message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update run instructions in file picker example Removed 'uv run' from the example run command in pick_and_upload.py to simplify the instructions for running the file picker example. * Enhance Shimmer tests and docs with images and options Added new golden images and updated integration tests for Shimmer controls, including tests for basic placeholder and custom gradient examples. Updated documentation to include example images. Introduced 'skip_pump_and_settle' option to FletTestApp and test configuration for more flexible test setup. Fixed image output handling in test utilities. * Update shimmer docs and tests to use GIF animation Replaces static PNG images with animated GIFs for shimmer documentation and golden test outputs. Updates documentation references and image generation logic in flet_test_app.py to produce GIFs from PNG frames, improving visual representation of shimmer effects. * Skip flaky shimmer tests on CI Added @pytest.mark.skip to shimmer integration tests due to flakiness on CI environments. This prevents unreliable test failures while investigating the root cause. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 4195a66 commit 725f1af

34 files changed

+539
-9
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
* Controls in Python are now defined as plain dataclasses
1414
* Unified diffing algorithm supports both imperative and declarative styles
1515
* Refactored Flutter layer using inherited widgets and `Provider`
16+
* Added a Shimmer control for building skeleton loaders and animated placeholders.
1617
* Added `FletApp.appErrorMessage` template to customize loading screen errors.
1718
* See the list of [breaking changes](https://github.com/flet-dev/flet/issues/5238)
1819

client/pubspec.lock

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,14 @@ packages:
12061206
url: "https://pub.dev"
12071207
source: hosted
12081208
version: "2.4.1"
1209+
shimmer:
1210+
dependency: transitive
1211+
description:
1212+
name: shimmer
1213+
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
1214+
url: "https://pub.dev"
1215+
source: hosted
1216+
version: "3.0.0"
12091217
sky_engine:
12101218
dependency: transitive
12111219
description: flutter

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class SemanticsControl extends StatelessWidget {
3131
hint: control.getString("hint"),
3232
onTapHint: control.getString("on_tap_hint"),
3333
onLongPressHint: control.getString("on_long_press_hint"),
34-
container: control.getBool("container")!,
34+
container: control.getBool("container", false)!,
3535
liveRegion: control.getBool("live_region"),
3636
obscured: control.getBool("obscured"),
3737
multiline: control.getBool("multiline"),
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:shimmer/shimmer.dart';
4+
5+
import '../extensions/control.dart';
6+
import '../models/control.dart';
7+
import '../utils/colors.dart';
8+
import '../utils/gradient.dart';
9+
import '../utils/numbers.dart';
10+
import '../utils/time.dart';
11+
import '../widgets/error.dart';
12+
import 'base_controls.dart';
13+
14+
class ShimmerControl extends StatelessWidget {
15+
final Control control;
16+
17+
const ShimmerControl({super.key, required this.control});
18+
19+
@override
20+
Widget build(BuildContext context) {
21+
debugPrint("Shimmer build: ${control.id}");
22+
23+
final content = control.buildWidget("content");
24+
if (content == null) {
25+
return const ErrorControl("Shimmer.content must be specified");
26+
}
27+
28+
final gradient = control.getGradient("gradient", Theme.of(context));
29+
final baseColor = control.getColor("base_color", context);
30+
final highlightColor = control.getColor("highlight_color", context);
31+
32+
if (gradient == null && (baseColor == null || highlightColor == null)) {
33+
return const ErrorControl(
34+
"Shimmer requires either gradient or base/highlight colors");
35+
}
36+
37+
final direction = _parseDirection(control.getString("direction"));
38+
final period =
39+
control.getDuration("period", const Duration(milliseconds: 1500))!;
40+
final loop = control.getInt("loop", 0)!;
41+
42+
final shimmerWidget = gradient != null
43+
? Shimmer(
44+
gradient: gradient,
45+
direction: direction,
46+
period: period,
47+
loop: loop,
48+
enabled: !control.disabled,
49+
child: content,
50+
)
51+
: Shimmer.fromColors(
52+
baseColor: baseColor!,
53+
highlightColor: highlightColor!,
54+
direction: direction,
55+
period: period,
56+
loop: loop,
57+
enabled: !control.disabled,
58+
child: content,
59+
);
60+
61+
return LayoutControl(control: control, child: shimmerWidget);
62+
}
63+
}
64+
65+
ShimmerDirection _parseDirection(String? value,
66+
[ShimmerDirection defaultValue = ShimmerDirection.ltr]) {
67+
if (value == null) {
68+
return defaultValue;
69+
}
70+
return ShimmerDirection.values.firstWhereOrNull(
71+
(dir) => dir.name.toLowerCase() == value.toLowerCase()) ??
72+
defaultValue;
73+
}

packages/flet/lib/src/flet_core_extension.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ import 'controls/segmented_button.dart';
9292
import 'controls/selection_area.dart';
9393
import 'controls/semantics.dart';
9494
import 'controls/shader_mask.dart';
95+
import 'controls/shimmer.dart';
9596
import 'controls/snack_bar.dart';
9697
import 'controls/stack.dart';
9798
import 'controls/submenu_button.dart';
@@ -325,6 +326,8 @@ class FletCoreExtension extends FletExtension {
325326
return SemanticsControl(key: key, control: control);
326327
case "ShaderMask":
327328
return ShaderMaskControl(key: key, control: control);
329+
case "Shimmer":
330+
return ShimmerControl(key: key, control: control);
328331
case "Slider":
329332
return AdaptiveSliderControl(key: key, control: control);
330333
case "SnackBar":

packages/flet/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ dependencies:
3838
screenshot: ^3.0.0
3939
sensors_plus: ^6.1.1
4040
shared_preferences: 2.5.3
41+
shimmer: ^3.0.0
4142
url_launcher: 6.3.2
4243
vector_math: ^2.2.0
4344
web: ^1.1.1

sdk/python/examples/controls/file_picker/pick_and_upload.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,21 @@ class State:
2020

2121

2222
def main(page: ft.Page):
23+
if not page.web:
24+
page.add(
25+
ft.Text(
26+
"This example is only available in Flet Web mode.\n"
27+
"\n"
28+
"Run this example with:\n"
29+
" export FLET_SECRET_KEY=<some_secret_key>\n"
30+
" flet run --web "
31+
"examples/controls/file_picker/pick_and_upload.py",
32+
color=ft.Colors.RED,
33+
selectable=True,
34+
)
35+
)
36+
return
37+
2338
prog_bars: dict[str, ft.ProgressRing] = {}
2439

2540
def on_upload_progress(e: ft.FilePickerUploadEvent):

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33

44
def main(page: ft.Page):
5-
page.overlay.append(hf := ft.HapticFeedback())
5+
hf = ft.HapticFeedback()
66

77
async def heavy_impact():
88
await hf.heavy_impact()

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33

44
def main(page: ft.Page):
5-
# just need hold a reference to ShakeDetector in the session store
6-
page.session.store.set(
7-
"shake_detector",
5+
page.services.append(
86
ft.ShakeDetector(
97
minimum_shake_count=2,
108
shake_slop_time_ms=300,
119
shake_count_reset_time_ms=1000,
1210
on_shake=lambda _: page.add(ft.Text("Shake detected!")),
13-
),
11+
)
1412
)
1513

1614
page.add(ft.Text("Shake your device!"))

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

Whitespace-only changes.

0 commit comments

Comments
 (0)