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
5import 'dart:developer';
6import 'dart:math' as math;
7
8import 'package:flutter/gestures.dart' show DragStartBehavior;
9import 'package:flutter/material.dart';
10import 'package:flutter/services.dart';
11
12import 'backdrop.dart';
13import 'demos.dart';
14
15const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
16const Color _kFlutterBlue = Color(0xFF003D75);
17const double _kDemoItemHeight = 64.0;
18const Duration _kFrontLayerSwitchDuration = Duration(milliseconds: 300);
19
20class _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
39class _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
85class _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
150class _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
218class _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
249class 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
263class _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

Provided by KDAB

Privacy Policy
Learn more about Flutter for embedded and desktop on industrialflutter.com