blob: d6936b0ed6909ef4d04d3baa4929e6713b2904b7 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'basic.dart';
import 'container.dart';
import 'debug.dart';
import 'framework.dart';
import 'gesture_detector.dart';
import 'navigator.dart';
import 'transitions.dart';
/// A widget that prevents the user from interacting with widgets behind itself.
///
/// The modal barrier is the scrim that is rendered behind each route, which
/// generally prevents the user from interacting with the route below the
/// current route, and normally partially obscures such routes.
///
/// For example, when a dialog is on the screen, the page below the dialog is
/// usually darkened by the modal barrier.
///
/// See also:
///
/// * [ModalRoute], which indirectly uses this widget.
/// * [AnimatedModalBarrier], which is similar but takes an animated [color]
/// instead of a single color value.
class ModalBarrier extends StatelessWidget {
/// Creates a widget that blocks user interaction.
const ModalBarrier({
Key key,
this.color,
this.dismissible = true,
this.semanticsLabel,
this.barrierSemanticsDismissible = true,
}) : super(key: key);
/// If non-null, fill the barrier with this color.
///
/// See also:
///
/// * [ModalRoute.barrierColor], which controls this property for the
/// [ModalBarrier] built by [ModalRoute] pages.
final Color color;
/// Whether touching the barrier will pop the current route off the [Navigator].
///
/// See also:
///
/// * [ModalRoute.barrierDismissible], which controls this property for the
/// [ModalBarrier] built by [ModalRoute] pages.
final bool dismissible;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
///
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final bool barrierSemanticsDismissible;
/// Semantics label used for the barrier if it is [dismissible].
///
/// The semantics label is read out by accessibility tools (e.g. TalkBack
/// on Android and VoiceOver on iOS) when the barrier is focused.
///
/// See also:
///
/// * [ModalRoute.barrierLabel], which controls this property for the
/// [ModalBarrier] built by [ModalRoute] pages.
final String semanticsLabel;
@override
Widget build(BuildContext context) {
assert(!dismissible || semanticsLabel == null || debugCheckHasDirectionality(context));
bool platformSupportsDismissingBarrier;
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
platformSupportsDismissingBarrier = false;
break;
case TargetPlatform.iOS:
platformSupportsDismissingBarrier = true;
break;
}
assert(platformSupportsDismissingBarrier != null);
final bool semanticsDismissible = dismissible && platformSupportsDismissingBarrier;
final bool modalBarrierSemanticsDismissible = barrierSemanticsDismissible ?? semanticsDismissible;
return BlockSemantics(
child: ExcludeSemantics(
// On Android, the back button is used to dismiss a modal. On iOS, some
// modal barriers are not dismissible in accessibility mode.
excluding: !semanticsDismissible || !modalBarrierSemanticsDismissible,
child: _ModalBarrierGestureDetector(
onAnyTapDown: () {
if (dismissible)
Navigator.maybePop(context);
},
child: Semantics(
label: semanticsDismissible ? semanticsLabel : null,
textDirection: semanticsDismissible && semanticsLabel != null ? Directionality.of(context) : null,
child: ConstrainedBox(
constraints: const BoxConstraints.expand(),
child: color == null ? null : DecoratedBox(
decoration: BoxDecoration(
color: color,
),
),
),
),
),
),
);
}
}
/// A widget that prevents the user from interacting with widgets behind itself,
/// and can be configured with an animated color value.
///
/// The modal barrier is the scrim that is rendered behind each route, which
/// generally prevents the user from interacting with the route below the
/// current route, and normally partially obscures such routes.
///
/// For example, when a dialog is on the screen, the page below the dialog is
/// usually darkened by the modal barrier.
///
/// This widget is similar to [ModalBarrier] except that it takes an animated
/// [color] instead of a single color.
///
/// See also:
///
/// * [ModalRoute], which uses this widget.
class AnimatedModalBarrier extends AnimatedWidget {
/// Creates a widget that blocks user interaction.
const AnimatedModalBarrier({
Key key,
Animation<Color> color,
this.dismissible = true,
this.semanticsLabel,
this.barrierSemanticsDismissible,
}) : super(key: key, listenable: color);
/// If non-null, fill the barrier with this color.
///
/// See also:
///
/// * [ModalRoute.barrierColor], which controls this property for the
/// [AnimatedModalBarrier] built by [ModalRoute] pages.
Animation<Color> get color => listenable;
/// Whether touching the barrier will pop the current route off the [Navigator].
///
/// See also:
///
/// * [ModalRoute.barrierDismissible], which controls this property for the
/// [AnimatedModalBarrier] built by [ModalRoute] pages.
final bool dismissible;
/// Semantics label used for the barrier if it is [dismissible].
///
/// The semantics label is read out by accessibility tools (e.g. TalkBack
/// on Android and VoiceOver on iOS) when the barrier is focused.
/// See also:
///
/// * [ModalRoute.barrierLabel], which controls this property for the
/// [ModalBarrier] built by [ModalRoute] pages.
final String semanticsLabel;
/// Whether the modal barrier semantics are included in the semantics tree.
///
/// See also:
///
/// * [ModalRoute.semanticsDismissible], which controls this property for
/// the [ModalBarrier] built by [ModalRoute] pages.
final bool barrierSemanticsDismissible;
@override
Widget build(BuildContext context) {
return ModalBarrier(
color: color?.value,
dismissible: dismissible,
semanticsLabel: semanticsLabel,
barrierSemanticsDismissible: barrierSemanticsDismissible,
);
}
}
// Recognizes tap down by any pointer button.
//
// It is similar to [TapGestureRecognizer.onTapDown], but accepts any single
// button, which means the gesture also takes parts in gesture arenas.
class _AnyTapGestureRecognizer extends PrimaryPointerGestureRecognizer {
_AnyTapGestureRecognizer({
Object debugOwner,
}) : super(debugOwner: debugOwner);
VoidCallback onAnyTapDown;
bool _sentTapDown = false;
bool _wonArenaForPrimaryPointer = false;
// The buttons sent by `PointerDownEvent`. If a `PointerMoveEvent` comes with a
// different set of buttons, the gesture is canceled.
int _initialButtons;
@override
void addAllowedPointer(PointerDownEvent event) {
super.addAllowedPointer(event);
// `_initialButtons` must be assigned here instead of `handlePrimaryPointer`,
// because `acceptGesture` might be called before `handlePrimaryPointer`,
// which relies on `_initialButtons` to create `TapDownDetails`.
_initialButtons = event.buttons;
}
@override
void handlePrimaryPointer(PointerEvent event) {
if (event is PointerUpEvent || event is PointerCancelEvent) {
resolve(GestureDisposition.rejected);
_reset();
} else if (event.buttons != _initialButtons) {
resolve(GestureDisposition.rejected);
stopTrackingPointer(primaryPointer);
}
}
@override
void resolve(GestureDisposition disposition) {
if (_wonArenaForPrimaryPointer && disposition == GestureDisposition.rejected) {
// This can happen if the gesture has been canceled. For example, when
// the pointer has exceeded the touch slop, the buttons have been changed,
// or if the recognizer is disposed.
assert(_sentTapDown);
_reset();
}
super.resolve(disposition);
}
@override
void didExceedDeadlineWithEvent(PointerDownEvent event) {
_checkDown(event.pointer);
}
@override
void acceptGesture(int pointer) {
super.acceptGesture(pointer);
if (pointer == primaryPointer) {
_checkDown(pointer);
_wonArenaForPrimaryPointer = true;
_reset();
}
}
@override
void rejectGesture(int pointer) {
super.rejectGesture(pointer);
if (pointer == primaryPointer) {
// Another gesture won the arena.
assert(state != GestureRecognizerState.possible);
_reset();
}
}
void _checkDown(int pointer) {
if (_sentTapDown)
return;
if (onAnyTapDown != null)
onAnyTapDown();
_sentTapDown = true;
}
void _reset() {
_sentTapDown = false;
_wonArenaForPrimaryPointer = false;
_initialButtons = null;
}
@override
String get debugDescription => 'any tap';
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(FlagProperty('wonArenaForPrimaryPointer', value: _wonArenaForPrimaryPointer, ifTrue: 'won arena'));
properties.add(FlagProperty('sentTapDown', value: _sentTapDown, ifTrue: 'sent tap down'));
}
}
class _ModalBarrierSemanticsDelegate extends SemanticsGestureDelegate {
const _ModalBarrierSemanticsDelegate({this.onAnyTapDown});
final VoidCallback onAnyTapDown;
@override
void assignSemantics(RenderSemanticsGestureHandler renderObject) {
renderObject.onTap = onAnyTapDown;
}
}
class _AnyTapGestureRecognizerFactory extends GestureRecognizerFactory<_AnyTapGestureRecognizer> {
const _AnyTapGestureRecognizerFactory({this.onAnyTapDown});
final VoidCallback onAnyTapDown;
@override
_AnyTapGestureRecognizer constructor() => _AnyTapGestureRecognizer();
@override
void initializer(_AnyTapGestureRecognizer instance) {
instance.onAnyTapDown = onAnyTapDown;
}
}
// A GestureDetector used by ModalBarrier. It only has one callback,
// [onAnyTapDown], which recognizes tap down unconditionally.
class _ModalBarrierGestureDetector extends StatelessWidget {
const _ModalBarrierGestureDetector({
Key key,
@required this.child,
@required this.onAnyTapDown,
}) : assert(child != null),
assert(onAnyTapDown != null),
super(key: key);
/// The widget below this widget in the tree.
/// See [RawGestureDetector.child].
final Widget child;
/// Immediately called when a pointer causes a tap down.
/// See [_AnyTapGestureRecognizer.onAnyTapDown].
final VoidCallback onAnyTapDown;
@override
Widget build(BuildContext context) {
final Map<Type, GestureRecognizerFactory> gestures = <Type, GestureRecognizerFactory>{
_AnyTapGestureRecognizer: _AnyTapGestureRecognizerFactory(onAnyTapDown: onAnyTapDown),
};
return RawGestureDetector(
gestures: gestures,
behavior: HitTestBehavior.opaque,
semantics: _ModalBarrierSemanticsDelegate(onAnyTapDown: onAnyTapDown),
child: child,
);
}
}