Unverified Commit ebbb4b38 authored by Justin McCandless's avatar Justin McCandless Committed by GitHub

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>
parent 9cc4f943
......@@ -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,
);
});
}
}
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