From 73c9ebab192dd72c3e2c831832f7eae19ed7377c Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Fri, 15 Jan 2016 23:41:16 -0800 Subject: [PATCH] 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. --- .../lib/src/material/icon_theme_data.dart | 6 + .../flutter/lib/src/material/material.dart | 10 +- .../lib/src/material/material_app.dart | 3 +- packages/flutter/lib/src/material/tabs.dart | 12 +- packages/flutter/lib/src/material/theme.dart | 55 ++++ .../flutter/lib/src/material/theme_data.dart | 262 ++++++++++++------ .../flutter/lib/src/material/typography.dart | 37 +-- .../flutter/lib/src/painting/text_style.dart | 25 ++ .../lib/src/widgets/implicit_animations.dart | 2 +- 9 files changed, 309 insertions(+), 103 deletions(-) diff --git a/packages/flutter/lib/src/material/icon_theme_data.dart b/packages/flutter/lib/src/material/icon_theme_data.dart index e4a925c697e..6fea8105ca2 100644 --- a/packages/flutter/lib/src/material/icon_theme_data.dart +++ b/packages/flutter/lib/src/material/icon_theme_data.dart @@ -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; diff --git a/packages/flutter/lib/src/material/material.dart b/packages/flutter/lib/src/material/material.dart index 056720580a8..d63941a401c 100644 --- a/packages/flutter/lib/src/material/material.dart +++ b/packages/flutter/lib/src/material/material.dart @@ -160,12 +160,18 @@ class _MaterialState extends State { 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; diff --git a/packages/flutter/lib/src/material/material_app.dart b/packages/flutter/lib/src/material/material_app.dart index af270ca55c7..3541e50c46e 100644 --- a/packages/flutter/lib/src/material/material_app.dart +++ b/packages/flutter/lib/src/material/material_app.dart @@ -141,8 +141,9 @@ class _MaterialAppState extends State 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( diff --git a/packages/flutter/lib/src/material/tabs.dart b/packages/flutter/lib/src/material/tabs.dart index 67872fce22a..a2ba0a68f00 100644 --- a/packages/flutter/lib/src/material/tabs.dart +++ b/packages/flutter/lib/src/material/tabs.dart @@ -717,9 +717,17 @@ class _TabBarState extends ScrollableState> 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; diff --git a/packages/flutter/lib/src/material/theme.dart b/packages/flutter/lib/src/material/theme.dart index ef0ff42047b..0aee6d370d7 100644 --- a/packages/flutter/lib/src/material/theme.dart +++ b/packages/flutter/lib/src/material/theme.dart @@ -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 { + 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 { + 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 description) { + super.debugFillDescription(description); + if (_data != null) + description.add('$_data'); + } +} diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 4cdd06e4de6..483929b2c17 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -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({ + 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); + // 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); + } + + factory ThemeData({ ThemeBrightness brightness: ThemeBrightness.light, Map primarySwatch, + Color primaryColor, + ThemeBrightness primaryColorBrightness, + Color canvasColor, + Color cardColor, + Color dividerColor, + Color highlightColor, + Color splashColor, + Color unselectedColor, + Color disabledColor, 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 { - assert(brightness != null); - - if (primarySwatch == null) { - if (brightness == ThemeBrightness.dark) { - _primaryColor = Colors.grey[900]; - } else { - _primaryColor = Colors.grey[100]; - } - } else { - _primaryColor = primarySwatch[500]; - } - - if (accentColor == null) { - _accentColor = primarySwatch == null ? Colors.blue[500] : primarySwatch[500]; - } else { - _accentColor = 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,93 +146,129 @@ 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 primarySwatch; - final Color canvasColor; - final Color cardColor; - final Color dividerColor; - final Color hintColor; - final Color highlightColor; - final Color splashColor; - 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; + 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; - /// A text theme that contrasts with the primary color. - TextTheme get primaryTextTheme { - if (primaryColorBrightness == ThemeBrightness.dark) - return Typography.white; - return Typography.black; - } - - IconThemeData get primaryIconTheme { - if (primaryColorBrightness == ThemeBrightness.dark) - return const IconThemeData(color: IconThemeColor.white); - return const IconThemeData(color: IconThemeColor.black); - } - - Color get unselectedColor { - if (brightness == ThemeBrightness.dark) - return Colors.white70; - return Colors.black54; - } - - Color get disabledColor { - if (brightness == ThemeBrightness.dark) - return Colors.white30; - return Colors.black26; - } + final Color canvasColor; + final Color cardColor; + final Color dividerColor; + final Color highlightColor; + final Color splashColor; + final Color unselectedColor; + final Color disabledColor; /// The foreground color for widgets (knobs, text, etc) - Color get accentColor => _accentColor; - Color _accentColor; + 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; + + /// A text theme that contrasts with the primary color. + final TextTheme primaryTextTheme; + + final IconThemeData primaryIconTheme; + + static ThemeData lerp(ThemeData begin, ThemeData end, double t) { + Map primarySwatch; + if (begin.primarySwatch != null && end.primarySwatch != null) { + primarySwatch = {}; + for (int index in begin.primarySwatch.keys) { + if (!end.primarySwatch.containsKey(index)) + continue; + primarySwatch[index] = Color.lerp(begin.primarySwatch[index], end.primarySwatch[index], t); + } + } + 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) return false; 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 ); } diff --git a/packages/flutter/lib/src/material/typography.dart b/packages/flutter/lib/src/material/typography.dart index a868db4660e..01ed31a9f56 100644 --- a/packages/flutter/lib/src/material/typography.dart +++ b/packages/flutter/lib/src/material/typography.dart @@ -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 - ); } diff --git a/packages/flutter/lib/src/painting/text_style.dart b/packages/flutter/lib/src/painting/text_style.dart index 17a9a69e0eb..8ac9a211888 100644 --- a/packages/flutter/lib/src/painting/text_style.dart +++ b/packages/flutter/lib/src/painting/text_style.dart @@ -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, diff --git a/packages/flutter/lib/src/widgets/implicit_animations.dart b/packages/flutter/lib/src/widgets/implicit_animations.dart index 0973cc82712..145403966c6 100644 --- a/packages/flutter/lib/src/widgets/implicit_animations.dart +++ b/packages/flutter/lib/src/widgets/implicit_animations.dart @@ -317,7 +317,7 @@ class _AnimatedContainerState extends AnimatedWidgetBaseState } /// 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 {