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/rendering.dart';
|
||||
|
||||
import 'color_scheme.dart';
|
||||
import 'debug.dart';
|
||||
import 'icon_button.dart';
|
||||
import 'icons.dart';
|
||||
import 'material.dart';
|
||||
import 'material_localizations.dart';
|
||||
import 'theme.dart';
|
||||
|
||||
const double _kToolbarHeight = 44.0;
|
||||
const double _kToolbarContentDistance = 8.0;
|
||||
@ -650,13 +652,34 @@ class _TextSelectionToolbarContainer extends StatelessWidget {
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
final ThemeData theme = Theme.of(context);
|
||||
return Material(
|
||||
// This value was eyeballed to match the native text selection menu on
|
||||
// a Pixel 2 running Android 10.
|
||||
borderRadius: const BorderRadius.all(Radius.circular(7.0)),
|
||||
// a Pixel 6 emulator running Android API level 34.
|
||||
borderRadius: const BorderRadius.all(Radius.circular(_kToolbarHeight / 2)),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: _getColor(theme.colorScheme),
|
||||
elevation: 1.0,
|
||||
type: MaterialType.card,
|
||||
child: child,
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'colors.dart';
|
||||
import 'color_scheme.dart';
|
||||
import 'constants.dart';
|
||||
import 'text_button.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
|
||||
Widget build(BuildContext context) {
|
||||
// TODO(hansmuller): Should be colorScheme.onSurface
|
||||
final ThemeData theme = Theme.of(context);
|
||||
final bool isDark = theme.colorScheme.brightness == Brightness.dark;
|
||||
final Color foregroundColor = isDark ? Colors.white : Colors.black87;
|
||||
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: foregroundColor,
|
||||
foregroundColor: _getForegroundColor(colorScheme),
|
||||
shape: const RoundedRectangleBorder(),
|
||||
minimumSize: const Size(kMinInteractiveDimension, kMinInteractiveDimension),
|
||||
padding: padding,
|
||||
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,
|
||||
child: child,
|
||||
|
@ -204,4 +204,93 @@ void main() {
|
||||
expect(find.text('Paste'), findsNothing);
|
||||
expect(find.text('Select all'), findsNothing);
|
||||
}, 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(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