Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 7f7a877

Browse files
authored
Implemented Scrim Focus for BottomSheet (#116743)
* Implemented Scrim Focus for BottomSheet so that assistive technology users can focus and tap on the scrim to close the BottomSheet, which they could not do before the change . The Scrim Focus's size changes to avoid overlapping the BottomSheet.
1 parent 50a23d9 commit 7f7a877

File tree

89 files changed

+1458
-87
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+1458
-87
lines changed

packages/flutter/lib/src/material/bottom_sheet.dart

+215-13
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'dart:ui' show lerpDouble;
66

77
import 'package:flutter/foundation.dart';
8+
import 'package:flutter/rendering.dart';
89
import 'package:flutter/widgets.dart';
910

1011
import 'bottom_sheet_theme.dart';
@@ -319,16 +320,134 @@ class _BottomSheetState extends State<BottomSheet> {
319320

320321
// See scaffold.dart
321322

323+
typedef _SizeChangeCallback<Size> = void Function(Size);
322324

323-
// MODAL BOTTOM SHEETS
324-
class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
325-
_ModalBottomSheetLayout(this.progress, this.isScrollControlled);
325+
class _BottomSheetLayoutWithSizeListener extends SingleChildRenderObjectWidget {
326326

327-
final double progress;
327+
const _BottomSheetLayoutWithSizeListener({
328+
required this.animationValue,
329+
required this.isScrollControlled,
330+
required this.onChildSizeChanged,
331+
super.child,
332+
}) : assert(animationValue != null);
333+
334+
final double animationValue;
328335
final bool isScrollControlled;
336+
final _SizeChangeCallback<Size> onChildSizeChanged;
337+
338+
@override
339+
_RenderBottomSheetLayoutWithSizeListener createRenderObject(BuildContext context) {
340+
return _RenderBottomSheetLayoutWithSizeListener(
341+
animationValue: animationValue,
342+
isScrollControlled: isScrollControlled,
343+
onChildSizeChanged: onChildSizeChanged,
344+
);
345+
}
346+
347+
@override
348+
void updateRenderObject(BuildContext context, _RenderBottomSheetLayoutWithSizeListener renderObject) {
349+
renderObject.onChildSizeChanged = onChildSizeChanged;
350+
renderObject.animationValue = animationValue;
351+
renderObject.isScrollControlled = isScrollControlled;
352+
}
353+
}
354+
355+
class _RenderBottomSheetLayoutWithSizeListener extends RenderShiftedBox {
356+
_RenderBottomSheetLayoutWithSizeListener({
357+
RenderBox? child,
358+
required _SizeChangeCallback<Size> onChildSizeChanged,
359+
required double animationValue,
360+
required bool isScrollControlled,
361+
}) : assert(animationValue != null),
362+
_animationValue = animationValue,
363+
_isScrollControlled = isScrollControlled,
364+
_onChildSizeChanged = onChildSizeChanged,
365+
super(child);
366+
367+
Size _lastSize = Size.zero;
368+
369+
_SizeChangeCallback<Size> get onChildSizeChanged => _onChildSizeChanged;
370+
_SizeChangeCallback<Size> _onChildSizeChanged;
371+
set onChildSizeChanged(_SizeChangeCallback<Size> newCallback) {
372+
assert(newCallback != null);
373+
if (_onChildSizeChanged == newCallback) {
374+
return;
375+
}
376+
377+
_onChildSizeChanged = newCallback;
378+
markNeedsLayout();
379+
}
380+
381+
double get animationValue => _animationValue;
382+
double _animationValue;
383+
set animationValue(double newValue) {
384+
assert(newValue != null);
385+
if (_animationValue == newValue) {
386+
return;
387+
}
388+
389+
_animationValue = newValue;
390+
markNeedsLayout();
391+
}
392+
393+
bool get isScrollControlled => _isScrollControlled;
394+
bool _isScrollControlled;
395+
set isScrollControlled(bool newValue) {
396+
assert(newValue != null);
397+
if (_isScrollControlled == newValue) {
398+
return;
399+
}
400+
401+
_isScrollControlled = newValue;
402+
markNeedsLayout();
403+
}
404+
405+
Size _getSize(BoxConstraints constraints) {
406+
return constraints.constrain(constraints.biggest);
407+
}
408+
409+
@override
410+
double computeMinIntrinsicWidth(double height) {
411+
final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
412+
if (width.isFinite) {
413+
return width;
414+
}
415+
return 0.0;
416+
}
329417

330418
@override
331-
BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
419+
double computeMaxIntrinsicWidth(double height) {
420+
final double width = _getSize(BoxConstraints.tightForFinite(height: height)).width;
421+
if (width.isFinite) {
422+
return width;
423+
}
424+
return 0.0;
425+
}
426+
427+
@override
428+
double computeMinIntrinsicHeight(double width) {
429+
final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
430+
if (height.isFinite) {
431+
return height;
432+
}
433+
return 0.0;
434+
}
435+
436+
@override
437+
double computeMaxIntrinsicHeight(double width) {
438+
final double height = _getSize(BoxConstraints.tightForFinite(width: width)).height;
439+
if (height.isFinite) {
440+
return height;
441+
}
442+
return 0.0;
443+
}
444+
445+
@override
446+
Size computeDryLayout(BoxConstraints constraints) {
447+
return _getSize(constraints);
448+
}
449+
450+
BoxConstraints _getConstraintsForChild(BoxConstraints constraints) {
332451
return BoxConstraints(
333452
minWidth: constraints.maxWidth,
334453
maxWidth: constraints.maxWidth,
@@ -338,14 +457,26 @@ class _ModalBottomSheetLayout extends SingleChildLayoutDelegate {
338457
);
339458
}
340459

341-
@override
342-
Offset getPositionForChild(Size size, Size childSize) {
343-
return Offset(0.0, size.height - childSize.height * progress);
460+
Offset _getPositionForChild(Size size, Size childSize) {
461+
return Offset(0.0, size.height - childSize.height * animationValue);
344462
}
345463

346464
@override
347-
bool shouldRelayout(_ModalBottomSheetLayout oldDelegate) {
348-
return progress != oldDelegate.progress;
465+
void performLayout() {
466+
size = _getSize(constraints);
467+
if (child != null) {
468+
final BoxConstraints childConstraints = _getConstraintsForChild(constraints);
469+
assert(childConstraints.debugAssertIsValid(isAppliedConstraint: true));
470+
child!.layout(childConstraints, parentUsesSize: !childConstraints.isTight);
471+
final BoxParentData childParentData = child!.parentData! as BoxParentData;
472+
childParentData.offset = _getPositionForChild(size, childConstraints.isTight ? childConstraints.smallest : child!.size);
473+
final Size childSize = childConstraints.isTight ? childConstraints.smallest : child!.size;
474+
475+
if (_lastSize != childSize) {
476+
_lastSize = childSize;
477+
_onChildSizeChanged.call(_lastSize);
478+
}
479+
}
349480
}
350481
}
351482

@@ -392,6 +523,10 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
392523
}
393524
}
394525

526+
EdgeInsets _getNewClipDetails(Size topLayerSize) {
527+
return EdgeInsets.fromLTRB(0, 0, 0, topLayerSize.height);
528+
}
529+
395530
void handleDragStart(DragStartDetails details) {
396531
// Allow the bottom sheet to track the user's finger accurately.
397532
animationCurve = Curves.linear;
@@ -443,8 +578,14 @@ class _ModalBottomSheetState<T> extends State<_ModalBottomSheet<T>> {
443578
label: routeLabel,
444579
explicitChildNodes: true,
445580
child: ClipRect(
446-
child: CustomSingleChildLayout(
447-
delegate: _ModalBottomSheetLayout(animationValue, widget.isScrollControlled),
581+
child: _BottomSheetLayoutWithSizeListener(
582+
onChildSizeChanged: (Size size) {
583+
widget.route._didChangeBarrierSemanticsClip(
584+
_getNewClipDetails(size),
585+
);
586+
},
587+
animationValue: animationValue,
588+
isScrollControlled: widget.isScrollControlled,
448589
child: child,
449590
),
450591
),
@@ -516,6 +657,7 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
516657
required this.builder,
517658
this.capturedThemes,
518659
this.barrierLabel,
660+
this.barrierOnTapHint,
519661
this.backgroundColor,
520662
this.elevation,
521663
this.shape,
@@ -646,6 +788,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
646788
/// Default is false.
647789
final bool useSafeArea;
648790

791+
/// {@template flutter.material.ModalBottomSheetRoute.barrierOnTapHint}
792+
/// The semantic hint text that informs users what will happen if they
793+
/// tap on the widget. Announced in the format of 'Double tap to ...'.
794+
///
795+
/// If the field is null, the default hint will be used, which results in
796+
/// announcement of 'Double tap to activate'.
797+
/// {@endtemplate}
798+
///
799+
/// See also:
800+
///
801+
/// * [barrierDismissible], which controls the behavior of the barrier when
802+
/// tapped.
803+
/// * [ModalBarrier], which uses this field as onTapHint when it has an onTap action.
804+
final String? barrierOnTapHint;
805+
806+
final ValueNotifier<EdgeInsets> _clipDetailsNotifier = ValueNotifier<EdgeInsets>(EdgeInsets.zero);
807+
808+
/// Updates the details regarding how the [SemanticsNode.rect] (focus) of
809+
/// the barrier for this [ModalBottomSheetRoute] should be clipped.
810+
///
811+
/// returns true if the clipDetails did change and false otherwise.
812+
bool _didChangeBarrierSemanticsClip(EdgeInsets newClipDetails) {
813+
if (_clipDetailsNotifier.value == newClipDetails) {
814+
return false;
815+
}
816+
_clipDetailsNotifier.value = newClipDetails;
817+
return true;
818+
}
819+
649820
@override
650821
Duration get transitionDuration => _bottomSheetEnterDuration;
651822

@@ -710,6 +881,35 @@ class ModalBottomSheetRoute<T> extends PopupRoute<T> {
710881

711882
return capturedThemes?.wrap(bottomSheet) ?? bottomSheet;
712883
}
884+
885+
@override
886+
Widget buildModalBarrier() {
887+
if (barrierColor != null && barrierColor.alpha != 0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
888+
assert(barrierColor != barrierColor.withOpacity(0.0));
889+
final Animation<Color?> color = animation!.drive(
890+
ColorTween(
891+
begin: barrierColor.withOpacity(0.0),
892+
end: barrierColor, // changedInternalState is called if barrierColor updates
893+
).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
894+
);
895+
return AnimatedModalBarrier(
896+
color: color,
897+
dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
898+
semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
899+
barrierSemanticsDismissible: semanticsDismissible,
900+
clipDetailsNotifier: _clipDetailsNotifier,
901+
semanticsOnTapHint: barrierOnTapHint,
902+
);
903+
} else {
904+
return ModalBarrier(
905+
dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
906+
semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
907+
barrierSemanticsDismissible: semanticsDismissible,
908+
clipDetailsNotifier: _clipDetailsNotifier,
909+
semanticsOnTapHint: barrierOnTapHint,
910+
);
911+
}
912+
}
713913
}
714914

715915
// TODO(guidezpl): Look into making this public. A copy of this class is in
@@ -844,11 +1044,13 @@ Future<T?> showModalBottomSheet<T>({
8441044
assert(debugCheckHasMaterialLocalizations(context));
8451045

8461046
final NavigatorState navigator = Navigator.of(context, rootNavigator: useRootNavigator);
1047+
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
8471048
return navigator.push(ModalBottomSheetRoute<T>(
8481049
builder: builder,
8491050
capturedThemes: InheritedTheme.capture(from: context, to: navigator.context),
8501051
isScrollControlled: isScrollControlled,
851-
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
1052+
barrierLabel: localizations.scrimLabel,
1053+
barrierOnTapHint: localizations.scrimOnTapHint(localizations.bottomSheetLabel),
8521054
backgroundColor: backgroundColor,
8531055
elevation: elevation,
8541056
shape: shape,

packages/flutter/lib/src/material/material_localizations.dart

+19
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ abstract class MaterialLocalizations {
163163
/// Label indicating that a given date is the current date.
164164
String get currentDateLabel;
165165

166+
/// Label for the scrim rendered underneath the content of a modal route.
167+
String get scrimLabel;
168+
169+
/// Label for a BottomSheet.
170+
String get bottomSheetLabel;
171+
172+
/// Hint text announced when tapping on the scrim underneath the content of
173+
/// a modal route.
174+
String scrimOnTapHint(String modalRouteContentName);
175+
166176
/// The format used to lay out the time picker.
167177
///
168178
/// The documentation for [TimeOfDayFormat] enum values provides details on
@@ -1024,6 +1034,15 @@ class DefaultMaterialLocalizations implements MaterialLocalizations {
10241034
@override
10251035
String get currentDateLabel => 'Today';
10261036

1037+
@override
1038+
String get scrimLabel => 'Scrim';
1039+
1040+
@override
1041+
String get bottomSheetLabel => 'Bottom Sheet';
1042+
1043+
@override
1044+
String scrimOnTapHint(String modalRouteContentName) => 'Close $modalRouteContentName';
1045+
10271046
@override
10281047
String aboutListTileTitle(String applicationName) => 'About $applicationName';
10291048

0 commit comments

Comments
 (0)