dropdown.dart 9.73 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
// 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 'package:flutter/animation.dart';
import 'package:flutter/painting.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

12
import 'debug.dart';
13 14 15 16
import 'icon.dart';
import 'ink_well.dart';
import 'shadows.dart';
import 'theme.dart';
17
import 'material.dart';
18

Hixie's avatar
Hixie committed
19
const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
20
const double _kMenuItemHeight = 48.0;
21
const EdgeDims _kMenuHorizontalPadding = const EdgeDims.only(left: 36.0, right: 36.0);
22
const double _kBaselineOffsetFromBottom = 20.0;
Hixie's avatar
Hixie committed
23
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: 2.0));
24

Hixie's avatar
Hixie committed
25 26
class _DropDownMenuPainter extends CustomPainter {
  const _DropDownMenuPainter({
27 28 29 30 31 32 33 34 35 36 37 38 39 40
    this.color,
    this.elevation,
    this.menuTop,
    this.menuBottom,
    this.renderBox
  });

  final Color color;
  final int elevation;
  final double menuTop;
  final double menuBottom;
  final RenderBox renderBox;

  void paint(Canvas canvas, Size size) {
41
    final BoxPainter painter = new BoxDecoration(
42 43 44
      backgroundColor: color,
      borderRadius: 2.0,
      boxShadow: elevationToShadow[elevation]
45
    ).createBoxPainter();
46 47 48 49 50 51

    double top = renderBox.globalToLocal(new Point(0.0, menuTop)).y;
    double bottom = renderBox.globalToLocal(new Point(0.0, menuBottom)).y;
    painter.paint(canvas, new Rect.fromLTRB(0.0, top, size.width, bottom));
  }

Hixie's avatar
Hixie committed
52
  bool shouldRepaint(_DropDownMenuPainter oldPainter) {
53 54 55 56 57 58 59 60
    return oldPainter.color != color
        || oldPainter.elevation != elevation
        || oldPainter.menuTop != menuTop
        || oldPainter.menuBottom != menuBottom
        || oldPainter.renderBox != renderBox;
  }
}

Hixie's avatar
Hixie committed
61 62
class _DropDownMenu<T> extends StatusTransitionComponent {
  _DropDownMenu({
63
    Key key,
Hixie's avatar
Hixie committed
64
    _DropDownRoute<T> route
65
  }) : route = route, super(key: key, performance: route.performance);
66

Hixie's avatar
Hixie committed
67
  final _DropDownRoute<T> route;
68 69 70

  Widget build(BuildContext context) {
    // The menu is shown in three stages (unit timing in brackets):
Hixie's avatar
Hixie committed
71 72
    // [0s - 0.25s] - Fade in a rect-sized menu container with the selected item.
    // [0.25s - 0.5s] - Grow the otherwise empty menu container from the center
73
    //   until it's big enough for as many items as we're going to show.
Hixie's avatar
Hixie committed
74
    // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
75 76
    //
    // When the menu is dismissed we just fade the entire thing out
Hixie's avatar
Hixie committed
77
    // in the first 0.25s.
78

Adam Barth's avatar
Adam Barth committed
79
    final double unit = 0.5 / (route.items.length + 1.5);
80
    final List<Widget> children = <Widget>[];
Adam Barth's avatar
Adam Barth committed
81
    for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
82
      AnimatedValue<double> opacity;
Adam Barth's avatar
Adam Barth committed
83
      if (itemIndex == route.selectedIndex) {
84 85 86 87 88 89 90
        opacity = new AnimatedValue<double>(0.0, end: 1.0, curve: const Interval(0.0, 0.001), reverseCurve: const Interval(0.75, 1.0));
      } else {
        final double start = (0.5 + (itemIndex + 1) * unit).clamp(0.0, 1.0);
        final double end = (start + 1.5 * unit).clamp(0.0, 1.0);
        opacity = new AnimatedValue<double>(0.0, end: 1.0, curve: new Interval(start, end), reverseCurve: const Interval(0.75, 1.0));
      }
      children.add(new FadeTransition(
Adam Barth's avatar
Adam Barth committed
91
        performance: route.performance,
92 93
        opacity: opacity,
        child: new InkWell(
94 95 96 97
          child: new Container(
            padding: _kMenuHorizontalPadding,
            child: route.items[itemIndex]
          ),
98 99 100 101
          onTap: () => Navigator.pop(
            context,
            new _DropDownRouteResult<T>(route.items[itemIndex].value)
          )
102 103 104 105 106 107
        )
      ));
    }

    final AnimatedValue<double> menuOpacity = new AnimatedValue<double>(0.0,
      end: 1.0,
108 109
      curve: const Interval(0.0, 0.25),
      reverseCurve: const Interval(0.75, 1.0)
110 111
    );

Adam Barth's avatar
Adam Barth committed
112 113
    final AnimatedValue<double> menuTop = new AnimatedValue<double>(route.rect.top,
      end: route.rect.top - route.selectedIndex * route.rect.height,
114
      curve: const Interval(0.25, 0.5),
115 116
      reverseCurve: const Interval(0.0, 0.001)
    );
Adam Barth's avatar
Adam Barth committed
117 118
    final AnimatedValue<double> menuBottom = new AnimatedValue<double>(route.rect.bottom,
      end: menuTop.end + route.items.length * route.rect.height,
119
      curve: const Interval(0.25, 0.5),
120 121 122
      reverseCurve: const Interval(0.0, 0.001)
    );

123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143
    return new FadeTransition(
      performance: route.performance,
      opacity: menuOpacity,
      child: new BuilderTransition(
        performance: route.performance,
        variables: <AnimatedValue<double>>[menuTop, menuBottom],
        builder: (BuildContext context) {
          return new CustomPaint(
            painter: new _DropDownMenuPainter(
              color: Theme.of(context).canvasColor,
              elevation: route.elevation,
              menuTop: menuTop.value,
              menuBottom: menuBottom.value,
              renderBox: context.findRenderObject()
            ),
            child: new Material(
              type: MaterialType.transparency,
              child: new Block(children)
            )
          );
        }
144 145 146 147 148
      )
    );
  }
}

149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
// We box the return value so that the return value can be null. Otherwise,
// canceling the route (which returns null) would get confused with actually
// returning a real null value.
class _DropDownRouteResult<T> {
  const _DropDownRouteResult(this.result);
  final T result;
  bool operator ==(dynamic other) {
    if (other is! _DropDownRouteResult)
      return false;
    final _DropDownRouteResult<T> typedOther = other;
    return result == typedOther.result;
  }
  int get hashCode => result.hashCode;
}

class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
Hixie's avatar
Hixie committed
165
  _DropDownRoute({
166
    Completer<_DropDownRouteResult<T>> completer,
167 168 169
    this.items,
    this.selectedIndex,
    this.rect,
Hans Muller's avatar
Hans Muller committed
170
    this.elevation: 8
Hixie's avatar
Hixie committed
171
  }) : super(completer: completer);
172

Hixie's avatar
Hixie committed
173 174
  final List<DropDownMenuItem<T>> items;
  final int selectedIndex;
175
  final Rect rect;
Hans Muller's avatar
Hans Muller committed
176
  final int elevation;
177

Hixie's avatar
Hixie committed
178 179 180
  Duration get transitionDuration => _kDropDownMenuDuration;
  bool get barrierDismissable => true;
  Color get barrierColor => null;
181

182 183 184 185 186 187 188 189 190 191 192
  ModalPosition getPosition(BuildContext context) {
    RenderBox overlayBox = Overlay.of(context).context.findRenderObject();
    Size overlaySize = overlayBox.size;
    RelativeRect menuRect = new RelativeRect.fromSize(rect, overlaySize);
    return new ModalPosition(
      top: menuRect.top - selectedIndex * rect.height,
      left: menuRect.left,
      right: menuRect.right
    );
  }

193 194 195
  Widget buildPage(BuildContext context, PerformanceView performance, PerformanceView forwardPerformance) {
    return new _DropDownMenu(route: this);
  }
196 197
}

Hixie's avatar
Hixie committed
198 199
class DropDownMenuItem<T> extends StatelessComponent {
  DropDownMenuItem({
200 201 202 203 204 205
    Key key,
    this.value,
    this.child
  }) : super(key: key);

  final Widget child;
206
  final T value;
207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222

  Widget build(BuildContext context) {
    return new Container(
      height: _kMenuItemHeight,
      padding: const EdgeDims.only(left: 8.0, right: 8.0, top: 6.0),
      child: new DefaultTextStyle(
        style: Theme.of(context).text.subhead,
        child: new Baseline(
          baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
          child: child
        )
      )
    );
  }
}

223
class DropDownButton<T> extends StatefulComponent {
Hixie's avatar
Hixie committed
224
  DropDownButton({
225 226 227 228
    Key key,
    this.items,
    this.value,
    this.onChanged,
Hans Muller's avatar
Hans Muller committed
229
    this.elevation: 8
230 231 232
  }) : super(key: key) {
    assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
  }
233

Hixie's avatar
Hixie committed
234
  final List<DropDownMenuItem<T>> items;
235
  final T value;
Hixie's avatar
Hixie committed
236
  final ValueChanged<T> onChanged;
Hans Muller's avatar
Hans Muller committed
237
  final int elevation;
238

239 240 241 242 243 244 245 246 247
  _DropDownButtonState<T> createState() => new _DropDownButtonState<T>();
}

class _DropDownButtonState<T> extends State<DropDownButton<T>> {
  final GlobalKey indexedStackKey = new GlobalKey(debugLabel: 'DropDownButton.IndexedStack');

  void initState() {
    super.initState();
    _updateSelectedIndex();
248
    assert(_selectedIndex != null);
249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  }

  void didUpdateConfig(DropDownButton<T> oldConfig) {
    if (config.items[_selectedIndex].value != config.value)
      _updateSelectedIndex();
  }

  int _selectedIndex;

  void _updateSelectedIndex() {
    for (int itemIndex = 0; itemIndex < config.items.length; itemIndex++) {
      if (config.items[itemIndex].value == config.value) {
        _selectedIndex = itemIndex;
        return;
      }
    }
  }

  void _handleTap() {
268 269
    final RenderBox renderBox = indexedStackKey.currentContext.findRenderObject();
    final Rect rect = renderBox.localToGlobal(Point.origin) & renderBox.size;
270
    final Completer completer = new Completer<_DropDownRouteResult<T>>();
Hixie's avatar
Hixie committed
271
    Navigator.push(context, new _DropDownRoute<T>(
272
      completer: completer,
273 274
      items: config.items,
      selectedIndex: _selectedIndex,
275
      rect: _kMenuHorizontalPadding.inflateRect(rect),
276
      elevation: config.elevation
277
    ));
278 279
    completer.future.then((_DropDownRouteResult<T> newValue) {
      if (!mounted || newValue == null)
280 281
        return;
      if (config.onChanged != null)
282
        config.onChanged(newValue.result);
283 284 285 286
    });
  }

  Widget build(BuildContext context) {
287
    assert(debugCheckHasMaterial(context));
288
    return new GestureDetector(
289
      onTap: _handleTap,
290
      child: new Container(
Hixie's avatar
Hixie committed
291
        decoration: new BoxDecoration(border: _kDropDownUnderline),
292
        child: new Row(<Widget>[
Hixie's avatar
Hixie committed
293
          new IndexedStack(
294
            config.items,
295
            key: indexedStackKey,
296
            index: _selectedIndex,
297 298 299 300 301 302 303 304
            alignment: const FractionalOffset(0.5, 0.0)
          ),
          new Container(
            child: new Icon(icon: 'navigation/arrow_drop_down', size: IconSize.s36),
            padding: const EdgeDims.only(top: 6.0)
          )
        ],
          justifyContent: FlexJustifyContent.collapse
305
        )
306
      )
307 308 309
    );
  }
}