Skip to content

Commit a4a4e57

Browse files
Reland CupertinoPopupSurface (#159272)
flutter/flutter#151430 had to be reverted because of a black square appearing during animations. This PR fixes the issue by switching from BlendMode.src -> BlendMode.srcOver. I visually checked to make sure the issue was fixed on MacOS and iOS/Android simulators. Fixes flutter/flutter#154887 ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md --------- Co-authored-by: Tong Mu <dkwingsmt@users.noreply.github.com>
1 parent 4a4d7a7 commit a4a4e57

2 files changed

Lines changed: 561 additions & 21 deletions

File tree

packages/flutter/lib/src/cupertino/dialog.dart

Lines changed: 177 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ const TextStyle _kActionSheetContentStyle = TextStyle(
7979
);
8080

8181
// Generic constants shared between Dialog and ActionSheet.
82-
const double _kBlurAmount = 20.0;
8382
const double _kCornerRadius = 14.0;
8483
const 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

Comments
 (0)