Commit ed121703 authored by Hans Muller's avatar Hans Muller

Merge pull request #1190 from HansMuller/fab_switch

Floating Action Button transitions

When the scaffold's floating action button (FAB) is replaced the old FAB shrinks to zero and then the new one grows to its full size. When a FAB is shown, its child spins into place.

This is all intended to be a good approximation of the "Lateral Screens" section of the material design spec.

The fab.dart example is included as stop gap until the gallery and other demos have been updated.
parents e8e65446 cafea7f5
......@@ -14,3 +14,4 @@ material-design-icons:
- name: action/face
- name: action/language
- name: content/add
- name: content/create
// Copyright 2015 The Chromium 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 'package:flutter/material.dart';
class _Page {
_Page({this.label, this.color, this.icon});
final String label;
final Map<int, Color> color;
final String icon;
TabLabel get tabLabel => new TabLabel(text: label);
bool get fabDefined => color != null && icon != null;
Color get fabColor => color[400];
Icon get fabIcon => new Icon(icon: icon);
Key get fabKey => new ValueKey<Color>(fabColor);
}
List<_Page> _pages = <_Page>[
new _Page(label: "Blue", color: Colors.indigo, icon: 'content/add'),
new _Page(label: "Too", color: Colors.indigo, icon: 'content/add'),
new _Page(label: "Eco", color: Colors.green, icon: 'content/create'),
new _Page(label: "No"),
new _Page(label: "Teal", color: Colors.teal, icon: 'content/add'),
new _Page(label: "Red", color: Colors.red, icon: 'content/create')
];
class FabApp extends StatefulComponent {
FabApp();
FabAppState createState() => new FabAppState();
}
class FabAppState extends State<FabApp> {
_Page selectedPage = _pages[0];
void _handleTabSelection(_Page page) {
setState(() {
selectedPage = page;
});
}
Widget buildTabView(_Page page) {
return new Builder(
builder: (BuildContext context) {
final TextStyle textStyle = new TextStyle(
color: Theme.of(context).primaryColor,
fontSize: 32.0,
textAlign: TextAlign.center
);
return new Container(
key: new ValueKey<String>(page.label),
padding: const EdgeDims.TRBL(48.0, 48.0, 96.0, 48.0),
child: new Card(
child: new Center(
child: new Text(page.label, style: textStyle)
)
)
);
}
);
}
Widget build(BuildContext context) {
return new TabBarSelection<_Page>(
values: _pages,
onChanged: _handleTabSelection,
child: new Scaffold(
toolBar: new ToolBar(
elevation: 0,
center: new Text('FAB Transition Demo'),
tabBar: new TabBar<String>(
labels: new Map.fromIterable(_pages, value: (_Page page) => page.tabLabel)
)
),
body: new TabBarView(children: _pages.map(buildTabView).toList()),
floatingActionButton: !selectedPage.fabDefined ? null : new FloatingActionButton(
key: selectedPage.fabKey,
backgroundColor: selectedPage.fabColor,
child: selectedPage.fabIcon
)
)
);
}
}
void main() {
runApp(new MaterialApp(
title: 'FabApp',
routes: {
'/': (RouteArguments args) => new FabApp()
}
));
}
......@@ -10,6 +10,8 @@ material-design-icons:
- name: action/home
- name: action/language
- name: action/list
- name: content/add
- name: content/create
- name: device/dvr
- name: editor/format_align_center
- name: editor/format_align_left
......
......@@ -2,6 +2,7 @@
// 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 'icon_theme.dart';
......@@ -14,6 +15,8 @@ import 'theme.dart';
// http://www.google.com/design/spec/layout/metrics-keylines.html#metrics-keylines-keylines-spacing
const double _kSize = 56.0;
const double _kSizeMini = 40.0;
const Duration _kChildSegue = const Duration(milliseconds: 400);
const Interval _kChildSegueInterval = const Interval(0.65, 1.0);
class FloatingActionButton extends StatefulComponent {
const FloatingActionButton({
......@@ -37,6 +40,22 @@ class FloatingActionButton extends StatefulComponent {
}
class _FloatingActionButtonState extends State<FloatingActionButton> {
final Performance _childSegue = new Performance(duration: _kChildSegue);
void initState() {
super.initState();
_childSegue.play();
}
void didUpdateConfig(FloatingActionButton oldConfig) {
super.didUpdateConfig(oldConfig);
if (Widget.canUpdate(oldConfig.child, config.child) && config.backgroundColor == oldConfig.backgroundColor)
return;
_childSegue
..progress = 0.0
..play();
}
bool _highlight = false;
void _handleHighlightChanged(bool value) {
......@@ -67,7 +86,11 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
child: new Center(
child: new IconTheme(
data: new IconThemeData(color: iconThemeColor),
child: config.child
child: new RotationTransition(
performance: _childSegue,
turns: new AnimatedValue<double>(-0.125, end: 0.0, curve: _kChildSegueInterval),
child: config.child
)
)
)
)
......
......@@ -19,6 +19,7 @@ import 'drawer.dart';
import 'icon_button.dart';
const double _kFloatingActionButtonMargin = 16.0; // TODO(hmuller): should be device dependent
const Duration _kFloatingActionButtonSegue = const Duration(milliseconds: 400);
enum _Child {
body,
......@@ -96,6 +97,66 @@ class _ScaffoldLayout extends MultiChildLayoutDelegate {
bool shouldRelayout(MultiChildLayoutDelegate oldDelegate) => false;
}
class _FloatingActionButtonTransition extends StatefulComponent {
_FloatingActionButtonTransition({
Key key,
this.child
}) : super(key: key) {
assert(child != null);
}
final Widget child;
_FloatingActionButtonTransitionState createState() => new _FloatingActionButtonTransitionState();
}
class _FloatingActionButtonTransitionState extends State<_FloatingActionButtonTransition> {
final Performance performance = new Performance(duration: _kFloatingActionButtonSegue);
Widget oldChild;
void initState() {
super.initState();
performance.play().then((_) {
oldChild = null;
});
}
void dispose() {
performance.stop();
super.dispose();
}
void didUpdateConfig(_FloatingActionButtonTransition oldConfig) {
if (Widget.canUpdate(oldConfig.child, config.child))
return;
oldChild = oldConfig.child;
performance
..progress = 0.0
..play().then((_) {
oldChild = null;
});
}
Widget build(BuildContext context) {
final List<Widget> children = new List<Widget>();
if (oldChild != null) {
children.add(new ScaleTransition(
scale: new AnimatedValue<double>(1.0, end: 0.0, curve: new Interval(0.0, 0.5, curve: Curves.easeIn)),
performance: performance,
child: oldChild
));
}
children.add(new ScaleTransition(
scale: new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(0.5, 1.0, curve: Curves.easeIn)),
performance: performance,
child: config.child
));
return new Stack(children: children);
}
}
class Scaffold extends StatefulComponent {
Scaffold({
Key key,
......@@ -323,7 +384,13 @@ class ScaffoldState extends State<Scaffold> {
if (_snackBars.isNotEmpty)
_addIfNonNull(children, _snackBars.first._widget, _Child.snackBar);
_addIfNonNull(children, config.floatingActionButton, _Child.floatingActionButton);
if (config.floatingActionButton != null) {
Widget fab = new _FloatingActionButtonTransition(
key: new ValueKey<Key>(config.floatingActionButton.key),
child: config.floatingActionButton
);
children.add(new LayoutId(child: fab, id: _Child.floatingActionButton));
}
if (config.drawer != null) {
children.add(new LayoutId(
......
......@@ -230,6 +230,11 @@ abstract class Widget {
}
void debugFillDescription(List<String> description) { }
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
}
// TODO(ianh): move the next four classes to below InheritedWidget
......@@ -512,11 +517,6 @@ abstract class InheritedWidget extends _ProxyComponent {
// ELEMENTS
bool _canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
}
enum _ElementLifecycle {
initial,
active,
......@@ -691,7 +691,7 @@ abstract class Element<T extends Widget> implements BuildContext {
updateSlotForChild(child, newSlot);
return child;
}
if (_canUpdate(child.widget, newWidget)) {
if (Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
......@@ -741,7 +741,7 @@ abstract class Element<T extends Widget> implements BuildContext {
assert(newWidget != widget);
assert(depth != null);
assert(_active);
assert(_canUpdate(widget, newWidget));
assert(Widget.canUpdate(widget, newWidget));
_widget = newWidget;
}
......@@ -798,7 +798,7 @@ abstract class Element<T extends Widget> implements BuildContext {
Element element = key._currentElement;
if (element == null)
return null;
if (!_canUpdate(element.widget, newWidget))
if (!Widget.canUpdate(element.widget, newWidget))
return null;
if (element._parent != null && !element._parent.detachChild(element))
return null;
......@@ -1493,7 +1493,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
Element oldChild = oldChildren[childrenTop];
Widget newWidget = newWidgets[childrenTop];
assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
if (!_canUpdate(oldChild.widget, newWidget))
if (!Widget.canUpdate(oldChild.widget, newWidget))
break;
childrenTop += 1;
}
......@@ -1508,7 +1508,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
Element oldChild = oldChildren[oldChildrenBottom];
Widget newWidget = newWidgets[newChildrenBottom];
assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
if (!_canUpdate(oldChild.widget, newWidget))
if (!Widget.canUpdate(oldChild.widget, newWidget))
break;
Element newChild = updateChild(oldChild, newWidget, nextSibling);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
......@@ -1543,7 +1543,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
if (key != null) {
oldChild = oldKeyedChildren[newWidget.key];
if (oldChild != null) {
if (_canUpdate(oldChild.widget, newWidget)) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {
// we found a match!
// remove it from oldKeyedChildren so we don't unsync it later
oldKeyedChildren.remove(key);
......@@ -1554,7 +1554,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
}
}
}
assert(oldChild == null || _canUpdate(oldChild.widget, newWidget));
assert(oldChild == null || Widget.canUpdate(oldChild.widget, newWidget));
Element newChild = updateChild(oldChild, newWidget, nextSibling);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
assert(oldChild == newChild || oldChild == null || oldChild._debugLifecycleState != _ElementLifecycle.active);
......@@ -1571,7 +1571,7 @@ abstract class RenderObjectElement<T extends RenderObjectWidget> extends Buildab
Element oldChild = oldChildren[childrenTop];
assert(oldChild._debugLifecycleState == _ElementLifecycle.active);
Widget newWidget = newWidgets[childrenTop];
assert(_canUpdate(oldChild.widget, newWidget));
assert(Widget.canUpdate(oldChild.widget, newWidget));
Element newChild = updateChild(oldChild, newWidget, nextSibling);
assert(newChild._debugLifecycleState == _ElementLifecycle.active);
assert(oldChild == newChild || oldChild == null || oldChild._debugLifecycleState != _ElementLifecycle.active);
......
......@@ -97,6 +97,32 @@ class SlideTransition extends TransitionWithChild {
}
}
class ScaleTransition extends TransitionWithChild {
ScaleTransition({
Key key,
this.scale,
this.alignment: const FractionalOffset(0.5, 0.5),
PerformanceView performance,
Widget child
}) : super(key: key,
performance: performance,
child: child);
final AnimatedValue<double> scale;
final FractionalOffset alignment;
Widget buildWithChild(BuildContext context, Widget child) {
performance.updateVariable(scale);
Matrix4 transform = new Matrix4.identity()
..scale(scale.value, scale.value);
return new Transform(
transform: transform,
alignment: alignment,
child: child
);
}
}
class RotationTransition extends TransitionWithChild {
RotationTransition({
Key key,
......
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