home.dart 12.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter 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 6 7
import 'dart:developer';
import 'dart:math' as math;

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

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

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

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

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

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

49 50
  final GalleryDemoCategory? category;
  final VoidCallback? onTap;
51 52 53

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

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

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

105 106
  final Iterable<GalleryDemoCategory>? categories;
  final ValueChanged<GalleryDemoCategory>? onCategoryTap;
107 108 109 110

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

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

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

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

144
                      return SizedBox(
145 146
                        width: columnWidth,
                        height: rowHeight,
147
                        child: _CategoryItem(
148 149
                          category: category,
                          onTap: () {
150
                            onCategoryTap!(category);
151 152 153 154 155 156 157 158 159 160
                          },
                        ),
                      );
                    }),
                  );
                }),
              ),
            );
          },
        ),
161 162 163 164 165 166
      ),
    );
  }
}

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

169
  final GalleryDemo? demo;
170 171

  void _launchDemo(BuildContext context) {
172
    if (demo != null) {
173 174
      Timeline.instantSync('Start Transition', arguments: <String, String>{
        'from': '/',
175
        'to': demo!.routeName,
176
      });
177
      Navigator.pushNamed(context, demo!.routeName);
178 179
    }
  }
180

181 182 183 184
  @override
  Widget build(BuildContext context) {
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
185
    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
186
    return RawMaterialButton(
187 188 189 190 191 192
      padding: EdgeInsets.zero,
      splashColor: theme.primaryColor.withOpacity(0.12),
      highlightColor: Colors.transparent,
      onPressed: () {
        _launchDemo(context);
      },
193 194 195
      child: Container(
        constraints: BoxConstraints(minHeight: _kDemoItemHeight * textScaleFactor),
        child: Row(
196
          children: <Widget>[
197
            Container(
198 199 200
              width: 56.0,
              height: 56.0,
              alignment: Alignment.center,
201
              child: Icon(
202
                demo!.icon,
203 204 205 206
                size: 24.0,
                color: isDark ? Colors.white : _kFlutterBlue,
              ),
            ),
207 208
            Expanded(
              child: Column(
209 210
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.stretch,
211 212
                children: <Widget>[
                  Text(
213 214
                    demo!.title,
                    style: theme.textTheme.subtitle1!.copyWith(
215 216 217
                      color: isDark ? Colors.white : const Color(0xFF202124),
                    ),
                  ),
218
                  if (demo!.subtitle != null)
219
                    Text(
220 221
                      demo!.subtitle!,
                      style: theme.textTheme.bodyText2!.copyWith(
222 223 224 225
                        color: isDark ? Colors.white : const Color(0xFF60646B)
                      ),
                    ),
                ],
226 227 228 229 230 231 232 233 234
              ),
            ),
            const SizedBox(width: 44.0),
          ],
        ),
      ),
    );
  }
}
235

236 237
class _DemosPage extends StatelessWidget {
  const _DemosPage(this.category);
238

239
  final GalleryDemoCategory? category;
Eric Seidel's avatar
Eric Seidel committed
240

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

266 267
class GalleryHome extends StatefulWidget {
  const GalleryHome({
268
    Key? key,
269
    this.testMode = false,
270 271
    this.optionsPage,
  }) : super(key: key);
272

273
  final Widget? optionsPage;
274
  final bool testMode;
275

276 277 278 279
  // In checked mode our MaterialApp will show the default "debug" banner.
  // Otherwise show the "preview" banner.
  static bool showPreviewBanner = true;

280
  @override
281
  State<GalleryHome> createState() => _GalleryHomeState();
282 283
}

284
class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
285
  static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
286 287
  late AnimationController _controller;
  GalleryDemoCategory? _category;
288

289
  static Widget _topHomeLayout(Widget? currentChild, List<Widget> previousChildren) {
290
    return Stack(
291
      alignment: Alignment.topCenter,
292 293 294 295
      children: <Widget>[
        ...previousChildren,
        if (currentChild != null) currentChild,
      ],
296 297 298 299 300
    );
  }

  static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = AnimatedSwitcher.defaultLayoutBuilder;

301 302 303
  @override
  void initState() {
    super.initState();
304
    _controller = AnimationController(
305 306 307 308
      duration: const Duration(milliseconds: 600),
      debugLabel: 'preview banner',
      vsync: this,
    )..forward();
309 310 311 312 313 314 315 316
  }

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

317 318
  @override
  Widget build(BuildContext context) {
319 320
    final ThemeData theme = Theme.of(context);
    final bool isDark = theme.brightness == Brightness.dark;
321 322
    final MediaQueryData media = MediaQuery.of(context);
    final bool centerHome = media.orientation == Orientation.portrait && media.size.height < 800.0;
323

324 325
    const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
    const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
326

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

    assert(() {
383
      GalleryHome.showPreviewBanner = false;
384
      return true;
385
    }());
386

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

    return home;
408 409
  }
}