• Ian Hickson's avatar
    Revert "Rename DefaultTextStyle constructor to explicit (#3920)" (#3930) · c5ff156f
    Ian Hickson authored
    This reverts commit 55f9145e.
    
    Turns out that this commit breaks apps that use the material library,
    because of the _errorTextStyle DefaultTextStyle which has inherit:true.
    Just setting it to false doesn't work, unfortunately, because then you
    hit some sort of issue with merging that text style with others that
    have inherit:true.
    c5ff156f
drop_down.dart 13.8 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
// 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:math' as math;

import 'package:flutter/widgets.dart';

import 'debug.dart';
import 'icon.dart';
import 'icons.dart';
import 'ink_well.dart';
import 'shadows.dart';
import 'theme.dart';
import 'material.dart';

const Duration _kDropDownMenuDuration = const Duration(milliseconds: 300);
const double _kTopMargin = 6.0;
const double _kMenuItemHeight = 48.0;
const EdgeInsets _kMenuVerticalPadding = const EdgeInsets.symmetric(vertical: 8.0);
const EdgeInsets _kMenuHorizontalPadding = const EdgeInsets.symmetric(horizontal: 36.0);
const double _kBaselineOffsetFromBottom = 20.0;
const double _kBottomBorderHeight = 2.0;
const Border _kDropDownUnderline = const Border(bottom: const BorderSide(color: const Color(0xFFBDBDBD), width: _kBottomBorderHeight));

class _DropDownMenuPainter extends CustomPainter {
  _DropDownMenuPainter({
    Color color,
    int elevation,
    this.selectedIndex,
    Animation<double> resize
  }) : color = color,
       elevation = elevation,
       resize = resize,
       _painter = new BoxDecoration(
         backgroundColor: color,
         borderRadius: 2.0,
         boxShadow: kElevationToShadow[elevation]
       ).createBoxPainter(),
       super(repaint: resize);

  final Color color;
  final int elevation;
  final int selectedIndex;
  final Animation<double> resize;

  final BoxPainter _painter;

  @override
  void paint(Canvas canvas, Size size) {
    final Tween<double> top = new Tween<double>(
      begin: (selectedIndex * _kMenuItemHeight + _kMenuVerticalPadding.top).clamp(0.0, size.height - _kMenuItemHeight),
      end: 0.0
    );

    final Tween<double> bottom = new Tween<double>(
      begin: (top.begin + _kMenuItemHeight).clamp(_kMenuItemHeight, size.height),
      end: size.height
    );

    _painter.paint(canvas, new Rect.fromLTRB(0.0, top.evaluate(resize), size.width, bottom.evaluate(resize)));
  }

  @override
  bool shouldRepaint(_DropDownMenuPainter oldPainter) {
    return oldPainter.color != color
        || oldPainter.elevation != elevation
        || oldPainter.selectedIndex != selectedIndex
        || oldPainter.resize != resize;
  }
}

class _DropDownMenu<T> extends StatusTransitionWidget {
  _DropDownMenu({
    Key key,
    _DropDownRoute<T> route
  }) : route = route, super(key: key, animation: route.animation);

  final _DropDownRoute<T> route;

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

    final double unit = 0.5 / (route.items.length + 1.5);
    final List<Widget> children = <Widget>[];
    for (int itemIndex = 0; itemIndex < route.items.length; ++itemIndex) {
      CurvedAnimation opacity;
      Interval reverseCurve = const Interval(0.75, 1.0);
      if (itemIndex == route.selectedIndex) {
        opacity = new CurvedAnimation(parent: route.animation, curve: const Step(0.0), reverseCurve: reverseCurve);
      } 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 CurvedAnimation(parent: route.animation, curve: new Interval(start, end), reverseCurve: reverseCurve);
      }
      children.add(new FadeTransition(
        opacity: opacity,
        child: new InkWell(
          child: new Container(
            padding: _kMenuHorizontalPadding,
            child: route.items[itemIndex]
          ),
          onTap: () => Navigator.pop(
            context,
            new _DropDownRouteResult<T>(route.items[itemIndex].value)
          )
        )
      ));
    }

    return new FadeTransition(
      opacity: new CurvedAnimation(
        parent: route.animation,
        curve: const Interval(0.0, 0.25),
        reverseCurve: const Interval(0.75, 1.0)
      ),
      child: new CustomPaint(
        painter: new _DropDownMenuPainter(
          color: Theme.of(context).canvasColor,
          elevation: route.elevation,
          selectedIndex: route.selectedIndex,
          resize: new CurvedAnimation(
            parent: route.animation,
            curve: const Interval(0.25, 0.5),
            reverseCurve: const Step(0.0)
          )
        ),
        child: new Material(
          type: MaterialType.transparency,
          child: new ScrollableList(
            padding: _kMenuVerticalPadding,
            itemExtent: _kMenuItemHeight,
            children: children
          )
        )
      )
    );
  }
}

class _DropDownMenuRouteLayout extends SingleChildLayoutDelegate {
  _DropDownMenuRouteLayout(this.buttonRect, this.selectedIndex);

  final Rect buttonRect;
  final int selectedIndex;

  @override
  BoxConstraints getConstraintsForChild(BoxConstraints constraints) {
    // The maximum height of a simple menu should be one or more rows less than
    // the view height. This ensures a tappable area outside of the simple menu
    // with which to dismiss the menu.
    //   -- https://www.google.com/design/spec/components/menus.html#menus-simple-menus
    final double maxHeight = math.max(0.0, constraints.maxHeight - 2 * _kMenuItemHeight);
    final double width = buttonRect.width;
    return new BoxConstraints(
      minWidth: width,
      maxWidth: width,
      minHeight: 0.0,
      maxHeight: maxHeight
    );
  }

  @override
  Offset getPositionForChild(Size size, Size childSize) {
    final double buttonTop = buttonRect.top;
    double top = buttonTop - selectedIndex * _kMenuItemHeight - _kMenuVerticalPadding.top;
    double topPreferredLimit = _kMenuItemHeight;
    if (top < topPreferredLimit)
      top = math.min(buttonTop, topPreferredLimit);
    double bottom = top + childSize.height;
    double bottomPreferredLimit = size.height - _kMenuItemHeight;
    if (bottom > bottomPreferredLimit) {
      bottom = math.max(buttonTop + _kMenuItemHeight, bottomPreferredLimit);
      top = bottom - childSize.height;
    }
    assert(top >= 0.0);
    assert(top + childSize.height <= size.height);
    return new Offset(buttonRect.left, top);
  }

  @override
  bool shouldRelayout(_DropDownMenuRouteLayout oldDelegate) {
    return oldDelegate.buttonRect != buttonRect
        || oldDelegate.selectedIndex != selectedIndex;
  }
}

// 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;

  @override
  bool operator ==(dynamic other) {
    if (other is! _DropDownRouteResult<T>)
      return false;
    final _DropDownRouteResult<T> typedOther = other;
    return result == typedOther.result;
  }

  @override
  int get hashCode => result.hashCode;
}

class _DropDownRoute<T> extends PopupRoute<_DropDownRouteResult<T>> {
  _DropDownRoute({
    Completer<_DropDownRouteResult<T>> completer,
    this.items,
    this.buttonRect,
    this.selectedIndex,
    this.elevation: 8
  }) : super(completer: completer);

  final List<DropDownMenuItem<T>> items;
  final Rect buttonRect;
  final int selectedIndex;
  final int elevation;

  @override
  Duration get transitionDuration => _kDropDownMenuDuration;

  @override
  bool get barrierDismissable => true;

  @override
  Color get barrierColor => null;

  @override
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> forwardAnimation) {
    return new CustomSingleChildLayout(
      delegate: new _DropDownMenuRouteLayout(buttonRect, selectedIndex),
      child: new _DropDownMenu<T>(route: this)
    );
  }
}

/// An item in a menu created by a [DropDownButton].
///
/// The type `T` is the type of the value the entry represents. All the entries
/// in a given menu must represent values with consistent types.
class DropDownMenuItem<T> extends StatelessWidget {
  /// Creates an item for a drop down menu.
  ///
  /// The [child] argument is required.
  DropDownMenuItem({
    Key key,
    this.value,
    this.child
  }) : super(key: key) {
    assert(child != null);
  }

  /// The widget below this widget in the tree.
  ///
  /// Typically a [Text] widget.
  final Widget child;

  /// The value to return if the user selects this menu item.
  ///
  /// Eventually returned in a call to [DropDownButton.onChanged].
  final T value;

  @override
  Widget build(BuildContext context) {
    return new Container(
      height: _kMenuItemHeight,
      padding: const EdgeInsets.symmetric(horizontal: 8.0),
      child: new DefaultTextStyle(
        style: Theme.of(context).textTheme.subhead,
        child: new Baseline(
          baselineType: TextBaseline.alphabetic,
          baseline: _kMenuItemHeight - _kBaselineOffsetFromBottom,
          child: child
        )
      )
    );
  }
}

/// An inherited widget that causes any descendant [DropDownButton]
/// widgets to not include their regular underline.
///
/// This is used by [DataTable] to remove the underline from any
/// [DropDownButton] widgets placed within material data tables, as
/// required by the material design specification.
class DropDownButtonHideUnderline extends InheritedWidget {
  /// Creates a [DropDownButtonHideUnderline]. A non-null [child] must
  /// be given.
  DropDownButtonHideUnderline({
    Key key,
    Widget child
  }) : super(key: key, child: child) {
    assert(child != null);
  }

  /// Returns whether the underline of [DropDownButton] widgets should
  /// be hidden.
  static bool at(BuildContext context) {
    return context.inheritFromWidgetOfExactType(DropDownButtonHideUnderline) != null;
  }

  @override
  bool updateShouldNotify(DropDownButtonHideUnderline old) => false;
}

/// A material design button for selecting from a list of items.
///
/// A dropdown button lets the user select from a number of items. The button
/// shows the currently selected item as well as an arrow that opens a menu for
/// selecting another item.
///
/// Requires one of its ancestors to be a [Material] widget.
///
/// See also:
///
///  * [RaisedButton]
///  * [FlatButton]
///  * <https://www.google.com/design/spec/components/buttons.html#buttons-dropdown-buttons>
class DropDownButton<T> extends StatefulWidget {
  /// Creates a drop down button.
  ///
  /// The [items] must have distinct values and [value] must be among them.
  DropDownButton({
    Key key,
    this.items,
    this.value,
    this.onChanged,
    this.elevation: 8
  }) : super(key: key) {
    assert(items != null);
    assert(items.where((DropDownMenuItem<T> item) => item.value == value).length == 1);
  }

  /// The list of possible items to select among.
  final List<DropDownMenuItem<T>> items;

  /// The currently selected item.
  final T value;

  /// Called when the user selects an item.
  final ValueChanged<T> onChanged;

  /// The z-coordinate at which to place the menu when open.
  ///
  /// The following elevations have defined shadows: 1, 2, 3, 4, 6, 8, 9, 12, 16, 24
  final int elevation;

  @override
  _DropDownButtonState<T> createState() => new _DropDownButtonState<T>();
}

class _DropDownButtonState<T> extends State<DropDownButton<T>> {
  final GlobalKey _itemKey = new GlobalKey(debugLabel: 'DropDownButton item key');

  @override
  void initState() {
    super.initState();
    _updateSelectedIndex();
    assert(_selectedIndex != null);
  }

  @override
  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() {
    final RenderBox itemBox = _itemKey.currentContext.findRenderObject();
    final Rect itemRect = itemBox.localToGlobal(Point.origin) & itemBox.size;
    final Completer<_DropDownRouteResult<T>> completer = new Completer<_DropDownRouteResult<T>>();
    Navigator.push(context, new _DropDownRoute<T>(
      completer: completer,
      items: config.items,
      buttonRect: _kMenuHorizontalPadding.inflateRect(itemRect),
      selectedIndex: _selectedIndex,
      elevation: config.elevation
    ));
    completer.future.then((_DropDownRouteResult<T> newValue) {
      if (!mounted || newValue == null)
        return;
      if (config.onChanged != null)
        config.onChanged(newValue.result);
    });
  }

  @override
  Widget build(BuildContext context) {
    assert(debugCheckHasMaterial(context));
    Widget result = new Row(
      mainAxisAlignment: MainAxisAlignment.collapse,
      children: <Widget>[
        // We use an IndexedStack to make sure we have enough width to show any
        // possible item as the selected item without changing size.
        new IndexedStack(
          children: config.items,
          key: _itemKey,
          index: _selectedIndex,
          alignment: FractionalOffset.centerLeft
        ),
        new Icon(icon: Icons.arrow_drop_down, size: 36.0)
      ]
    );
    if (DropDownButtonHideUnderline.at(context)) {
      result = new Padding(
        padding: const EdgeInsets.only(top: _kTopMargin, bottom: _kBottomBorderHeight),
        child: result
      );
    } else {
      result = new Container(
        padding: const EdgeInsets.only(top: _kTopMargin),
        decoration: const BoxDecoration(border: _kDropDownUnderline),
        child: result
      );
    }
    return new GestureDetector(
      onTap: _handleTap,
      child: result
    );
  }
}