1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
---|---|
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:developer'; |
6 | import 'dart:math' as math; |
7 | |
8 | import 'package:flutter/gestures.dart' show DragStartBehavior; |
9 | import 'package:flutter/material.dart'; |
10 | import 'package:flutter/services.dart'; |
11 | |
12 | import 'backdrop.dart'; |
13 | 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 | @override |
24 | Widget build(BuildContext context) { |
25 | return Center( |
26 | child: Container( |
27 | width: 34.0, |
28 | height: 34.0, |
29 | decoration: const BoxDecoration( |
30 | image: DecorationImage( |
31 | image: AssetImage('logos/flutter_white/logo.png', package: _kGalleryAssetsPackage), |
32 | ), |
33 | ), |
34 | ), |
35 | ); |
36 | } |
37 | } |
38 | |
39 | class _CategoryItem extends StatelessWidget { |
40 | const _CategoryItem({this.category, this.onTap}); |
41 | |
42 | final GalleryDemoCategory? category; |
43 | final VoidCallback? onTap; |
44 | |
45 | @override |
46 | Widget build(BuildContext context) { |
47 | final ThemeData theme = Theme.of(context); |
48 | final bool isDark = theme.brightness == Brightness.dark; |
49 | |
50 | // This repaint boundary prevents the entire _CategoriesPage from being |
51 | // repainted when the button's ink splash animates. |
52 | return RepaintBoundary( |
53 | child: RawMaterialButton( |
54 | hoverColor: theme.primaryColor.withOpacity(0.05), |
55 | splashColor: theme.primaryColor.withOpacity(0.12), |
56 | highlightColor: Colors.transparent, |
57 | onPressed: onTap, |
58 | child: Column( |
59 | mainAxisAlignment: MainAxisAlignment.end, |
60 | children: <Widget>[ |
61 | Padding( |
62 | padding: const EdgeInsets.all(6.0), |
63 | child: Icon(category!.icon, size: 60.0, color: isDark ? Colors.white : _kFlutterBlue), |
64 | ), |
65 | const SizedBox(height: 10.0), |
66 | Container( |
67 | height: 48.0, |
68 | alignment: Alignment.center, |
69 | child: Text( |
70 | category!.name, |
71 | textAlign: TextAlign.center, |
72 | style: theme.textTheme.titleMedium!.copyWith( |
73 | fontFamily: 'GoogleSans', |
74 | color: isDark ? Colors.white : _kFlutterBlue, |
75 | ), |
76 | ), |
77 | ), |
78 | ], |
79 | ), |
80 | ), |
81 | ); |
82 | } |
83 | } |
84 | |
85 | class _CategoriesPage extends StatelessWidget { |
86 | const _CategoriesPage({this.categories, this.onCategoryTap}); |
87 | |
88 | final Iterable<GalleryDemoCategory>? categories; |
89 | final ValueChanged<GalleryDemoCategory>? onCategoryTap; |
90 | |
91 | @override |
92 | Widget build(BuildContext context) { |
93 | const double aspectRatio = 160.0 / 180.0; |
94 | final List<GalleryDemoCategory> categoriesList = categories!.toList(); |
95 | final int columnCount = (MediaQuery.of(context).orientation == Orientation.portrait) ? 2 : 3; |
96 | |
97 | return Semantics( |
98 | scopesRoute: true, |
99 | namesRoute: true, |
100 | label: 'categories', |
101 | explicitChildNodes: true, |
102 | child: SingleChildScrollView( |
103 | key: const PageStorageKey<String>('categories'), |
104 | child: LayoutBuilder( |
105 | builder: (BuildContext context, BoxConstraints constraints) { |
106 | final double columnWidth = constraints.biggest.width / columnCount.toDouble(); |
107 | final double rowHeight = math.min(225.0, columnWidth * aspectRatio); |
108 | final int rowCount = (categories!.length + columnCount - 1) ~/ columnCount; |
109 | |
110 | // This repaint boundary prevents the inner contents of the front layer |
111 | // from repainting when the backdrop toggle triggers a repaint on the |
112 | // LayoutBuilder. |
113 | return RepaintBoundary( |
114 | child: Column( |
115 | mainAxisSize: MainAxisSize.min, |
116 | crossAxisAlignment: CrossAxisAlignment.stretch, |
117 | children: List<Widget>.generate(rowCount, (int rowIndex) { |
118 | final int columnCountForRow = |
119 | rowIndex == rowCount - 1 |
120 | ? categories!.length - columnCount * math.max<int>(0, rowCount - 1) |
121 | : columnCount; |
122 | |
123 | return Row( |
124 | children: List<Widget>.generate(columnCountForRow, (int columnIndex) { |
125 | final int index = rowIndex * columnCount + columnIndex; |
126 | final GalleryDemoCategory category = categoriesList[index]; |
127 | |
128 | return SizedBox( |
129 | width: columnWidth, |
130 | height: rowHeight, |
131 | child: _CategoryItem( |
132 | category: category, |
133 | onTap: () { |
134 | onCategoryTap!(category); |
135 | }, |
136 | ), |
137 | ); |
138 | }), |
139 | ); |
140 | }), |
141 | ), |
142 | ); |
143 | }, |
144 | ), |
145 | ), |
146 | ); |
147 | } |
148 | } |
149 | |
150 | class _DemoItem extends StatelessWidget { |
151 | const _DemoItem({this.demo}); |
152 | |
153 | final GalleryDemo? demo; |
154 | |
155 | void _launchDemo(BuildContext context) { |
156 | if (demo != null) { |
157 | Timeline.instantSync( |
158 | 'Start Transition', |
159 | arguments: <String, String>{'from': '/', 'to': demo!.routeName}, |
160 | ); |
161 | Navigator.pushNamed(context, demo!.routeName); |
162 | } |
163 | } |
164 | |
165 | @override |
166 | Widget build(BuildContext context) { |
167 | final ThemeData theme = Theme.of(context); |
168 | final bool isDark = theme.brightness == Brightness.dark; |
169 | // The fontSize to use for computing the heuristic UI scaling factor. |
170 | const double defaultFontSize = 14.0; |
171 | final double containerScalingFactor = |
172 | MediaQuery.textScalerOf(context).scale(defaultFontSize) / defaultFontSize; |
173 | return RawMaterialButton( |
174 | splashColor: theme.primaryColor.withOpacity(0.12), |
175 | highlightColor: Colors.transparent, |
176 | onPressed: () { |
177 | _launchDemo(context); |
178 | }, |
179 | child: Container( |
180 | constraints: BoxConstraints(minHeight: _kDemoItemHeight * containerScalingFactor), |
181 | child: Row( |
182 | children: <Widget>[ |
183 | Container( |
184 | width: 56.0, |
185 | height: 56.0, |
186 | alignment: Alignment.center, |
187 | child: Icon(demo!.icon, size: 24.0, color: isDark ? Colors.white : _kFlutterBlue), |
188 | ), |
189 | Expanded( |
190 | child: Column( |
191 | mainAxisAlignment: MainAxisAlignment.center, |
192 | crossAxisAlignment: CrossAxisAlignment.stretch, |
193 | children: <Widget>[ |
194 | Text( |
195 | demo!.title, |
196 | style: theme.textTheme.titleMedium!.copyWith( |
197 | color: isDark ? Colors.white : const Color(0xFF202124), |
198 | ), |
199 | ), |
200 | if (demo!.subtitle != null) |
201 | Text( |
202 | demo!.subtitle!, |
203 | style: theme.textTheme.bodyMedium!.copyWith( |
204 | color: isDark ? Colors.white : const Color(0xFF60646B), |
205 | ), |
206 | ), |
207 | ], |
208 | ), |
209 | ), |
210 | const SizedBox(width: 44.0), |
211 | ], |
212 | ), |
213 | ), |
214 | ); |
215 | } |
216 | } |
217 | |
218 | class _DemosPage extends StatelessWidget { |
219 | const _DemosPage(this.category); |
220 | |
221 | final GalleryDemoCategory? category; |
222 | |
223 | @override |
224 | Widget build(BuildContext context) { |
225 | // When overriding ListView.padding, it is necessary to manually handle |
226 | // safe areas. |
227 | final double windowBottomPadding = MediaQuery.of(context).padding.bottom; |
228 | return KeyedSubtree( |
229 | key: const ValueKey<String>('GalleryDemoList'), // So the tests can find this ListView |
230 | child: Semantics( |
231 | scopesRoute: true, |
232 | namesRoute: true, |
233 | label: category!.name, |
234 | explicitChildNodes: true, |
235 | child: ListView( |
236 | dragStartBehavior: DragStartBehavior.down, |
237 | key: PageStorageKey<String>(category!.name), |
238 | padding: EdgeInsets.only(top: 8.0, bottom: windowBottomPadding), |
239 | children: |
240 | kGalleryCategoryToDemos[category!]!.map<Widget>((GalleryDemo demo) { |
241 | return _DemoItem(demo: demo); |
242 | }).toList(), |
243 | ), |
244 | ), |
245 | ); |
246 | } |
247 | } |
248 | |
249 | class GalleryHome extends StatefulWidget { |
250 | const GalleryHome({super.key, this.testMode = false, this.optionsPage}); |
251 | |
252 | final Widget? optionsPage; |
253 | final bool testMode; |
254 | |
255 | // In checked mode our MaterialApp will show the default "debug" banner. |
256 | // Otherwise show the "preview" banner. |
257 | static bool showPreviewBanner = true; |
258 | |
259 | @override |
260 | State<GalleryHome> createState() => _GalleryHomeState(); |
261 | } |
262 | |
263 | class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStateMixin { |
264 | static final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); |
265 | late AnimationController _controller; |
266 | GalleryDemoCategory? _category; |
267 | |
268 | static Widget _topHomeLayout(Widget? currentChild, List<Widget> previousChildren) { |
269 | return Stack( |
270 | alignment: Alignment.topCenter, |
271 | children: <Widget>[...previousChildren, if (currentChild != null) currentChild], |
272 | ); |
273 | } |
274 | |
275 | static const AnimatedSwitcherLayoutBuilder _centerHomeLayout = |
276 | AnimatedSwitcher.defaultLayoutBuilder; |
277 | |
278 | @override |
279 | void initState() { |
280 | super.initState(); |
281 | _controller = AnimationController( |
282 | duration: const Duration(milliseconds: 600), |
283 | debugLabel: 'preview banner', |
284 | vsync: this, |
285 | )..forward(); |
286 | } |
287 | |
288 | @override |
289 | void dispose() { |
290 | _controller.dispose(); |
291 | super.dispose(); |
292 | } |
293 | |
294 | @override |
295 | Widget build(BuildContext context) { |
296 | final ThemeData theme = Theme.of(context); |
297 | final bool isDark = theme.brightness == Brightness.dark; |
298 | final MediaQueryData media = MediaQuery.of(context); |
299 | final bool centerHome = media.orientation == Orientation.portrait && media.size.height < 800.0; |
300 | |
301 | const Curve switchOutCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); |
302 | const Curve switchInCurve = Interval(0.4, 1.0, curve: Curves.fastOutSlowIn); |
303 | |
304 | Widget home = Scaffold( |
305 | key: _scaffoldKey, |
306 | backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, |
307 | body: SafeArea( |
308 | bottom: false, |
309 | child: PopScope<Object?>( |
310 | canPop: _category == null, |
311 | onPopInvokedWithResult: (bool didPop, Object? result) { |
312 | if (didPop) { |
313 | return; |
314 | } |
315 | // Pop the category page if Android back button is pressed. |
316 | setState(() => _category = null); |
317 | }, |
318 | child: Backdrop( |
319 | backTitle: const Text('Options'), |
320 | backLayer: widget.optionsPage, |
321 | frontAction: AnimatedSwitcher( |
322 | duration: _kFrontLayerSwitchDuration, |
323 | switchOutCurve: switchOutCurve, |
324 | switchInCurve: switchInCurve, |
325 | child: |
326 | _category == null |
327 | ? const _FlutterLogo() |
328 | : IconButton( |
329 | icon: const BackButtonIcon(), |
330 | tooltip: 'Back', |
331 | onPressed: () => setState(() => _category = null), |
332 | ), |
333 | ), |
334 | frontTitle: AnimatedSwitcher( |
335 | duration: _kFrontLayerSwitchDuration, |
336 | child: _category == null ? const Text('Flutter gallery') : Text(_category!.name), |
337 | ), |
338 | frontHeading: widget.testMode ? null : Container(height: 24.0), |
339 | frontLayer: AnimatedSwitcher( |
340 | duration: _kFrontLayerSwitchDuration, |
341 | switchOutCurve: switchOutCurve, |
342 | switchInCurve: switchInCurve, |
343 | layoutBuilder: centerHome ? _centerHomeLayout : _topHomeLayout, |
344 | child: |
345 | _category != null |
346 | ? _DemosPage(_category) |
347 | : _CategoriesPage( |
348 | categories: kAllGalleryDemoCategories, |
349 | onCategoryTap: (GalleryDemoCategory category) { |
350 | setState(() => _category = category); |
351 | }, |
352 | ), |
353 | ), |
354 | ), |
355 | ), |
356 | ), |
357 | ); |
358 | |
359 | assert(() { |
360 | GalleryHome.showPreviewBanner = false; |
361 | return true; |
362 | }()); |
363 | |
364 | if (GalleryHome.showPreviewBanner) { |
365 | home = Stack( |
366 | fit: StackFit.expand, |
367 | children: <Widget>[ |
368 | home, |
369 | FadeTransition( |
370 | opacity: CurvedAnimation(parent: _controller, curve: Curves.easeInOut), |
371 | child: const Banner(message: 'PREVIEW', location: BannerLocation.topEnd), |
372 | ), |
373 | ], |
374 | ); |
375 | } |
376 | home = AnnotatedRegion<SystemUiOverlayStyle>(value: SystemUiOverlayStyle.light, child: home); |
377 | |
378 | return home; |
379 | } |
380 | } |
381 |
Definitions
- _kGalleryAssetsPackage
- _kFlutterBlue
- _kDemoItemHeight
- _kFrontLayerSwitchDuration
- _FlutterLogo
- _FlutterLogo
- build
- _CategoryItem
- _CategoryItem
- build
- _CategoriesPage
- _CategoriesPage
- build
- _DemoItem
- _DemoItem
- _launchDemo
- build
- _DemosPage
- _DemosPage
- build
- GalleryHome
- GalleryHome
- createState
- _GalleryHomeState
- _topHomeLayout
- initState
- dispose
Learn more about Flutter for embedded and desktop on industrialflutter.com