Make the FAB move up when a Snack Bar slides in.

This changes how SnackBar works so you can use it anywhere, not just on
the bottom edge of the screen (it used to rely on overflowing its bounds
and having negative offsets... I'm not really sure how hit testing
worked on it before!).

To do this I introduced a new RenderBox, RenderOverflowBox, that lets
you set your child's size independent of your own. I needed this so that
the snack bar could use a SquashTransition to change its size, while not
affecting the layout of its child. This is exposed as OverflowBox in
fn3. I'm not sure if it's the best API. It doesn't let you position the
child (which is an issue if the size you give is smaller), it doesn't
let you give a loose constraint (which maybe you might want?). But it
handles this use case, so for now it's probably ok.

Making the FAB get repositioned out of the way of the Snack Bar is now
done in the Scaffold, which is in charge of positioning both of those
and is the place that knows that both exist.
This commit is contained in:
Hixie 2015-09-28 10:03:23 -07:00
parent 64dfb8496c
commit 56d4033423
5 changed files with 149 additions and 29 deletions

View file

@ -236,7 +236,6 @@ class StockHomeState extends State<StockHome> {
if (_snackBarStatus == AnimationStatus.dismissed)
return null;
return new SnackBar(
transitionKey: snackBarKey,
showing: _isSnackBarShowing,
content: new Text("Stock purchased!"),
actions: [new SnackBarAction(label: "UNDO", onPressed: _handleUndo)],

View file

@ -211,7 +211,9 @@ class SizedBox extends OneChildRenderObjectWidget {
final double width;
final double height;
RenderConstrainedBox createRenderObject() => new RenderConstrainedBox(additionalConstraints: _additionalConstraints);
RenderConstrainedBox createRenderObject() => new RenderConstrainedBox(
additionalConstraints: _additionalConstraints
);
BoxConstraints get _additionalConstraints {
BoxConstraints result = const BoxConstraints();
@ -227,6 +229,24 @@ class SizedBox extends OneChildRenderObjectWidget {
}
}
class OverflowBox extends OneChildRenderObjectWidget {
OverflowBox({ Key key, this.width, this.height, Widget child })
: super(key: key, child: child);
final double width;
final double height;
RenderOverflowBox createRenderObject() => new RenderOverflowBox(
innerWidth: width,
innerHeight: height
);
void updateRenderObject(RenderOverflowBox renderObject, OverflowBox oldWidget) {
renderObject.innerWidth = width;
renderObject.innerHeight = height;
}
}
class ConstrainedBox extends OneChildRenderObjectWidget {
ConstrainedBox({ Key key, this.constraints, Widget child })
: super(key: key, child: child) {

View file

@ -102,6 +102,7 @@ class RenderScaffold extends RenderBox {
void performLayout() {
double bodyHeight = size.height;
double bodyPosition = 0.0;
double fabOffset = 0.0;
if (_slots[ScaffoldSlots.statusBar] != null) {
RenderBox statusBar = _slots[ScaffoldSlots.statusBar];
statusBar.layout(new BoxConstraints.tight(new Size(size.width, kStatusBarHeight)));
@ -127,18 +128,20 @@ class RenderScaffold extends RenderBox {
if (_slots[ScaffoldSlots.snackBar] != null) {
RenderBox snackBar = _slots[ScaffoldSlots.snackBar];
// TODO(jackson): On tablet/desktop, minWidth = 288, maxWidth = 568
snackBar.layout(new BoxConstraints(minWidth: size.width, maxWidth: size.width, minHeight: 0.0, maxHeight: size.height),
parentUsesSize: true);
snackBar.layout(
new BoxConstraints(minWidth: size.width, maxWidth: size.width, minHeight: 0.0, maxHeight: bodyHeight),
parentUsesSize: true
);
assert(snackBar.parentData is BoxParentData);
// Position it off-screen. SnackBar slides in with an animation.
snackBar.parentData.position = new Point(0.0, size.height);
snackBar.parentData.position = new Point(0.0, bodyPosition + bodyHeight - snackBar.size.height);
fabOffset += snackBar.size.height;
}
if (_slots[ScaffoldSlots.floatingActionButton] != null) {
RenderBox floatingActionButton = _slots[ScaffoldSlots.floatingActionButton];
Size area = new Size(size.width - kButtonX, size.height - kButtonY);
floatingActionButton.layout(new BoxConstraints.loose(area), parentUsesSize: true);
assert(floatingActionButton.parentData is BoxParentData);
floatingActionButton.parentData.position = (area - floatingActionButton.size).toPoint();
floatingActionButton.parentData.position = (area - floatingActionButton.size).toPoint() + new Offset(0.0, -fabOffset);
}
if (_slots[ScaffoldSlots.drawer] != null) {
RenderBox drawer = _slots[ScaffoldSlots.drawer];

View file

@ -17,7 +17,10 @@ import 'package:sky/src/fn3/transitions.dart';
typedef void SnackBarDismissedCallback();
const Duration _kSlideInDuration = const Duration(milliseconds: 200);
// TODO(ianh): factor out some of the constants below
const double kSnackHeight = 52.0;
const double kSideMargins = 24.0;
const double kVerticalPadding = 14.0;
const Color kSnackBackground = const Color(0xFF323232);
class SnackBarAction extends StatelessComponent {
SnackBarAction({Key key, this.label, this.onPressed }) : super(key: key) {
@ -31,8 +34,8 @@ class SnackBarAction extends StatelessComponent {
return new GestureDetector(
onTap: onPressed,
child: new Container(
margin: const EdgeDims.only(left: 24.0),
padding: const EdgeDims.only(top: 14.0, bottom: 14.0),
margin: const EdgeDims.only(left: kSideMargins),
padding: const EdgeDims.symmetric(vertical: kVerticalPadding),
child: new Text(label)
)
);
@ -42,7 +45,6 @@ class SnackBarAction extends StatelessComponent {
class SnackBar extends AnimatedComponent {
SnackBar({
Key key,
this.transitionKey,
this.content,
this.actions,
bool showing,
@ -51,7 +53,6 @@ class SnackBar extends AnimatedComponent {
assert(content != null);
}
final Key transitionKey;
final Widget content;
final List<SnackBarAction> actions;
final SnackBarDismissedCallback onDismissed;
@ -69,7 +70,7 @@ class SnackBarState extends AnimatedState<SnackBar> {
List<Widget> children = [
new Flexible(
child: new Container(
margin: const EdgeDims.symmetric(vertical: 14.0),
margin: const EdgeDims.symmetric(vertical: kVerticalPadding),
child: new DefaultTextStyle(
style: Typography.white.subhead,
child: config.content
@ -79,24 +80,28 @@ class SnackBarState extends AnimatedState<SnackBar> {
];
if (config.actions != null)
children.addAll(config.actions);
return new SlideTransition(
key: config.transitionKey,
return new SquashTransition(
performance: performance.view,
position: new AnimatedValue<Point>(
Point.origin,
end: const Point(0.0, -52.0),
height: new AnimatedValue<double>(
0.0,
end: kSnackHeight,
curve: easeIn,
reverseCurve: easeOut
),
child: new Material(
level: 2,
color: const Color(0xFF323232),
type: MaterialType.canvas,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: 24.0),
child: new DefaultTextStyle(
style: new TextStyle(color: Theme.of(context).accentColor),
child: new Row(children)
child: new ClipRect(
child: new OverflowBox(
height: kSnackHeight,
child: new Material(
level: 2,
color: kSnackBackground,
type: MaterialType.canvas,
child: new Container(
margin: const EdgeDims.symmetric(horizontal: kSideMargins),
child: new DefaultTextStyle(
style: new TextStyle(color: Theme.of(context).accentColor),
child: new Row(children)
)
)
)
)
)

View file

@ -83,7 +83,7 @@ class RenderProxyBox extends RenderBox with RenderObjectWithChildMixin<RenderBox
/// A render object that imposes additional constraints on its child
///
/// A render constrainted box proxies most functions in the render box protocol
/// A render constrained box proxies most functions in the render box protocol
/// to its child, except that when laying out its child, it tightens the
/// constraints provided by its parent by enforcing the [additionalConstraints]
/// as well.
@ -94,7 +94,7 @@ class RenderConstrainedBox extends RenderProxyBox {
RenderConstrainedBox({
RenderBox child,
BoxConstraints additionalConstraints
}) : super(child), _additionalConstraints = additionalConstraints {
}) : _additionalConstraints = additionalConstraints, super(child) {
assert(additionalConstraints != null);
}
@ -145,6 +145,99 @@ class RenderConstrainedBox extends RenderProxyBox {
String debugDescribeSettings(String prefix) => '${super.debugDescribeSettings(prefix)}${prefix}additionalConstraints: ${additionalConstraints}\n';
}
/// A render object that imposes different constraints on its child than it gets
/// from its parent, possibly allowing the child to overflow the parent.
///
/// A render overflow box proxies most functions in the render box protocol to
/// its child, except that when laying out its child, it passes constraints
/// based on the innerWidth and innerHeight fields instead of just passing the
/// parent's constraints in. It then sizes itself based on the parent's
/// constraints' maxWidth and maxHeight, ignoring the child's dimensions.
///
/// For example, if you wanted a box to always render 50x50, regardless of where
/// it was rendered, you would wrap it in a RenderOverflow with innerWidth and
/// innerHeight members set to 50.0. Generally speaking, to avoid confusing
/// behaviour around hit testing, a RenderOverflowBox should usually be wrapped
/// in a RenderClipRect.
///
/// The child is positioned at the top left of the box. To position a smaller
/// child inside a larger parent, use [RenderPositionedBox] and
/// [RenderConstrainedBox] rather than RenderOverflowBox.
///
/// If you pass null for innerWidth or innerHeight, the constraints from the
/// parent are passed instead.
class RenderOverflowBox extends RenderProxyBox {
RenderOverflowBox({
RenderBox child,
double innerWidth,
double innerHeight
}) : _innerWidth = innerWidth, _innerHeight = innerHeight, super(child);
/// The tight width constraint to give the child. Set this to null (the
/// default) to use the constraints from the parent instead.
double get innerWidth => _innerWidth;
double _innerWidth;
void set innerWidth (double value) {
if (_innerWidth == value)
return;
_innerWidth = value;
markNeedsLayout();
}
/// The tight height constraint to give the child. Set this to null (the
/// default) to use the constraints from the parent instead.
double get innerHeight => _innerHeight;
double _innerHeight;
void set innerHeight (double value) {
if (_innerHeight == value)
return;
_innerHeight = value;
markNeedsLayout();
}
BoxConstraints childConstraints(BoxConstraints constraints) {
return new BoxConstraints(
minWidth: _innerWidth ?? constraints.minWidth,
maxWidth: _innerWidth ?? constraints.maxWidth,
minHeight: _innerHeight ?? constraints.minHeight,
maxHeight: _innerHeight ?? constraints.maxHeight
);
}
double getMinIntrinsicWidth(BoxConstraints constraints) {
return constraints.constrainWidth();
}
double getMaxIntrinsicWidth(BoxConstraints constraints) {
return constraints.constrainWidth();
}
double getMinIntrinsicHeight(BoxConstraints constraints) {
return constraints.constrainHeight();
}
double getMaxIntrinsicHeight(BoxConstraints constraints) {
return constraints.constrainHeight();
}
bool get sizedByParent => true;
void performResize() {
size = constraints.biggest;
}
void performLayout() {
if (child != null)
child.layout(childConstraints(constraints));
}
String debugDescribeSettings(String prefix) {
return '${super.debugDescribeSettings(prefix)}' +
'${prefix}innerWidth: ${innerWidth ?? "use parent width constraints"}\n' +
'${prefix}innerHeight: ${innerHeight ?? "use parent height constraints"}\n';
}
}
/// Forces child to layout at a specific aspect ratio
///
/// The width of this render object is the largest width permited by the layout