dropdown.dart 9.71 KB
Newer Older
1 2 3 4 5 6 7 8
// 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/widgets.dart';

9
import 'debug.dart';
10
import 'icon.dart';
11
import 'icons.dart';
12 13 14
import 'ink_well.dart';
import 'shadows.dart';
import 'theme.dart';
15
import 'material.dart';
16

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

Hixie's avatar
Hixie committed
23 24
class _DropDownMenuPainter extends CustomPainter {
  const _DropDownMenuPainter({
25 26 27 28 29 30 31 32 33 34 35 36 37 38
    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) {
39
    final BoxPainter painter = new BoxDecoration(
40 41 42
      backgroundColor: color,
      borderRadius: 2.0,
      boxShadow: elevationToShadow[elevation]
43
    ).createBoxPainter();
44 45 46 47 48 49

    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
50
  bool shouldRepaint(_DropDownMenuPainter oldPainter) {
51 52 53 54 55 56 57 58
    return oldPainter.color != color
        || oldPainter.elevation != elevation
        || oldPainter.menuTop != menuTop
        || oldPainter.menuBottom != menuBottom
        || oldPainter.renderBox != renderBox;
  }
}

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

Hixie's avatar
Hixie committed
65
  final _DropDownRoute<T> route;
66 67 68

  Widget build(BuildContext context) {
    // The menu is shown in three stages (unit timing in brackets):
Hixie's avatar
Hixie committed
69 70
    // [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
71
    //   until it's big enough for as many items as we're going to show.
Hixie's avatar
Hixie committed
72
    // [0.5s - 1.0s] Fade in the remaining visible items from top to bottom.
73 74
    //
    // When the menu is dismissed we just fade the entire thing out
Hixie's avatar
Hixie committed
75
    // in the first 0.25s.
76

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

103 104
    final CurvedAnimation opacity = new CurvedAnimation(
      parent: route.animation,
105 106
      curve: const Interval(0.0, 0.25),
      reverseCurve: const Interval(0.75, 1.0)
107 108
    );

109 110
    final CurvedAnimation resize = new CurvedAnimation(
      parent: route.animation,
111
      curve: const Interval(0.25, 0.5),
112 113
      reverseCurve: const Interval(0.0, 0.001)
    );
114 115 116 117 118 119 120 121

    final Tween<double> menuTop = new Tween<double>(
      begin: route.rect.top,
      end: route.rect.top - route.selectedIndex * route.rect.height
    );
    final Tween<double> menuBottom = new Tween<double>(
      begin: route.rect.bottom,
      end: menuTop.end + route.items.length * route.rect.height
122 123
    );

124 125
    Widget child = new Material(
      type: MaterialType.transparency,
126
      child: new Block(children: children)
127 128 129 130 131
    );
    return new FadeTransition(
      opacity: opacity,
      child: new AnimatedBuilder(
        animation: resize,
132
        builder: (BuildContext context, Widget child) {
133 134 135 136
          return new CustomPaint(
            painter: new _DropDownMenuPainter(
              color: Theme.of(context).canvasColor,
              elevation: route.elevation,
137 138
              menuTop: menuTop.evaluate(resize),
              menuBottom: menuBottom.evaluate(resize),
139 140
              renderBox: context.findRenderObject()
            ),
141
            child: child
142
          );
143 144
        },
        child: child
145 146 147 148 149
      )
    );
  }
}

150 151 152 153 154 155 156
// 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) {
157
    if (other is! _DropDownRouteResult<T>)
158 159 160 161 162 163 164 165
      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
166
  _DropDownRoute({
167
    Completer<_DropDownRouteResult<T>> completer,
168 169 170
    this.items,
    this.selectedIndex,
    this.rect,
Hans Muller's avatar
Hans Muller committed
171
    this.elevation: 8
Hixie's avatar
Hixie committed
172
  }) : super(completer: completer);
173

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

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

183 184
  ModalPosition getPosition(BuildContext context) {
    RenderBox overlayBox = Overlay.of(context).context.findRenderObject();
185
    assert(overlayBox != null); // can't be null; routes get inserted by Navigator which has its own Overlay
186 187 188 189 190 191 192 193 194
    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
    );
  }

195
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
196
    return new _DropDownMenu<T>(route: this);
197
  }
198 199
}

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

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

  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
        )
      )
    );
  }
}

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

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

241 242 243 244 245 246 247 248 249
  _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();
250
    assert(_selectedIndex != null);
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
  }

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

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