@@ -79,7 +79,6 @@ const TextStyle _kActionSheetContentStyle = TextStyle(
7979);
8080
8181// Generic constants shared between Dialog and ActionSheet.
82- const double _kBlurAmount = 20.0 ;
8382const double _kCornerRadius = 14.0 ;
8483const double _kDividerThickness = 0.3 ;
8584
@@ -494,20 +493,34 @@ class _CupertinoAlertDialogState extends State<CupertinoAlertDialog> {
494493 }
495494}
496495
497- /// Rounded rectangle surface that looks like an iOS popup surface, e.g., alert dialog
498- /// and action sheet .
496+ /// An iOS-style component for creating modal overlays like dialogs and action
497+ /// sheets .
499498///
500- /// A [CupertinoPopupSurface] can be configured to paint or not paint a white
501- /// color on top of its blurred area. Typical usage should paint white on top
502- /// of the blur. However, the white paint can be disabled for the purpose of
503- /// rendering divider gaps for a more complicated layout, e.g., [CupertinoAlertDialog] .
504- /// Additionally, the white paint can be disabled to render a blurred rounded
505- /// rectangle without any color (similar to iOS's volume control popup).
499+ /// By default, [CupertinoPopupSurface] generates a rounded rectangle surface
500+ /// that applies two effects to the background content:
501+ ///
502+ /// 1. Background filter: Saturates and then blurs content behind the surface.
503+ /// 2. Overlay color: Covers the filtered background with a transparent
504+ /// surface color. The color adapts to the CupertinoTheme's brightness:
505+ /// light gray when the ambient [CupertinoTheme] brightness is
506+ /// [Brightness.light], and dark gray when [Brightness.dark].
507+ ///
508+ /// The blur strength can be changed by setting [blurSigma] to a positive value,
509+ /// or removed by setting the [blurSigma] to 0.
510+ ///
511+ /// The saturation effect can be removed for debugging by setting
512+ /// [debugIsVibrancePainted] to false. The saturation effect is not supported on
513+ /// web with the skwasm renderer and will not be applied regardless of the value
514+ /// of [debugIsVibrancePainted] .
515+ ///
516+ /// The surface color can be disabled by setting [isSurfacePainted] to false,
517+ /// which is useful for more complicated layouts, such as rendering divider gaps
518+ /// in [CupertinoAlertDialog] or rendering custom surface colors.
506519///
507520/// {@tool dartpad}
508521/// This sample shows how to use a [CupertinoPopupSurface] . The [CupertinoPopupSurface]
509- /// shows a model popup from the bottom of the screen.
510- /// Toggling the switch to configure its surface color.
522+ /// shows a modal popup from the bottom of the screen.
523+ /// Toggle the switch to configure its surface color.
511524///
512525/// ** See code in examples/api/lib/cupertino/dialog/cupertino_popup_surface.0.dart **
513526/// {@end-tool}
@@ -521,9 +534,17 @@ class CupertinoPopupSurface extends StatelessWidget {
521534 /// Creates an iOS-style rounded rectangle popup surface.
522535 const CupertinoPopupSurface ({
523536 super .key,
537+ this .blurSigma = defaultBlurSigma,
524538 this .isSurfacePainted = true ,
525- this .child,
526- });
539+ required this .child,
540+ }) : assert (blurSigma >= 0 , 'CupertinoPopupSurface requires a non-negative blur sigma.' );
541+
542+ /// The strength of the gaussian blur applied to the area beneath this
543+ /// surface.
544+ ///
545+ /// Defaults to [defaultBlurSigma] . Setting [blurSigma] to 0 will remove the
546+ /// blur filter.
547+ final double blurSigma;
527548
528549 /// Whether or not to paint a translucent white on top of this surface's
529550 /// blurred background. [isSurfacePainted] should be true for a typical popup
@@ -533,26 +554,158 @@ class CupertinoPopupSurface extends StatelessWidget {
533554 /// Some popups, like iOS's volume control popup, choose to render a blurred
534555 /// area without any white paint covering it. To achieve this effect,
535556 /// [isSurfacePainted] should be set to false.
557+ ///
558+ /// Defaults to true.
536559 final bool isSurfacePainted;
537560
538561 /// The widget below this widget in the tree.
539- final Widget ? child;
562+ // Because [CupertinoPopupSurface] is composed of proxy boxes, which mimic
563+ // the size of their child, a [child] is required to ensure that this surface
564+ // has a size.
565+ final Widget child;
566+
567+ /// The default strength of the blur applied to widgets underlying a
568+ /// [CupertinoPopupSurface] .
569+ ///
570+ /// Eyeballed from the iOS 17 simulator.
571+ static const double defaultBlurSigma = 30.0 ;
572+
573+ /// The default corner radius of a [CupertinoPopupSurface] .
574+ static const BorderRadius _clipper = BorderRadius .all (Radius .circular (14 ));
575+
576+ // The [ColorFilter] matrix used to saturate widgets underlying a
577+ // [CupertinoPopupSurface] when the ambient [CupertinoThemeData.brightness] is
578+ // [Brightness.light].
579+ //
580+ // To derive this matrix, the saturation matrix was taken from
581+ // https://docs.rainmeter.net/tips/colormatrix-guide/ and was tweaked to
582+ // resemble the iOS 17 simulator.
583+ //
584+ // The matrix can be derived from the following function:
585+ // static List<double> get _lightSaturationMatrix {
586+ // const double lightLumR = 0.26;
587+ // const double lightLumG = 0.4;
588+ // const double lightLumB = 0.17;
589+ // const double saturation = 2.0;
590+ // const double sr = (1 - saturation) * lightLumR;
591+ // const double sg = (1 - saturation) * lightLumG;
592+ // const double sb = (1 - saturation) * lightLumB;
593+ // return <double>[
594+ // sr + saturation, sg, sb, 0.0, 0.0,
595+ // sr, sg + saturation, sb, 0.0, 0.0,
596+ // sr, sg, sb + saturation, 0.0, 0.0,
597+ // 0.0, 0.0, 0.0, 1.0, 0.0,
598+ // ];
599+ // }
600+ static const List <double > _lightSaturationMatrix = < double > [
601+ 1.74 , - 0.40 , - 0.17 , 0.00 , 0.00 ,
602+ - 0.26 , 1.60 , - 0.17 , 0.00 , 0.00 ,
603+ - 0.26 , - 0.40 , 1.83 , 0.00 , 0.00 ,
604+ 0.00 , 0.00 , 0.00 , 1.00 , 0.00
605+ ];
606+
607+ // The [ColorFilter] matrix used to saturate widgets underlying a
608+ // [CupertinoPopupSurface] when the ambient [CupertinoThemeData.brightness] is
609+ // [Brightness.dark].
610+ //
611+ // To derive this matrix, the saturation matrix was taken from
612+ // https://docs.rainmeter.net/tips/colormatrix-guide/ and was tweaked to
613+ // resemble the iOS 17 simulator.
614+ //
615+ // The matrix can be derived from the following function:
616+ // static List<double> get _darkSaturationMatrix {
617+ // const double additive = 0.3;
618+ // const double darkLumR = 0.45;
619+ // const double darkLumG = 0.8;
620+ // const double darkLumB = 0.16;
621+ // const double saturation = 1.7;
622+ // const double sr = (1 - saturation) * darkLumR;
623+ // const double sg = (1 - saturation) * darkLumG;
624+ // const double sb = (1 - saturation) * darkLumB;
625+ // return <double>[
626+ // sr + saturation, sg, sb, 0.0, additive,
627+ // sr, sg + saturation, sb, 0.0, additive,
628+ // sr, sg, sb + saturation, 0.0, additive,
629+ // 0.0, 0.0, 0.0, 1.0, 0.0,
630+ // ];
631+ // }
632+ static const List <double > _darkSaturationMatrix = < double > [
633+ 1.39 , - 0.56 , - 0.11 , 0.00 , 0.30 ,
634+ - 0.32 , 1.14 , - 0.11 , 0.00 , 0.30 ,
635+ - 0.32 , - 0.56 , 1.59 , 0.00 , 0.30 ,
636+ 0.00 , 0.00 , 0.00 , 1.00 , 0.00
637+ ];
638+
639+ /// Whether or not the area beneath this surface should be saturated with a
640+ /// [ColorFilter] .
641+ ///
642+ /// The appearance of the [ColorFilter] is determined by the [Brightness]
643+ /// value obtained from the ambient [CupertinoTheme] .
644+ ///
645+ /// The vibrance is always painted if asserts are disabled.
646+ ///
647+ /// Defaults to true.
648+ static bool debugIsVibrancePainted = true ;
649+
650+ ImageFilter ? _buildFilter (Brightness ? brightness) {
651+ bool isVibrancePainted = true ;
652+ assert (() {
653+ isVibrancePainted = debugIsVibrancePainted;
654+ return true ;
655+ }());
656+ if ((kIsWeb && ! isSkiaWeb) || ! isVibrancePainted) {
657+ if (blurSigma == 0 ) {
658+ return null ;
659+ }
660+ return ImageFilter .blur (
661+ sigmaX: blurSigma,
662+ sigmaY: blurSigma,
663+ );
664+ }
665+
666+ final ColorFilter colorFilter = switch (brightness) {
667+ Brightness .dark => const ColorFilter .matrix (_darkSaturationMatrix),
668+ Brightness .light || null => const ColorFilter .matrix (_lightSaturationMatrix)
669+ };
670+
671+ if (blurSigma == 0 ) {
672+ return colorFilter;
673+ }
674+
675+ return ImageFilter .compose (
676+ inner: colorFilter,
677+ outer: ImageFilter .blur (
678+ sigmaX: blurSigma,
679+ sigmaY: blurSigma,
680+ ),
681+ );
682+ }
540683
541684 @override
542685 Widget build (BuildContext context) {
543- Widget ? contents = child;
686+ final ImageFilter ? filter = _buildFilter (CupertinoTheme .maybeBrightnessOf (context));
687+ Widget contents = child;
688+
544689 if (isSurfacePainted) {
545690 contents = ColoredBox (
546691 color: CupertinoDynamicColor .resolve (_kDialogColor, context),
547692 child: contents,
548693 );
549694 }
695+
696+ if (filter != null ) {
697+ return ClipRRect (
698+ borderRadius: _clipper,
699+ child: BackdropFilter (
700+ filter: filter,
701+ child: contents,
702+ ),
703+ );
704+ }
705+
550706 return ClipRRect (
551- borderRadius: const BorderRadius .all (Radius .circular (_kCornerRadius)),
552- child: BackdropFilter (
553- filter: ImageFilter .blur (sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
554- child: contents,
555- ),
707+ borderRadius: _clipper,
708+ child: contents,
556709 );
557710 }
558711}
@@ -1138,7 +1291,10 @@ class _CupertinoActionSheetState extends State<CupertinoActionSheet> {
11381291 child: ClipRRect (
11391292 borderRadius: const BorderRadius .all (Radius .circular (12.0 )),
11401293 child: BackdropFilter (
1141- filter: ImageFilter .blur (sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
1294+ filter: ImageFilter .blur (
1295+ sigmaX: CupertinoPopupSurface .defaultBlurSigma,
1296+ sigmaY: CupertinoPopupSurface .defaultBlurSigma,
1297+ ),
11421298 child: _ActionSheetMainSheet (
11431299 pressedIndex: _pressedIndex,
11441300 onPressedUpdate: _onPressedUpdate,
0 commit comments