Android context menu theming and visual update (#131816)
Fixes https://github.com/flutter/flutter/issues/89939 and updates the look of the Android context menu to match API 34. ## The problem Before this PR, setting `surface` in the color scheme caused the background color of the Android context menu to change, but it wasn't possible to change the text color. ```dart MaterialApp( theme: ThemeData( // Using a dark theme made the context menu text color be white. colorScheme: ThemeData.dark().colorScheme.copyWith( // Setting the surface here worked. surface: Colors.white, // But there was no way to set the text color. This didn't work. onSurface: Colors.black, ), ), ), ``` | Expected (after PR) | Actual (before PR) | | --- | --- | | <img width="239" alt="Screenshot 2023-08-07 at 11 45 37 AM" src="https://github.com/flutter/flutter/assets/389558/a9fb75e5-b6c3-4f8e-8c59-2021780c44a7"> | <img width="250" alt="Screenshot 2023-08-07 at 11 51 10 AM" src="https://github.com/flutter/flutter/assets/389558/a5abd2d2-49bb-47a0-836f-864d56af2f58"> | ## Other examples <table> <tr> <th>Scenario</th> <th>Result</th> </tr> <tr> <td> ```dart MaterialApp( theme: ThemeData( colorScheme: ThemeData.light(), ), ... ), ``` </td> <td> <img width="244" alt="Screenshot 2023-08-07 at 11 42 05 AM" src="https://github.com/flutter/flutter/assets/389558/74c6870b-5ff7-4b1a-9e0c-b2bb4809ef1e"> </td> </tr> <tr> <td> ```dart MaterialApp( theme: ThemeData( colorScheme: ThemeData.dark(), ), ... ), ``` </td> <td> <img width="239" alt="Screenshot 2023-08-07 at 11 42 23 AM" src="https://github.com/flutter/flutter/assets/389558/91fe32f8-bd62-4d9b-96e8-ae5a9a769745"> </td> </tr> <tr> <td> ```dart MaterialApp( theme: ThemeData( colorScheme: ThemeData.light().colorScheme.copyWith( surface: Colors.blue, onSurface: Colors.red, ), ), ... ), ``` </td> <td> <img width="240" alt="Screenshot 2023-08-07 at 11 43 06 AM" src="https://github.com/flutter/flutter/assets/389558/e5752f8b-3738-4391-9055-15c38bd4af21"> </td> </tr> <tr> <td> ```dart MaterialApp( theme: ThemeData( colorScheme: ThemeData.light().colorScheme.copyWith( surface: Colors.blue, onSurface: Colors.red, ), ), ... ), ``` </td> <td> <img width="244" alt="Screenshot 2023-08-07 at 11 42 47 AM" src="https://github.com/flutter/flutter/assets/389558/68cc68f0-b338-4d94-8810-d8e46fb1e48e"> </td> </tr> </table>
This commit is contained in:
parent
9cc4f94397
commit
ebbb4b3887
@ -8,11 +8,13 @@ import 'package:flutter/cupertino.dart';
|
|||||||
import 'package:flutter/foundation.dart' show listEquals;
|
import 'package:flutter/foundation.dart' show listEquals;
|
||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
import 'color_scheme.dart';
|
||||||
import 'debug.dart';
|
import 'debug.dart';
|
||||||
import 'icon_button.dart';
|
import 'icon_button.dart';
|
||||||
import 'icons.dart';
|
import 'icons.dart';
|
||||||
import 'material.dart';
|
import 'material.dart';
|
||||||
import 'material_localizations.dart';
|
import 'material_localizations.dart';
|
||||||
|
import 'theme.dart';
|
||||||
|
|
||||||
const double _kToolbarHeight = 44.0;
|
const double _kToolbarHeight = 44.0;
|
||||||
const double _kToolbarContentDistance = 8.0;
|
const double _kToolbarContentDistance = 8.0;
|
||||||
@ -650,13 +652,34 @@ class _TextSelectionToolbarContainer extends StatelessWidget {
|
|||||||
|
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
|
||||||
|
// These colors were taken from a screenshot of a Pixel 6 emulator running
|
||||||
|
// Android API level 34.
|
||||||
|
static const Color _defaultColorLight = Color(0xffffffff);
|
||||||
|
static const Color _defaultColorDark = Color(0xff424242);
|
||||||
|
|
||||||
|
static Color _getColor(ColorScheme colorScheme) {
|
||||||
|
final bool isDefaultSurface = switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => identical(ThemeData().colorScheme.surface, colorScheme.surface),
|
||||||
|
Brightness.dark => identical(ThemeData.dark().colorScheme.surface, colorScheme.surface),
|
||||||
|
};
|
||||||
|
if (!isDefaultSurface) {
|
||||||
|
return colorScheme.surface;
|
||||||
|
}
|
||||||
|
return switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => _defaultColorLight,
|
||||||
|
Brightness.dark => _defaultColorDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final ThemeData theme = Theme.of(context);
|
||||||
return Material(
|
return Material(
|
||||||
// This value was eyeballed to match the native text selection menu on
|
// This value was eyeballed to match the native text selection menu on
|
||||||
// a Pixel 2 running Android 10.
|
// a Pixel 6 emulator running Android API level 34.
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(_kToolbarHeight / 2)),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
|
color: _getColor(theme.colorScheme),
|
||||||
elevation: 1.0,
|
elevation: 1.0,
|
||||||
type: MaterialType.card,
|
type: MaterialType.card,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'colors.dart';
|
import 'color_scheme.dart';
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
import 'text_button.dart';
|
import 'text_button.dart';
|
||||||
import 'theme.dart';
|
import 'theme.dart';
|
||||||
@ -130,20 +130,40 @@ class TextSelectionToolbarTextButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These colors were taken from a screenshot of a Pixel 6 emulator running
|
||||||
|
// Android API level 34.
|
||||||
|
static const Color _defaultForegroundColorLight = Color(0xff000000);
|
||||||
|
static const Color _defaultForegroundColorDark = Color(0xffffffff);
|
||||||
|
|
||||||
|
static Color _getForegroundColor(ColorScheme colorScheme) {
|
||||||
|
final bool isDefaultOnSurface = switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => identical(ThemeData().colorScheme.onSurface, colorScheme.onSurface),
|
||||||
|
Brightness.dark => identical(ThemeData.dark().colorScheme.onSurface, colorScheme.onSurface),
|
||||||
|
};
|
||||||
|
if (!isDefaultOnSurface) {
|
||||||
|
return colorScheme.onSurface;
|
||||||
|
}
|
||||||
|
return switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => _defaultForegroundColorLight,
|
||||||
|
Brightness.dark => _defaultForegroundColorDark,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// TODO(hansmuller): Should be colorScheme.onSurface
|
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||||
final ThemeData theme = Theme.of(context);
|
|
||||||
final bool isDark = theme.colorScheme.brightness == Brightness.dark;
|
|
||||||
final Color foregroundColor = isDark ? Colors.white : Colors.black87;
|
|
||||||
|
|
||||||
return TextButton(
|
return TextButton(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: foregroundColor,
|
foregroundColor: _getForegroundColor(colorScheme),
|
||||||
shape: const RoundedRectangleBorder(),
|
shape: const RoundedRectangleBorder(),
|
||||||
minimumSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension),
|
minimumSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension),
|
||||||
padding: padding,
|
padding: padding,
|
||||||
alignment: alignment,
|
alignment: alignment,
|
||||||
|
textStyle: const TextStyle(
|
||||||
|
// This value was eyeballed from a screenshot of a Pixel 6 emulator
|
||||||
|
// running Android API level 34.
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: child,
|
child: child,
|
||||||
|
@ -204,4 +204,93 @@ void main() {
|
|||||||
expect(find.text('Paste'), findsNothing);
|
expect(find.text('Paste'), findsNothing);
|
||||||
expect(find.text('Select all'), findsNothing);
|
expect(find.text('Select all'), findsNothing);
|
||||||
}, skip: kIsWeb); // [intended] We don't show the toolbar on the web.
|
}, skip: kIsWeb); // [intended] We don't show the toolbar on the web.
|
||||||
|
|
||||||
|
for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) {
|
||||||
|
testWidgetsWithLeakTracking('default background color', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextSelectionToolbar(
|
||||||
|
anchorAbove: Offset.zero,
|
||||||
|
anchorBelow: Offset.zero,
|
||||||
|
children: <Widget>[
|
||||||
|
TextSelectionToolbarTextButton(
|
||||||
|
padding: TextSelectionToolbarTextButton.getPadding(0, 1),
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text('Custom button'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Finder findToolbarContainer() {
|
||||||
|
return find.descendant(
|
||||||
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(findToolbarContainer(), findsAtLeastNWidgets(1));
|
||||||
|
|
||||||
|
final Material toolbarContainer = tester.widget(findToolbarContainer().first);
|
||||||
|
expect(
|
||||||
|
toolbarContainer.color,
|
||||||
|
// The default colors are hardcoded and don't take the default value of
|
||||||
|
// the theme's surface color.
|
||||||
|
switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => const Color(0xffffffff),
|
||||||
|
Brightness.dark => const Color(0xff424242),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('custom background color', (WidgetTester tester) async {
|
||||||
|
const Color customBackgroundColor = Colors.red;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: colorScheme.copyWith(
|
||||||
|
surface: customBackgroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextSelectionToolbar(
|
||||||
|
anchorAbove: Offset.zero,
|
||||||
|
anchorBelow: Offset.zero,
|
||||||
|
children: <Widget>[
|
||||||
|
TextSelectionToolbarTextButton(
|
||||||
|
padding: TextSelectionToolbarTextButton.getPadding(0, 1),
|
||||||
|
onPressed: () {},
|
||||||
|
child: const Text('Custom button'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Finder findToolbarContainer() {
|
||||||
|
return find.descendant(
|
||||||
|
of: find.byWidgetPredicate((Widget w) => '${w.runtimeType}' == '_TextSelectionToolbarContainer'),
|
||||||
|
matching: find.byType(Material),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
expect(findToolbarContainer(), findsAtLeastNWidgets(1));
|
||||||
|
|
||||||
|
final Material toolbarContainer = tester.widget(findToolbarContainer().first);
|
||||||
|
expect(
|
||||||
|
toolbarContainer.color,
|
||||||
|
customBackgroundColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,4 +62,67 @@ void main() {
|
|||||||
expect(onlySize.width, greaterThan(firstSize.width));
|
expect(onlySize.width, greaterThan(firstSize.width));
|
||||||
expect(onlySize.width, greaterThan(lastSize.width));
|
expect(onlySize.width, greaterThan(lastSize.width));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (final ColorScheme colorScheme in <ColorScheme>[ThemeData.light().colorScheme, ThemeData.dark().colorScheme]) {
|
||||||
|
testWidgetsWithLeakTracking('foreground color by default', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: colorScheme,
|
||||||
|
),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextSelectionToolbarTextButton(
|
||||||
|
padding: TextSelectionToolbarTextButton.getPadding(0, 1),
|
||||||
|
child: const Text('button'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextButton), findsOneWidget);
|
||||||
|
|
||||||
|
final TextButton textButton = tester.widget(find.byType(TextButton));
|
||||||
|
// The foreground color is hardcoded to black or white by default, not the
|
||||||
|
// default value from ColorScheme.onSurface.
|
||||||
|
expect(
|
||||||
|
textButton.style!.foregroundColor!.resolve(<MaterialState>{}),
|
||||||
|
switch (colorScheme.brightness) {
|
||||||
|
Brightness.light => const Color(0xff000000),
|
||||||
|
Brightness.dark => const Color(0xffffffff),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgetsWithLeakTracking('custom foreground color', (WidgetTester tester) async {
|
||||||
|
const Color customForegroundColor = Colors.red;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
colorScheme: colorScheme.copyWith(
|
||||||
|
onSurface: customForegroundColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: TextSelectionToolbarTextButton(
|
||||||
|
padding: TextSelectionToolbarTextButton.getPadding(0, 1),
|
||||||
|
child: const Text('button'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.byType(TextButton), findsOneWidget);
|
||||||
|
|
||||||
|
final TextButton textButton = tester.widget(find.byType(TextButton));
|
||||||
|
expect(
|
||||||
|
textButton.style!.foregroundColor!.resolve(<MaterialState>{}),
|
||||||
|
customForegroundColor,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user