home.dart 12.7 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();
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 44 45
class _CategoryItem extends StatelessWidget {
  const _CategoryItem({
    this.category,
    this.onTap,
46
  });
47

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

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

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

95 96 97 98
class _CategoriesPage extends StatelessWidget {
  const _CategoriesPage({
    this.categories,
    this.onCategoryTap,
99
  });
100

101 102
  final Iterable<GalleryDemoCategory>? categories;
  final ValueChanged<GalleryDemoCategory>? onCategoryTap;
103 104 105 106

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

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

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

135 136
                  return Row(
                    children: List<Widget>.generate(columnCountForRow, (int columnIndex) {
137 138 139
                      final int index = rowIndex * columnCount + columnIndex;
                      final GalleryDemoCategory category = categoriesList[index];

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

class _DemoItem extends StatelessWidget {
163
  const _DemoItem({ this.demo });
164

165
  final GalleryDemo? demo;
166 167

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

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

232 233
class _DemosPage extends StatelessWidget {
  const _DemosPage(this.category);
234

235
  final GalleryDemoCategory? category;
Eric Seidel's avatar
Eric Seidel committed
236

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

262 263
class GalleryHome extends StatefulWidget {
  const GalleryHome({
264
    super.key,
265
    this.testMode = false,
266
    this.optionsPage,
267
  });
268

269
  final Widget? optionsPage;
270
  final bool testMode;
271

272 273 274 275
  // In checked mode our MaterialApp will show the default "debug" banner.
  // Otherwise show the "preview" banner.
  static bool showPreviewBanner = true;

276
  @override
277
  State<GalleryHome> createState() => _GalleryHomeState();
278 279
}

280
class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin {
281
  static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
282 283
  late AnimationController _controller;
  GalleryDemoCategory? _category;
284

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

  static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = AnimatedSwitcher.defaultLayoutBuilder;

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

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

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

320 321
    const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
    const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn);
322

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

    assert(() {
379
      GalleryHome.showPreviewBanner = false;
380
      return true;
381
    }());
382

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

    return home;
404 405
  }
}