Skip to content

Conversation

@HansMuller
Copy link
Contributor

@HansMuller HansMuller commented Sep 19, 2018

The goal of this PR is to introduce an overall material "color scheme" and to make it possible to redefine how a component's visual properties bind to the color scheme and to the existing material Theme.

This PR addresses the goal, but only for RaisedButton, FlatButton, OutlineButton. Hopefully it defines a pattern that can be extended to other components.

The new ColorScheme class is essentially a struct with 13 Material Design colors. ThemeData and ButtonThemeData now have-a ColorScheme. In theory all of the colors in all of the components are defined by these colors. In practice, for the sake backwards compatibility, the Theme's buttonColor, disabledColor, highlightColor, and splashColor are also used if specified.

With one exception (the button text style) all of the buttons' visual properties are computed by ButtonThemeData. In all cases widget constructor parameters override the theme values.

All three button classes now inherit from MaterialButton. The .icon factory constructors, like RaisedButton.icon() create a new subclass, like RaisedButtonWithIcon, so that ButtonTheme can always compute colors based on the "type" of its MaterialButton.

All three button classes just configure an internal RawMaterialButton (as before). RawMaterialButtons do not depend on any theme.

The following new ButtonThemeData methods are used to configure the RawMaterialButton:

 Brightness getBrightness(MaterialButton button)
 ButtonTextTheme getTextTheme(MaterialButton button)
 Color getDisabledTextColor(MaterialButton button)
 Color getTextColor(MaterialButton button)
 Color getSplashColor(MaterialButton button)
 Color getHighlightColor(MaterialButton button)
 double getElevation(MaterialButton button)
 double getHighlightElevation(MaterialButton button)
 double getDisabledElevation(MaterialButton button)
 EdgeInsetsGeometry getPadding(MaterialButton button)
 ShapeBorder getShape(MaterialButton button)
 Duration getAnimationDuration(MaterialButton button)
 MaterialTapTargetSize getMaterialTapTargetSize(MaterialButton button)

This is RaisedButton's build method (it's typical).

@override
Widget build(BuildContext context) {
  final ThemeData theme = Theme.of(context);
  final ButtonThemeData buttonTheme = ButtonTheme.of(context);

  return RawMaterialButton(
    onPressed: onPressed,
    onHighlightChanged: onHighlightChanged,
    clipBehavior: clipBehavior,
    fillColor: buttonTheme.getFillColor(this),
    textStyle: theme.textTheme.button.copyWith(color: buttonTheme.getTextColor(this)),
    highlightColor: buttonTheme.getHighlightColor(this),
    splashColor: buttonTheme.getSplashColor(this),
    elevation: buttonTheme.getElevation(this),
    highlightElevation: buttonTheme.getHighlightElevation(this),
    disabledElevation: buttonTheme.getDisabledElevation(this),
    padding: buttonTheme.getPadding(this),
    constraints: buttonTheme.getConstraints(this),
    shape: buttonTheme.getShape(this),
    animationDuration: buttonTheme.getAnimationDuration(this),
    materialTapTargetSize: buttonTheme.getMaterialTapTargetSize(this),
    child: child,
  );
}

One consequence of moving this code from the button classes to ButtonTheme has been to eliminate some duplicate code.

When Theme is an InheritedModel, buttons will only depend on its 'ButtonTheme' aspect.

This PR introduces some (hopefully small) visual incompatibilities.

  • The highlight (pressed) color is now the button's text color, with opacity 0.16. Previously it was Color(0x66BCBCBC).
  • The ink splash color is now the button's text color, with opacity 0.12. Previously it was Color(0x66C8C8C8).

There's also one significant concession to backwards compatibility: ButtonTheme.buttonColor. This value is only used as the fill color for RaisedButtons. Its old default value, Colors.grey[300] is used instead of the button theme's ColorScheme.primary color. Apps can avoid the concession by configuring their Theme's buttonTheme.buttonColor with the primary color (the Flutter Gallery does this).

Still TBD (although probably not in this PR):

ThemeData.copyWith() doesn't neccessarily do what you want. If its ColorScheme or one of the colors that ButtonTheme depends on is changed, then it will be necessary to rebuild the ButtonTheme. But copyWith is intended to be a simple field copying method. So we'll need a .apply copying method, as with TextTheme.

@willlarche
Copy link
Contributor

image

Copy link
Contributor

Choose a reason for hiding this comment

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

In general, I'm a little skeptical of string based APIs. I can definitely see the value in essentially having a tag/selector like value for determining theme values though. Is there an advantage over using an enumeration - besides visibility?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I agree. Enums have some serious disadvantages which put me off finding a better solution (than Strings):

  • They can't be extended. The "type" of subclasses of MaterialButton will be limited to the enum values we define.
  • They're difficult for us to extend. Adding an enum value later will break code that switches on the value and doesn't include a default clause.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some general concerns:

  • are we doing too much work here? For example, getTextTheme is called 5 or 6 times in various parts of the buttonTheme helper methods.

  • are the tags useful or a bit of a hack?

Consider an alternative design, where instead of having tags we have n different functions which take a MaterialButton and return some RawMaterialButton or other widget. This might be worse in someways, but I think it connects the tag directly to the produced theme value. It also still allows us to avoid computing theme values we don't need while avoiding repetitive calculations.

Might not matter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I don't think we're actually doing too much work, although it's true that some small computations are done repeatedly.

The current design, with its "type" property, is a compromise. It avoids the API explosion that would come from having a complete set of methods for RaisedButton, OutlineButton, FlatButton (and perhaps IconButton and FloatingActionButton in the future).

@HansMuller HansMuller force-pushed the color_scheme branch 2 times, most recently from b5fc167 to eebb5c8 Compare September 25, 2018 23:57
@HansMuller HansMuller changed the title [WIP] ColorScheme Added ColorScheme, updated THemeData, ButtonTheme, material buttons Sep 25, 2018
@HansMuller
Copy link
Contributor Author

@willlarche Currently the ColorScheme colors don't have doc strings. I'd appreciate it if you'd suggest some.

Currently primaryVariant and (oh boy) primaryVariant2 appear to be referred to as the "dark" and "light" variants in the "primary color" section of https://material.io/design/color/the-color-system.html. There's still time to call them "primaryDark" and "primaryLight", if only to make them easier to explain :-)

@zoechi zoechi changed the title Added ColorScheme, updated THemeData, ButtonTheme, material buttons Added ColorScheme, updated ThemeData, ButtonTheme, material buttons Sep 26, 2018
Copy link
Contributor

Choose a reason for hiding this comment

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

As long as button is exactly a FlatButton, I think it is faster to check the runtimeType than use an is check. Since the latter has to verify the full type hierarchy.

You could also pass the type as method generic:

double getElevation<T extends materialButton>(T button) {
  if (T == FlatButton) { ... }
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I guess the above wouldn't work if you wanted to delegate to MaterialButton, and it might not be obvious but inference could mess up users here:

class MyButton extends MaterialButton {

Widget build(_) => RawMaterialButton(foo: theme.getFoo(this)) // <- T is inferred as MyButton instead of MaterialButton
}

Copy link
Contributor Author

@HansMuller HansMuller Sep 26, 2018

Choose a reason for hiding this comment

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

If someone wants to write a button that's configured like a FlatButton but builds a little differently, they could subclass FlatButton and override its build method. FlatButtonWithIcon is essentially that.

The generic approach would be a little cheaper, and it would obviate creating classes like FlatButtonWithIcon. It's similar to an earlier approach I'd taken, where a value that represented the button's type was passed along as a MaterialButton property. It makes using the API a bit clunkier, although it also clarifies the meaning of each getFoo(button) method: getElevation(myFlatButton) is a little clearer that we want the FlatButton elevation.

I don't think this would be practical problem with the type parameter approach:

class MyButton extends MaterialButton {

  Widget build(_) => RawMaterialButton(foo: theme.getFoo(this)) // <- T is inferred as MyButton instead of MaterialButton
}

Only because ButtonThemeData doesn't discriminate based on is MaterialButton. The same scenario with a subclass of a type that is part of the rules, say FlatButton, would have to use theme.getFoo<FlatButton>(), to get the "right" answer. I don't know if that qualifies as a footgun or not.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think the advantage of the is checks is that they will just work. If it's slow we can always revisit

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is almost a one-liner I would use ?? instead.

@willlarche
Copy link
Contributor

@ajsecord can you resolve the questions in #22013 (comment) ?

@ajsecord
Copy link
Contributor

I can work with design to get descriptions of each of these colors. Don't block on me, I'll send a PR with the results. I agree about primaryVariant2, btw, it's a serious design smell.

@ajsecord
Copy link
Contributor

@HansMuller if you create an issue for the doc strings and assign me we'll have a better chance of landing them.

Copy link
Contributor

Choose a reason for hiding this comment

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

I have an idea to both reduce the number of is checks for the isIcon case and allow users to correctly extend an IconTheme.

Define an empty mixin called MaterialIconButton, or similar.

/// Something something indicates that we use icon theme.s
mixin MaterialIconButton {}

Then make FlatButtonWithIcon and friends mix this in instead of being public class/private constructor.

_FlatButtonWithIcon extends FlatButton with MaterialIconButton { ... }

You can then simplify the isIcon check to a single is:

 bool _isIconButton(MaterialButton button)  => button is MaterialIconButton

And users can mixin this class to make custom subclasses take the icon config as a bonus.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea. I added MaterialButtonWithIconMixin and made the FooButtonWithIcon classes private.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: here and else where it should be a lowercase null-the-value instead of uppcase Null-the-type

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, done.

Copy link
Contributor

Choose a reason for hiding this comment

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

For clarity, I would place the "If" first and drop the otherwise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I tried various permutations of this doc. It needs to convey the fact that the first non-null production is returned (I used "otherwise"). I agree that it's a little clunky as presented, and I wish the derivation of fillColor wasn't so complicated, but I haven't been able to improve it.

Copy link
Contributor

Choose a reason for hiding this comment

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

This concerns me a bit, we won't break tests that look for OutlineButton will we?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. The tricky Finder wasn't really needed, I've removed it.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should just leave this as part of the regular theme. While it needs to exist for backwards compatibility, ideally we don't want new apps to find it easy to set to anything besides padded.

Copy link
Contributor Author

@HansMuller HansMuller Oct 2, 2018

Choose a reason for hiding this comment

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

It's already a property of the button classes and it needs to be funneled through ButtonTheme so that buttons can (just) depend on ButtonThemeData and not on the overall Theme.

I've made the ButtonThemeData.materialTapTargetSize property private. It's initialized by the constructor, which will usually be called by ThemeData, and returned by getMaterialTapTargetSize() as before.

It seemed like a good idea to similarly not expose the new (color) ButtonThemeData color parameters as properties, since they're available via the getFoo(button) methods. I've made them private as well.

Copy link
Contributor

Choose a reason for hiding this comment

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

I believe you can use the new mixin syntax now: mixin MaterialButtonWithIconMixin { } right @yjbanov ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The mixin syntax appears to work.

Copy link
Contributor

Choose a reason for hiding this comment

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

The analyzer and the linter still produce warnings on some valid code, but if you are not triggering those warnings, you should be OK.

Copy link
Contributor

@jonahwilliams jonahwilliams Oct 2, 2018

Choose a reason for hiding this comment

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

Is there a way we could detect a new material theme and switch to the constrained layout behavior?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

New themes can specify constrained layout behavior.

Copy link
Contributor

@jonahwilliams jonahwilliams Oct 2, 2018

Choose a reason for hiding this comment

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

Nit: here and next method, extra space

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like this doc comment got duplicated from above

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, copy and pasteo.

Copy link
Contributor

@jonahwilliams jonahwilliams left a comment

Choose a reason for hiding this comment

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

LGTM modulo nits and documentation for color scheme (now or later..)

mixin MaterialButtonWithIconMixin {
// This class is intended to be used as a mixin, and should not be
// extended directly.
factory MaterialButtonWithIconMixin._() => null;
Copy link
Contributor

Choose a reason for hiding this comment

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

You can remove the factory constructor trick, since it isn't possible to instantiate a mixin

@HansMuller HansMuller merged commit eea3465 into flutter:master Oct 2, 2018
@HansMuller HansMuller deleted the color_scheme branch October 2, 2018 21:50
@HansMuller
Copy link
Contributor Author

Fixed the dartdoc problems: #22592

@Hixie
Copy link
Contributor

Hixie commented Oct 8, 2018

Can we rename the onFoo colors (e.g. onSecondary) to something that doesn't look like a callback per the style guide naming convention?

@willlarche
Copy link
Contributor

willlarche commented Oct 9, 2018 via email

@HansMuller
Copy link
Contributor Author

The onFoo naming convention is described in the MD spec: https://material.io/design/color/the-color-system.html, search for: "On" colors (including the double quotes).

It's hard to say how confusing the spec'd names would actually be, given the clash with our conventions.

We could call them secondaryContrast (but that sounds like an aspect of the secondary color) or if we were seeking new naming lows "constrastsWithSecondary" or "legibleWhenPaintedOnSecondary". Maybe secondaryForeground?

matthew-carroll pushed a commit to matthew-carroll/flutter that referenced this pull request Oct 11, 2018
matthew-carroll added a commit that referenced this pull request Oct 12, 2018
For G3 Roll:

* Revert "MaterialButton must honor its minWidth and height parameters (#22919)"

This reverts commit a023323.

* Revert "Update uses of ButtonTheme.bar: pass along the current Theme's colorScheme (#22827)"

This reverts commit 655bf6a.

* Revert "ButtonTheme.of().colorScheme defers to Theme (#22880)"

This reverts commit a590940.

* Revert "Bring TextTheme into alignment with the current Material spec (#22330)"

This reverts commit 8bfb4b3.

* Revert "Added ColorScheme, updated ThemeData, ButtonTheme, material buttons (#22013)"

This reverts commit eea3465.

* Manual adjustments to fix reversion issues.
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 9, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants