Commit 73c9ebab authored by Ian Hickson's avatar Ian Hickson

Reimplement the theme transition animation by actually animating the Theme.

As part of this:
 - A lot of classes got new lerp functions, including e.g. TextStyle.
 - Theme's constructor story got overhauled. You can now configure
   everything if you really want to, and we're better about defaults.
 - Material no longer automatically animates its background color.
   (It still does for its shadow.)
 - Tabs try to get the indicator color from the theme.
 - The fields in ThemeData got reordered for sanity.
 - Theme.== and Theme.hashCode got fixed.
 - Typography got a bit of a spring cleaning.

Fixes #613.
parent 098249f7
......@@ -8,6 +8,12 @@ class IconThemeData {
const IconThemeData({ this.color });
final IconThemeColor color;
static IconThemeData lerp(IconThemeData begin, IconThemeData end, double t) {
return new IconThemeData(
color: t < 0.5 ? begin.color : end.color
);
}
bool operator ==(dynamic other) {
if (other is! IconThemeData)
return false;
......
......@@ -160,12 +160,18 @@ class _MaterialState extends State<Material> {
curve: Curves.ease,
duration: kThemeChangeDuration,
decoration: new BoxDecoration(
backgroundColor: backgroundColor,
borderRadius: kMaterialEdges[config.type],
boxShadow: config.elevation == 0 ? null : elevationToShadow[config.elevation],
shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
),
child: new Container(
decoration: new BoxDecoration(
borderRadius: kMaterialEdges[config.type],
backgroundColor: backgroundColor,
shape: config.type == MaterialType.circle ? BoxShape.circle : BoxShape.rectangle
),
child: contents
)
);
}
return contents;
......
......@@ -141,8 +141,9 @@ class _MaterialAppState extends State<MaterialApp> implements BindingObserver {
data: new MediaQueryData(size: _size),
child: new LocaleQuery(
data: _localeData,
child: new Theme(
child: new AnimatedTheme(
data: theme,
duration: kThemeAnimationDuration,
child: new DefaultTextStyle(
style: _errorTextStyle,
child: new DefaultAssetBundle(
......
......@@ -717,9 +717,17 @@ class _TabBarState<T> extends ScrollableState<TabBar<T>> implements TabBarSelect
ThemeData themeData = Theme.of(context);
Color backgroundColor = Material.of(context).color;
Color indicatorColor = themeData.accentColor;
if (indicatorColor == backgroundColor)
Color indicatorColor = themeData.indicatorColor;
if (indicatorColor == backgroundColor) {
// ThemeData tries to avoid this by having indicatorColor avoid being the
// primaryColor. However, it's possible that the tab strip is on a
// Material that isn't the primaryColor. In that case, if the indicator
// color ends up clashing, then this overrides it. When that happens,
// automatic transitions of the theme will likely look ugly as the
// indicator color suddenly snaps to white at one end, but it's not clear
// how to avoid that any further.
indicatorColor = Colors.white;
}
TextStyle textStyle = themeData.primaryTextTheme.body1;
IconThemeData iconTheme = themeData.primaryIconTheme;
......
......@@ -2,12 +2,15 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/animation.dart';
import 'package:flutter/widgets.dart';
import 'theme_data.dart';
export 'theme_data.dart' show ThemeData, ThemeBrightness;
const kThemeAnimationDuration = const Duration(milliseconds: 200);
class Theme extends InheritedWidget {
Theme({
Key key,
......@@ -37,3 +40,55 @@ class Theme extends InheritedWidget {
description.add('$data');
}
}
/// An animated value that interpolates [BoxConstraint]s.
class AnimatedThemeDataValue extends AnimatedValue<ThemeData> {
AnimatedThemeDataValue(ThemeData begin, { ThemeData end, Curve curve, Curve reverseCurve })
: super(begin, end: end, curve: curve, reverseCurve: reverseCurve);
ThemeData lerp(double t) => ThemeData.lerp(begin, end, t);
}
/// Animated version of [Theme] which automatically transitions the colours,
/// etc, over a given duration whenever the given theme changes.
class AnimatedTheme extends AnimatedWidgetBase {
AnimatedTheme({
Key key,
this.data,
Curve curve: Curves.linear,
Duration duration,
this.child
}) : super(key: key, curve: curve, duration: duration) {
assert(child != null);
assert(data != null);
}
final ThemeData data;
final Widget child;
_AnimatedThemeState createState() => new _AnimatedThemeState();
}
class _AnimatedThemeState extends AnimatedWidgetBaseState<AnimatedTheme> {
AnimatedThemeDataValue _data;
void forEachVariable(VariableVisitor visitor) {
// TODO(ianh): Use constructor tear-offs when it becomes possible
_data = visitor(_data, config.data, (dynamic value) => new AnimatedThemeDataValue(value));
assert(_data != null);
}
Widget build(BuildContext context) {
return new Theme(
child: config.child,
data: _data.value
);
}
void debugFillDescription(List<String> description) {
super.debugFillDescription(description);
if (_data != null)
description.add('$_data');
}
}
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show Color, hashValues;
import 'dart:ui' show Color, hashValues, hashList, lerpDouble;
import 'colors.dart';
import 'icon_theme_data.dart';
......@@ -29,41 +29,109 @@ const Color _kDarkThemeSplashColor = const Color(0x40CCCCCC);
class ThemeData {
ThemeData({
ThemeBrightness brightness: ThemeBrightness.light,
Map<int, Color> primarySwatch,
Color accentColor,
this.accentColorBrightness: ThemeBrightness.dark,
TextTheme text
}): this.brightness = brightness,
this.primarySwatch = primarySwatch,
primaryColorBrightness = primarySwatch == null ? brightness : ThemeBrightness.dark,
canvasColor = brightness == ThemeBrightness.dark ? Colors.grey[850] : Colors.grey[50],
cardColor = brightness == ThemeBrightness.dark ? Colors.grey[800] : Colors.white,
dividerColor = brightness == ThemeBrightness.dark ? const Color(0x1FFFFFFF) : const Color(0x1F000000),
// Some users want the pre-multiplied color, others just want the opacity.
hintColor = brightness == ThemeBrightness.dark ? const Color(0x42FFFFFF) : const Color(0x4C000000),
hintOpacity = brightness == ThemeBrightness.dark ? 0.26 : 0.30,
highlightColor = brightness == ThemeBrightness.dark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor,
splashColor = brightness == ThemeBrightness.dark ? _kDarkThemeSplashColor : _kLightThemeSplashColor,
text = brightness == ThemeBrightness.dark ? Typography.white : Typography.black {
ThemeData.raw({
this.brightness,
this.primarySwatch,
this.primaryColor,
this.primaryColorBrightness,
this.canvasColor,
this.cardColor,
this.dividerColor,
this.highlightColor,
this.splashColor,
this.unselectedColor,
this.disabledColor,
this.accentColor,
this.accentColorBrightness,
this.indicatorColor,
this.hintColor,
this.hintOpacity,
this.text,
this.primaryTextTheme,
this.primaryIconTheme
}) {
assert(brightness != null);
if (primarySwatch == null) {
if (brightness == ThemeBrightness.dark) {
_primaryColor = Colors.grey[900];
} else {
_primaryColor = Colors.grey[100];
}
} else {
_primaryColor = primarySwatch[500];
// primarySwatch can be null; TODO(ianh): see https://github.com/flutter/flutter/issues/1277
assert(primaryColor != null);
assert(primaryColorBrightness != null);
assert(canvasColor != null);
assert(cardColor != null);
assert(dividerColor != null);
assert(highlightColor != null);
assert(splashColor != null);
assert(unselectedColor != null);
assert(disabledColor != null);
assert(accentColor != null);
assert(accentColorBrightness != null);
assert(indicatorColor != null);
assert(hintColor != null);
assert(hintOpacity != null);
assert(text != null);
assert(primaryTextTheme != null);
assert(primaryIconTheme != null);
}
if (accentColor == null) {
_accentColor = primarySwatch == null ? Colors.blue[500] : primarySwatch[500];
} else {
_accentColor = accentColor;
}
factory ThemeData({
ThemeBrightness brightness: ThemeBrightness.light,
Map<int, Color> primarySwatch,
Color primaryColor,
ThemeBrightness primaryColorBrightness,
Color canvasColor,
Color cardColor,
Color dividerColor,
Color highlightColor,
Color splashColor,
Color unselectedColor,
Color disabledColor,
Color accentColor,
ThemeBrightness accentColorBrightness: ThemeBrightness.dark,
Color indicatorColor,
Color hintColor,
double hintOpacity,
TextTheme text,
TextTheme primaryTextTheme,
IconThemeData primaryIconTheme
}) {
// brightness default is in the arguments list
bool isDark = brightness == ThemeBrightness.dark;
primaryColor ??= primarySwatch == null ? isDark ? Colors.grey[900] : Colors.grey[100] : primarySwatch[500];
primaryColorBrightness ??= primarySwatch == null ? brightness : ThemeBrightness.dark /* swatch[500] is always dark */;
canvasColor ??= isDark ? Colors.grey[850] : Colors.grey[50];
cardColor ??= isDark ? Colors.grey[800] : Colors.white;
dividerColor ??= isDark ? const Color(0x1FFFFFFF) : const Color(0x1F000000);
highlightColor ??= isDark ? _kDarkThemeHighlightColor : _kLightThemeHighlightColor;
splashColor ??= isDark ? _kDarkThemeSplashColor : _kLightThemeSplashColor;
unselectedColor ??= isDark ? Colors.white70 : Colors.black54;
disabledColor ??= isDark ? Colors.white30 : Colors.black26;
accentColor ??= primarySwatch == null ? Colors.blue[500] : primarySwatch[500];
// accentColorBrightness default is in the arguments list
indicatorColor ??= accentColor == primaryColor ? Colors.white : accentColor;
hintColor ??= isDark ? const Color(0x42FFFFFF) : const Color(0x4C000000);
hintOpacity ??= hintColor != null ? hintColor.alpha / 0xFF : isDark ? 0.26 : 0.30;
text ??= isDark ? Typography.white : Typography.black;
primaryTextTheme ??= primaryColorBrightness == ThemeBrightness.dark ? Typography.white : Typography.black;
primaryIconTheme ??= primaryColorBrightness == ThemeBrightness.dark ? const IconThemeData(color: IconThemeColor.white) : const IconThemeData(color: IconThemeColor.black);
return new ThemeData.raw(
brightness: brightness,
primarySwatch: primarySwatch,
primaryColor: primaryColor,
primaryColorBrightness: primaryColorBrightness,
canvasColor: canvasColor,
cardColor: cardColor,
dividerColor: dividerColor,
highlightColor: highlightColor,
splashColor: splashColor,
unselectedColor: unselectedColor,
disabledColor: disabledColor,
accentColor: accentColor,
accentColorBrightness: accentColorBrightness,
indicatorColor: indicatorColor,
hintColor: hintColor,
hintOpacity: hintOpacity,
text: text,
primaryTextTheme: primaryTextTheme,
primaryIconTheme: primaryIconTheme
);
}
factory ThemeData.light() => new ThemeData(primarySwatch: Colors.blue, brightness: ThemeBrightness.light);
......@@ -78,63 +146,82 @@ class ThemeData {
/// all dark. When the ThemeBrightness is light, the canvas and card colors
/// are bright, and the primary color's darkness varies as described by
/// primaryColorBrightness. The primaryColor does not contrast well with the
/// card and canvas colors when the brightness is dask; when the birghtness is
/// card and canvas colors when the brightness is dark; when the brightness is
/// dark, use Colors.white or the accentColor for a contrasting color.
final ThemeBrightness brightness;
final Map<int, Color> primarySwatch;
/// The background colour for major parts of the app (toolbars, tab bars, etc)
final Color primaryColor;
/// The brightness of the primaryColor. Used to determine the colour of text and
/// icons placed on top of the primary color (e.g. toolbar text).
final ThemeBrightness primaryColorBrightness;
final Color canvasColor;
final Color cardColor;
final Color dividerColor;
final Color hintColor;
final Color highlightColor;
final Color splashColor;
final Color unselectedColor;
final Color disabledColor;
/// The foreground color for widgets (knobs, text, etc)
final Color accentColor;
/// The brightness of the accentColor. Used to determine the colour of text
/// and icons placed on top of the accent color (e.g. the icons on a floating
/// action button).
final ThemeBrightness accentColorBrightness;
/// The color of the selected tab indicator in a tab strip.
final Color indicatorColor;
// Some users want the pre-multiplied color, others just want the opacity.
final Color hintColor;
final double hintOpacity;
/// Text with a color that contrasts with the card and canvas colors.
final TextTheme text;
/// The background colour for major parts of the app (toolbars, tab bars, etc)
Color get primaryColor => _primaryColor;
Color _primaryColor;
/// The brightness of the primaryColor. Used to determine the colour of text and
/// icons placed on top of the primary color (e.g. toolbar text).
final ThemeBrightness primaryColorBrightness;
/// A text theme that contrasts with the primary color.
TextTheme get primaryTextTheme {
if (primaryColorBrightness == ThemeBrightness.dark)
return Typography.white;
return Typography.black;
}
final TextTheme primaryTextTheme;
IconThemeData get primaryIconTheme {
if (primaryColorBrightness == ThemeBrightness.dark)
return const IconThemeData(color: IconThemeColor.white);
return const IconThemeData(color: IconThemeColor.black);
}
final IconThemeData primaryIconTheme;
Color get unselectedColor {
if (brightness == ThemeBrightness.dark)
return Colors.white70;
return Colors.black54;
static ThemeData lerp(ThemeData begin, ThemeData end, double t) {
Map<int, Color> primarySwatch;
if (begin.primarySwatch != null && end.primarySwatch != null) {
primarySwatch = <int, Color>{};
for (int index in begin.primarySwatch.keys) {
if (!end.primarySwatch.containsKey(index))
continue;
primarySwatch[index] = Color.lerp(begin.primarySwatch[index], end.primarySwatch[index], t);
}
Color get disabledColor {
if (brightness == ThemeBrightness.dark)
return Colors.white30;
return Colors.black26;
}
/// The foreground color for widgets (knobs, text, etc)
Color get accentColor => _accentColor;
Color _accentColor;
/// The brightness of the accentColor. Used to determine the colour of text
/// and icons placed on top of the accent color (e.g. the icons on a floating
/// action button).
final ThemeBrightness accentColorBrightness;
return new ThemeData.raw(
brightness: t < 0.5 ? begin.brightness : end.brightness,
primarySwatch: primarySwatch,
primaryColor: Color.lerp(begin.primaryColor, end.primaryColor, t),
primaryColorBrightness: t < 0.5 ? begin.primaryColorBrightness : end.primaryColorBrightness,
canvasColor: Color.lerp(begin.canvasColor, end.canvasColor, t),
cardColor: Color.lerp(begin.cardColor, end.cardColor, t),
dividerColor: Color.lerp(begin.dividerColor, end.dividerColor, t),
highlightColor: Color.lerp(begin.highlightColor, end.highlightColor, t),
splashColor: Color.lerp(begin.splashColor, end.splashColor, t),
unselectedColor: Color.lerp(begin.unselectedColor, end.unselectedColor, t),
disabledColor: Color.lerp(begin.disabledColor, end.disabledColor, t),
accentColor: Color.lerp(begin.accentColor, end.accentColor, t),
accentColorBrightness: t < 0.5 ? begin.accentColorBrightness : end.accentColorBrightness,
indicatorColor: Color.lerp(begin.indicatorColor, end.indicatorColor, t),
hintColor: Color.lerp(begin.hintColor, end.hintColor, t),
hintOpacity: lerpDouble(begin.hintOpacity, end.hintOpacity, t),
text: TextTheme.lerp(begin.text, end.text, t),
primaryTextTheme: TextTheme.lerp(begin.primaryTextTheme, end.primaryTextTheme, t),
primaryIconTheme: IconThemeData.lerp(begin.primaryIconTheme, end.primaryIconTheme, t)
);
}
bool operator==(Object other) {
if (other.runtimeType != runtimeType)
......@@ -142,29 +229,46 @@ class ThemeData {
ThemeData otherData = other;
return (otherData.brightness == brightness) &&
(otherData.primarySwatch == primarySwatch) &&
(otherData.primaryColor == primaryColor) &&
(otherData.primaryColorBrightness == primaryColorBrightness) &&
(otherData.canvasColor == canvasColor) &&
(otherData.cardColor == cardColor) &&
(otherData.dividerColor == dividerColor) &&
(otherData.hintColor == hintColor) &&
(otherData.highlightColor == highlightColor) &&
(otherData.splashColor == splashColor) &&
(otherData.unselectedColor == unselectedColor) &&
(otherData.disabledColor == disabledColor) &&
(otherData.accentColor == accentColor) &&
(otherData.accentColorBrightness == accentColorBrightness) &&
(otherData.indicatorColor == indicatorColor) &&
(otherData.hintColor == hintColor) &&
(otherData.hintOpacity == hintOpacity) &&
(otherData.text == text) &&
(otherData.primaryColorBrightness == primaryColorBrightness) &&
(otherData.accentColorBrightness == accentColorBrightness);
(otherData.primaryTextTheme == primaryTextTheme) &&
(otherData.primaryIconTheme == primaryIconTheme);
}
int get hashCode {
return hashValues(
brightness,
primarySwatch,
hashList(primarySwatch.keys),
hashList(primarySwatch.values),
primaryColor,
primaryColorBrightness,
canvasColor,
cardColor,
dividerColor,
hintColor,
highlightColor,
splashColor,
unselectedColor,
disabledColor,
accentColor,
accentColorBrightness,
indicatorColor,
hintColor,
hintOpacity,
text,
primaryColorBrightness,
accentColorBrightness
primaryTextTheme,
primaryIconTheme
);
}
......
......@@ -4,8 +4,6 @@
// See http://www.google.com/design/spec/style/typography.html
import 'dart:ui' show Color;
import 'package:flutter/painting.dart';
import 'colors.dart';
......@@ -13,8 +11,11 @@ import 'colors.dart';
// TODO(eseidel): Font weights are supposed to be language relative!
// TODO(jackson): Baseline should be language relative!
// These values are for English-like text.
// TODO(ianh): There's no font-family specified here.
class TextTheme {
const TextTheme._(this.display4, this.display3, this.display2, this.display1, this.headline, this.title, this.subhead, this.body2, this.body1, this.caption, this.button);
const TextTheme._black()
: display4 = const TextStyle(inherit: false, fontSize: 112.0, fontWeight: FontWeight.w100, color: Colors.black54, textBaseline: TextBaseline.alphabetic),
display3 = const TextStyle(inherit: false, fontSize: 56.0, fontWeight: FontWeight.w400, color: Colors.black54, textBaseline: TextBaseline.alphabetic),
......@@ -53,26 +54,26 @@ class TextTheme {
final TextStyle caption;
final TextStyle button;
static TextTheme lerp(TextTheme begin, TextTheme end, double t) {
return new TextTheme._(
TextStyle.lerp(begin.display4, end.display4, t),
TextStyle.lerp(begin.display3, end.display3, t),
TextStyle.lerp(begin.display2, end.display2, t),
TextStyle.lerp(begin.display1, end.display1, t),
TextStyle.lerp(begin.headline, end.headline, t),
TextStyle.lerp(begin.title, end.title, t),
TextStyle.lerp(begin.subhead, end.subhead, t),
TextStyle.lerp(begin.body2, end.body2, t),
TextStyle.lerp(begin.body1, end.body1, t),
TextStyle.lerp(begin.caption, end.caption, t),
TextStyle.lerp(begin.button, end.button, t)
);
}
}
class Typography {
Typography._();
static const TextTheme black = const TextTheme._black();
static const TextTheme white = const TextTheme._white();
// TODO(abarth): Maybe this should be hard-coded in Scaffold?
static const String typeface = 'font-family: sans-serif';
// TODO(ianh): Remove this when we remove fn2, now that it's hard-coded in App.
static const TextStyle error = const TextStyle(
color: const Color(0xD0FF0000),
fontFamily: 'monospace',
fontSize: 48.0,
fontWeight: FontWeight.w900,
textAlign: TextAlign.right,
decoration: TextDecoration.underline,
decorationColor: const Color(0xFFFF00),
decorationStyle: TextDecorationStyle.double
);
}
......@@ -127,6 +127,31 @@ class TextStyle {
);
}
/// Interpolate between two text styles.
///
/// This will not work well if the styles don't set the same fields.
static TextStyle lerp(TextStyle begin, TextStyle end, double t) {
assert(begin.inherit == end.inherit);
return new TextStyle(
inherit: end.inherit,
color: Color.lerp(begin.color, end.color, t),
fontFamily: t < 0.5 ? begin.fontFamily : end.fontFamily,
fontSize: ui.lerpDouble(begin.fontSize ?? end.fontSize, end.fontSize ?? begin.fontSize, t),
// TODO(ianh): Replace next line with "fontWeight: FontWeight.lerp(begin.fontWeight, end.fontWeight, t)," once engine is revved
fontWeight: FontWeight.values[ui.lerpDouble(begin?.fontWeight.index ?? FontWeight.normal.index, end?.fontWeight.index ?? FontWeight.normal.index, t.clamp(0.0, 1.0)).round()],
fontStyle: t < 0.5 ? begin.fontStyle : end.fontStyle,
letterSpacing: ui.lerpDouble(begin.letterSpacing ?? end.letterSpacing, end.letterSpacing ?? begin.letterSpacing, t),
wordSpacing: ui.lerpDouble(begin.wordSpacing ?? end.wordSpacing, end.wordSpacing ?? begin.wordSpacing, t),
textAlign: t < 0.5 ? begin.textAlign : end.textAlign,
textBaseline: t < 0.5 ? begin.textBaseline : end.textBaseline,
height: ui.lerpDouble(begin.height ?? end.height, end.height ?? begin.height, t),
decoration: t < 0.5 ? begin.decoration : end.decoration,
decorationColor: Color.lerp(begin.decorationColor, end.decorationColor, t),
decorationStyle: t < 0.5 ? begin.decorationStyle : end.decorationStyle
);
}
ui.TextStyle get textStyle {
return new ui.TextStyle(
color: color,
......
......@@ -317,7 +317,7 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState<AnimatedContainer>
}
/// Animated version of [Positioned] which automatically transitions the child's
/// position over a given duration whenever the given positon changes.
/// position over a given duration whenever the given position changes.
///
/// Only works if it's the child of a [Stack].
class AnimatedPositioned extends AnimatedWidgetBase {
......
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