Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dev/tools/gen_defaults/bin/gen_defaults.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import 'package:gen_defaults/button_template.dart';
import 'package:gen_defaults/card_template.dart';
import 'package:gen_defaults/dialog_template.dart';
import 'package:gen_defaults/fab_template.dart';
import 'package:gen_defaults/icon_button_template.dart';
import 'package:gen_defaults/navigation_bar_template.dart';
import 'package:gen_defaults/navigation_rail_template.dart';
import 'package:gen_defaults/surface_tint.dart';
Expand Down Expand Up @@ -55,6 +56,9 @@ Future<void> main(List<String> args) async {
'fab_large_primary.json',
'fab_primary.json',
'fab_small_primary.json',
'icon_button.json',
'icon_button_filled.json',
'icon_button_filled_tonal.json',
'motion.json',
'navigation_bar.json',
'navigation_rail.json',
Expand Down Expand Up @@ -86,6 +90,7 @@ Future<void> main(List<String> args) async {
CardTemplate('$materialLib/card.dart', tokens).updateFile();
DialogTemplate('$materialLib/dialog.dart', tokens).updateFile();
FABTemplate('$materialLib/floating_action_button.dart', tokens).updateFile();
IconButtonTemplate('$materialLib/icon_button.dart', tokens).updateFile();
NavigationBarTemplate('$materialLib/navigation_bar.dart', tokens).updateFile();
NavigationRailTemplate('$materialLib/navigation_rail.dart', tokens).updateFile();
SurfaceTintTemplate('$materialLib/elevation_overlay.dart', tokens).updateFile();
Expand Down
99 changes: 99 additions & 0 deletions dev/tools/gen_defaults/lib/icon_button_template.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2014 The Flutter 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 'template.dart';

class IconButtonTemplate extends TokenTemplate {
const IconButtonTemplate(super.fileName, super.tokens)
: super(colorSchemePrefix: '_colors.',
);

@override
String generate() => '''
// Generated version ${tokens["version"]}
class _TokenDefaultsM3 extends ButtonStyle {
_TokenDefaultsM3(this.context)
: super(
animationDuration: kThemeChangeDuration,
enableFeedback: true,
alignment: Alignment.center,
);

final BuildContext context;
late final ColorScheme _colors = Theme.of(context).colorScheme;

// No default text style

@override
MaterialStateProperty<Color?>? get backgroundColor =>
ButtonStyleButton.allOrNull<Color>(Colors.transparent);

@override
MaterialStateProperty<Color?>? get foregroundColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return ${componentColor('md.comp.icon-button.disabled.icon')};
return ${componentColor('md.comp.icon-button.unselected.icon')};
});

@override
MaterialStateProperty<Color?>? get overlayColor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered))
return ${componentColor('md.comp.icon-button.unselected.hover.state-layer')};
if (states.contains(MaterialState.focused))
return ${componentColor('md.comp.icon-button.unselected.focus.state-layer')};
if (states.contains(MaterialState.pressed))
return ${componentColor('md.comp.icon-button.unselected.pressed.state-layer')};
return null;
});

// No default shadow color

// No default surface tint color

@override
MaterialStateProperty<double>? get elevation =>
ButtonStyleButton.allOrNull<double>(0.0);

@override
MaterialStateProperty<EdgeInsetsGeometry>? get padding =>
ButtonStyleButton.allOrNull<EdgeInsetsGeometry>(const EdgeInsets.all(8.0));

@override
MaterialStateProperty<Size>? get minimumSize =>
ButtonStyleButton.allOrNull<Size>(const Size(${tokens["md.comp.icon-button.state-layer.size"]}, ${tokens["md.comp.icon-button.state-layer.size"]}));

// No default fixedSize

@override
MaterialStateProperty<Size>? get maximumSize =>
ButtonStyleButton.allOrNull<Size>(Size.infinite);

// No default side

@override
MaterialStateProperty<OutlinedBorder>? get shape =>
ButtonStyleButton.allOrNull<OutlinedBorder>(${shape("md.comp.icon-button.state-layer")});

@override
MaterialStateProperty<MouseCursor?>? get mouseCursor =>
MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.disabled))
return SystemMouseCursors.basic;
return SystemMouseCursors.click;
});

@override
VisualDensity? get visualDensity => Theme.of(context).visualDensity;

@override
MaterialTapTargetSize? get tapTargetSize => Theme.of(context).materialTapTargetSize;

@override
InteractiveInkFeatureFactory? get splashFactory => Theme.of(context).splashFactory;
}
''';

}
118 changes: 118 additions & 0 deletions examples/api/lib/material/icon_button/icon_button.2.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// Flutter code sample for IconButton

import 'package:flutter/material.dart';

void main() {
runApp(const IconButtonApp());
}

class IconButtonApp extends StatelessWidget {
const IconButtonApp({super.key});

@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: const Color(0xff6750a4), useMaterial3: true),
title: 'Icon Button Types',
home: const Scaffold(
body: ButtonTypesExample(),
),
);
}
}

class ButtonTypesExample extends StatelessWidget {
const ButtonTypesExample({super.key});

@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(4.0),
child: Row(
children: const <Widget>[
Spacer(),
ButtonTypesGroup(enabled: true),
ButtonTypesGroup(enabled: false),
Spacer(),
],
),
);
}
}

class ButtonTypesGroup extends StatelessWidget {
const ButtonTypesGroup({ super.key, required this.enabled });

final bool enabled;

@override
Widget build(BuildContext context) {
final VoidCallback? onPressed = enabled ? () {} : null;
final ColorScheme colors = Theme.of(context).colorScheme;

return Padding(
padding: const EdgeInsets.all(4.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
IconButton(icon: const Icon(Icons.filter_drama), onPressed: onPressed),

// Use a standard IconButton with specific style to implement the
// 'Filled' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
foregroundColor: colors.onPrimary,
backgroundColor: colors.primary,
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
hoverColor: colors.onPrimary.withOpacity(0.08),
focusColor: colors.onPrimary.withOpacity(0.12),
highlightColor: colors.onPrimary.withOpacity(0.12),
Comment on lines +69 to +75
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use tokens for these. We should also provide the three default styles, so that devs don't have to copy paste and have a nice starting point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we have a similar problem with the variants in the common buttons as well. I didn't realize the variants would need that much configuration.

I really wish we had a simpler way of providing these variants without a lot of extra API baggage to go with them (i.e. either full button subclasses for each variant, or named constructors that have to duplicate all the constructor parameters and use an enum or something to indicate which set of defaults to use in the build method).

That said, I would go with this example for this PR and we can look at cleaning up the variant support in a future PR.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current Flutter master channel (9160359), IconButton.styleFrom gives me stadium shapes by default. This is consistent with icon_button.dart#L614, below.

I believe the spec for md.sys.shape.corner.full indicates they should use shape: const CircleBorder() instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, the icon button is migrated to be a subclass of ButtonStyleButton now and has a compact VisualDensity on desktop platform, so the buttons don't look like circular. If the app is ran on iOS/Android, the buttons will show a standard VisualDensity and have a circular shape.

If this is the problem that you are facing, adding visualDensity: VisualDensity.standard to IconButton or IconButton.styleFrom() will solve the problem on desktop platform:)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@QuncCccccc Interesting, thanks! I didn't appreciate that property. The spec does have a "high-emphasis filled button for End call" in an example, but it's a bit subtle.

For reference, I see the issue in Flutter web.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I assumed you are using a desktop platform. Both desktop and web have a compact visual density as a default value because they have a larger screen, compared to phone devices. So on web, just adding same thing as desktop (visualDensity: VisualDensity.standard) will provide a circular icon button:)

Please let me know if there's any other problems.

)
),

// Use a standard IconButton with specific style to implement the
// 'Filled Tonal' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
foregroundColor: colors.onSecondaryContainer,
backgroundColor: colors.secondaryContainer,
disabledBackgroundColor: colors.onSurface.withOpacity(0.12),
hoverColor: colors.onSecondaryContainer.withOpacity(0.08),
focusColor: colors.onSecondaryContainer.withOpacity(0.12),
highlightColor: colors.onSecondaryContainer.withOpacity(0.12),
),
),

// Use a standard IconButton with specific style to implement the
// 'Outlined' type.
IconButton(
icon: const Icon(Icons.filter_drama),
onPressed: onPressed,
style: IconButton.styleFrom(
focusColor: colors.onSurfaceVariant.withOpacity(0.12),
highlightColor: colors.onSurface.withOpacity(0.12),
side: onPressed == null
? BorderSide(color: Theme.of(context).colorScheme.onSurface.withOpacity(0.12))
: BorderSide(color: colors.outline),
).copyWith(
foregroundColor: MaterialStateProperty.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.pressed)) {
return colors.onSurface;
}
return null;
}),
),
),
],
),
);
}
}
Loading