bottom_app_bar_demo.dart 14.7 KB
Newer Older
1 2 3 4 5 6
// Copyright 2018 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:flutter/material.dart';

7 8
import '../../gallery/demo.dart';

9 10 11 12
class BottomAppBarDemo extends StatefulWidget {
  static const String routeName = '/material/bottom_app_bar';

  @override
13
  State createState() => _BottomAppBarDemoState();
14 15
}

16 17 18 19
// Flutter generally frowns upon abbrevation however this class uses two
// abbrevations extensively: "fab" for floating action button, and "bab"
// for bottom application bar.

20
class _BottomAppBarDemoState extends State<BottomAppBarDemo> {
21
  static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
22

23 24
  // FAB shape

25
  static const _ChoiceValue<Widget> kNoFab = _ChoiceValue<Widget>(
26 27 28 29 30
    title: 'None',
    label: 'do not show a floating action button',
    value: null,
  );

31
  static const _ChoiceValue<Widget> kCircularFab = _ChoiceValue<Widget>(
32 33
    title: 'Circular',
    label: 'circular floating action button',
34
    value: FloatingActionButton(
35
      onPressed: _showSnackbar,
36
      child: Icon(Icons.add, semanticLabel: 'Action'),
37 38 39 40
      backgroundColor: Colors.orange,
    ),
  );

41
  static const _ChoiceValue<Widget> kDiamondFab = _ChoiceValue<Widget>(
42 43
    title: 'Diamond',
    label: 'diamond shape floating action button',
44
    value: _DiamondFab(
45
      onPressed: _showSnackbar,
46
      child: Icon(Icons.add, semanticLabel: 'Action'),
47 48 49 50 51
    ),
  );

  // Notch

52
  static const _ChoiceValue<bool> kShowNotchTrue = _ChoiceValue<bool>(
53 54 55 56 57
    title: 'On',
    label: 'show bottom appbar notch',
    value: true,
  );

58
  static const _ChoiceValue<bool> kShowNotchFalse = _ChoiceValue<bool>(
59 60 61 62 63 64 65
    title: 'Off',
    label: 'do not show bottom appbar notch',
    value: false,
  );

  // FAB Position

66
  static const _ChoiceValue<FloatingActionButtonLocation> kFabEndDocked = _ChoiceValue<FloatingActionButtonLocation>(
67 68 69 70 71
    title: 'Attached - End',
    label: 'floating action button is docked at the end of the bottom app bar',
    value: FloatingActionButtonLocation.endDocked,
  );

72
  static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterDocked = _ChoiceValue<FloatingActionButtonLocation>(
73 74 75 76 77
    title: 'Attached - Center',
    label: 'floating action button is docked at the center of the bottom app bar',
    value: FloatingActionButtonLocation.centerDocked,
  );

78
  static const _ChoiceValue<FloatingActionButtonLocation> kFabEndFloat= _ChoiceValue<FloatingActionButtonLocation>(
79 80 81 82 83
    title: 'Free - End',
    label: 'floating action button floats above the end of the bottom app bar',
    value: FloatingActionButtonLocation.endFloat,
  );

84
  static const _ChoiceValue<FloatingActionButtonLocation> kFabCenterFloat = _ChoiceValue<FloatingActionButtonLocation>(
85 86 87 88
    title: 'Free - Center',
    label: 'floating action button is floats above the center of the bottom app bar',
    value: FloatingActionButtonLocation.centerFloat,
  );
89

90 91 92 93 94 95
  static void _showSnackbar() {
    const String text =
      "When the Scaffold's floating action button location changes, "
      'the floating action button animates to its new position.'
      'The BottomAppBar adapts its shape appropriately.';
    _scaffoldKey.currentState.showSnackBar(
96
      const SnackBar(content: Text(text)),
97 98 99 100
    );
  }

  // App bar color
101

102 103 104 105 106 107 108
  static const List<_NamedColor> kBabColors = <_NamedColor>[
    _NamedColor(null, 'Clear'),
    _NamedColor(Color(0xFFFFC100), 'Orange'),
    _NamedColor(Color(0xFF91FAFF), 'Light Blue'),
    _NamedColor(Color(0xFF00D1FF), 'Cyan'),
    _NamedColor(Color(0xFF00BCFF), 'Cerulean'),
    _NamedColor(Color(0xFF009BEE), 'Blue'),
109 110
  ];

111 112 113
  _ChoiceValue<Widget> _fabShape = kCircularFab;
  _ChoiceValue<bool> _showNotch = kShowNotchTrue;
  _ChoiceValue<FloatingActionButtonLocation> _fabLocation = kFabEndDocked;
114
  Color _babColor = kBabColors.first.color;
115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

  void _onShowNotchChanged(_ChoiceValue<bool> value) {
    setState(() {
      _showNotch = value;
    });
  }

  void _onFabShapeChanged(_ChoiceValue<Widget> value) {
    setState(() {
      _fabShape = value;
    });
  }

  void _onFabLocationChanged(_ChoiceValue<FloatingActionButtonLocation> value) {
    setState(() {
      _fabLocation = value;
    });
  }

  void _onBabColorChanged(Color value) {
    setState(() {
      _babColor = value;
    });
  }
139 140 141

  @override
  Widget build(BuildContext context) {
142
    return Scaffold(
143
      key: _scaffoldKey,
144
      appBar: AppBar(
145 146
        title: const Text('Bottom app bar'),
        elevation: 0.0,
147
        actions: <Widget>[
148
          MaterialDemoDocumentationButton(BottomAppBarDemo.routeName),
149
          IconButton(
150
            icon: const Icon(Icons.sentiment_very_satisfied, semanticLabel: 'Update shape'),
151 152 153 154 155 156 157
            onPressed: () {
              setState(() {
                _fabShape = _fabShape == kCircularFab ? kDiamondFab : kCircularFab;
              });
            },
          ),
        ],
158
      ),
159 160 161 162 163
      body: Scrollbar(
        child: ListView(
          padding: const EdgeInsets.only(bottom: 88.0),
          children: <Widget>[
            const _Heading('FAB Shape'),
164

165 166 167
            _RadioItem<Widget>(kCircularFab, _fabShape, _onFabShapeChanged),
            _RadioItem<Widget>(kDiamondFab, _fabShape, _onFabShapeChanged),
            _RadioItem<Widget>(kNoFab, _fabShape, _onFabShapeChanged),
168

169 170
            const Divider(),
            const _Heading('Notch'),
171

172 173
            _RadioItem<bool>(kShowNotchTrue, _showNotch, _onShowNotchChanged),
            _RadioItem<bool>(kShowNotchFalse, _showNotch, _onShowNotchChanged),
174

175 176
            const Divider(),
            const _Heading('FAB Position'),
177

178 179 180 181
            _RadioItem<FloatingActionButtonLocation>(kFabEndDocked, _fabLocation, _onFabLocationChanged),
            _RadioItem<FloatingActionButtonLocation>(kFabCenterDocked, _fabLocation, _onFabLocationChanged),
            _RadioItem<FloatingActionButtonLocation>(kFabEndFloat, _fabLocation, _onFabLocationChanged),
            _RadioItem<FloatingActionButtonLocation>(kFabCenterFloat, _fabLocation, _onFabLocationChanged),
182

183 184
            const Divider(),
            const _Heading('App bar color'),
185

186 187 188
            _ColorsItem(kBabColors, _babColor, _onBabColorChanged),
          ],
        ),
189
      ),
190 191
      floatingActionButton: _fabShape.value,
      floatingActionButtonLocation: _fabLocation.value,
192
      bottomNavigationBar: _DemoBottomAppBar(
193 194
        color: _babColor,
        fabLocation: _fabLocation.value,
195
        shape: _selectNotch(),
196 197 198
      ),
    );
  }
199 200 201 202 203 204 205 206 207 208

  NotchedShape _selectNotch() {
    if (!_showNotch.value)
      return null;
    if (_fabShape == kCircularFab)
      return const CircularNotchedRectangle();
    if (_fabShape == kDiamondFab)
      return const _DiamondNotchedRectangle();
    return null;
  }
209
}
210

211 212
class _ChoiceValue<T> {
  const _ChoiceValue({ this.value, this.title, this.label });
213

214 215 216
  final T value;
  final String title;
  final String label; // For the Semantics widget that contains title
217

218 219 220
  @override
  String toString() => '$runtimeType("$title")';
}
221

222 223 224 225 226 227 228 229 230 231
class _RadioItem<T> extends StatelessWidget {
  const _RadioItem(this.value, this.groupValue, this.onChanged);

  final _ChoiceValue<T> value;
  final _ChoiceValue<T> groupValue;
  final ValueChanged<_ChoiceValue<T>> onChanged;

  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
232
    return Container(
233 234 235
      height: 56.0,
      padding: const EdgeInsetsDirectional.only(start: 16.0),
      alignment: AlignmentDirectional.centerStart,
236 237
      child: MergeSemantics(
        child: Row(
238
          children: <Widget>[
239
            Radio<_ChoiceValue<T>>(
240 241 242
              value: value,
              groupValue: groupValue,
              onChanged: onChanged,
243
            ),
244 245
            Expanded(
              child: Semantics(
246 247
                container: true,
                button: true,
248
                label: value.label,
249
                child: GestureDetector(
250 251 252 253
                  behavior: HitTestBehavior.opaque,
                  onTap: () {
                    onChanged(value);
                  },
254
                  child: Text(
255 256 257
                    value.title,
                    style: theme.textTheme.subhead,
                  ),
258
                ),
259 260
              ),
            ),
261
          ],
262
        ),
263
      ),
264 265 266 267
    );
  }
}

268 269 270 271 272 273 274
class _NamedColor {
  const _NamedColor(this.color, this.name);

  final Color color;
  final String name;
}

275 276
class _ColorsItem extends StatelessWidget {
  const _ColorsItem(this.colors, this.selectedColor, this.onChanged);
277

278
  final List<_NamedColor> colors;
279 280
  final Color selectedColor;
  final ValueChanged<Color> onChanged;
281

282 283
  @override
  Widget build(BuildContext context) {
284
    return Row(
285
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
286
      children: colors.map<Widget>((_NamedColor namedColor) {
287
        return RawMaterialButton(
288 289 290 291 292 293 294 295
          onPressed: () {
            onChanged(namedColor.color);
          },
          constraints: const BoxConstraints.tightFor(
            width: 32.0,
            height: 32.0,
          ),
          fillColor: namedColor.color,
296 297
          shape: CircleBorder(
            side: BorderSide(
298 299
              color: namedColor.color == selectedColor ? Colors.black : const Color(0xFFD5D7DA),
              width: 2.0,
300
            ),
301
          ),
302
          child: Semantics(
303 304 305 306 307
            value: namedColor.name,
            selected: namedColor.color == selectedColor,
          ),
        );
      }).toList(),
308 309
    );
  }
310 311
}

312 313 314 315
class _Heading extends StatelessWidget {
  const _Heading(this.text);

  final String text;
316

317 318 319
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
320
    return Container(
321 322 323
      height: 48.0,
      padding: const EdgeInsetsDirectional.only(start: 56.0),
      alignment: AlignmentDirectional.centerStart,
324
      child: Text(
325 326 327 328 329 330 331
        text,
        style: theme.textTheme.body1.copyWith(
          color: theme.primaryColor,
        ),
      ),
    );
  }
332 333 334
}

class _DemoBottomAppBar extends StatelessWidget {
335 336 337
  const _DemoBottomAppBar({
    this.color,
    this.fabLocation,
338
    this.shape,
339
  });
340 341

  final Color color;
342
  final FloatingActionButtonLocation fabLocation;
343
  final NotchedShape shape;
344

Hans Muller's avatar
Hans Muller committed
345
  static final List<FloatingActionButtonLocation> kCenterLocations = <FloatingActionButtonLocation>[
346 347 348
    FloatingActionButtonLocation.centerDocked,
    FloatingActionButtonLocation.centerFloat,
  ];
349 350 351

  @override
  Widget build(BuildContext context) {
352
    return BottomAppBar(
353
      color: color,
354
      shape: shape,
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
      child: Row(children: <Widget>[
        IconButton(
          icon: const Icon(Icons.menu, semanticLabel: 'Show bottom sheet'),
          onPressed: () {
            showModalBottomSheet<void>(
              context: context,
              builder: (BuildContext context) => const _DemoDrawer(),
            );
          },
        ),
        if (kCenterLocations.contains(fabLocation)) const Expanded(child: SizedBox()),
        IconButton(
          icon: const Icon(Icons.search, semanticLabel: 'show search action',),
          onPressed: () {
            Scaffold.of(context).showSnackBar(
              const SnackBar(content: Text('This is a dummy search action.')),
            );
          },
        ),
        IconButton(
          icon: Icon(
            Theme.of(context).platform == TargetPlatform.iOS
                ? Icons.more_horiz
                : Icons.more_vert,
            semanticLabel: 'Show menu actions',
          ),
          onPressed: () {
            Scaffold.of(context).showSnackBar(
              const SnackBar(content: Text('This is a dummy menu action.')),
            );
          },
        ),
      ]),
388 389 390 391 392 393 394 395 396 397
    );
  }
}

// A drawer that pops up from the bottom of the screen.
class _DemoDrawer extends StatelessWidget {
  const _DemoDrawer();

  @override
  Widget build(BuildContext context) {
398 399
    return Drawer(
      child: Column(
400
        children: const <Widget>[
401 402 403
          ListTile(
            leading: Icon(Icons.search),
            title: Text('Search'),
404
          ),
405 406 407
          ListTile(
            leading: Icon(Icons.threed_rotation),
            title: Text('3D'),
408 409 410 411 412 413 414 415
          ),
        ],
      ),
    );
  }
}

// A diamond-shaped floating action button.
416
class _DiamondFab extends StatelessWidget {
417 418 419 420 421 422 423 424 425 426
  const _DiamondFab({
    this.child,
    this.onPressed,
  });

  final Widget child;
  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
427
    return Material(
428 429
      shape: const _DiamondBorder(),
      color: Colors.orange,
430
      child: InkWell(
431
        onTap: onPressed,
432
        child: Container(
433 434 435
          width: 56.0,
          height: 56.0,
          child: IconTheme.merge(
436
            data: IconThemeData(color: Theme.of(context).accentIconTheme.color),
437
            child: child,
438 439 440 441 442 443
          ),
        ),
      ),
      elevation: 6.0,
    );
  }
444
}
445

446 447
class _DiamondNotchedRectangle implements NotchedShape {
  const _DiamondNotchedRectangle();
448 449

  @override
450 451
  Path getOuterPath(Rect host, Rect guest) {
    if (!host.overlaps(guest))
452
      return Path()..addRect(host);
453
    assert(guest.width > 0.0);
454

455
    final Rect intersection = guest.intersect(host);
456 457 458 459 460 461 462 463 464 465 466 467 468
    // We are computing a "V" shaped notch, as in this diagram:
    //    -----\****   /-----
    //          \     /
    //           \   /
    //            \ /
    //
    //  "-" marks the top edge of the bottom app bar.
    //  "\" and "/" marks the notch outline
    //
    //  notchToCenter is the horizontal distance between the guest's center and
    //  the host's top edge where the notch starts (marked with "*").
    //  We compute notchToCenter by similar triangles:
    final double notchToCenter =
469 470
      intersection.height * (guest.height / 2.0)
      / (guest.width / 2.0);
471

472
    return Path()
473 474 475 476 477 478 479 480
      ..moveTo(host.left, host.top)
      ..lineTo(guest.center.dx - notchToCenter, host.top)
      ..lineTo(guest.left + guest.width / 2.0, guest.bottom)
      ..lineTo(guest.center.dx + notchToCenter, host.top)
      ..lineTo(host.right, host.top)
      ..lineTo(host.right, host.bottom)
      ..lineTo(host.left, host.bottom)
      ..close();
481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
  }
}

class _DiamondBorder extends ShapeBorder {
  const _DiamondBorder();

  @override
  EdgeInsetsGeometry get dimensions {
    return const EdgeInsets.only();
  }

  @override
  Path getInnerPath(Rect rect, { TextDirection textDirection }) {
    return getOuterPath(rect, textDirection: textDirection);
  }

  @override
  Path getOuterPath(Rect rect, { TextDirection textDirection }) {
499
    return Path()
500 501 502 503 504 505 506 507
      ..moveTo(rect.left + rect.width / 2.0, rect.top)
      ..lineTo(rect.right, rect.top + rect.height / 2.0)
      ..lineTo(rect.left + rect.width  / 2.0, rect.bottom)
      ..lineTo(rect.left, rect.top + rect.height / 2.0)
      ..close();
  }

  @override
508
  void paint(Canvas canvas, Rect rect, { TextDirection textDirection }) { }
509 510 511 512 513 514 515

  // This border doesn't support scaling.
  @override
  ShapeBorder scale(double t) {
    return null;
  }
}