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: contents
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');
}
}
......@@ -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