Skip to content

Commit 6b4e27f

Browse files
Added Container.blur and Container.shadow properties (#1076)
* Container.blur initial check-in [skip ci] * Added `blur` property to `Container` Fix #1062 * Grey color added * Added `Container.shadow` See https://blog.logrocket.com/enhance-mobile-apps-with-flutter-boxshadow/ for examples Close #348 * Fix tests * Fix blur types
1 parent 34fb1d9 commit 6b4e27f

File tree

7 files changed

+210
-36
lines changed

7 files changed

+210
-36
lines changed

package/lib/src/controls/container.dart

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:convert';
22
import 'dart:typed_data';
33

44
import 'package:collection/collection.dart';
5+
import 'package:flet/src/utils/shadows.dart';
56
import 'package:flutter/material.dart';
67
import 'package:flutter_redux/flutter_redux.dart';
78

@@ -64,6 +65,7 @@ class ContainerControl extends StatelessWidget {
6465
: null;
6566

6667
var animation = parseAnimation(control, "animate");
68+
var blur = parseBlur(control, "blur");
6769

6870
final server = FletAppServices.of(context).server;
6971

@@ -107,15 +109,20 @@ class ContainerControl extends StatelessWidget {
107109
control.attrString("shape", "")!.toLowerCase(),
108110
orElse: () => BoxShape.rectangle);
109111

112+
var borderRadius = parseBorderRadius(control, "borderRadius");
113+
110114
var boxDecor = BoxDecoration(
111115
color: bgColor,
112116
gradient: gradient,
113117
image: image,
114118
backgroundBlendMode:
115119
bgColor != null || gradient != null ? blendMode : null,
116120
border: parseBorder(Theme.of(context), control, "border"),
117-
borderRadius: parseBorderRadius(control, "borderRadius"),
118-
shape: shape);
121+
borderRadius: borderRadius,
122+
shape: shape,
123+
boxShadow: parseBoxShadow(Theme.of(context), control, "shadow"));
124+
125+
Widget? result;
119126

120127
if ((onClick || onLongPress || onHover) && ink && !disabled) {
121128
var ink = Ink(
@@ -165,36 +172,33 @@ class ContainerControl extends StatelessWidget {
165172
child: child,
166173
),
167174
));
168-
return constrainedControl(
169-
context,
170-
animation == null
171-
? Container(
172-
width: control.attrDouble("width"),
173-
height: control.attrDouble("height"),
174-
margin: parseEdgeInsets(control, "margin"),
175-
clipBehavior: clipBehavior,
176-
child: ink,
177-
)
178-
: AnimatedContainer(
179-
duration: animation.duration,
180-
curve: animation.curve,
181-
width: control.attrDouble("width"),
182-
height: control.attrDouble("height"),
183-
margin: parseEdgeInsets(control, "margin"),
184-
clipBehavior: clipBehavior,
185-
onEnd: control.attrBool("onAnimationEnd", false)!
186-
? () {
187-
server.sendPageEvent(
188-
eventTarget: control.id,
189-
eventName: "animation_end",
190-
eventData: "container");
191-
}
192-
: null,
193-
child: ink),
194-
parent,
195-
control);
175+
176+
result = animation == null
177+
? Container(
178+
width: control.attrDouble("width"),
179+
height: control.attrDouble("height"),
180+
margin: parseEdgeInsets(control, "margin"),
181+
clipBehavior: clipBehavior,
182+
child: ink,
183+
)
184+
: AnimatedContainer(
185+
duration: animation.duration,
186+
curve: animation.curve,
187+
width: control.attrDouble("width"),
188+
height: control.attrDouble("height"),
189+
margin: parseEdgeInsets(control, "margin"),
190+
clipBehavior: clipBehavior,
191+
onEnd: control.attrBool("onAnimationEnd", false)!
192+
? () {
193+
server.sendPageEvent(
194+
eventTarget: control.id,
195+
eventName: "animation_end",
196+
eventData: "container");
197+
}
198+
: null,
199+
child: ink);
196200
} else {
197-
Widget container = animation == null
201+
result = animation == null
198202
? Container(
199203
width: control.attrDouble("width"),
200204
height: control.attrDouble("height"),
@@ -225,7 +229,7 @@ class ContainerControl extends StatelessWidget {
225229
child: child);
226230

227231
if ((onClick || onLongPress || onHover) && !disabled) {
228-
container = MouseRegion(
232+
result = MouseRegion(
229233
cursor: SystemMouseCursors.click,
230234
onEnter: onHover
231235
? (value) {
@@ -271,12 +275,21 @@ class ContainerControl extends StatelessWidget {
271275
eventData: "");
272276
}
273277
: null,
274-
child: container,
278+
child: result,
275279
),
276280
);
277281
}
278-
return constrainedControl(context, container, parent, control);
279282
}
283+
284+
if (blur != null) {
285+
result = borderRadius != null
286+
? ClipRRect(
287+
borderRadius: borderRadius,
288+
child: BackdropFilter(filter: blur, child: result))
289+
: ClipRect(child: BackdropFilter(filter: blur, child: result));
290+
}
291+
292+
return constrainedControl(context, result, parent, control);
280293
});
281294
}
282295
}

package/lib/src/utils/colors.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ Map<String, MaterialColor> _materialColors = {
100100
"deeporange": Colors.deepOrange,
101101
"brown": Colors.brown,
102102
"bluegrey": Colors.blueGrey,
103+
"grey": Colors.grey
103104
};
104105

105106
Map<String, MaterialAccentColor> _materialAccentColors = {

package/lib/src/utils/images.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
import 'dart:convert';
2+
import 'dart:ui';
3+
14
import 'package:collection/collection.dart';
5+
import 'package:flet/src/utils/numbers.dart';
26
import 'package:flutter/material.dart';
37

48
import '../models/control.dart';
9+
import 'gradient.dart';
510

611
export 'images_io.dart' if (dart.library.js) "images_web.dart";
712

@@ -17,3 +22,31 @@ BoxFit? parseBoxFit(Control control, String propName) {
1722
return BoxFit.values.firstWhereOrNull((e) =>
1823
e.name.toLowerCase() == control.attrString(propName, "")!.toLowerCase());
1924
}
25+
26+
ImageFilter? parseBlur(Control control, String propName) {
27+
var v = control.attrString(propName, null);
28+
if (v == null) {
29+
return null;
30+
}
31+
32+
final j1 = json.decode(v);
33+
return blurImageFilterFromJSON(j1);
34+
}
35+
36+
ImageFilter blurImageFilterFromJSON(dynamic json) {
37+
double sigmaX = 0.0;
38+
double sigmaY = 0.0;
39+
TileMode tileMode = TileMode.clamp;
40+
if (json is int || json is double) {
41+
sigmaX = sigmaY = parseDouble(json);
42+
} else if (json is List && json.length > 1) {
43+
sigmaX = parseDouble(json[0]);
44+
sigmaY = parseDouble(json[1]);
45+
} else {
46+
sigmaX = parseDouble(json["sigma_x"]);
47+
sigmaY = parseDouble(json["sigma_y"]);
48+
tileMode = parseTileMode(json["tile_mode"]);
49+
}
50+
51+
return ImageFilter.blur(sigmaX: sigmaX, sigmaY: sigmaY, tileMode: tileMode);
52+
}

package/lib/src/utils/shadows.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'dart:convert';
2+
3+
import 'package:flutter/material.dart';
4+
5+
import '../models/control.dart';
6+
import '../utils/numbers.dart';
7+
import '../utils/transforms.dart';
8+
import 'colors.dart';
9+
10+
List<BoxShadow> parseBoxShadow(
11+
ThemeData theme, Control control, String propName) {
12+
var v = control.attrString(propName, null);
13+
if (v == null) {
14+
return [];
15+
}
16+
17+
final j1 = json.decode(v);
18+
return boxShadowsFromJSON(theme, j1);
19+
}
20+
21+
List<BoxShadow> boxShadowsFromJSON(ThemeData theme, dynamic json) {
22+
if (json is List) {
23+
return json.map((e) => boxShadowFromJSON(theme, e)).toList();
24+
} else {
25+
return [boxShadowFromJSON(theme, json)];
26+
}
27+
}
28+
29+
BoxShadow boxShadowFromJSON(ThemeData theme, dynamic json) {
30+
var offset = json["offset"] != null ? offsetFromJSON(json["offset"]) : null;
31+
return BoxShadow(
32+
color: json["color"] != null
33+
? HexColor.fromString(theme, json["color"]) ?? const Color(0xFF000000)
34+
: const Color(0xFF000000),
35+
offset: offset != null ? Offset(offset.x, offset.y) : Offset.zero,
36+
blurStyle: json["blur_style"] != null
37+
? BlurStyle.values
38+
.firstWhere((e) => e.name.toLowerCase() == json["blur_style"])
39+
: BlurStyle.normal,
40+
blurRadius:
41+
json["blur_radius"] != null ? parseDouble(json["blur_radius"]) : 0.0,
42+
spreadRadius: json["spread_radius"] != null
43+
? parseDouble(json["spread_radius"])
44+
: 0.0);
45+
}

sdk/python/packages/flet-core/src/flet_core/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,14 @@
3535
from flet_core.checkbox import Checkbox
3636
from flet_core.circle_avatar import CircleAvatar
3737
from flet_core.column import Column
38-
from flet_core.container import Container, ContainerTapEvent
38+
from flet_core.container import (
39+
Blur,
40+
BlurTileMode,
41+
BoxShadow,
42+
Container,
43+
ContainerTapEvent,
44+
ShadowBlurStyle,
45+
)
3946
from flet_core.control import Control, OptionalNumber
4047
from flet_core.control_event import ControlEvent
4148
from flet_core.datatable import (

sdk/python/packages/flet-core/src/flet_core/colors.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,14 @@
356356
DEEP_ORANGE_ACCENT_200 = "deeporangeaccent200"
357357
DEEP_ORANGE_ACCENT_400 = "deeporangeaccent400"
358358
DEEP_ORANGE_ACCENT_700 = "deeporangeaccent700"
359+
GREY = "grey"
360+
GREY_50 = "grey50"
361+
GREY_100 = "grey100"
362+
GREY_200 = "grey200"
363+
GREY_300 = "grey300"
364+
GREY_400 = "grey400"
365+
GREY_500 = "grey500"
366+
GREY_600 = "grey600"
367+
GREY_700 = "grey700"
368+
GREY_800 = "grey800"
369+
GREY_900 = "grey900"

sdk/python/packages/flet-core/src/flet_core/container.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
import dataclasses
12
import json
2-
from typing import Any, Optional, Union
3+
from dataclasses import field
4+
from enum import Enum
5+
from typing import Any, List, Optional, Tuple, Union
36

47
from flet_core.alignment import Alignment
58
from flet_core.border import Border
@@ -35,6 +38,36 @@
3538
from typing_extensions import Literal
3639

3740

41+
class BlurTileMode(Enum):
42+
CLAMP = "clamp"
43+
DECAL = "decal"
44+
MIRROR = "mirror"
45+
REPEATED = "repeated"
46+
47+
48+
class ShadowBlurStyle(Enum):
49+
NORMAL = "normal"
50+
SOLID = "solid"
51+
OUTER = "outer"
52+
INNER = "inner"
53+
54+
55+
@dataclasses.dataclass
56+
class Blur:
57+
sigma_x: float
58+
sigma_y: float
59+
tile_mode: BlurTileMode = field(default=BlurTileMode.CLAMP)
60+
61+
62+
@dataclasses.dataclass
63+
class BoxShadow:
64+
spread_radius: Optional[float] = field(default=None)
65+
blur_radius: Optional[float] = field(default=None)
66+
color: Optional[str] = field(default=None)
67+
offset: OffsetValue = field(default=None)
68+
tile_mode: ShadowBlurStyle = field(default=ShadowBlurStyle.NORMAL)
69+
70+
3871
class Container(ConstrainedControl):
3972
"""
4073
Container allows to decorate a control with background color and border and position it with padding, margin and alignment.
@@ -110,6 +143,10 @@ def __init__(
110143
clip_behavior: Optional[ClipBehavior] = None,
111144
ink: Optional[bool] = None,
112145
animate: AnimationValue = None,
146+
blur: Union[
147+
None, float, int, Tuple[Union[float, int], Union[float, int]], Blur
148+
] = None,
149+
shadow: Union[None, BoxShadow, List[BoxShadow]] = None,
113150
on_click=None,
114151
on_long_press=None,
115152
on_hover=None,
@@ -168,6 +205,8 @@ def convert_container_tap_event_data(e):
168205
self.clip_behavior = clip_behavior
169206
self.ink = ink
170207
self.animate = animate
208+
self.blur = blur
209+
self.shadow = shadow
171210
self.on_click = on_click
172211
self.on_long_press = on_long_press
173212
self.on_hover = on_hover
@@ -184,6 +223,8 @@ def _before_build_command(self):
184223
self._set_attr_json("alignment", self.__alignment)
185224
self._set_attr_json("gradient", self.__gradient)
186225
self._set_attr_json("animate", self.__animate)
226+
self._set_attr_json("blur", self.__blur)
227+
self._set_attr_json("shadow", self.__shadow if self.__shadow else None)
187228

188229
def _get_children(self):
189230
children = []
@@ -258,6 +299,29 @@ def blend_mode(self, value: BlendMode):
258299
def __set_blend_mode(self, value: BlendModeString):
259300
self._set_attr("blendMode", value)
260301

302+
# blur
303+
@property
304+
def blur(self):
305+
return self.__blur
306+
307+
@blur.setter
308+
def blur(
309+
self,
310+
value: Union[
311+
None, float, int, Tuple[Union[float, int], Union[float, int]], Blur
312+
],
313+
):
314+
self.__blur = value
315+
316+
# shadow
317+
@property
318+
def shadow(self):
319+
return self.__shadow
320+
321+
@shadow.setter
322+
def shadow(self, value: Union[None, BoxShadow, List[BoxShadow]]):
323+
self.__shadow = value if value is not None else []
324+
261325
# border
262326
@property
263327
def border(self) -> Optional[Border]:

0 commit comments

Comments
 (0)