Adaptive icons (#69119)
* refactor update_icons
* fix trailing space
* address feedback
* add platform adaptive icons
* fix merge conflict
* Update dartdoc
* Address feedback
* Specify types
* Add tests
* fix indentation
* Remove trailing space
* Remove isCupertino static bool
diff --git a/dev/tools/update_icons.dart b/dev/tools/update_icons.dart
index 41a5c4c..cc0bed7 100644
--- a/dev/tools/update_icons.dart
+++ b/dev/tools/update_icons.dart
@@ -20,8 +20,20 @@
const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints';
const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart';
-const String _beginGeneratedMark = '// BEGIN GENERATED';
-const String _endGeneratedMark = '// END GENERATED';
+const String _beginGeneratedMark = '// BEGIN GENERATED ICONS';
+const String _endGeneratedMark = '// END GENERATED ICONS';
+const String _beginPlatformAdaptiveGeneratedMark = '// BEGIN GENERATED PLATFORM ADAPTIVE ICONS';
+const String _endPlatformAdaptiveGeneratedMark = '// END GENERATED PLATFORM ADAPTIVE ICONS';
+
+const Map<String, List<String>> _platformAdaptiveIdentifiers = <String, List<String>>{
+ // Mapping of Flutter IDs to an Android/agnostic ID and an iOS ID.
+ // Flutter IDs can be anything, but should be chosen to be agnostic.
+ 'arrow_back': <String>['arrow_back', 'arrow_back_ios'],
+ 'arrow_forward': <String>['arrow_forward', 'arrow_forward_ios'],
+ 'flip_camera': <String>['flip_camera_android', 'flip_camera_ios'],
+ 'more': <String>['more_vert', 'more_horiz'],
+ 'share': <String>['share', 'ios_share'],
+};
const Map<String, String> _identifierRewrites = <String, String>{
'360': 'threesixty',
@@ -231,20 +243,54 @@
// Do not make this method private as it is used by g3 roll.
String regenerateIconsFile(String iconData, Map<String, String> tokenPairMap) {
+ final Iterable<_Icon> newIcons = tokenPairMap.entries.map((MapEntry<String, String> entry) => _Icon(entry));
final StringBuffer buf = StringBuffer();
bool generating = false;
+
for (final String line in LineSplitter.split(iconData)) {
if (!generating) {
buf.writeln(line);
}
- if (line.contains(_beginGeneratedMark)) {
+
+ // Generate for _PlatformAdaptiveIcons
+ if (line.contains(_beginPlatformAdaptiveGeneratedMark)) {
generating = true;
- final String iconDeclarationsString = <String>[
- for (MapEntry<String, String> entry in tokenPairMap.entries)
- _Icon(entry).fullDeclaration
- ].join();
+ final List<String> platformAdaptiveDeclarations = <String>[];
+ _platformAdaptiveIdentifiers.forEach((String flutterId, List<String> ids) {
+ // Automatically finds and generates styled icon declarations.
+ for (final IconStyle iconStyle in IconStyle.values) {
+ final String style = iconStyle.idSuffix();
+ try {
+ final _Icon agnosticIcon = newIcons.firstWhere(
+ (_Icon icon) => icon.id == '${ids[0]}$style',
+ orElse: () => throw ids[0]);
+ final _Icon iOSIcon = newIcons.firstWhere(
+ (_Icon icon) => icon.id == '${ids[1]}$style',
+ orElse: () => throw ids[1]);
+ platformAdaptiveDeclarations.add(_Icon.platformAdaptiveDeclaration('$flutterId$style', agnosticIcon, iOSIcon));
+ } catch (e) {
+ if (iconStyle == IconStyle.regular) {
+ stderr.writeln("Error while generating platformAdaptiveDeclarations: Icon '$e' not found.");
+ exit(1);
+ } else {
+ // Ignore errors for styled icons since some don't exist.
+ }
+ }
+ }
+ });
+
+ buf.write(platformAdaptiveDeclarations.join());
+ } else if (line.contains(_endPlatformAdaptiveGeneratedMark)) {
+ generating = false;
+ buf.writeln(line);
+ }
+
+ // Generate for Icons
+ if (line.contains(_beginGeneratedMark)) {
+ generating = true;
+ final String iconDeclarationsString = newIcons.map((_Icon icon) => icon.fullDeclaration).join('');
buf.write(iconDeclarationsString);
} else if (line.contains(_endGeneratedMark)) {
generating = false;
@@ -275,9 +321,9 @@
sharp,
}
-extension IconStyleSuffix on IconStyle {
+extension IconStyleExtension on IconStyle {
// The suffix for the 'material-icons' HTML class.
- String suffix() {
+ String htmlSuffix() {
switch (this) {
case IconStyle.outlined: return '-outlined';
case IconStyle.rounded: return '-round';
@@ -285,6 +331,17 @@
default: return '';
}
}
+
+ // The suffix for icon ids.
+ String idSuffix() {
+ switch (this) {
+ case IconStyle.outlined:
+ case IconStyle.rounded:
+ case IconStyle.sharp:
+ return '_' + toString().split('.').last;
+ default: return '';
+ }
+ }
}
class _Icon {
@@ -332,12 +389,25 @@
String get name => id.replaceAll('_', ' ');
String get dartDoc =>
- '/// <i class="material-icons${style.suffix()} md-36">$shortId</i> — material icon named "$name".';
+ '<i class="material-icons${style.htmlSuffix()} md-36">$shortId</i> — material icon named "$name"';
String get declaration =>
"static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: 'MaterialIcons'$mirroredInRTL);";
- String get fullDeclaration => '''\n $dartDoc\n $declaration\n''';
+ String get fullDeclaration => '''
+
+ /// $dartDoc.
+ $declaration
+''';
+
+ static String platformAdaptiveDeclaration(String flutterId, _Icon agnosticIcon, _Icon iOSIcon) => '''
+
+ /// Platform-adaptive icon for ${agnosticIcon.dartDoc} and ${iOSIcon.dartDoc}.;
+ IconData get $flutterId => !_isCupertino() ? Icons.${agnosticIcon.flutterId} : Icons.${iOSIcon.flutterId};
+''';
+
+ @override
+ String toString() => id;
}
// Replace the old codepoints file with the new.
diff --git a/packages/flutter/lib/src/material/icons.dart b/packages/flutter/lib/src/material/icons.dart
index f39bc80..65d5880 100644
--- a/packages/flutter/lib/src/material/icons.dart
+++ b/packages/flutter/lib/src/material/icons.dart
@@ -2,8 +2,81 @@
// 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' show defaultTargetPlatform;
import 'package:flutter/widgets.dart';
+// ignore_for_file: non_constant_identifier_names
+class _PlatformAdaptiveIcons {
+ static bool _isCupertino() {
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.linux:
+ case TargetPlatform.windows:
+ return false;
+ case TargetPlatform.iOS:
+ case TargetPlatform.macOS:
+ return true;
+ }
+ }
+
+ // Generated code: do not hand-edit.
+ // See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts
+ // BEGIN GENERATED PLATFORM ADAPTIVE ICONS
+
+ /// Platform-adaptive icon for <i class="material-icons md-36">arrow_back</i> — material icon named "arrow back" and <i class="material-icons md-36">arrow_back_ios</i> — material icon named "arrow back ios".;
+ IconData get arrow_back => !_isCupertino() ? Icons.arrow_back : Icons.arrow_back_ios;
+
+ /// Platform-adaptive icon for <i class="material-icons-outlined md-36">arrow_back</i> — material icon named "arrow back outlined" and <i class="material-icons-outlined md-36">arrow_back_ios</i> — material icon named "arrow back ios outlined".;
+ IconData get arrow_back_outlined => !_isCupertino() ? Icons.arrow_back_outlined : Icons.arrow_back_ios_outlined;
+
+ /// Platform-adaptive icon for <i class="material-icons-round md-36">arrow_back</i> — material icon named "arrow back rounded" and <i class="material-icons-round md-36">arrow_back_ios</i> — material icon named "arrow back ios rounded".;
+ IconData get arrow_back_rounded => !_isCupertino() ? Icons.arrow_back_rounded : Icons.arrow_back_ios_rounded;
+
+ /// Platform-adaptive icon for <i class="material-icons-sharp md-36">arrow_back</i> — material icon named "arrow back sharp" and <i class="material-icons-sharp md-36">arrow_back_ios</i> — material icon named "arrow back ios sharp".;
+ IconData get arrow_back_sharp => !_isCupertino() ? Icons.arrow_back_sharp : Icons.arrow_back_ios_sharp;
+
+ /// Platform-adaptive icon for <i class="material-icons md-36">arrow_forward</i> — material icon named "arrow forward" and <i class="material-icons md-36">arrow_forward_ios</i> — material icon named "arrow forward ios".;
+ IconData get arrow_forward => !_isCupertino() ? Icons.arrow_forward : Icons.arrow_forward_ios;
+
+ /// Platform-adaptive icon for <i class="material-icons-outlined md-36">arrow_forward</i> — material icon named "arrow forward outlined" and <i class="material-icons-outlined md-36">arrow_forward_ios</i> — material icon named "arrow forward ios outlined".;
+ IconData get arrow_forward_outlined => !_isCupertino() ? Icons.arrow_forward_outlined : Icons.arrow_forward_ios_outlined;
+
+ /// Platform-adaptive icon for <i class="material-icons-round md-36">arrow_forward</i> — material icon named "arrow forward rounded" and <i class="material-icons-round md-36">arrow_forward_ios</i> — material icon named "arrow forward ios rounded".;
+ IconData get arrow_forward_rounded => !_isCupertino() ? Icons.arrow_forward_rounded : Icons.arrow_forward_ios_rounded;
+
+ /// Platform-adaptive icon for <i class="material-icons-sharp md-36">arrow_forward</i> — material icon named "arrow forward sharp" and <i class="material-icons-sharp md-36">arrow_forward_ios</i> — material icon named "arrow forward ios sharp".;
+ IconData get arrow_forward_sharp => !_isCupertino() ? Icons.arrow_forward_sharp : Icons.arrow_forward_ios_sharp;
+
+ /// Platform-adaptive icon for <i class="material-icons md-36">flip_camera_android</i> — material icon named "flip camera android" and <i class="material-icons md-36">flip_camera_ios</i> — material icon named "flip camera ios".;
+ IconData get flip_camera => !_isCupertino() ? Icons.flip_camera_android : Icons.flip_camera_ios;
+
+ /// Platform-adaptive icon for <i class="material-icons-outlined md-36">flip_camera_android</i> — material icon named "flip camera android outlined" and <i class="material-icons-outlined md-36">flip_camera_ios</i> — material icon named "flip camera ios outlined".;
+ IconData get flip_camera_outlined => !_isCupertino() ? Icons.flip_camera_android_outlined : Icons.flip_camera_ios_outlined;
+
+ /// Platform-adaptive icon for <i class="material-icons-round md-36">flip_camera_android</i> — material icon named "flip camera android rounded" and <i class="material-icons-round md-36">flip_camera_ios</i> — material icon named "flip camera ios rounded".;
+ IconData get flip_camera_rounded => !_isCupertino() ? Icons.flip_camera_android_rounded : Icons.flip_camera_ios_rounded;
+
+ /// Platform-adaptive icon for <i class="material-icons-sharp md-36">flip_camera_android</i> — material icon named "flip camera android sharp" and <i class="material-icons-sharp md-36">flip_camera_ios</i> — material icon named "flip camera ios sharp".;
+ IconData get flip_camera_sharp => !_isCupertino() ? Icons.flip_camera_android_sharp : Icons.flip_camera_ios_sharp;
+
+ /// Platform-adaptive icon for <i class="material-icons md-36">more_vert</i> — material icon named "more vert" and <i class="material-icons md-36">more_horiz</i> — material icon named "more horiz".;
+ IconData get more => !_isCupertino() ? Icons.more_vert : Icons.more_horiz;
+
+ /// Platform-adaptive icon for <i class="material-icons-outlined md-36">more_vert</i> — material icon named "more vert outlined" and <i class="material-icons-outlined md-36">more_horiz</i> — material icon named "more horiz outlined".;
+ IconData get more_outlined => !_isCupertino() ? Icons.more_vert_outlined : Icons.more_horiz_outlined;
+
+ /// Platform-adaptive icon for <i class="material-icons-round md-36">more_vert</i> — material icon named "more vert rounded" and <i class="material-icons-round md-36">more_horiz</i> — material icon named "more horiz rounded".;
+ IconData get more_rounded => !_isCupertino() ? Icons.more_vert_rounded : Icons.more_horiz_rounded;
+
+ /// Platform-adaptive icon for <i class="material-icons-sharp md-36">more_vert</i> — material icon named "more vert sharp" and <i class="material-icons-sharp md-36">more_horiz</i> — material icon named "more horiz sharp".;
+ IconData get more_sharp => !_isCupertino() ? Icons.more_vert_sharp : Icons.more_horiz_sharp;
+
+ /// Platform-adaptive icon for <i class="material-icons md-36">share</i> — material icon named "share" and <i class="material-icons md-36">ios_share</i> — material icon named "ios share".;
+ IconData get share => !_isCupertino() ? Icons.share : Icons.ios_share;
+ // END GENERATED PLATFORM ADAPTIVE ICONS
+}
+
/// Identifiers for the supported material design icons.
///
/// Use with the [Icon] class to show specific icons.
@@ -64,9 +137,35 @@
// ignore: unused_element
Icons._();
+ /// A set of platform-adaptive material design icons.
+ ///
+ /// Provides a convenient way to show a certain set of platform-appropriate
+ /// icons on Apple platforms.
+ ///
+ /// Use with the [Icon] class to show specific icons.
+ ///
+ /// {@tool snippet}
+ /// This example shows how to create a share icon that uses the material icon
+ /// named "share" on non-Apple platforms, and the icon named "ios share" on
+ /// Apple platforms.
+ ///
+ /// ```dart
+ /// Icon(
+ /// Icons.adaptive.share,
+ /// )
+ /// ```
+ /// {@end-tool}
+ ///
+ /// See also:
+ ///
+ /// * [Icon]
+ /// * [IconButton]
+ /// * <https://design.google.com/icons/>
+ static _PlatformAdaptiveIcons get adaptive => _PlatformAdaptiveIcons();
+
// Generated code: do not hand-edit.
// See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts
- // BEGIN GENERATED
+ // BEGIN GENERATED ICONS
/// <i class="material-icons md-36">10k</i> — material icon named "10k".
static const IconData ten_k = IconData(0xe52a, fontFamily: 'MaterialIcons');
@@ -17029,5 +17128,5 @@
/// <i class="material-icons-sharp md-36">zoom_out</i> — material icon named "zoom out sharp".
static const IconData zoom_out_sharp = IconData(0xf02d, fontFamily: 'MaterialIcons');
- // END GENERATED
+ // END GENERATED ICONS
}
diff --git a/packages/flutter/test/material/icons_test.dart b/packages/flutter/test/material/icons_test.dart
index 06a30a7..6ba3989 100644
--- a/packages/flutter/test/material/icons_test.dart
+++ b/packages/flutter/test/material/icons_test.dart
@@ -16,4 +16,29 @@
expect(Icons.clear.fontFamily, 'MaterialIcons');
expect(Icons.search.fontFamily, 'MaterialIcons');
});
+
+ testWidgets('Adaptive icons are correct on cupertino platforms',
+ (WidgetTester tester) async {
+ expect(Icons.adaptive.arrow_back, Icons.arrow_back_ios);
+ expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_ios_outlined);
+ },
+ variant: const TargetPlatformVariant(<TargetPlatform>{
+ TargetPlatform.iOS,
+ TargetPlatform.macOS,
+ }),
+ );
+
+ testWidgets(
+ 'Adaptive icons are correct on non-cupertino platforms',
+ (WidgetTester tester) async {
+ expect(Icons.adaptive.arrow_back, Icons.arrow_back);
+ expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_outlined);
+ },
+ variant: const TargetPlatformVariant(<TargetPlatform>{
+ TargetPlatform.android,
+ TargetPlatform.fuchsia,
+ TargetPlatform.windows,
+ TargetPlatform.linux,
+ }),
+ );
}