Unverified Commit 9734754a authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

CupertinoContextMenu (iOS 13) (#43918)

Adds the CupertinoContextMenu widget for iOS 13 support.
parent 28b5cc38
......@@ -15,6 +15,8 @@ export 'src/cupertino/app.dart';
export 'src/cupertino/bottom_tab_bar.dart';
export 'src/cupertino/button.dart';
export 'src/cupertino/colors.dart';
export 'src/cupertino/context_menu.dart';
export 'src/cupertino/context_menu_action.dart';
export 'src/cupertino/date_picker.dart';
export 'src/cupertino/dialog.dart';
export 'src/cupertino/icon_theme_data.dart';
......
This diff is collapsed.
// Copyright 2019 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/rendering.dart';
import 'package:flutter/widgets.dart';
import 'colors.dart';
/// A button in a _ContextMenuSheet.
///
/// A typical use case is to pass a [Text] as the [child] here, but be sure to
/// use [TextOverflow.ellipsis] for the [Text.overflow] field if the text may be
/// long, as without it the text will wrap to the next line.
class CupertinoContextMenuAction extends StatefulWidget {
/// Construct a CupertinoContextMenuAction.
const CupertinoContextMenuAction({
Key key,
@required this.child,
this.isDefaultAction = false,
this.isDestructiveAction = false,
this.onPressed,
this.trailingIcon,
}) : assert(child != null),
assert(isDefaultAction != null),
assert(isDestructiveAction != null),
super(key: key);
/// The widget that will be placed inside the action.
final Widget child;
/// Indicates whether this action should receive the style of an emphasized,
/// default action.
final bool isDefaultAction;
/// Indicates whether this action should receive the style of a destructive
/// action.
final bool isDestructiveAction;
/// Called when the action is pressed.
final VoidCallback onPressed;
/// An optional icon to display to the right of the child.
///
/// Will be colored in the same way as the [TextStyle] used for [child] (for
/// example, if using [isDestructiveAction]).
final IconData trailingIcon;
@override
_CupertinoContextMenuActionState createState() => _CupertinoContextMenuActionState();
}
class _CupertinoContextMenuActionState extends State<CupertinoContextMenuAction> {
static const Color _kBackgroundColor = Color(0xFFEEEEEE);
static const Color _kBackgroundColorPressed = Color(0xFFDDDDDD);
static const double _kButtonHeight = 56.0;
static const TextStyle _kActionSheetActionStyle = TextStyle(
fontFamily: '.SF UI Text',
inherit: false,
fontSize: 20.0,
fontWeight: FontWeight.w400,
color: CupertinoColors.black,
textBaseline: TextBaseline.alphabetic,
);
final GlobalKey _globalKey = GlobalKey();
bool _isPressed = false;
void onTapDown(TapDownDetails details) {
setState(() {
_isPressed = true;
});
}
void onTapUp(TapUpDetails details) {
setState(() {
_isPressed = false;
});
}
void onTapCancel() {
setState(() {
_isPressed = false;
});
}
TextStyle get _textStyle {
if (widget.isDefaultAction) {
return _kActionSheetActionStyle.copyWith(
fontWeight: FontWeight.w600,
);
}
if (widget.isDestructiveAction) {
return _kActionSheetActionStyle.copyWith(
color: CupertinoColors.destructiveRed,
);
}
return _kActionSheetActionStyle;
}
@override
Widget build(BuildContext context) {
return GestureDetector(
key: _globalKey,
onTapDown: onTapDown,
onTapUp: onTapUp,
onTapCancel: onTapCancel,
onTap: widget.onPressed,
behavior: HitTestBehavior.opaque,
child: ConstrainedBox(
constraints: const BoxConstraints(
minHeight: _kButtonHeight,
),
child: Semantics(
button: true,
child: Container(
decoration: BoxDecoration(
color: _isPressed ? _kBackgroundColorPressed : _kBackgroundColor,
border: const Border(
bottom: BorderSide(width: 1.0, color: _kBackgroundColorPressed),
),
),
padding: const EdgeInsets.symmetric(
vertical: 16.0,
horizontal: 10.0,
),
child: DefaultTextStyle(
style: _textStyle,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: widget.child,
),
if (widget.trailingIcon != null)
Icon(
widget.trailingIcon,
color: CupertinoColors.destructiveRed,
),
],
),
),
),
),
),
);
}
}
......@@ -4,7 +4,7 @@
import 'dart:async';
import 'dart:math';
import 'dart:ui' show lerpDouble;
import 'dart:ui' show lerpDouble, ImageFilter;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
......@@ -794,11 +794,15 @@ class _CupertinoEdgeShadowPainter extends BoxPainter {
class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
_CupertinoModalPopupRoute({
this.builder,
this.barrierLabel,
this.barrierColor,
this.barrierLabel,
this.builder,
ImageFilter filter,
RouteSettings settings,
}) : super(settings: settings);
}) : super(
filter: filter,
settings: settings,
);
final WidgetBuilder builder;
......@@ -890,14 +894,16 @@ class _CupertinoModalPopupRoute<T> extends PopupRoute<T> {
Future<T> showCupertinoModalPopup<T>({
@required BuildContext context,
@required WidgetBuilder builder,
ImageFilter filter,
bool useRootNavigator = true,
}) {
assert(useRootNavigator != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push(
_CupertinoModalPopupRoute<T>(
builder: builder,
barrierLabel: 'Dismiss',
barrierColor: CupertinoDynamicColor.resolve(_kModalBarrierColor, context),
barrierLabel: 'Dismiss',
builder: builder,
filter: filter,
),
);
}
......
......@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
......@@ -735,7 +736,15 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
/// Creates a route that blocks interaction with previous routes.
ModalRoute({
RouteSettings settings,
}) : super(settings: settings);
ui.ImageFilter filter,
}) : _filter = filter,
super(settings: settings);
/// The filter to add to the barrier.
///
/// If given, this filter will be applied to the modal barrier using
/// [BackdropFilter]. This allows blur effects, for example.
final ui.ImageFilter _filter;
// The API for general users of this class
......@@ -1286,6 +1295,12 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
barrierSemanticsDismissible: semanticsDismissible,
);
}
if (_filter != null) {
barrier = BackdropFilter(
filter: _filter,
child: barrier,
);
}
return IgnorePointer(
ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates
animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
......@@ -1321,7 +1336,11 @@ abstract class PopupRoute<T> extends ModalRoute<T> {
/// Initializes the [PopupRoute].
PopupRoute({
RouteSettings settings,
}) : super(settings: settings);
ui.ImageFilter filter,
}) : super(
filter: filter,
settings: settings,
);
@override
bool get opaque => false;
......
// Copyright 2019 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_test/flutter_test.dart';
import 'package:flutter/cupertino.dart';
void main() {
// Constants taken from _ContextMenuActionState.
const Color _kBackgroundColor = Color(0xFFEEEEEE);
const Color _kBackgroundColorPressed = Color(0xFFDDDDDD);
Widget _getApp([VoidCallback onPressed]) {
final UniqueKey actionKey = UniqueKey();
final CupertinoContextMenuAction action = CupertinoContextMenuAction(
key: actionKey,
child: const Text('I am a CupertinoContextMenuAction'),
onPressed: onPressed,
);
return CupertinoApp(
home: CupertinoPageScaffold(
child: Center(
child: action,
),
),
);
}
BoxDecoration _getDecoration(WidgetTester tester) {
final Finder finder = find.descendant(
of: find.byType(CupertinoContextMenuAction),
matching: find.byType(Container),
);
expect(finder, findsOneWidget);
final Container container = tester.widget(finder);
return container.decoration;
}
testWidgets('responds to taps', (WidgetTester tester) async {
bool wasPressed = false;
await tester.pumpWidget(_getApp(() {
wasPressed = true;
}));
expect(wasPressed, false);
await tester.tap(find.byType(CupertinoContextMenuAction));
expect(wasPressed, true);
});
testWidgets('turns grey when pressed and held', (WidgetTester tester) async {
await tester.pumpWidget(_getApp());
expect(_getDecoration(tester).color, _kBackgroundColor);
final Offset actionCenter = tester.getCenter(find.byType(CupertinoContextMenuAction));
final TestGesture gesture = await tester.startGesture(actionCenter);
await tester.pump();
expect(_getDecoration(tester).color, _kBackgroundColorPressed);
await gesture.up();
await tester.pump();
expect(_getDecoration(tester).color, _kBackgroundColor);
});
}
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment