Port PopupMenu to fn3

This changes how PopupMenu works slightly because there's no long an onPressed
callback on PopupMenuItem. Instead, callers should use showPopupMenu.
This commit is contained in:
Adam Barth 2015-09-24 12:36:25 -07:00
parent 82dd9d65df
commit 0206f1a65d
2 changed files with 243 additions and 0 deletions

View File

@ -0,0 +1,206 @@
// 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 'dart:async';
import 'dart:sky' as sky;
import 'package:sky/animation.dart';
import 'package:sky/painting.dart';
import 'package:sky/material.dart';
import 'package:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/focus.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/gesture_detector.dart';
import 'package:sky/src/fn3/navigator.dart';
import 'package:sky/src/fn3/popup_menu_item.dart';
import 'package:sky/src/fn3/scrollable.dart';
import 'package:sky/src/fn3/transitions.dart';
const Duration _kMenuDuration = const Duration(milliseconds: 300);
const double _kMenuCloseIntervalEnd = 2.0 / 3.0;
const double _kMenuWidthStep = 56.0;
const double _kMenuMargin = 16.0; // 24.0 on tablet
const double _kMenuMinWidth = 2.0 * _kMenuWidthStep;
const double _kMenuMaxWidth = 5.0 * _kMenuWidthStep;
const double _kMenuHorizontalPadding = 16.0;
const double _kMenuVerticalPadding = 8.0;
class PopupMenu extends StatefulComponent {
PopupMenu({
Key key,
this.items,
this.level: 4,
this.navigator,
this.performance
}) : super(key: key) {
assert(items != null);
assert(performance != null);
}
final List<PopupMenuItem> items;
final int level;
final NavigatorState navigator;
final WatchableAnimationPerformance performance;
PopupMenuState createState() => new PopupMenuState(this);
}
class PopupMenuState extends ComponentState<PopupMenu> {
PopupMenuState(PopupMenu config) : super(config) {
_updateBoxPainter();
config.performance.addListener(_performanceChanged);
}
BoxPainter _painter;
void _updateBoxPainter() {
_painter = new BoxPainter(
new BoxDecoration(
backgroundColor: Colors.grey[50],
borderRadius: 2.0,
boxShadow: shadows[config.level]
)
);
}
void didUpdateConfig(PopupMenu oldConfig) {
if (config.level != config.level)
_updateBoxPainter();
if (config.performance != oldConfig.performance) {
oldConfig.performance.removeListener(_performanceChanged);
config.performance.addListener(_performanceChanged);
}
}
void dispose() {
config.performance.removeListener(_performanceChanged);
}
void _performanceChanged() {
setState(() {
// the performance changed, and our state is tied up with the performance
});
}
Widget build(BuildContext context) {
double unit = 1.0 / (config.items.length + 1.5); // 1.0 for the width and 0.5 for the last item's fade.
List<Widget> children = [];
for (int i = 0; i < config.items.length; ++i) {
double start = (i + 1) * unit;
double end = (start + 1.5 * unit).clamp(0.0, 1.0);
children.add(new FadeTransition(
performance: config.performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(start, end)),
child: new GestureDetector(
onTap: () { config.navigator.pop(config.items[i].value); },
child: config.items[i]
))
);
}
final width = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit));
final height = new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, unit * config.items.length));
return new FadeTransition(
performance: config.performance,
opacity: new AnimatedValue<double>(0.0, end: 1.0, interval: new Interval(0.0, 1.0 / 3.0)),
child: new Container(
margin: new EdgeDims.all(_kMenuMargin),
child: new BuilderTransition(
performance: config.performance,
variables: [width, height],
builder: (BuildContext context) {
return new CustomPaint(
callback: (sky.Canvas canvas, Size size) {
double widthValue = width.value * size.width;
double heightValue = height.value * size.height;
_painter.paint(canvas, new Rect.fromLTWH(size.width - widthValue, 0.0, widthValue, heightValue));
},
child: new ConstrainedBox(
constraints: new BoxConstraints(
minWidth: _kMenuMinWidth,
maxWidth: _kMenuMaxWidth
),
child: new IntrinsicWidth(
stepWidth: _kMenuWidthStep,
child: new ScrollableViewport(
child: new Container(
padding: const EdgeDims.symmetric(
horizontal: _kMenuHorizontalPadding,
vertical: _kMenuVerticalPadding
),
child: new BlockBody(children)
)
)
)
)
);
}
)
)
);
}
}
class MenuPosition {
const MenuPosition({ this.top, this.right, this.bottom, this.left });
final double top;
final double right;
final double bottom;
final double left;
}
class MenuRoute extends RouteBase {
MenuRoute({ this.completer, this.position, this.builder, this.level });
final Completer completer;
final MenuPosition position;
final PopupMenuItemsBuilder builder;
final int level;
AnimationPerformance createPerformance() {
AnimationPerformance result = super.createPerformance();
AnimationTiming timing = new AnimationTiming();
timing.reverseInterval = new Interval(0.0, _kMenuCloseIntervalEnd);
result.timing = timing;
return result;
}
Duration get transitionDuration => _kMenuDuration;
bool get isOpaque => false;
Widget build(Key key, NavigatorState navigator, WatchableAnimationPerformance performance) {
return new Positioned(
top: position?.top,
right: position?.right,
bottom: position?.bottom,
left: position?.left,
child: new Focus(
key: new GlobalObjectKey(this),
autofocus: true,
child: new PopupMenu(
key: key,
items: builder != null ? builder(navigator) : const <PopupMenuItem>[],
level: level,
navigator: navigator,
performance: performance
)
)
);
}
void popState([dynamic result]) {
completer.complete(result);
}
}
typedef List<PopupMenuItem> PopupMenuItemsBuilder(NavigatorState navigator);
Future showMenu({ NavigatorState navigator, MenuPosition position, PopupMenuItemsBuilder builder, int level: 4 }) {
Completer completer = new Completer();
navigator.push(new MenuRoute(
completer: completer,
position: position,
builder: builder,
level: level
));
return completer.future;
}

View File

@ -0,0 +1,37 @@
// 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:sky/src/fn3/basic.dart';
import 'package:sky/src/fn3/framework.dart';
import 'package:sky/src/fn3/ink_well.dart';
import 'package:sky/src/fn3/theme.dart';
const double _kMenuItemHeight = 48.0;
const double _kBaselineOffsetFromBottom = 20.0;
class PopupMenuItem extends StatelessComponent {
PopupMenuItem({
Key key,
this.value,
this.child
}) : super(key: key);
final Widget child;
final dynamic value;
Widget build(BuildContext context) {
return new InkWell(
child: new Container(
height: _kMenuItemHeight,
child: new DefaultTextStyle(
style: Theme.of(context).text.subhead,
child: new Baseline(
baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
child: child
)
)
)
);
}
}