home.dart 12.8 KB
Newer Older
1
// Copyright 2018 The Chromium Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:async';
6 7 8
import 'dart:developer';
import 'dart:math' as math;

9
import 'package:flutter/material.dart';
10
import 'package:flutter/services.dart';
11
import 'package:flutter/gestures.dart' show DragStartBehavior;
12

13 14
import 'backdrop.dart';
import 'demos.dart';
15

16
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
17
const Color _kFlutterBlue = Color(0xFF003D75);
18
const double _kDemoItemHeight = 64.0;
19
const Duration _kFrontLayerSwitchDuration = Duration(milliseconds: 300);
20

21 22
class _FlutterLogo extends StatelessWidget {
  const _FlutterLogo({ Key key }) : super(key: key);
23

24 25
  @override
  Widget build(BuildContext context) {
26 27
    return Center(
      child: Container(
28 29 30
        width: 34.0,
        height: 34.0,
        decoration: const BoxDecoration(
31 32
          image: DecorationImage(
            image: AssetImage(
33
              'logos/flutter_white/logo.png',
34 35 36 37 38 39 40 41
              package: _kGalleryAssetsPackage,
            ),
          ),
        ),
      ),
    );
  }
}
42

43 44 45 46 47 48
class _CategoryItem extends StatelessWidget {
  const _CategoryItem({
    Key key,
    this.category,
    this.onTap,
  }) : super (key: key);
49

50 51
  final GalleryDemoCategory category;
  final VoidCallback onTap;
52 53 54

  @override
  Widget build(BuildContext context) {
55 56 57
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;

58 59
    // This repaint boundary prevents the entire _CategoriesPage from being
    // repainted when the button's ink splash animates.
60 61
    return RepaintBoundary(
      child: RawMaterialButton(
62
        padding: EdgeInsets.zero,
63
        hoverColor: theme.primaryColor.withOpacity(0.05),
64 65 66
        splashColor: theme.primaryColor.withOpacity(0.12),
        highlightColor: Colors.transparent,
        onPressed: onTap,
67
        child: Column(
68 69 70
          mainAxisAlignment: MainAxisAlignment.end,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: <Widget>[
71
            Padding(
72
              padding: const EdgeInsets.all(6.0),
73
              child: Icon(
74 75
                category.icon,
                size: 60.0,
76 77 78
                color: isDark ? Colors.white : _kFlutterBlue,
              ),
            ),
79
            const SizedBox(height: 10.0),
80
            Container(
81 82
              height: 48.0,
              alignment: Alignment.center,
83
              child: Text(
84 85 86 87 88 89 90 91 92 93
                category.name,
                textAlign: TextAlign.center,
                style: theme.textTheme.subhead.copyWith(
                  fontFamily: 'GoogleSans',
                  color: isDark ? Colors.white : _kFlutterBlue,
                ),
              ),
            ),
          ],
        ),
94
      ),
95 96 97 98
    );
  }
}

99 100
class _CategoriesPage extends StatelessWidget {
  const _CategoriesPage({
101
    Key key,
102 103 104
    this.categories,
    this.onCategoryTap,
  }) : super(key: key);
105

106 107 108 109 110 111 112 113 114
  final Iterable<GalleryDemoCategory> categories;
  final ValueChanged<GalleryDemoCategory> onCategoryTap;

  @override
  Widget build(BuildContext context) {
    const double aspectRatio = 160.0 / 180.0;
    final List<GalleryDemoCategory> categoriesList = categories.toList();
    final int columnCount = (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3;

115
    return Semantics(
116 117 118 119
      scopesRoute: true,
      namesRoute: true,
      label: 'categories',
      explicitChildNodes: true,
120
      child: SingleChildScrollView(
121
        key: const PageStorageKey<String>('categories'),
122
        child: LayoutBuilder(
123 124
          builder: (BuildContext context, BoxConstraints constraints) {
            final double columnWidth = constraints.biggest.width / columnCount.toDouble();
125
            final double rowHeight = math.min(225.0, columnWidth * aspectRatio);
126 127 128 129 130
            final int rowCount = (categories.length + columnCount - 1) ~/ columnCount;

            // This repaint boundary prevents the inner contents of the front layer
            // from repainting when the backdrop toggle triggers a repaint on the
            // LayoutBuilder.
131 132
            return RepaintBoundary(
              child: Column(
133 134
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.stretch,
135
                children: List<Widget>.generate(rowCount, (int rowIndex) {
136 137 138 139
                  final int columnCountForRow = rowIndex == rowCount - 1
                    ? categories.length - columnCount * math.max(0, rowCount - 1)
                    : columnCount;

140 141
                  return Row(
                    children: List<Widget>.generate(columnCountForRow, (int columnIndex) {
142 143 144
                      final int index = rowIndex * columnCount + columnIndex;
                      final GalleryDemoCategory category = categoriesList[index];

145
                      return SizedBox(
146 147
                        width: columnWidth,
                        height: rowHeight,
148
                        child: _CategoryItem(
149 150 151 152 153 154 155 156 157 158 159 160 161
                          category: category,
                          onTap: () {
                            onCategoryTap(category);
                          },
                        ),
                      );
                    }),
                  );
                }),
              ),
            );
          },
        ),
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
      ),
    );
  }
}

class _DemoItem extends StatelessWidget {
  const _DemoItem({ Key key, this.demo }) : super(key: key);

  final GalleryDemo demo;

  void _launchDemo(BuildContext context) {
    if (demo.routeName != null) {
      Timeline.instantSync('Start Transition', arguments: <String, String>{
        'from': '/',
        'to': demo.routeName,
      });
      Navigator.pushNamed(context, demo.routeName);
    }
  }
181

182 183 184 185
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
186
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
187

188
    final List<Widget> titleChildren = <Widget>[
189
      Text(
190 191 192 193 194 195 196 197
        demo.title,
        style: theme.textTheme.subhead.copyWith(
          color: isDark ? Colors.white : const Color(0xFF202124),
        ),
      ),
    ];
    if (demo.subtitle != null) {
      titleChildren.add(
198
        Text(
199 200 201 202 203 204 205 206
          demo.subtitle,
          style: theme.textTheme.body1.copyWith(
            color: isDark ? Colors.white : const Color(0xFF60646B)
          ),
        ),
      );
    }

207
    return RawMaterialButton(
208 209 210 211 212 213
      padding: EdgeInsets.zero,
      splashColor: theme.primaryColor.withOpacity(0.12),
      highlightColor: Colors.transparent,
      onPressed: () {
        _launchDemo(context);
      },
214 215 216
      child: Container(
        constraints: BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor),
        child: Row(
217
          children: <Widget>[
218
            Container(
219 220 221
              width: 56.0,
              height: 56.0,
              alignment: Alignment.center,
222
              child: Icon(
223 224 225 226 227
                demo.icon,
                size: 24.0,
                color: isDark ? Colors.white : _kFlutterBlue,
              ),
            ),
228 229
            Expanded(
              child: Column(
230 231
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
232
                children: titleChildren,
233 234 235 236 237 238 239 240 241
              ),
            ),
            const SizedBox(width: 44.0),
          ],
        ),
      ),
    );
  }
}
242

243 244
class _DemosPage extends StatelessWidget {
  const _DemosPage(this.category);
245

246
  final GalleryDemoCategory category;
Eric Seidel's avatar
Eric Seidel committed
247

248 249
  @override
  Widget build(BuildContext context) {
Jonah Williams's avatar
Jonah Williams committed
250
    // When overriding ListView.padding, it is necessary to manually handle
251 252
    // safe areas.
    final double windowBottomPadding = MediaQuery.of(context).padding.bottom;
253
    return KeyedSubtree(
254
      key: const ValueKey<String>('GalleryDemoList'), // So the tests can find this ListView
255
      child: Semantics(
256 257 258 259
        scopesRoute: true,
        namesRoute: true,
        label: category.name,
        explicitChildNodes: true,
260
        child: ListView(
261
          dragStartBehavior: DragStartBehavior.down,
262 263
          key: PageStorageKey<String>(category.name),
          padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding),
264
          children: kGalleryCategoryToDemos[category].map<Widget>((GalleryDemo demo) {
265
            return _DemoItem(demo: demo);
266 267
          }).toList(),
        ),
268 269 270 271
      ),
    );
  }
}
272

273 274 275
class GalleryHome extends StatefulWidget {
  const GalleryHome({
    Key key,
276
    this.testMode = false,
277 278
    this.optionsPage,
  }) : super(key: key);
279

280
  final Widget optionsPage;
281
  final bool testMode;
282

283 284 285 286
  // In checked mode our MaterialApp will show the default "debug" banner.
  // Otherwise show the "preview" banner.
  static bool showPreviewBanner = true;

287
  @override
288
  _GalleryHomeState createState() => _GalleryHomeState();
289 290
}

291
class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
292
  static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
293
  AnimationController _controller;
294
  GalleryDemoCategory _category;
295

296 297 298 299
  static Widget _topHomeLayout(Widget currentChild, List<Widget> previousChildren) {
    List<Widget> children = previousChildren;
    if (currentChild != null)
      children = children.toList()..add(currentChild);
300
    return Stack(
301 302 303 304 305 306 307
      children: children,
      alignment: Alignment.topCenter,
    );
  }

  static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = AnimatedSwitcher.defaultLayoutBuilder;

308 309 310
  @override
  void initState() {
    super.initState();
311
    _controller = AnimationController(
312 313 314 315
      duration: const Duration(milliseconds: 600),
      debugLabel: 'preview banner',
      vsync: this,
    )..forward();
316 317 318 319 320 321 322 323
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

324 325
  @override
  Widget build(BuildContext context) {
326 327
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
328 329
    final MediaQueryData media = MediaQuery.of(context);
    final bool centerHome = media.orientation == Orientation.portrait && media.size.height < 800.0;
330

331 332
    const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
    const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
333

334
    Widget home = Scaffold(
335
      key: _scaffoldKey,
336
      backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
337
      body: SafeArea(
338
        bottom: false,
339
        child: WillPopScope(
340 341 342 343
          onWillPop: () {
            // Pop the category page if Android back button is pressed.
            if (_category != null) {
              setState(() => _category = null);
344
              return Future<bool>.value(false);
345
            }
346
            return Future<bool>.value(true);
347
          },
348
          child: Backdrop(
349 350
            backTitle: const Text('Options'),
            backLayer: widget.optionsPage,
351
            frontAction: AnimatedSwitcher(
352
              duration: _kFrontLayerSwitchDuration,
353 354
              switchOutCurve: switchOutCurve,
              switchInCurve: switchInCurve,
355 356
              child: _category == null
                ? const _FlutterLogo()
357
                : IconButton(
358 359 360 361 362
                  icon: const BackButtonIcon(),
                  tooltip: 'Back',
                  onPressed: () => setState(() => _category = null),
                ),
            ),
363
            frontTitle: AnimatedSwitcher(
364 365 366
              duration: _kFrontLayerSwitchDuration,
              child: _category == null
                ? const Text('Flutter gallery')
367
                : Text(_category.name),
368
            ),
369 370
            frontHeading: widget.testMode ? null : Container(height: 24.0),
            frontLayer: AnimatedSwitcher(
371
              duration: _kFrontLayerSwitchDuration,
372 373
              switchOutCurve: switchOutCurve,
              switchInCurve: switchInCurve,
374
              layoutBuilder: centerHome ? _centerHomeLayout : _topHomeLayout,
375
              child: _category != null
376 377
                ? _DemosPage(_category)
                : _CategoriesPage(
378 379 380 381 382 383
                  categories: kAllGalleryDemoCategories,
                  onCategoryTap: (GalleryDemoCategory category) {
                    setState(() => _category = category);
                  },
                ),
            ),
384
          ),
385 386
        ),
      ),
387
    );
388 389

    assert(() {
390
      GalleryHome.showPreviewBanner = false;
391
      return true;
392
    }());
393

394
    if (GalleryHome.showPreviewBanner) {
395
      home = Stack(
396
        fit: StackFit.expand,
397 398
        children: <Widget>[
          home,
399 400
          FadeTransition(
            opacity: CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
401
            child: const Banner(
402
              message: 'PREVIEW',
403
              location: BannerLocation.topEnd,
404
            ),
405
          ),
406
        ],
407 408
      );
    }
409
    home = AnnotatedRegion<SystemUiOverlayStyle>(
410
      child: home,
411
      value: SystemUiOverlayStyle.light,
412
    );
413 414

    return home;
415 416
  }
}