[devicelab] introduce new old gallery. (#143486)

Fixes https://github.com/flutter/flutter/issues/143482

This brings in the gallery more or less as is:

* Removed localizations
* Ensure tests still run (locally verified, will switch CI later).
* Removed deferred components
* Fixup pubspec
This commit is contained in:
Jonah Williams 2024-02-15 12:01:14 -08:00 committed by GitHub
parent c530276f78
commit 2d4f5a65c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
171 changed files with 46803 additions and 0 deletions

View File

@ -0,0 +1,29 @@
# Flutter Gallery
**NOTE**: The Flutter Gallery is now deprecated, and no longer being active maintained.
Flutter Gallery was a resource to help developers evaluate and use Flutter.
It is now being used primarily for testing. For posterity, the web version
remains [hosted here](https://flutter-gallery-archive.web.app).
We recommend Flutter developers check out the following resources:
* **Wonderous**
([web demo](https://wonderous.app/web/),
[App Store](https://apps.apple.com/us/app/wonderous/id1612491897),
[Google Play](https://play.google.com/store/apps/details?id=com.gskinner.flutter.wonders),
[source code](https://github.com/gskinnerTeam/flutter-wonderous-app)):<br>
A Flutter app that showcases Flutter's support for elegant design and rich animations.
* **Material 3 Demo**
([web demo](https://flutter.github.io/samples/web/material_3_demo/),
[source code](https://github.com/flutter/samples/tree/main/material_3_demo)):<br>
A Flutter app that showcases Material 3 features in the Flutter Material library.
* **Flutter Samples**
([samples](https://flutter.github.io/samples), [source code](https://github.com/flutter/samples)):<br>
A collection of open source samples that illustrate best practices for Flutter.
* **Widget catalogs**
([Material](https://docs.flutter.dev/ui/widgets/material), [Cupertino](https://docs.flutter.dev/ui/widgets/cupertino)):<br>
Catalogs for Material, Cupertino, and other widgets available for use in UI.

View File

@ -0,0 +1,7 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
typedef CodeDisplayer = TextSpan Function(BuildContext context);

View File

@ -0,0 +1,44 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class CodeStyle extends InheritedWidget {
const CodeStyle({
super.key,
this.baseStyle,
this.numberStyle,
this.commentStyle,
this.keywordStyle,
this.stringStyle,
this.punctuationStyle,
this.classStyle,
this.constantStyle,
required super.child,
});
final TextStyle? baseStyle;
final TextStyle? numberStyle;
final TextStyle? commentStyle;
final TextStyle? keywordStyle;
final TextStyle? stringStyle;
final TextStyle? punctuationStyle;
final TextStyle? classStyle;
final TextStyle? constantStyle;
static CodeStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CodeStyle>()!;
}
@override
bool updateShouldNotify(CodeStyle oldWidget) =>
oldWidget.baseStyle != baseStyle ||
oldWidget.numberStyle != numberStyle ||
oldWidget.commentStyle != commentStyle ||
oldWidget.keywordStyle != keywordStyle ||
oldWidget.stringStyle != stringStyle ||
oldWidget.punctuationStyle != punctuationStyle ||
oldWidget.classStyle != classStyle ||
oldWidget.constantStyle != constantStyle;
}

View File

@ -0,0 +1,53 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Only put constants shared between files here.
import 'dart:typed_data';
// Height of the 'Gallery' header
const double galleryHeaderHeight = 64;
// The font size delta for headline4 font.
const double desktopDisplay1FontDelta = 16;
// The width of the settingsDesktop.
const double desktopSettingsWidth = 520;
// Sentinel value for the system text scale factor option.
const double systemTextScaleFactorOption = -1;
// The splash page animation duration.
const Duration splashPageAnimationDuration = Duration(milliseconds: 300);
// Half the splash page animation duration.
const Duration halfSplashPageAnimationDuration = Duration(milliseconds: 150);
// Duration for settings panel to open on mobile.
const Duration settingsPanelMobileAnimationDuration =
Duration(milliseconds: 200);
// Duration for settings panel to open on desktop.
const Duration settingsPanelDesktopAnimationDuration =
Duration(milliseconds: 600);
// Duration for home page elements to fade in.
const Duration entranceAnimationDuration = Duration(milliseconds: 200);
// The desktop top padding for a page's first header (e.g. Gallery, Settings)
const double firstHeaderDesktopTopPadding = 5.0;
// A transparent image used to avoid loading images when they are not needed.
final Uint8List kTransparentImage = Uint8List.fromList(<int>[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49,
0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
0x00, 0x00, 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x06, 0x62, 0x4B,
0x47, 0x44, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0xA0, 0xBD, 0xA7, 0x93, 0x00,
0x00, 0x00, 0x09, 0x70, 0x48, 0x59, 0x73, 0x00, 0x00, 0x0B, 0x13, 0x00, 0x00,
0x0B, 0x13, 0x01, 0x00, 0x9A, 0x9C, 0x18, 0x00, 0x00, 0x00, 0x07, 0x74, 0x49,
0x4D, 0x45, 0x07, 0xE6, 0x03, 0x10, 0x17, 0x07, 0x1D, 0x2E, 0x5E, 0x30, 0x9B,
0x00, 0x00, 0x00, 0x0B, 0x49, 0x44, 0x41, 0x54, 0x08, 0xD7, 0x63, 0x60, 0x00,
0x02, 0x00, 0x00, 0x05, 0x00, 0x01, 0xE2, 0x26, 0x05, 0x9B, 0x00, 0x00, 0x00,
0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82,
]);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,282 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import '../constants.dart';
enum CustomTextDirection {
localeBased,
ltr,
rtl,
}
// See http://en.wikipedia.org/wiki/Right-to-left
const List<String> rtlLanguages = <String>[
'ar', // Arabic
'fa', // Farsi
'he', // Hebrew
'ps', // Pashto
'ur', // Urdu
];
// Fake locale to represent the system Locale option.
const Locale systemLocaleOption = Locale('system');
Locale? _deviceLocale;
Locale? get deviceLocale => _deviceLocale;
set deviceLocale(Locale? locale) {
_deviceLocale ??= locale;
}
@immutable
class GalleryOptions {
const GalleryOptions({
required this.themeMode,
required double? textScaleFactor,
required this.customTextDirection,
required Locale? locale,
required this.timeDilation,
required this.platform,
required this.isTestMode,
}) : _textScaleFactor = textScaleFactor ?? 1.0,
_locale = locale;
final ThemeMode themeMode;
final double _textScaleFactor;
final CustomTextDirection customTextDirection;
final Locale? _locale;
final double timeDilation;
final TargetPlatform? platform;
final bool isTestMode; // True for integration tests.
// We use a sentinel value to indicate the system text scale option. By
// default, return the actual text scale factor, otherwise return the
// sentinel value.
double textScaleFactor(BuildContext context, {bool useSentinel = false}) {
if (_textScaleFactor == systemTextScaleFactorOption) {
return useSentinel
? systemTextScaleFactorOption
// ignore: deprecated_member_use
: MediaQuery.of(context).textScaleFactor;
} else {
return _textScaleFactor;
}
}
Locale? get locale => _locale ?? deviceLocale;
/// Returns a text direction based on the [CustomTextDirection] setting.
/// If it is based on locale and the locale cannot be determined, returns
/// null.
TextDirection? resolvedTextDirection() {
switch (customTextDirection) {
case CustomTextDirection.localeBased:
final String? language = locale?.languageCode.toLowerCase();
if (language == null) {
return null;
}
return rtlLanguages.contains(language)
? TextDirection.rtl
: TextDirection.ltr;
case CustomTextDirection.rtl:
return TextDirection.rtl;
case CustomTextDirection.ltr:
return TextDirection.ltr;
}
}
/// Returns a [SystemUiOverlayStyle] based on the [ThemeMode] setting.
/// In other words, if the theme is dark, returns light; if the theme is
/// light, returns dark.
SystemUiOverlayStyle resolvedSystemUiOverlayStyle() {
Brightness brightness;
switch (themeMode) {
case ThemeMode.light:
brightness = Brightness.light;
case ThemeMode.dark:
brightness = Brightness.dark;
case ThemeMode.system:
brightness =
WidgetsBinding.instance.platformDispatcher.platformBrightness;
}
final SystemUiOverlayStyle overlayStyle = brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark;
return overlayStyle;
}
GalleryOptions copyWith({
ThemeMode? themeMode,
double? textScaleFactor,
CustomTextDirection? customTextDirection,
Locale? locale,
double? timeDilation,
TargetPlatform? platform,
bool? isTestMode,
}) {
return GalleryOptions(
themeMode: themeMode ?? this.themeMode,
textScaleFactor: textScaleFactor ?? _textScaleFactor,
customTextDirection: customTextDirection ?? this.customTextDirection,
locale: locale ?? this.locale,
timeDilation: timeDilation ?? this.timeDilation,
platform: platform ?? this.platform,
isTestMode: isTestMode ?? this.isTestMode,
);
}
@override
bool operator ==(Object other) =>
other is GalleryOptions &&
themeMode == other.themeMode &&
_textScaleFactor == other._textScaleFactor &&
customTextDirection == other.customTextDirection &&
locale == other.locale &&
timeDilation == other.timeDilation &&
platform == other.platform &&
isTestMode == other.isTestMode;
@override
int get hashCode => Object.hash(
themeMode,
_textScaleFactor,
customTextDirection,
locale,
timeDilation,
platform,
isTestMode,
);
static GalleryOptions of(BuildContext context) {
final _ModelBindingScope scope =
context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!;
return scope.modelBindingState.currentModel;
}
static void update(BuildContext context, GalleryOptions newModel) {
final _ModelBindingScope scope =
context.dependOnInheritedWidgetOfExactType<_ModelBindingScope>()!;
scope.modelBindingState.updateModel(newModel);
}
}
// Applies text GalleryOptions to a widget
class ApplyTextOptions extends StatelessWidget {
const ApplyTextOptions({
super.key,
required this.child,
});
final Widget child;
@override
Widget build(BuildContext context) {
final GalleryOptions options = GalleryOptions.of(context);
final TextDirection? textDirection = options.resolvedTextDirection();
final double textScaleFactor = options.textScaleFactor(context);
final Widget widget = MediaQuery(
data: MediaQuery.of(context).copyWith(
// ignore: deprecated_member_use
textScaleFactor: textScaleFactor,
),
child: child,
);
return textDirection == null
? widget
: Directionality(
textDirection: textDirection,
child: widget,
);
}
}
// Everything below is boilerplate except code relating to time dilation.
// See https://medium.com/flutter/managing-flutter-application-state-with-inheritedwidgets-1140452befe1
class _ModelBindingScope extends InheritedWidget {
const _ModelBindingScope({
required this.modelBindingState,
required super.child,
});
final _ModelBindingState modelBindingState;
@override
bool updateShouldNotify(_ModelBindingScope oldWidget) => true;
}
class ModelBinding extends StatefulWidget {
const ModelBinding({
super.key,
required this.initialModel,
required this.child,
});
final GalleryOptions initialModel;
final Widget child;
@override
State<ModelBinding> createState() => _ModelBindingState();
}
class _ModelBindingState extends State<ModelBinding> {
late GalleryOptions currentModel;
Timer? _timeDilationTimer;
@override
void initState() {
super.initState();
currentModel = widget.initialModel;
}
@override
void dispose() {
_timeDilationTimer?.cancel();
_timeDilationTimer = null;
super.dispose();
}
void handleTimeDilation(GalleryOptions newModel) {
if (currentModel.timeDilation != newModel.timeDilation) {
_timeDilationTimer?.cancel();
_timeDilationTimer = null;
if (newModel.timeDilation > 1) {
// We delay the time dilation change long enough that the user can see
// that UI has started reacting and then we slam on the brakes so that
// they see that the time is in fact now dilated.
_timeDilationTimer = Timer(const Duration(milliseconds: 150), () {
timeDilation = newModel.timeDilation;
});
} else {
timeDilation = newModel.timeDilation;
}
}
}
void updateModel(GalleryOptions newModel) {
if (newModel != currentModel) {
handleTimeDilation(newModel);
setState(() {
currentModel = newModel;
});
}
}
@override
Widget build(BuildContext context) {
return _ModelBindingScope(
modelBindingState: this,
child: widget.child,
);
}
}

View File

@ -0,0 +1,174 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class GalleryIcons {
GalleryIcons._();
static const IconData tooltip = IconData(
0xe900,
fontFamily: 'GalleryIcons',
);
static const IconData textFieldsAlt = IconData(
0xe901,
fontFamily: 'GalleryIcons',
);
static const IconData tabs = IconData(
0xe902,
fontFamily: 'GalleryIcons',
);
static const IconData switches = IconData(
0xe903,
fontFamily: 'GalleryIcons',
);
static const IconData sliders = IconData(
0xe904,
fontFamily: 'GalleryIcons',
);
static const IconData shrine = IconData(
0xe905,
fontFamily: 'GalleryIcons',
);
static const IconData sentimentVerySatisfied = IconData(
0xe906,
fontFamily: 'GalleryIcons',
);
static const IconData refresh = IconData(
0xe907,
fontFamily: 'GalleryIcons',
);
static const IconData progressActivity = IconData(
0xe908,
fontFamily: 'GalleryIcons',
);
static const IconData phoneIphone = IconData(
0xe909,
fontFamily: 'GalleryIcons',
);
static const IconData pageControl = IconData(
0xe90a,
fontFamily: 'GalleryIcons',
);
static const IconData moreVert = IconData(
0xe90b,
fontFamily: 'GalleryIcons',
);
static const IconData menu = IconData(
0xe90c,
fontFamily: 'GalleryIcons',
);
static const IconData listAlt = IconData(
0xe90d,
fontFamily: 'GalleryIcons',
);
static const IconData gridOn = IconData(
0xe90e,
fontFamily: 'GalleryIcons',
);
static const IconData expandAll = IconData(
0xe90f,
fontFamily: 'GalleryIcons',
);
static const IconData event = IconData(
0xe910,
fontFamily: 'GalleryIcons',
);
static const IconData driveVideo = IconData(
0xe911,
fontFamily: 'GalleryIcons',
);
static const IconData dialogs = IconData(
0xe912,
fontFamily: 'GalleryIcons',
);
static const IconData dataTable = IconData(
0xe913,
fontFamily: 'GalleryIcons',
);
static const IconData customTypography = IconData(
0xe914,
fontFamily: 'GalleryIcons',
);
static const IconData colors = IconData(
0xe915,
fontFamily: 'GalleryIcons',
);
static const IconData chips = IconData(
0xe916,
fontFamily: 'GalleryIcons',
);
static const IconData checkBox = IconData(
0xe917,
fontFamily: 'GalleryIcons',
);
static const IconData cards = IconData(
0xe918,
fontFamily: 'GalleryIcons',
);
static const IconData buttons = IconData(
0xe919,
fontFamily: 'GalleryIcons',
);
static const IconData bottomSheets = IconData(
0xe91a,
fontFamily: 'GalleryIcons',
);
static const IconData bottomNavigation = IconData(
0xe91b,
fontFamily: 'GalleryIcons',
);
static const IconData animation = IconData(
0xe91c,
fontFamily: 'GalleryIcons',
);
static const IconData accountBox = IconData(
0xe91d,
fontFamily: 'GalleryIcons',
);
static const IconData snackbar = IconData(
0xe91e,
fontFamily: 'GalleryIcons',
);
static const IconData categoryMdc = IconData(
0xe91f,
fontFamily: 'GalleryIcons',
);
static const IconData cupertinoProgress = IconData(
0xe920,
fontFamily: 'GalleryIcons',
);
static const IconData cupertinoPullToRefresh = IconData(
0xe921,
fontFamily: 'GalleryIcons',
);
static const IconData cupertinoSwitch = IconData(
0xe922,
fontFamily: 'GalleryIcons',
);
static const IconData genericButtons = IconData(
0xe923,
fontFamily: 'GalleryIcons',
);
static const IconData backdrop = IconData(
0xe924,
fontFamily: 'GalleryIcons',
);
static const IconData bottomAppBar = IconData(
0xe925,
fontFamily: 'GalleryIcons',
);
static const IconData bottomSheetPersistent = IconData(
0xe926,
fontFamily: 'GalleryIcons',
);
static const IconData listsLeaveBehind = IconData(
0xe927,
fontFamily: 'GalleryIcons',
);
static const IconData navigationRail = Icons.vertical_split;
static const IconData appbar = Icons.web_asset;
static const IconData divider = Icons.credit_card;
static const IconData search = Icons.search;
}

View File

@ -0,0 +1,120 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
typedef LibraryLoader = Future<void> Function();
typedef DeferredWidgetBuilder = Widget Function();
/// Wraps the child inside a deferred module loader.
///
/// The child is created and a single instance of the Widget is maintained in
/// state as long as closure to create widget stays the same.
///
class DeferredWidget extends StatefulWidget {
DeferredWidget(
this.libraryLoader,
this.createWidget, {
super.key,
Widget? placeholder,
}) : placeholder = placeholder ?? Container();
final LibraryLoader libraryLoader;
final DeferredWidgetBuilder createWidget;
final Widget placeholder;
static final Map<LibraryLoader, Future<void>> _moduleLoaders = <LibraryLoader, Future<void>>{};
static final Set<LibraryLoader> _loadedModules = <LibraryLoader>{};
static Future<void> preload(LibraryLoader loader) {
if (!_moduleLoaders.containsKey(loader)) {
_moduleLoaders[loader] = loader().then((dynamic _) {
_loadedModules.add(loader);
});
}
return _moduleLoaders[loader]!;
}
@override
State<DeferredWidget> createState() => _DeferredWidgetState();
}
class _DeferredWidgetState extends State<DeferredWidget> {
_DeferredWidgetState();
Widget? _loadedChild;
DeferredWidgetBuilder? _loadedCreator;
@override
void initState() {
/// If module was already loaded immediately create widget instead of
/// waiting for future or zone turn.
if (DeferredWidget._loadedModules.contains(widget.libraryLoader)) {
_onLibraryLoaded();
} else {
DeferredWidget.preload(widget.libraryLoader)
.then((dynamic _) => _onLibraryLoaded());
}
super.initState();
}
void _onLibraryLoaded() {
setState(() {
_loadedCreator = widget.createWidget;
_loadedChild = _loadedCreator!();
});
}
@override
Widget build(BuildContext context) {
/// If closure to create widget changed, create new instance, otherwise
/// treat as const Widget.
if (_loadedCreator != widget.createWidget && _loadedCreator != null) {
_loadedCreator = widget.createWidget;
_loadedChild = _loadedCreator!();
}
return _loadedChild ?? widget.placeholder;
}
}
/// Displays a progress indicator and text description explaining that
/// the widget is a deferred component and is currently being installed.
class DeferredLoadingPlaceholder extends StatelessWidget {
const DeferredLoadingPlaceholder({
super.key,
this.name = 'This widget',
});
final String name;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.grey[700],
border: Border.all(
width: 20,
color: Colors.grey[700]!,
),
borderRadius: const BorderRadius.all(Radius.circular(10))),
width: 250,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('$name is installing.',
style: Theme.of(context).textTheme.headlineMedium),
Container(height: 10),
Text(
'$name is a deferred component which are downloaded and installed at runtime.',
style: Theme.of(context).textTheme.bodyLarge),
Container(height: 20),
const Center(child: CircularProgressIndicator()),
],
),
),
);
}
}

View File

@ -0,0 +1,29 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoActivityIndicatorDemo
class CupertinoProgressIndicatorDemo extends StatelessWidget {
const CupertinoProgressIndicatorDemo({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(
GalleryLocalizations.of(context)!.demoCupertinoActivityIndicatorTitle,
),
),
child: const Center(
child: CupertinoActivityIndicator(),
),
);
}
}
// END

View File

@ -0,0 +1,432 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../data/gallery_options.dart';
import '../../gallery_localizations.dart';
import 'demo_types.dart';
// BEGIN cupertinoAlertDemo
class CupertinoAlertDemo extends StatefulWidget {
const CupertinoAlertDemo({
super.key,
required this.type,
});
final AlertDemoType type;
@override
State<CupertinoAlertDemo> createState() => _CupertinoAlertDemoState();
}
class _CupertinoAlertDemoState extends State<CupertinoAlertDemo>
with RestorationMixin {
RestorableStringN lastSelectedValue = RestorableStringN(null);
late RestorableRouteFuture<String> _alertDialogRoute;
late RestorableRouteFuture<String> _alertWithTitleDialogRoute;
late RestorableRouteFuture<String> _alertWithButtonsDialogRoute;
late RestorableRouteFuture<String> _alertWithButtonsOnlyDialogRoute;
late RestorableRouteFuture<String> _modalPopupRoute;
@override
String get restorationId => 'cupertino_alert_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(
lastSelectedValue,
'last_selected_value',
);
registerForRestoration(
_alertDialogRoute,
'alert_demo_dialog_route',
);
registerForRestoration(
_alertWithTitleDialogRoute,
'alert_with_title_press_demo_dialog_route',
);
registerForRestoration(
_alertWithButtonsDialogRoute,
'alert_with_title_press_demo_dialog_route',
);
registerForRestoration(
_alertWithButtonsOnlyDialogRoute,
'alert_with_title_press_demo_dialog_route',
);
registerForRestoration(
_modalPopupRoute,
'modal_popup_route',
);
}
void _setSelectedValue(String value) {
setState(() {
lastSelectedValue.value = value;
});
}
@override
void initState() {
super.initState();
_alertDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertDemoDialog);
},
onComplete: _setSelectedValue,
);
_alertWithTitleDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertWithTitleDialog);
},
onComplete: _setSelectedValue,
);
_alertWithButtonsDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertWithButtonsDialog);
},
onComplete: _setSelectedValue,
);
_alertWithButtonsOnlyDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertWithButtonsOnlyDialog);
},
onComplete: _setSelectedValue,
);
_modalPopupRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_modalRoute);
},
onComplete: _setSelectedValue,
);
}
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (widget.type) {
case AlertDemoType.alert:
return localizations.demoCupertinoAlertTitle;
case AlertDemoType.alertTitle:
return localizations.demoCupertinoAlertWithTitleTitle;
case AlertDemoType.alertButtons:
return localizations.demoCupertinoAlertButtonsTitle;
case AlertDemoType.alertButtonsOnly:
return localizations.demoCupertinoAlertButtonsOnlyTitle;
case AlertDemoType.actionSheet:
return localizations.demoCupertinoActionSheetTitle;
}
}
static Route<String> _alertDemoDialog(
BuildContext context,
Object? arguments,
) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoDialogRoute<String>(
context: context,
builder: (BuildContext context) => ApplyTextOptions(
child: CupertinoAlertDialog(
title: Text(localizations.dialogDiscardTitle),
actions: <Widget>[
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.of(
context,
).pop(localizations.cupertinoAlertDiscard);
},
child: Text(
localizations.cupertinoAlertDiscard,
),
),
CupertinoDialogAction(
isDefaultAction: true,
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertCancel,
),
child: Text(
localizations.cupertinoAlertCancel,
),
),
],
),
),
);
}
static Route<String> _alertWithTitleDialog(
BuildContext context,
Object? arguments,
) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoDialogRoute<String>(
context: context,
builder: (BuildContext context) => ApplyTextOptions(
child: CupertinoAlertDialog(
title: Text(
localizations.cupertinoAlertLocationTitle,
),
content: Text(
localizations.cupertinoAlertLocationDescription,
),
actions: <Widget>[
CupertinoDialogAction(
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertDontAllow,
),
child: Text(
localizations.cupertinoAlertDontAllow,
),
),
CupertinoDialogAction(
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertAllow,
),
child: Text(
localizations.cupertinoAlertAllow,
),
),
],
),
),
);
}
static Route<String> _alertWithButtonsDialog(
BuildContext context,
Object? arguments,
) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoDialogRoute<String>(
context: context,
builder: (BuildContext context) => ApplyTextOptions(
child: CupertinoDessertDialog(
title: Text(
localizations.cupertinoAlertFavoriteDessert,
),
content: Text(
localizations.cupertinoAlertDessertDescription,
),
),
),
);
}
static Route<String> _alertWithButtonsOnlyDialog(
BuildContext context,
Object? arguments,
) {
return CupertinoDialogRoute<String>(
context: context,
builder: (BuildContext context) => const ApplyTextOptions(
child: CupertinoDessertDialog(),
),
);
}
static Route<String> _modalRoute(
BuildContext context,
Object? arguments,
) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoModalPopupRoute<String>(
builder: (BuildContext context) => ApplyTextOptions(
child: CupertinoActionSheet(
title: Text(
localizations.cupertinoAlertFavoriteDessert,
),
message: Text(
localizations.cupertinoAlertDessertDescription,
),
actions: <Widget>[
CupertinoActionSheetAction(
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertCheesecake,
),
child: Text(
localizations.cupertinoAlertCheesecake,
),
),
CupertinoActionSheetAction(
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertTiramisu,
),
child: Text(
localizations.cupertinoAlertTiramisu,
),
),
CupertinoActionSheetAction(
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertApplePie,
),
child: Text(
localizations.cupertinoAlertApplePie,
),
),
],
cancelButton: CupertinoActionSheetAction(
isDefaultAction: true,
onPressed: () => Navigator.of(
context,
).pop(
localizations.cupertinoAlertCancel,
),
child: Text(
localizations.cupertinoAlertCancel,
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(_title(context)),
),
child: Builder(
builder: (BuildContext context) {
return Column(
children: <Widget>[
Expanded(
child: Center(
child: CupertinoButton.filled(
onPressed: () {
switch (widget.type) {
case AlertDemoType.alert:
_alertDialogRoute.present();
case AlertDemoType.alertTitle:
_alertWithTitleDialogRoute.present();
case AlertDemoType.alertButtons:
_alertWithButtonsDialogRoute.present();
case AlertDemoType.alertButtonsOnly:
_alertWithButtonsOnlyDialogRoute.present();
case AlertDemoType.actionSheet:
_modalPopupRoute.present();
}
},
child: Text(
GalleryLocalizations.of(context)!.cupertinoShowAlert,
),
),
),
),
if (lastSelectedValue.value != null)
Padding(
padding: const EdgeInsets.all(16),
child: Text(
GalleryLocalizations.of(context)!
.dialogSelectedOption(lastSelectedValue.value!),
style: CupertinoTheme.of(context).textTheme.textStyle,
textAlign: TextAlign.center,
),
),
],
);
},
),
);
}
}
class CupertinoDessertDialog extends StatelessWidget {
const CupertinoDessertDialog({
super.key,
this.title,
this.content,
});
final Widget? title;
final Widget? content;
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoAlertDialog(
title: title,
content: content,
actions: <Widget>[
CupertinoDialogAction(
onPressed: () {
Navigator.of(
context,
).pop(
localizations.cupertinoAlertCheesecake,
);
},
child: Text(
localizations.cupertinoAlertCheesecake,
),
),
CupertinoDialogAction(
onPressed: () {
Navigator.of(
context,
).pop(
localizations.cupertinoAlertTiramisu,
);
},
child: Text(
localizations.cupertinoAlertTiramisu,
),
),
CupertinoDialogAction(
onPressed: () {
Navigator.of(
context,
).pop(
localizations.cupertinoAlertApplePie,
);
},
child: Text(
localizations.cupertinoAlertApplePie,
),
),
CupertinoDialogAction(
onPressed: () {
Navigator.of(
context,
).pop(
localizations.cupertinoAlertChocolateBrownie,
);
},
child: Text(
localizations.cupertinoAlertChocolateBrownie,
),
),
CupertinoDialogAction(
isDestructiveAction: true,
onPressed: () {
Navigator.of(
context,
).pop(
localizations.cupertinoAlertCancel,
);
},
child: Text(
localizations.cupertinoAlertCancel,
),
),
],
);
}
}
// END

View File

@ -0,0 +1,60 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoButtonDemo
class CupertinoButtonDemo extends StatelessWidget {
const CupertinoButtonDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(localizations.demoCupertinoButtonsTitle),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoButton(
onPressed: () {},
child: Text(
localizations.cupertinoButton,
),
),
const SizedBox(height: 16),
CupertinoButton.filled(
onPressed: () {},
child: Text(
localizations.cupertinoButtonWithBackground,
),
),
const SizedBox(height: 30),
// Disabled buttons
CupertinoButton(
onPressed: null,
child: Text(
localizations.cupertinoButton,
),
),
const SizedBox(height: 16),
CupertinoButton.filled(
onPressed: null,
child: Text(
localizations.cupertinoButtonWithBackground,
),
),
],
),
),
);
}
}
// END

View File

@ -0,0 +1,71 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoContextMenuDemo
class CupertinoContextMenuDemo extends StatelessWidget {
const CupertinoContextMenuDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations galleryLocalizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(
galleryLocalizations.demoCupertinoContextMenuTitle,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Center(
child: SizedBox(
width: 100,
height: 100,
child: CupertinoContextMenu(
actions: <Widget>[
CupertinoContextMenuAction(
onPressed: () {
Navigator.pop(context);
},
child: Text(
galleryLocalizations.demoCupertinoContextMenuActionOne,
),
),
CupertinoContextMenuAction(
onPressed: () {
Navigator.pop(context);
},
child: Text(
galleryLocalizations.demoCupertinoContextMenuActionTwo,
),
),
],
child: const FlutterLogo(size: 250),
),
),
),
const SizedBox(height: 20),
Padding(
padding: const EdgeInsets.all(30),
child: Text(
galleryLocalizations.demoCupertinoContextMenuActionText,
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.black,
),
),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,17 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'package:gallery/demos/cupertino/cupertino_activity_indicator_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_alert_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_button_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_context_menu_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_navigation_bar_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_picker_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_scrollbar_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_search_text_field_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_segmented_control_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_slider_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_switch_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_tab_bar_demo.dart';
export 'package:gallery/demos/cupertino/cupertino_text_field_demo.dart';

View File

@ -0,0 +1,112 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoNavigationBarDemo
class CupertinoNavigationBarDemo extends StatelessWidget {
const CupertinoNavigationBarDemo({super.key});
static const String homeRoute = '/home';
static const String secondPageRoute = '/home/item';
@override
Widget build(BuildContext context) {
return Navigator(
restorationScopeId: 'navigator',
initialRoute: CupertinoNavigationBarDemo.homeRoute,
onGenerateRoute: (RouteSettings settings) {
switch (settings.name) {
case CupertinoNavigationBarDemo.homeRoute:
return _NoAnimationCupertinoPageRoute<void>(
title: GalleryLocalizations.of(context)!
.demoCupertinoNavigationBarTitle,
settings: settings,
builder: (BuildContext context) => _FirstPage(),
);
case CupertinoNavigationBarDemo.secondPageRoute:
final Map<dynamic, dynamic> arguments = settings.arguments! as Map<dynamic, dynamic>;
final String? title = arguments['pageTitle'] as String?;
return CupertinoPageRoute<void>(
title: title,
settings: settings,
builder: (BuildContext context) => _SecondPage(),
);
}
return null;
},
);
}
}
class _FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
child: CustomScrollView(
slivers: <Widget>[
const CupertinoSliverNavigationBar(
automaticallyImplyLeading: false,
),
SliverPadding(
padding:
MediaQuery.of(context).removePadding(removeTop: true).padding,
sliver: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
final String title = GalleryLocalizations.of(context)!
.starterAppDrawerItem(index + 1);
return ListTile(
onTap: () {
Navigator.of(context).restorablePushNamed<void>(
CupertinoNavigationBarDemo.secondPageRoute,
arguments: <String, String>{'pageTitle': title},
);
},
title: Text(title),
);
},
childCount: 20,
),
),
),
],
),
);
}
}
class _SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(),
child: Container(),
);
}
}
/// A CupertinoPageRoute without any transition animations.
class _NoAnimationCupertinoPageRoute<T> extends CupertinoPageRoute<T> {
_NoAnimationCupertinoPageRoute({
required super.builder,
super.settings,
super.title,
});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
// END

View File

@ -0,0 +1,322 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:intl/intl.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoPickersDemo
class CupertinoPickerDemo extends StatefulWidget {
const CupertinoPickerDemo({super.key});
@override
State<CupertinoPickerDemo> createState() => _CupertinoPickerDemoState();
}
class _CupertinoPickerDemoState extends State<CupertinoPickerDemo> {
Duration timer = Duration.zero;
// Value that is shown in the date picker in date mode.
DateTime date = DateTime.now();
// Value that is shown in the date picker in time mode.
DateTime time = DateTime.now();
// Value that is shown in the date picker in dateAndTime mode.
DateTime dateTime = DateTime.now();
int _selectedWeekday = 0;
static List<String> getDaysOfWeek([String? locale]) {
final DateTime now = DateTime.now();
final DateTime firstDayOfWeek = now.subtract(Duration(days: now.weekday - 1));
return List<int>.generate(7, (int index) => index)
.map((int value) => DateFormat(DateFormat.WEEKDAY, locale)
.format(firstDayOfWeek.add(Duration(days: value))))
.toList();
}
void _showDemoPicker({
required BuildContext context,
required Widget child,
}) {
final CupertinoThemeData themeData = CupertinoTheme.of(context);
final CupertinoTheme dialogBody = CupertinoTheme(
data: themeData,
child: child,
);
showCupertinoModalPopup<void>(
context: context,
builder: (BuildContext context) => dialogBody,
);
}
Widget _buildDatePicker(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_showDemoPicker(
context: context,
child: _BottomPicker(
child: CupertinoDatePicker(
backgroundColor:
CupertinoColors.systemBackground.resolveFrom(context),
mode: CupertinoDatePickerMode.date,
initialDateTime: date,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => date = newDateTime);
},
),
),
);
},
child: _Menu(
children: <Widget>[
Text(GalleryLocalizations.of(context)!.demoCupertinoPickerDate),
Text(
DateFormat.yMMMMd().format(date),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
),
);
}
Widget _buildTimePicker(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_showDemoPicker(
context: context,
child: _BottomPicker(
child: CupertinoDatePicker(
backgroundColor:
CupertinoColors.systemBackground.resolveFrom(context),
mode: CupertinoDatePickerMode.time,
initialDateTime: time,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => time = newDateTime);
},
),
),
);
},
child: _Menu(
children: <Widget>[
Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTime),
Text(
DateFormat.jm().format(time),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
),
);
}
Widget _buildDateAndTimePicker(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_showDemoPicker(
context: context,
child: _BottomPicker(
child: CupertinoDatePicker(
backgroundColor:
CupertinoColors.systemBackground.resolveFrom(context),
initialDateTime: dateTime,
onDateTimeChanged: (DateTime newDateTime) {
setState(() => dateTime = newDateTime);
},
),
),
);
},
child: _Menu(
children: <Widget>[
Text(GalleryLocalizations.of(context)!.demoCupertinoPickerDateTime),
Flexible(
child: Text(
DateFormat.yMMMd().add_jm().format(dateTime),
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
),
],
),
),
);
}
Widget _buildCountdownTimerPicker(BuildContext context) {
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_showDemoPicker(
context: context,
child: _BottomPicker(
child: CupertinoTimerPicker(
backgroundColor:
CupertinoColors.systemBackground.resolveFrom(context),
initialTimerDuration: timer,
onTimerDurationChanged: (Duration newTimer) {
setState(() => timer = newTimer);
},
),
),
);
},
child: _Menu(
children: <Widget>[
Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTimer),
Text(
'${timer.inHours}:'
'${(timer.inMinutes % 60).toString().padLeft(2, '0')}:'
'${(timer.inSeconds % 60).toString().padLeft(2, '0')}',
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
),
);
}
Widget _buildPicker(BuildContext context) {
final String? locale = GalleryLocalizations.of(context)?.localeName;
final List<String> daysOfWeek = getDaysOfWeek(locale);
return MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: () {
_showDemoPicker(
context: context,
child: _BottomPicker(
child: CupertinoPicker(
backgroundColor:
CupertinoColors.systemBackground.resolveFrom(context),
itemExtent: 32.0,
magnification: 1.22,
squeeze: 1.2,
useMagnifier: true,
// This is called when selected item is changed.
onSelectedItemChanged: (int selectedItem) {
setState(() {
_selectedWeekday = selectedItem;
});
},
children: List<Widget>.generate(daysOfWeek.length, (int index) {
return Center(
child: Text(
daysOfWeek[index],
),
);
}),
),
),
);
},
child: _Menu(
children: <Widget>[
Text(GalleryLocalizations.of(context)!.demoCupertinoPicker),
Text(
daysOfWeek[_selectedWeekday],
style: const TextStyle(color: CupertinoColors.inactiveGray),
),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle:
Text(GalleryLocalizations.of(context)!.demoCupertinoPickerTitle),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: ListView(
children: <Widget>[
const SizedBox(height: 32),
_buildDatePicker(context),
_buildTimePicker(context),
_buildDateAndTimePicker(context),
_buildCountdownTimerPicker(context),
_buildPicker(context),
],
),
),
);
}
}
class _BottomPicker extends StatelessWidget {
const _BottomPicker({required this.child});
final Widget child;
@override
Widget build(BuildContext context) {
return Container(
height: 216,
padding: const EdgeInsets.only(top: 6),
margin: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
color: CupertinoColors.systemBackground.resolveFrom(context),
child: DefaultTextStyle(
style: TextStyle(
color: CupertinoColors.label.resolveFrom(context),
fontSize: 22,
),
child: GestureDetector(
// Blocks taps from propagating to the modal sheet and popping.
onTap: () {},
child: SafeArea(
top: false,
child: child,
),
),
),
);
}
}
class _Menu extends StatelessWidget {
const _Menu({required this.children});
final List<Widget> children;
@override
Widget build(BuildContext context) {
return Container(
decoration: const BoxDecoration(
border: Border(
top: BorderSide(color: CupertinoColors.inactiveGray, width: 0),
bottom: BorderSide(color: CupertinoColors.inactiveGray, width: 0),
),
),
height: 44,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: children,
),
),
);
}
}
// END

View File

@ -0,0 +1,40 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoScrollbarDemo
class CupertinoScrollbarDemo extends StatelessWidget {
const CupertinoScrollbarDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(localizations.demoCupertinoScrollbarTitle),
),
child: CupertinoScrollbar(
thickness: 6.0,
thicknessWhileDragging: 10.0,
radius: const Radius.circular(34.0),
radiusWhileDragging: Radius.zero,
child: ListView.builder(
itemCount: 120,
itemBuilder: (BuildContext context, int index) {
return Center(
child: Text('item $index',
style: CupertinoTheme.of(context).textTheme.textStyle),
);
},
),
),
);
}
}
// END

View File

@ -0,0 +1,107 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoSearchTextFieldDemo
class CupertinoSearchTextFieldDemo extends StatefulWidget {
const CupertinoSearchTextFieldDemo({super.key});
@override
State<CupertinoSearchTextFieldDemo> createState() =>
_CupertinoSearchTextFieldDemoState();
}
class _CupertinoSearchTextFieldDemoState
extends State<CupertinoSearchTextFieldDemo> {
final List<String> platforms = <String>[
'Android',
'iOS',
'Windows',
'Linux',
'MacOS',
'Web'
];
final TextEditingController _queryTextController = TextEditingController();
String _searchPlatform = '';
List<String> filteredPlatforms = <String>[];
@override
void initState() {
super.initState();
filteredPlatforms = platforms;
_queryTextController.addListener(() {
if (_queryTextController.text.isEmpty) {
setState(() {
_searchPlatform = '';
filteredPlatforms = platforms;
});
} else {
setState(() {
_searchPlatform = _queryTextController.text;
});
}
});
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(localizations.demoCupertinoSearchTextFieldTitle),
),
child: SafeArea(
child: Column(
children: <Widget>[
CupertinoSearchTextField(
controller: _queryTextController,
restorationId: 'search_text_field',
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0,
color: CupertinoColors.inactiveGray,
),
),
),
placeholder:
localizations.demoCupertinoSearchTextFieldPlaceholder,
),
_buildPlatformList(),
],
),
),
);
}
Widget _buildPlatformList() {
if (_searchPlatform.isNotEmpty) {
final List<String> tempList = <String>[];
for (int i = 0; i < filteredPlatforms.length; i++) {
if (filteredPlatforms[i]
.toLowerCase()
.contains(_searchPlatform.toLowerCase())) {
tempList.add(filteredPlatforms[i]);
}
}
filteredPlatforms = tempList;
}
return ListView.builder(
itemCount: filteredPlatforms.length,
shrinkWrap: true,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text(filteredPlatforms[index]));
},
);
}
}
// END

View File

@ -0,0 +1,96 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoSegmentedControlDemo
class CupertinoSegmentedControlDemo extends StatefulWidget {
const CupertinoSegmentedControlDemo({super.key});
@override
State<CupertinoSegmentedControlDemo> createState() =>
_CupertinoSegmentedControlDemoState();
}
class _CupertinoSegmentedControlDemoState
extends State<CupertinoSegmentedControlDemo> with RestorationMixin {
RestorableInt currentSegment = RestorableInt(0);
@override
String get restorationId => 'cupertino_segmented_control';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(currentSegment, 'current_segment');
}
void onValueChanged(int? newValue) {
setState(() {
currentSegment.value = newValue!;
});
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
const double segmentedControlMaxWidth = 500.0;
final Map<int, Widget> children = <int, Widget>{
0: Text(localizations.colorsIndigo),
1: Text(localizations.colorsTeal),
2: Text(localizations.colorsCyan),
};
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(
localizations.demoCupertinoSegmentedControlTitle,
),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context)
.textTheme
.textStyle
.copyWith(fontSize: 13),
child: SafeArea(
child: ListView(
children: <Widget>[
const SizedBox(height: 16),
SizedBox(
width: segmentedControlMaxWidth,
child: CupertinoSegmentedControl<int>(
children: children,
onValueChanged: onValueChanged,
groupValue: currentSegment.value,
),
),
SizedBox(
width: segmentedControlMaxWidth,
child: Padding(
padding: const EdgeInsets.all(16),
child: CupertinoSlidingSegmentedControl<int>(
children: children,
onValueChanged: onValueChanged,
groupValue: currentSegment.value,
),
),
),
Container(
padding: const EdgeInsets.all(16),
height: 300,
alignment: Alignment.center,
child: children[currentSegment.value],
),
],
),
),
),
);
}
}
// END

View File

@ -0,0 +1,110 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoSliderDemo
class CupertinoSliderDemo extends StatefulWidget {
const CupertinoSliderDemo({super.key});
@override
State<CupertinoSliderDemo> createState() => _CupertinoSliderDemoState();
}
class _CupertinoSliderDemoState extends State<CupertinoSliderDemo>
with RestorationMixin {
final RestorableDouble _value = RestorableDouble(25.0);
final RestorableDouble _discreteValue = RestorableDouble(20.0);
@override
String get restorationId => 'cupertino_slider_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_value, 'value');
registerForRestoration(_discreteValue, 'discrete_value');
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(localizations.demoCupertinoSliderTitle),
),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: Center(
child: Wrap(
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 32),
CupertinoSlider(
value: _value.value,
max: 100.0,
onChanged: (double value) {
setState(() {
_value.value = value;
});
},
),
CupertinoSlider(
value: _value.value,
max: 100.0,
onChanged: null,
),
MergeSemantics(
child: Text(
localizations.demoCupertinoSliderContinuous(
_value.value.toStringAsFixed(1),
),
),
),
],
),
// Disabled sliders
// TODO(guidezpl): See https://github.com/flutter/flutter/issues/106691
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(height: 32),
CupertinoSlider(
value: _discreteValue.value,
max: 100.0,
divisions: 5,
onChanged: (double value) {
setState(() {
_discreteValue.value = value;
});
},
),
CupertinoSlider(
value: _discreteValue.value,
max: 100.0,
divisions: 5,
onChanged: null,
),
MergeSemantics(
child: Text(
localizations.demoCupertinoSliderDiscrete(
_discreteValue.value.toStringAsFixed(1),
),
),
),
],
),
],
),
),
),
);
}
}
// END

View File

@ -0,0 +1,91 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoSwitchDemo
class CupertinoSwitchDemo extends StatefulWidget {
const CupertinoSwitchDemo({super.key});
@override
State<CupertinoSwitchDemo> createState() => _CupertinoSwitchDemoState();
}
class _CupertinoSwitchDemoState extends State<CupertinoSwitchDemo>
with RestorationMixin {
final RestorableBool _switchValueA = RestorableBool(false);
final RestorableBool _switchValueB = RestorableBool(true);
@override
String get restorationId => 'cupertino_switch_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_switchValueA, 'switch_valueA');
registerForRestoration(_switchValueB, 'switch_valueB');
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(
localizations.demoSelectionControlsSwitchTitle,
),
),
child: Center(
child: Semantics(
container: true,
label: localizations.demoSelectionControlsSwitchTitle,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoSwitch(
value: _switchValueA.value,
onChanged: (bool value) {
setState(() {
_switchValueA.value = value;
});
},
),
CupertinoSwitch(
value: _switchValueB.value,
onChanged: (bool value) {
setState(() {
_switchValueB.value = value;
});
},
),
],
),
// Disabled switches
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CupertinoSwitch(
value: _switchValueA.value,
onChanged: null,
),
CupertinoSwitch(
value: _switchValueB.value,
onChanged: null,
),
],
),
],
),
),
),
);
}
}
// END

View File

@ -0,0 +1,92 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoNavigationDemo
class _TabInfo {
const _TabInfo(this.title, this.icon);
final String title;
final IconData icon;
}
class CupertinoTabBarDemo extends StatelessWidget {
const CupertinoTabBarDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<_TabInfo> tabInfo = <_TabInfo>[
_TabInfo(
localizations.cupertinoTabBarHomeTab,
CupertinoIcons.home,
),
_TabInfo(
localizations.cupertinoTabBarChatTab,
CupertinoIcons.conversation_bubble,
),
_TabInfo(
localizations.cupertinoTabBarProfileTab,
CupertinoIcons.profile_circled,
),
];
return DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoTabScaffold(
restorationId: 'cupertino_tab_scaffold',
tabBar: CupertinoTabBar(
items: <BottomNavigationBarItem>[
for (final _TabInfo tabInfo in tabInfo)
BottomNavigationBarItem(
label: tabInfo.title,
icon: Icon(tabInfo.icon),
),
],
),
tabBuilder: (BuildContext context, int index) {
return CupertinoTabView(
restorationScopeId: 'cupertino_tab_view_$index',
builder: (BuildContext context) => _CupertinoDemoTab(
title: tabInfo[index].title,
icon: tabInfo[index].icon,
),
defaultTitle: tabInfo[index].title,
);
},
),
);
}
}
class _CupertinoDemoTab extends StatelessWidget {
const _CupertinoDemoTab({
required this.title,
required this.icon,
});
final String title;
final IconData icon;
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(),
backgroundColor: CupertinoColors.systemBackground,
child: Center(
child: Icon(
icon,
semanticLabel: title,
size: 100,
),
),
);
}
}
// END

View File

@ -0,0 +1,88 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/cupertino.dart';
import '../../gallery_localizations.dart';
// BEGIN cupertinoTextFieldDemo
class CupertinoTextFieldDemo extends StatelessWidget {
const CupertinoTextFieldDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
automaticallyImplyLeading: false,
middle: Text(localizations.demoCupertinoTextFieldTitle),
),
child: SafeArea(
child: ListView(
restorationId: 'text_field_demo_list_view',
padding: const EdgeInsets.all(16),
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CupertinoTextField(
textInputAction: TextInputAction.next,
restorationId: 'email_address_text_field',
placeholder: localizations.demoTextFieldEmail,
keyboardType: TextInputType.emailAddress,
clearButtonMode: OverlayVisibilityMode.editing,
autocorrect: false,
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CupertinoTextField(
textInputAction: TextInputAction.next,
restorationId: 'login_password_text_field',
placeholder: localizations.rallyLoginPassword,
clearButtonMode: OverlayVisibilityMode.editing,
obscureText: true,
autocorrect: false,
),
),
// Disabled text field
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CupertinoTextField(
enabled: false,
textInputAction: TextInputAction.next,
restorationId: 'login_password_text_field_disabled',
placeholder: localizations.rallyLoginPassword,
clearButtonMode: OverlayVisibilityMode.editing,
obscureText: true,
autocorrect: false,
),
),
CupertinoTextField(
textInputAction: TextInputAction.done,
restorationId: 'pin_number_text_field',
prefix: const Icon(
CupertinoIcons.padlock_solid,
size: 28,
),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
clearButtonMode: OverlayVisibilityMode.editing,
keyboardType: TextInputType.number,
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
width: 0,
color: CupertinoColors.inactiveGray,
),
),
),
placeholder: localizations.demoCupertinoTextFieldPIN,
),
],
),
),
);
}
}
// END

View File

@ -0,0 +1,11 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
enum AlertDemoType {
alert,
alertTitle,
alertButtons,
alertButtonsOnly,
actionSheet,
}

View File

@ -0,0 +1,73 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN appbarDemo
class AppBarDemo extends StatelessWidget {
const AppBarDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localization = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
leading: IconButton(
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
icon: const Icon(Icons.menu),
onPressed: () {},
),
title: Text(
localization.demoAppBarTitle,
),
actions: <Widget>[
IconButton(
tooltip: localization.starterAppTooltipFavorite,
icon: const Icon(
Icons.favorite,
),
onPressed: () {},
),
IconButton(
tooltip: localization.starterAppTooltipSearch,
icon: const Icon(
Icons.search,
),
onPressed: () {},
),
PopupMenuButton<Text>(
itemBuilder: (BuildContext context) {
return <PopupMenuEntry<Text>>[
PopupMenuItem<Text>(
child: Text(
localization.demoNavigationRailFirst,
),
),
PopupMenuItem<Text>(
child: Text(
localization.demoNavigationRailSecond,
),
),
PopupMenuItem<Text>(
child: Text(
localization.demoNavigationRailThird,
),
),
];
},
)
],
),
body: Center(
child: Text(
localization.cupertinoTabBarHomeTab,
),
),
);
}
}
// END

View File

@ -0,0 +1,143 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN bannerDemo
enum BannerDemoAction {
reset,
showMultipleActions,
showLeading,
}
class BannerDemo extends StatefulWidget {
const BannerDemo({super.key});
@override
State<BannerDemo> createState() => _BannerDemoState();
}
class _BannerDemoState extends State<BannerDemo> with RestorationMixin {
static const int _itemCount = 20;
@override
String get restorationId => 'banner_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_displayBanner, 'display_banner');
registerForRestoration(_showMultipleActions, 'show_multiple_actions');
registerForRestoration(_showLeading, 'show_leading');
}
final RestorableBool _displayBanner = RestorableBool(true);
final RestorableBool _showMultipleActions = RestorableBool(true);
final RestorableBool _showLeading = RestorableBool(true);
@override
void dispose() {
_displayBanner.dispose();
_showMultipleActions.dispose();
_showLeading.dispose();
super.dispose();
}
void handleDemoAction(BannerDemoAction action) {
setState(() {
switch (action) {
case BannerDemoAction.reset:
_displayBanner.value = true;
_showMultipleActions.value = true;
_showLeading.value = true;
case BannerDemoAction.showMultipleActions:
_showMultipleActions.value = !_showMultipleActions.value;
case BannerDemoAction.showLeading:
_showLeading.value = !_showLeading.value;
}
});
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final MaterialBanner banner = MaterialBanner(
content: Text(localizations.bannerDemoText),
leading: _showLeading.value
? CircleAvatar(
backgroundColor: colorScheme.primary,
child: Icon(Icons.access_alarm, color: colorScheme.onPrimary),
)
: null,
actions: <Widget>[
TextButton(
onPressed: () {
setState(() {
_displayBanner.value = false;
});
},
child: Text(localizations.signIn),
),
if (_showMultipleActions.value)
TextButton(
onPressed: () {
setState(() {
_displayBanner.value = false;
});
},
child: Text(localizations.dismiss),
),
],
backgroundColor: colorScheme.background,
);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoBannerTitle),
actions: <Widget>[
PopupMenuButton<BannerDemoAction>(
onSelected: handleDemoAction,
itemBuilder: (BuildContext context) => <PopupMenuEntry<BannerDemoAction>>[
PopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.reset,
child: Text(localizations.bannerDemoResetText),
),
const PopupMenuDivider(),
CheckedPopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.showMultipleActions,
checked: _showMultipleActions.value,
child: Text(localizations.bannerDemoMultipleText),
),
CheckedPopupMenuItem<BannerDemoAction>(
value: BannerDemoAction.showLeading,
checked: _showLeading.value,
child: Text(localizations.bannerDemoLeadingText),
),
],
),
],
),
body: ListView.builder(
restorationId: 'banner_demo_list_view',
itemCount: _displayBanner.value ? _itemCount + 1 : _itemCount,
itemBuilder: (BuildContext context, int index) {
if (index == 0 && _displayBanner.value) {
return banner;
}
return ListTile(
title: Text(
localizations.starterAppDrawerItem(
_displayBanner.value ? index : index + 1),
),
);
},
),
);
}
}
// END

View File

@ -0,0 +1,201 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import '../../gallery_localizations.dart';
// BEGIN bottomAppBarDemo
class BottomAppBarDemo extends StatefulWidget {
const BottomAppBarDemo({super.key});
@override
State createState() => _BottomAppBarDemoState();
}
class _BottomAppBarDemoState extends State<BottomAppBarDemo>
with RestorationMixin {
final RestorableBool _showFab = RestorableBool(true);
final RestorableBool _showNotch = RestorableBool(true);
final RestorableInt _currentFabLocation = RestorableInt(0);
@override
String get restorationId => 'bottom_app_bar_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_showFab, 'show_fab');
registerForRestoration(_showNotch, 'show_notch');
registerForRestoration(_currentFabLocation, 'fab_location');
}
@override
void dispose() {
_showFab.dispose();
_showNotch.dispose();
_currentFabLocation.dispose();
super.dispose();
}
// Since FloatingActionButtonLocation is not an enum, the index of the
// selected FloatingActionButtonLocation is used for state restoration.
static const List<FloatingActionButtonLocation> _fabLocations = <FloatingActionButtonLocation>[
FloatingActionButtonLocation.endDocked,
FloatingActionButtonLocation.centerDocked,
FloatingActionButtonLocation.endFloat,
FloatingActionButtonLocation.centerFloat,
];
void _onShowNotchChanged(bool value) {
setState(() {
_showNotch.value = value;
});
}
void _onShowFabChanged(bool value) {
setState(() {
_showFab.value = value;
});
}
void _onFabLocationChanged(int? value) {
setState(() {
_currentFabLocation.value = value!;
});
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoBottomAppBarTitle),
),
body: ListView(
padding: const EdgeInsets.only(bottom: 88),
children: <Widget>[
SwitchListTile(
title: Text(
localizations.demoFloatingButtonTitle,
),
value: _showFab.value,
onChanged: _onShowFabChanged,
),
SwitchListTile(
title: Text(localizations.bottomAppBarNotch),
value: _showNotch.value,
onChanged: _onShowNotchChanged,
),
Padding(
padding: const EdgeInsets.all(16),
child: Text(localizations.bottomAppBarPosition),
),
RadioListTile<int>(
title: Text(
localizations.bottomAppBarPositionDockedEnd,
),
value: 0,
groupValue: _currentFabLocation.value,
onChanged: _onFabLocationChanged,
),
RadioListTile<int>(
title: Text(
localizations.bottomAppBarPositionDockedCenter,
),
value: 1,
groupValue: _currentFabLocation.value,
onChanged: _onFabLocationChanged,
),
RadioListTile<int>(
title: Text(
localizations.bottomAppBarPositionFloatingEnd,
),
value: 2,
groupValue: _currentFabLocation.value,
onChanged: _onFabLocationChanged,
),
RadioListTile<int>(
title: Text(
localizations.bottomAppBarPositionFloatingCenter,
),
value: 3,
groupValue: _currentFabLocation.value,
onChanged: _onFabLocationChanged,
),
],
),
floatingActionButton: _showFab.value
? Semantics(
container: true,
sortKey: const OrdinalSortKey(0),
child: FloatingActionButton(
onPressed: () {},
tooltip: localizations.buttonTextCreate,
child: const Icon(Icons.add),
),
)
: null,
floatingActionButtonLocation: _fabLocations[_currentFabLocation.value],
bottomNavigationBar: _DemoBottomAppBar(
fabLocation: _fabLocations[_currentFabLocation.value],
shape: _showNotch.value ? const CircularNotchedRectangle() : null,
),
);
}
}
class _DemoBottomAppBar extends StatelessWidget {
const _DemoBottomAppBar({
required this.fabLocation,
this.shape,
});
final FloatingActionButtonLocation fabLocation;
final NotchedShape? shape;
static final List<FloatingActionButtonLocation> centerLocations = <FloatingActionButtonLocation>[
FloatingActionButtonLocation.centerDocked,
FloatingActionButtonLocation.centerFloat,
];
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Semantics(
sortKey: const OrdinalSortKey(1),
container: true,
label: localizations.bottomAppBar,
child: BottomAppBar(
shape: shape,
child: IconTheme(
data: IconThemeData(color: Theme.of(context).colorScheme.onPrimary),
child: Row(
children: <Widget>[
IconButton(
tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
icon: const Icon(Icons.menu),
onPressed: () {},
),
if (centerLocations.contains(fabLocation)) const Spacer(),
IconButton(
tooltip: localizations.starterAppTooltipSearch,
icon: const Icon(Icons.search),
onPressed: () {},
),
IconButton(
tooltip: localizations.starterAppTooltipFavorite,
icon: const Icon(Icons.favorite),
onPressed: () {},
)
],
),
),
),
);
}
}
// END

View File

@ -0,0 +1,179 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN bottomNavigationDemo
class BottomNavigationDemo extends StatefulWidget {
const BottomNavigationDemo({
super.key,
required this.restorationId,
required this.type,
});
final String restorationId;
final BottomNavigationDemoType type;
@override
State<BottomNavigationDemo> createState() => _BottomNavigationDemoState();
}
class _BottomNavigationDemoState extends State<BottomNavigationDemo>
with RestorationMixin {
final RestorableInt _currentIndex = RestorableInt(0);
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_currentIndex, 'bottom_navigation_tab_index');
}
@override
void dispose() {
_currentIndex.dispose();
super.dispose();
}
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (widget.type) {
case BottomNavigationDemoType.withLabels:
return localizations.demoBottomNavigationPersistentLabels;
case BottomNavigationDemoType.withoutLabels:
return localizations.demoBottomNavigationSelectedLabel;
}
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
List<BottomNavigationBarItem> bottomNavigationBarItems = <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.add_comment),
label: localizations.bottomNavigationCommentsTab,
),
BottomNavigationBarItem(
icon: const Icon(Icons.calendar_today),
label: localizations.bottomNavigationCalendarTab,
),
BottomNavigationBarItem(
icon: const Icon(Icons.account_circle),
label: localizations.bottomNavigationAccountTab,
),
BottomNavigationBarItem(
icon: const Icon(Icons.alarm_on),
label: localizations.bottomNavigationAlarmTab,
),
BottomNavigationBarItem(
icon: const Icon(Icons.camera_enhance),
label: localizations.bottomNavigationCameraTab,
),
];
if (widget.type == BottomNavigationDemoType.withLabels) {
bottomNavigationBarItems = bottomNavigationBarItems.sublist(
0, bottomNavigationBarItems.length - 2);
_currentIndex.value = _currentIndex.value
.clamp(0, bottomNavigationBarItems.length - 1)
;
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: Center(
child: PageTransitionSwitcher(
transitionBuilder: (Widget child, Animation<double> animation, Animation<double> secondaryAnimation) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: _NavigationDestinationView(
// Adding [UniqueKey] to make sure the widget rebuilds when transitioning.
key: UniqueKey(),
item: bottomNavigationBarItems[_currentIndex.value],
),
),
),
bottomNavigationBar: BottomNavigationBar(
showUnselectedLabels:
widget.type == BottomNavigationDemoType.withLabels,
items: bottomNavigationBarItems,
currentIndex: _currentIndex.value,
type: BottomNavigationBarType.fixed,
selectedFontSize: textTheme.bodySmall!.fontSize!,
unselectedFontSize: textTheme.bodySmall!.fontSize!,
onTap: (int index) {
setState(() {
_currentIndex.value = index;
});
},
selectedItemColor: colorScheme.onPrimary,
unselectedItemColor: colorScheme.onPrimary.withOpacity(0.38),
backgroundColor: colorScheme.primary,
),
);
}
}
class _NavigationDestinationView extends StatelessWidget {
const _NavigationDestinationView({
super.key,
required this.item,
});
final BottomNavigationBarItem item;
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
ExcludeSemantics(
child: Center(
child: Padding(
padding: const EdgeInsets.all(16),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
'assets/demos/bottom_navigation_background.png',
package: 'flutter_gallery_assets',
),
),
),
),
),
Center(
child: IconTheme(
data: const IconThemeData(
color: Colors.white,
size: 80,
),
child: Semantics(
label: GalleryLocalizations.of(context)!
.bottomNavigationContentPlaceholder(
item.label!,
),
child: item.icon,
),
),
),
],
);
}
}
// END

View File

@ -0,0 +1,189 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class BottomSheetDemo extends StatelessWidget {
const BottomSheetDemo({
super.key,
required this.type,
});
final BottomSheetDemoType type;
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (type) {
case BottomSheetDemoType.persistent:
return localizations.demoBottomSheetPersistentTitle;
case BottomSheetDemoType.modal:
return localizations.demoBottomSheetModalTitle;
}
}
Widget _bottomSheetDemo(BuildContext context) {
switch (type) {
case BottomSheetDemoType.persistent:
return _PersistentBottomSheetDemo();
case BottomSheetDemoType.modal:
return _ModalBottomSheetDemo();
}
}
@override
Widget build(BuildContext context) {
// We wrap the demo in a [Navigator] to make sure that the modal bottom
// sheets gets dismissed when changing demo.
return Navigator(
// Adding [ValueKey] to make sure that the widget gets rebuilt when
// changing type.
key: ValueKey<BottomSheetDemoType>(type),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
floatingActionButton: FloatingActionButton(
onPressed: () {},
backgroundColor: Theme.of(context).colorScheme.secondary,
child: Icon(
Icons.add,
semanticLabel:
GalleryLocalizations.of(context)!.demoBottomSheetAddLabel,
),
),
body: _bottomSheetDemo(context),
),
);
},
);
}
}
// BEGIN bottomSheetDemoModal#1 bottomSheetDemoPersistent#1
class _BottomSheetContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return SizedBox(
height: 300,
child: Column(
children: <Widget>[
SizedBox(
height: 70,
child: Center(
child: Text(
localizations.demoBottomSheetHeader,
textAlign: TextAlign.center,
),
),
),
const Divider(thickness: 1),
Expanded(
child: ListView.builder(
itemCount: 21,
itemBuilder: (BuildContext context, int index) {
return ListTile(
title: Text(localizations.demoBottomSheetItem(index)),
);
},
),
),
],
),
);
}
}
// END bottomSheetDemoModal#1 bottomSheetDemoPersistent#1
// BEGIN bottomSheetDemoModal#2
class _ModalBottomSheetDemo extends StatelessWidget {
void _showModalBottomSheet(BuildContext context) {
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return _BottomSheetContent();
},
);
}
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: () {
_showModalBottomSheet(context);
},
child:
Text(GalleryLocalizations.of(context)!.demoBottomSheetButtonText),
),
);
}
}
// END
// BEGIN bottomSheetDemoPersistent#2
class _PersistentBottomSheetDemo extends StatefulWidget {
@override
_PersistentBottomSheetDemoState createState() =>
_PersistentBottomSheetDemoState();
}
class _PersistentBottomSheetDemoState
extends State<_PersistentBottomSheetDemo> {
VoidCallback? _showBottomSheetCallback;
@override
void initState() {
super.initState();
_showBottomSheetCallback = _showPersistentBottomSheet;
}
void _showPersistentBottomSheet() {
setState(() {
// Disable the show bottom sheet button.
_showBottomSheetCallback = null;
});
Scaffold.of(context)
.showBottomSheet(
(BuildContext context) {
return _BottomSheetContent();
},
elevation: 25,
)
.closed
.whenComplete(() {
if (mounted) {
setState(() {
// Re-enable the bottom sheet button.
_showBottomSheetCallback = _showPersistentBottomSheet;
});
}
});
}
@override
Widget build(BuildContext context) {
return Center(
child: ElevatedButton(
onPressed: _showBottomSheetCallback,
child:
Text(GalleryLocalizations.of(context)!.demoBottomSheetButtonText),
),
);
}
}
// END

View File

@ -0,0 +1,297 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class ButtonDemo extends StatelessWidget {
const ButtonDemo({super.key, required this.type});
final ButtonDemoType type;
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (type) {
case ButtonDemoType.text:
return localizations.demoTextButtonTitle;
case ButtonDemoType.elevated:
return localizations.demoElevatedButtonTitle;
case ButtonDemoType.outlined:
return localizations.demoOutlinedButtonTitle;
case ButtonDemoType.toggle:
return localizations.demoToggleButtonTitle;
case ButtonDemoType.floating:
return localizations.demoFloatingButtonTitle;
}
}
@override
Widget build(BuildContext context) {
Widget? buttons;
switch (type) {
case ButtonDemoType.text:
buttons = _TextButtonDemo();
case ButtonDemoType.elevated:
buttons = _ElevatedButtonDemo();
case ButtonDemoType.outlined:
buttons = _OutlinedButtonDemo();
case ButtonDemoType.toggle:
buttons = _ToggleButtonsDemo();
case ButtonDemoType.floating:
buttons = _FloatingActionButtonDemo();
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: buttons,
);
}
}
// BEGIN buttonDemoText
class _TextButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
onPressed: () {},
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
TextButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: () {},
),
],
),
const SizedBox(height: 12),
// Disabled buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextButton(
onPressed: null,
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
TextButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: null,
),
],
),
],
);
}
}
// END
// BEGIN buttonDemoElevated
class _ElevatedButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {},
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
ElevatedButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: () {},
),
],
),
const SizedBox(height: 12),
// Disabled buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: null,
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
ElevatedButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: null,
),
],
),
],
);
}
}
// END
// BEGIN buttonDemoOutlined
class _OutlinedButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: () {},
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
OutlinedButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: () {},
),
],
),
const SizedBox(height: 12),
// Disabled buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
OutlinedButton(
onPressed: null,
child: Text(localizations.buttonText),
),
const SizedBox(width: 12),
OutlinedButton.icon(
icon: const Icon(Icons.add, size: 18),
label: Text(localizations.buttonText),
onPressed: null,
),
],
),
],
);
}
}
// END
// BEGIN buttonDemoToggle
class _ToggleButtonsDemo extends StatefulWidget {
@override
_ToggleButtonsDemoState createState() => _ToggleButtonsDemoState();
}
class _ToggleButtonsDemoState extends State<_ToggleButtonsDemo>
with RestorationMixin {
final List<RestorableBool> isSelected = <RestorableBool>[
RestorableBool(false),
RestorableBool(true),
RestorableBool(false),
];
@override
String get restorationId => 'toggle_button_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(isSelected[0], 'first_item');
registerForRestoration(isSelected[1], 'second_item');
registerForRestoration(isSelected[2], 'third_item');
}
@override
void dispose() {
for (final RestorableBool restorableBool in isSelected) {
restorableBool.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ToggleButtons(
onPressed: (int index) {
setState(() {
isSelected[index].value = !isSelected[index].value;
});
},
isSelected: isSelected.map((RestorableBool element) => element.value).toList(),
children: const <Widget>[
Icon(Icons.format_bold),
Icon(Icons.format_italic),
Icon(Icons.format_underline),
],
),
const SizedBox(height: 12),
// Disabled toggle buttons
ToggleButtons(
isSelected: isSelected.map((RestorableBool element) => element.value).toList(),
children: const <Widget>[
Icon(Icons.format_bold),
Icon(Icons.format_italic),
Icon(Icons.format_underline),
],
),
],
),
);
}
}
// END
// BEGIN buttonDemoFloating
class _FloatingActionButtonDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FloatingActionButton(
onPressed: () {},
tooltip: localizations.buttonTextCreate,
child: const Icon(Icons.add),
),
const SizedBox(width: 12),
FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(localizations.buttonTextCreate),
onPressed: () {},
),
],
),
);
}
}
// END

View File

@ -0,0 +1,441 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
const String _kGalleryAssetsPackage = 'flutter_gallery_assets';
// BEGIN cardsDemo
enum CardType {
standard,
tappable,
selectable,
}
class TravelDestination {
const TravelDestination({
required this.assetName,
required this.assetPackage,
required this.title,
required this.description,
required this.city,
required this.location,
this.cardType = CardType.standard,
});
final String assetName;
final String assetPackage;
final String title;
final String description;
final String city;
final String location;
final CardType cardType;
}
List<TravelDestination> destinations(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <TravelDestination>[
TravelDestination(
assetName: 'places/india_thanjavur_market.png',
assetPackage: _kGalleryAssetsPackage,
title: localizations.cardsDemoTravelDestinationTitle1,
description: localizations.cardsDemoTravelDestinationDescription1,
city: localizations.cardsDemoTravelDestinationCity1,
location: localizations.cardsDemoTravelDestinationLocation1,
),
TravelDestination(
assetName: 'places/india_chettinad_silk_maker.png',
assetPackage: _kGalleryAssetsPackage,
title: localizations.cardsDemoTravelDestinationTitle2,
description: localizations.cardsDemoTravelDestinationDescription2,
city: localizations.cardsDemoTravelDestinationCity2,
location: localizations.cardsDemoTravelDestinationLocation2,
cardType: CardType.tappable,
),
TravelDestination(
assetName: 'places/india_tanjore_thanjavur_temple.png',
assetPackage: _kGalleryAssetsPackage,
title: localizations.cardsDemoTravelDestinationTitle3,
description: localizations.cardsDemoTravelDestinationDescription3,
city: localizations.cardsDemoTravelDestinationCity1,
location: localizations.cardsDemoTravelDestinationLocation1,
cardType: CardType.selectable,
),
];
}
class TravelDestinationItem extends StatelessWidget {
const TravelDestinationItem(
{super.key, required this.destination, this.shape});
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 360.0;
final TravelDestination destination;
final ShapeBorder? shape;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: <Widget>[
SectionTitle(
title: GalleryLocalizations.of(context)!
.settingsTextScalingNormal),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: Semantics(
label: destination.title,
child: TravelDestinationContent(destination: destination),
),
),
),
],
),
),
);
}
}
class TappableTravelDestinationItem extends StatelessWidget {
const TappableTravelDestinationItem({
super.key,
required this.destination,
this.shape,
});
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 298.0;
final TravelDestination destination;
final ShapeBorder? shape;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: <Widget>[
SectionTitle(
title: GalleryLocalizations.of(context)!.cardsDemoTappable),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children (including the ink splash) are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: InkWell(
onTap: () {},
// Generally, material cards use onSurface with 12% opacity for the pressed state.
splashColor:
Theme.of(context).colorScheme.onSurface.withOpacity(0.12),
// Generally, material cards do not have a highlight overlay.
highlightColor: Colors.transparent,
child: Semantics(
label: destination.title,
child: TravelDestinationContent(destination: destination),
),
),
),
),
],
),
),
);
}
}
class SelectableTravelDestinationItem extends StatelessWidget {
const SelectableTravelDestinationItem({
super.key,
required this.destination,
required this.isSelected,
required this.onSelected,
this.shape,
});
final TravelDestination destination;
final ShapeBorder? shape;
final bool isSelected;
final VoidCallback onSelected;
// This height will allow for all the Card's content to fit comfortably within the card.
static const double height = 298.0;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final String selectedStatus = isSelected
? GalleryLocalizations.of(context)!.selected
: GalleryLocalizations.of(context)!.notSelected;
return SafeArea(
top: false,
bottom: false,
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: <Widget>[
SectionTitle(title: GalleryLocalizations.of(context)!.selectable),
SizedBox(
height: height,
child: Card(
// This ensures that the Card's children (including the ink splash) are clipped correctly.
clipBehavior: Clip.antiAlias,
shape: shape,
child: InkWell(
onLongPress: () {
onSelected();
},
// Generally, material cards use onSurface with 12% opacity for the pressed state.
splashColor: colorScheme.onSurface.withOpacity(0.12),
// Generally, material cards do not have a highlight overlay.
highlightColor: Colors.transparent,
child: Stack(
children: <Widget>[
Container(
color: isSelected
// Generally, material cards use primary with 8% opacity for the selected state.
// See: https://material.io/design/interaction/states.html#anatomy
? colorScheme.primary.withOpacity(0.08)
: Colors.transparent,
),
Semantics(
label: '${destination.title}, $selectedStatus',
onLongPressHint: isSelected
? GalleryLocalizations.of(context)!.deselect
: GalleryLocalizations.of(context)!.select,
child:
TravelDestinationContent(destination: destination),
),
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.all(8),
child: Icon(
Icons.check_circle,
color: isSelected
? colorScheme.primary
: Colors.transparent,
),
),
),
],
),
//),
),
),
),
],
),
),
);
}
}
class SectionTitle extends StatelessWidget {
const SectionTitle({
super.key,
required this.title,
});
final String title;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(4, 4, 4, 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(title, style: Theme.of(context).textTheme.titleMedium),
),
);
}
}
class TravelDestinationContent extends StatelessWidget {
const TravelDestinationContent({super.key, required this.destination});
final TravelDestination destination;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final TextStyle titleStyle = theme.textTheme.headlineSmall!.copyWith(
color: Colors.white,
);
final TextStyle descriptionStyle = theme.textTheme.titleMedium!;
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 184,
child: Stack(
children: <Widget>[
Positioned.fill(
// In order to have the ink splash appear above the image, you
// must use Ink.image. This allows the image to be painted as
// part of the Material and display ink effects above it. Using
// a standard Image will obscure the ink splash.
child: Ink.image(
image: AssetImage(
destination.assetName,
package: destination.assetPackage,
),
fit: BoxFit.cover,
child: Container(),
),
),
Positioned(
bottom: 16,
left: 16,
right: 16,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Semantics(
container: true,
header: true,
child: Text(
destination.title,
style: titleStyle,
),
),
),
),
],
),
),
// Description and share/explore buttons.
Semantics(
container: true,
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: DefaultTextStyle(
softWrap: false,
overflow: TextOverflow.ellipsis,
style: descriptionStyle,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// This array contains the three line description on each card
// demo.
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
destination.description,
style: descriptionStyle.copyWith(color: Colors.black54),
),
),
Text(destination.city),
Text(destination.location),
],
),
),
),
),
if (destination.cardType == CardType.standard)
// share, explore buttons
Padding(
padding: const EdgeInsets.all(8),
child: OverflowBar(
alignment: MainAxisAlignment.start,
spacing: 8,
children: <Widget>[
TextButton(
onPressed: () {},
child: Text(localizations.demoMenuShare,
semanticsLabel: localizations
.cardsDemoShareSemantics(destination.title)),
),
TextButton(
onPressed: () {},
child: Text(localizations.cardsDemoExplore,
semanticsLabel: localizations
.cardsDemoExploreSemantics(destination.title)),
),
],
),
),
],
);
}
}
class CardsDemo extends StatefulWidget {
const CardsDemo({super.key});
@override
State<CardsDemo> createState() => _CardsDemoState();
}
class _CardsDemoState extends State<CardsDemo> with RestorationMixin {
final RestorableBool _isSelected = RestorableBool(false);
@override
String get restorationId => 'cards_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_isSelected, 'is_selected');
}
@override
void dispose() {
_isSelected.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoCardTitle),
),
body: Scrollbar(
child: ListView(
restorationId: 'cards_demo_list_view',
padding: const EdgeInsets.only(top: 8, left: 8, right: 8),
children: <Widget>[
for (final TravelDestination destination in destinations(context))
Container(
margin: const EdgeInsets.only(bottom: 8),
child: (destination.cardType == CardType.standard)
? TravelDestinationItem(destination: destination)
: destination.cardType == CardType.tappable
? TappableTravelDestinationItem(
destination: destination)
: SelectableTravelDestinationItem(
destination: destination,
isSelected: _isSelected.value,
onSelected: () {
setState(() {
_isSelected.value = !_isSelected.value;
});
},
),
),
],
),
),
);
}
}
// END

View File

@ -0,0 +1,306 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class ChipDemo extends StatelessWidget {
const ChipDemo({
super.key,
required this.type,
});
final ChipDemoType type;
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (type) {
case ChipDemoType.action:
return localizations.demoActionChipTitle;
case ChipDemoType.choice:
return localizations.demoChoiceChipTitle;
case ChipDemoType.filter:
return localizations.demoFilterChipTitle;
case ChipDemoType.input:
return localizations.demoInputChipTitle;
}
}
@override
Widget build(BuildContext context) {
Widget? buttons;
switch (type) {
case ChipDemoType.action:
buttons = _ActionChipDemo();
case ChipDemoType.choice:
buttons = _ChoiceChipDemo();
case ChipDemoType.filter:
buttons = _FilterChipDemo();
case ChipDemoType.input:
buttons = _InputChipDemo();
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: buttons,
);
}
}
// BEGIN chipDemoAction
class _ActionChipDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: ActionChip(
onPressed: () {},
avatar: const Icon(
Icons.brightness_5,
color: Colors.black54,
),
label: Text(GalleryLocalizations.of(context)!.chipTurnOnLights),
),
);
}
}
// END
// BEGIN chipDemoChoice
class _ChoiceChipDemo extends StatefulWidget {
@override
_ChoiceChipDemoState createState() => _ChoiceChipDemoState();
}
class _ChoiceChipDemoState extends State<_ChoiceChipDemo>
with RestorationMixin {
final RestorableIntN _indexSelected = RestorableIntN(null);
@override
String get restorationId => 'choice_chip_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_indexSelected, 'choice_chip');
}
@override
void dispose() {
_indexSelected.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Wrap(
children: <Widget>[
ChoiceChip(
label: Text(localizations.chipSmall),
selected: _indexSelected.value == 0,
onSelected: (bool value) {
setState(() {
_indexSelected.value = value ? 0 : -1;
});
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(localizations.chipMedium),
selected: _indexSelected.value == 1,
onSelected: (bool value) {
setState(() {
_indexSelected.value = value ? 1 : -1;
});
},
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(localizations.chipLarge),
selected: _indexSelected.value == 2,
onSelected: (bool value) {
setState(() {
_indexSelected.value = value ? 2 : -1;
});
},
),
],
),
const SizedBox(height: 12),
// Disabled chips
Wrap(
children: <Widget>[
ChoiceChip(
label: Text(localizations.chipSmall),
selected: _indexSelected.value == 0,
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(localizations.chipMedium),
selected: _indexSelected.value == 1,
),
const SizedBox(width: 8),
ChoiceChip(
label: Text(localizations.chipLarge),
selected: _indexSelected.value == 2,
),
],
),
],
),
);
}
}
// END
// BEGIN chipDemoFilter
class _FilterChipDemo extends StatefulWidget {
@override
_FilterChipDemoState createState() => _FilterChipDemoState();
}
class _FilterChipDemoState extends State<_FilterChipDemo>
with RestorationMixin {
final RestorableBool isSelectedElevator = RestorableBool(false);
final RestorableBool isSelectedWasher = RestorableBool(false);
final RestorableBool isSelectedFireplace = RestorableBool(false);
@override
String get restorationId => 'filter_chip_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(isSelectedElevator, 'selected_elevator');
registerForRestoration(isSelectedWasher, 'selected_washer');
registerForRestoration(isSelectedFireplace, 'selected_fireplace');
}
@override
void dispose() {
isSelectedElevator.dispose();
isSelectedWasher.dispose();
isSelectedFireplace.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Wrap(
spacing: 8.0,
children: <Widget>[
FilterChip(
label: Text(localizations.chipElevator),
selected: isSelectedElevator.value,
onSelected: (bool value) {
setState(() {
isSelectedElevator.value = !isSelectedElevator.value;
});
},
),
FilterChip(
label: Text(localizations.chipWasher),
selected: isSelectedWasher.value,
onSelected: (bool value) {
setState(() {
isSelectedWasher.value = !isSelectedWasher.value;
});
},
),
FilterChip(
label: Text(localizations.chipFireplace),
selected: isSelectedFireplace.value,
onSelected: (bool value) {
setState(() {
isSelectedFireplace.value = !isSelectedFireplace.value;
});
},
),
],
),
const SizedBox(height: 12),
// Disabled chips
Wrap(
spacing: 8.0,
children: <Widget>[
FilterChip(
label: Text(localizations.chipElevator),
selected: isSelectedElevator.value,
onSelected: null,
),
FilterChip(
label: Text(localizations.chipWasher),
selected: isSelectedWasher.value,
onSelected: null,
),
FilterChip(
label: Text(localizations.chipFireplace),
selected: isSelectedFireplace.value,
onSelected: null,
),
],
),
],
),
);
}
}
// END
// BEGIN chipDemoInput
class _InputChipDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
InputChip(
onPressed: () {},
onDeleted: () {},
avatar: const Icon(
Icons.directions_bike,
size: 20,
color: Colors.black54,
),
deleteIconColor: Colors.black54,
label: Text(GalleryLocalizations.of(context)!.chipBiking),
),
const SizedBox(height: 12),
// Disabled chip
InputChip(
avatar: const Icon(
Icons.directions_bike,
size: 20,
color: Colors.black54,
),
deleteIconColor: Colors.black54,
label: Text(GalleryLocalizations.of(context)!.chipBiking),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,680 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../data/gallery_options.dart';
import '../../gallery_localizations.dart';
// BEGIN dataTableDemo
class DataTableDemo extends StatefulWidget {
const DataTableDemo({super.key});
@override
State<DataTableDemo> createState() => _DataTableDemoState();
}
class _RestorableDessertSelections extends RestorableProperty<Set<int>> {
Set<int> _dessertSelections = <int>{};
/// Returns whether or not a dessert row is selected by index.
bool isSelected(int index) => _dessertSelections.contains(index);
/// Takes a list of [_Dessert]s and saves the row indices of selected rows
/// into a [Set].
void setDessertSelections(List<_Dessert> desserts) {
final Set<int> updatedSet = <int>{};
for (int i = 0; i < desserts.length; i += 1) {
final _Dessert dessert = desserts[i];
if (dessert.selected) {
updatedSet.add(i);
}
}
_dessertSelections = updatedSet;
notifyListeners();
}
@override
Set<int> createDefaultValue() => _dessertSelections;
@override
Set<int> fromPrimitives(Object? data) {
final List<dynamic> selectedItemIndices = data! as List<dynamic>;
_dessertSelections = <int>{
...selectedItemIndices.map<int>((dynamic id) => id as int),
};
return _dessertSelections;
}
@override
void initWithValue(Set<int> value) {
_dessertSelections = value;
}
@override
Object toPrimitives() => _dessertSelections.toList();
}
class _DataTableDemoState extends State<DataTableDemo> with RestorationMixin {
final _RestorableDessertSelections _dessertSelections =
_RestorableDessertSelections();
final RestorableInt _rowIndex = RestorableInt(0);
final RestorableInt _rowsPerPage =
RestorableInt(PaginatedDataTable.defaultRowsPerPage);
final RestorableBool _sortAscending = RestorableBool(true);
final RestorableIntN _sortColumnIndex = RestorableIntN(null);
_DessertDataSource? _dessertsDataSource;
@override
String get restorationId => 'data_table_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_dessertSelections, 'selected_row_indices');
registerForRestoration(_rowIndex, 'current_row_index');
registerForRestoration(_rowsPerPage, 'rows_per_page');
registerForRestoration(_sortAscending, 'sort_ascending');
registerForRestoration(_sortColumnIndex, 'sort_column_index');
_dessertsDataSource ??= _DessertDataSource(context);
switch (_sortColumnIndex.value) {
case 0:
_dessertsDataSource!._sort<String>((_Dessert d) => d.name, _sortAscending.value);
case 1:
_dessertsDataSource!
._sort<num>((_Dessert d) => d.calories, _sortAscending.value);
case 2:
_dessertsDataSource!._sort<num>((_Dessert d) => d.fat, _sortAscending.value);
case 3:
_dessertsDataSource!._sort<num>((_Dessert d) => d.carbs, _sortAscending.value);
case 4:
_dessertsDataSource!._sort<num>((_Dessert d) => d.protein, _sortAscending.value);
case 5:
_dessertsDataSource!._sort<num>((_Dessert d) => d.sodium, _sortAscending.value);
case 6:
_dessertsDataSource!._sort<num>((_Dessert d) => d.calcium, _sortAscending.value);
case 7:
_dessertsDataSource!._sort<num>((_Dessert d) => d.iron, _sortAscending.value);
}
_dessertsDataSource!.updateSelectedDesserts(_dessertSelections);
_dessertsDataSource!.addListener(_updateSelectedDessertRowListener);
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_dessertsDataSource ??= _DessertDataSource(context);
_dessertsDataSource!.addListener(_updateSelectedDessertRowListener);
}
void _updateSelectedDessertRowListener() {
_dessertSelections.setDessertSelections(_dessertsDataSource!._desserts);
}
void _sort<T>(
Comparable<T> Function(_Dessert d) getField,
int columnIndex,
bool ascending,
) {
_dessertsDataSource!._sort<T>(getField, ascending);
setState(() {
_sortColumnIndex.value = columnIndex;
_sortAscending.value = ascending;
});
}
@override
void dispose() {
_rowsPerPage.dispose();
_sortColumnIndex.dispose();
_sortAscending.dispose();
_dessertsDataSource!.removeListener(_updateSelectedDessertRowListener);
_dessertsDataSource!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoDataTableTitle),
),
body: Scrollbar(
child: ListView(
restorationId: 'data_table_list_view',
padding: const EdgeInsets.all(16),
children: <Widget>[
PaginatedDataTable(
header: Text(localizations.dataTableHeader),
rowsPerPage: _rowsPerPage.value,
onRowsPerPageChanged: (int? value) {
setState(() {
_rowsPerPage.value = value!;
});
},
initialFirstRowIndex: _rowIndex.value,
onPageChanged: (int rowIndex) {
setState(() {
_rowIndex.value = rowIndex;
});
},
sortColumnIndex: _sortColumnIndex.value,
sortAscending: _sortAscending.value,
onSelectAll: _dessertsDataSource!._selectAll,
columns: <DataColumn>[
DataColumn(
label: Text(localizations.dataTableColumnDessert),
onSort: (int columnIndex, bool ascending) =>
_sort<String>((_Dessert d) => d.name, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnCalories),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.calories, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnFat),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.fat, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnCarbs),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.carbs, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnProtein),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.protein, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnSodium),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.sodium, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnCalcium),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.calcium, columnIndex, ascending),
),
DataColumn(
label: Text(localizations.dataTableColumnIron),
numeric: true,
onSort: (int columnIndex, bool ascending) =>
_sort<num>((_Dessert d) => d.iron, columnIndex, ascending),
),
],
source: _dessertsDataSource!,
),
],
),
),
);
}
}
class _Dessert {
_Dessert(
this.name,
this.calories,
this.fat,
this.carbs,
this.protein,
this.sodium,
this.calcium,
this.iron,
);
final String name;
final int calories;
final double fat;
final int carbs;
final double protein;
final int sodium;
final int calcium;
final int iron;
bool selected = false;
}
class _DessertDataSource extends DataTableSource {
_DessertDataSource(this.context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
_desserts = <_Dessert>[
_Dessert(
localizations.dataTableRowFrozenYogurt,
159,
6.0,
24,
4.0,
87,
14,
1,
),
_Dessert(
localizations.dataTableRowIceCreamSandwich,
237,
9.0,
37,
4.3,
129,
8,
1,
),
_Dessert(
localizations.dataTableRowEclair,
262,
16.0,
24,
6.0,
337,
6,
7,
),
_Dessert(
localizations.dataTableRowCupcake,
305,
3.7,
67,
4.3,
413,
3,
8,
),
_Dessert(
localizations.dataTableRowGingerbread,
356,
16.0,
49,
3.9,
327,
7,
16,
),
_Dessert(
localizations.dataTableRowJellyBean,
375,
0.0,
94,
0.0,
50,
0,
0,
),
_Dessert(
localizations.dataTableRowLollipop,
392,
0.2,
98,
0.0,
38,
0,
2,
),
_Dessert(
localizations.dataTableRowHoneycomb,
408,
3.2,
87,
6.5,
562,
0,
45,
),
_Dessert(
localizations.dataTableRowDonut,
452,
25.0,
51,
4.9,
326,
2,
22,
),
_Dessert(
localizations.dataTableRowApplePie,
518,
26.0,
65,
7.0,
54,
12,
6,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowFrozenYogurt,
),
168,
6.0,
26,
4.0,
87,
14,
1,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowIceCreamSandwich,
),
246,
9.0,
39,
4.3,
129,
8,
1,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowEclair,
),
271,
16.0,
26,
6.0,
337,
6,
7,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowCupcake,
),
314,
3.7,
69,
4.3,
413,
3,
8,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowGingerbread,
),
345,
16.0,
51,
3.9,
327,
7,
16,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowJellyBean,
),
364,
0.0,
96,
0.0,
50,
0,
0,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowLollipop,
),
401,
0.2,
100,
0.0,
38,
0,
2,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowHoneycomb,
),
417,
3.2,
89,
6.5,
562,
0,
45,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowDonut,
),
461,
25.0,
53,
4.9,
326,
2,
22,
),
_Dessert(
localizations.dataTableRowWithSugar(
localizations.dataTableRowApplePie,
),
527,
26.0,
67,
7.0,
54,
12,
6,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowFrozenYogurt,
),
223,
6.0,
36,
4.0,
87,
14,
1,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowIceCreamSandwich,
),
301,
9.0,
49,
4.3,
129,
8,
1,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowEclair,
),
326,
16.0,
36,
6.0,
337,
6,
7,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowCupcake,
),
369,
3.7,
79,
4.3,
413,
3,
8,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowGingerbread,
),
420,
16.0,
61,
3.9,
327,
7,
16,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowJellyBean,
),
439,
0.0,
106,
0.0,
50,
0,
0,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowLollipop,
),
456,
0.2,
110,
0.0,
38,
0,
2,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowHoneycomb,
),
472,
3.2,
99,
6.5,
562,
0,
45,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowDonut,
),
516,
25.0,
63,
4.9,
326,
2,
22,
),
_Dessert(
localizations.dataTableRowWithHoney(
localizations.dataTableRowApplePie,
),
582,
26.0,
77,
7.0,
54,
12,
6,
),
];
}
final BuildContext context;
late List<_Dessert> _desserts;
void _sort<T>(Comparable<T> Function(_Dessert d) getField, bool ascending) {
_desserts.sort((_Dessert a, _Dessert b) {
final Comparable<T> aValue = getField(a);
final Comparable<T> bValue = getField(b);
return ascending
? Comparable.compare(aValue, bValue)
: Comparable.compare(bValue, aValue);
});
notifyListeners();
}
int _selectedCount = 0;
void updateSelectedDesserts(_RestorableDessertSelections selectedRows) {
_selectedCount = 0;
for (int i = 0; i < _desserts.length; i += 1) {
final _Dessert dessert = _desserts[i];
if (selectedRows.isSelected(i)) {
dessert.selected = true;
_selectedCount += 1;
} else {
dessert.selected = false;
}
}
notifyListeners();
}
@override
DataRow? getRow(int index) {
final NumberFormat format = NumberFormat.decimalPercentPattern(
locale: GalleryOptions.of(context).locale.toString(),
decimalDigits: 0,
);
assert(index >= 0);
if (index >= _desserts.length) {
return null;
}
final _Dessert dessert = _desserts[index];
return DataRow.byIndex(
index: index,
selected: dessert.selected,
onSelectChanged: (bool? value) {
if (dessert.selected != value) {
_selectedCount += value! ? 1 : -1;
assert(_selectedCount >= 0);
dessert.selected = value;
notifyListeners();
}
},
cells: <DataCell>[
DataCell(Text(dessert.name)),
DataCell(Text('${dessert.calories}')),
DataCell(Text(dessert.fat.toStringAsFixed(1))),
DataCell(Text('${dessert.carbs}')),
DataCell(Text(dessert.protein.toStringAsFixed(1))),
DataCell(Text('${dessert.sodium}')),
DataCell(Text(format.format(dessert.calcium / 100))),
DataCell(Text(format.format(dessert.iron / 100))),
],
);
}
@override
int get rowCount => _desserts.length;
@override
bool get isRowCountApproximate => false;
@override
int get selectedRowCount => _selectedCount;
void _selectAll(bool? checked) {
for (final _Dessert dessert in _desserts) {
dessert.selected = checked ?? false;
}
_selectedCount = checked! ? _desserts.length : 0;
notifyListeners();
}
}
// END

View File

@ -0,0 +1,344 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../data/gallery_options.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN dialogDemo
class DialogDemo extends StatefulWidget {
const DialogDemo({super.key, required this.type});
final DialogDemoType type;
@override
State<DialogDemo> createState() => _DialogDemoState();
}
class _DialogDemoState extends State<DialogDemo> with RestorationMixin {
late RestorableRouteFuture<String> _alertDialogRoute;
late RestorableRouteFuture<String> _alertDialogWithTitleRoute;
late RestorableRouteFuture<String> _simpleDialogRoute;
@override
String get restorationId => 'dialog_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(
_alertDialogRoute,
'alert_demo_dialog_route',
);
registerForRestoration(
_alertDialogWithTitleRoute,
'alert_demo_with_title_dialog_route',
);
registerForRestoration(
_simpleDialogRoute,
'simple_dialog_route',
);
}
// Displays the popped String value in a SnackBar.
void _showInSnackBar(String value) {
// The value passed to Navigator.pop() or null.
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
GalleryLocalizations.of(context)!.dialogSelectedOption(value),
),
),
);
}
@override
void initState() {
super.initState();
_alertDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertDialogDemoRoute);
},
onComplete: _showInSnackBar,
);
_alertDialogWithTitleRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_alertDialogWithTitleDemoRoute);
},
onComplete: _showInSnackBar,
);
_simpleDialogRoute = RestorableRouteFuture<String>(
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(_simpleDialogDemoRoute);
},
onComplete: _showInSnackBar,
);
}
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (widget.type) {
case DialogDemoType.alert:
return localizations.demoAlertDialogTitle;
case DialogDemoType.alertTitle:
return localizations.demoAlertTitleDialogTitle;
case DialogDemoType.simple:
return localizations.demoSimpleDialogTitle;
case DialogDemoType.fullscreen:
return localizations.demoFullscreenDialogTitle;
}
}
static Route<String> _alertDialogDemoRoute(
BuildContext context,
Object? arguments,
) {
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!
.copyWith(color: theme.textTheme.bodySmall!.color);
return DialogRoute<String>(
context: context,
builder: (BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return ApplyTextOptions(
child: AlertDialog(
content: Text(
localizations.dialogDiscardTitle,
style: dialogTextStyle,
),
actions: <Widget>[
_DialogButton(text: localizations.dialogCancel),
_DialogButton(text: localizations.dialogDiscard),
],
));
},
);
}
static Route<String> _alertDialogWithTitleDemoRoute(
BuildContext context,
Object? arguments,
) {
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!
.copyWith(color: theme.textTheme.bodySmall!.color);
return DialogRoute<String>(
context: context,
builder: (BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return ApplyTextOptions(
child: AlertDialog(
title: Text(localizations.dialogLocationTitle),
content: Text(
localizations.dialogLocationDescription,
style: dialogTextStyle,
),
actions: <Widget>[
_DialogButton(text: localizations.dialogDisagree),
_DialogButton(text: localizations.dialogAgree),
],
),
);
},
);
}
static Route<String> _simpleDialogDemoRoute(
BuildContext context,
Object? arguments,
) {
final ThemeData theme = Theme.of(context);
return DialogRoute<String>(
context: context,
builder: (BuildContext context) => ApplyTextOptions(
child: SimpleDialog(
title: Text(GalleryLocalizations.of(context)!.dialogSetBackup),
children: <Widget>[
_DialogDemoItem(
icon: Icons.account_circle,
color: theme.colorScheme.primary,
text: 'username@gmail.com',
),
_DialogDemoItem(
icon: Icons.account_circle,
color: theme.colorScheme.secondary,
text: 'user02@gmail.com',
),
_DialogDemoItem(
icon: Icons.add_circle,
text: GalleryLocalizations.of(context)!.dialogAddAccount,
color: theme.disabledColor,
),
],
),
),
);
}
static Route<void> _fullscreenDialogRoute(
BuildContext context,
Object? arguments,
) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => _FullScreenDialogDemo(),
fullscreenDialog: true,
);
}
@override
Widget build(BuildContext context) {
return Navigator(
// Adding [ValueKey] to make sure that the widget gets rebuilt when
// changing type.
key: ValueKey<DialogDemoType>(widget.type),
restorationScopeId: 'navigator',
onGenerateRoute: (RouteSettings settings) {
return _NoAnimationMaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: Center(
child: ElevatedButton(
onPressed: () {
switch (widget.type) {
case DialogDemoType.alert:
_alertDialogRoute.present();
case DialogDemoType.alertTitle:
_alertDialogWithTitleRoute.present();
case DialogDemoType.simple:
_simpleDialogRoute.present();
case DialogDemoType.fullscreen:
Navigator.restorablePush<void>(
context, _fullscreenDialogRoute);
}
},
child: Text(GalleryLocalizations.of(context)!.dialogShow),
),
),
),
);
},
);
}
}
/// A MaterialPageRoute without any transition animations.
class _NoAnimationMaterialPageRoute<T> extends MaterialPageRoute<T> {
_NoAnimationMaterialPageRoute({
required super.builder,
super.settings,
super.maintainState,
super.fullscreenDialog,
});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class _DialogButton extends StatelessWidget {
const _DialogButton({required this.text});
final String text;
@override
Widget build(BuildContext context) {
return TextButton(
onPressed: () {
Navigator.of(context).pop(text);
},
child: Text(text),
);
}
}
class _DialogDemoItem extends StatelessWidget {
const _DialogDemoItem({
this.icon,
this.color,
required this.text,
});
final IconData? icon;
final Color? color;
final String text;
@override
Widget build(BuildContext context) {
return SimpleDialogOption(
onPressed: () {
Navigator.of(context).pop(text);
},
child: Row(
children: <Widget>[
Icon(icon, size: 36, color: color),
Flexible(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Text(text),
),
),
],
),
);
}
}
class _FullScreenDialogDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
// Remove the MediaQuery padding because the demo is rendered inside of a
// different page that already accounts for this padding.
return MediaQuery.removePadding(
context: context,
removeTop: true,
removeBottom: true,
child: ApplyTextOptions(
child: Scaffold(
appBar: AppBar(
title: Text(localizations.dialogFullscreenTitle),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(
localizations.dialogFullscreenSave,
style: theme.textTheme.bodyMedium!.copyWith(
color: theme.colorScheme.onPrimary,
),
),
),
],
),
body: Center(
child: Text(
localizations.dialogFullscreenDescription,
),
),
),
),
);
}
}
// END

View File

@ -0,0 +1,123 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class DividerDemo extends StatelessWidget {
const DividerDemo({super.key, required this.type});
final DividerDemoType type;
String _title(BuildContext context) {
switch (type) {
case DividerDemoType.horizontal:
return GalleryLocalizations.of(context)!.demoDividerTitle;
case DividerDemoType.vertical:
return GalleryLocalizations.of(context)!.demoVerticalDividerTitle;
}
}
@override
Widget build(BuildContext context) {
late Widget dividers;
switch (type) {
case DividerDemoType.horizontal:
dividers = _HorizontalDividerDemo();
case DividerDemoType.vertical:
dividers = _VerticalDividerDemo();
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
_title(context),
),
),
body: dividers,
);
}
}
// BEGIN dividerDemo
class _HorizontalDividerDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
child: Column(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.deepPurpleAccent,
),
),
),
const Divider(
color: Colors.grey,
height: 20,
thickness: 1,
indent: 20,
endIndent: 0,
),
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.deepOrangeAccent,
),
),
),
],
),
);
}
}
// END
// BEGIN verticalDividerDemo
class _VerticalDividerDemo extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
child: Row(
children: <Widget>[
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.deepPurpleAccent,
),
),
),
const VerticalDivider(
color: Colors.grey,
thickness: 1,
indent: 20,
endIndent: 0,
width: 20,
),
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.deepOrangeAccent,
),
),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,196 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN gridListsDemo
class GridListDemo extends StatelessWidget {
const GridListDemo({super.key, required this.type});
final GridListDemoType type;
List<_Photo> _photos(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <_Photo>[
_Photo(
assetName: 'places/india_chennai_flower_market.png',
title: localizations.placeChennai,
subtitle: localizations.placeFlowerMarket,
),
_Photo(
assetName: 'places/india_tanjore_bronze_works.png',
title: localizations.placeTanjore,
subtitle: localizations.placeBronzeWorks,
),
_Photo(
assetName: 'places/india_tanjore_market_merchant.png',
title: localizations.placeTanjore,
subtitle: localizations.placeMarket,
),
_Photo(
assetName: 'places/india_tanjore_thanjavur_temple.png',
title: localizations.placeTanjore,
subtitle: localizations.placeThanjavurTemple,
),
_Photo(
assetName: 'places/india_tanjore_thanjavur_temple_carvings.png',
title: localizations.placeTanjore,
subtitle: localizations.placeThanjavurTemple,
),
_Photo(
assetName: 'places/india_pondicherry_salt_farm.png',
title: localizations.placePondicherry,
subtitle: localizations.placeSaltFarm,
),
_Photo(
assetName: 'places/india_chennai_highway.png',
title: localizations.placeChennai,
subtitle: localizations.placeScooters,
),
_Photo(
assetName: 'places/india_chettinad_silk_maker.png',
title: localizations.placeChettinad,
subtitle: localizations.placeSilkMaker,
),
_Photo(
assetName: 'places/india_chettinad_produce.png',
title: localizations.placeChettinad,
subtitle: localizations.placeLunchPrep,
),
_Photo(
assetName: 'places/india_tanjore_market_technology.png',
title: localizations.placeTanjore,
subtitle: localizations.placeMarket,
),
_Photo(
assetName: 'places/india_pondicherry_beach.png',
title: localizations.placePondicherry,
subtitle: localizations.placeBeach,
),
_Photo(
assetName: 'places/india_pondicherry_fisherman.png',
title: localizations.placePondicherry,
subtitle: localizations.placeFisherman,
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoGridListsTitle),
),
body: GridView.count(
restorationId: 'grid_view_demo_grid_offset',
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
padding: const EdgeInsets.all(8),
children: _photos(context).map<Widget>((_Photo photo) {
return _GridDemoPhotoItem(
photo: photo,
tileStyle: type,
);
}).toList(),
),
);
}
}
class _Photo {
_Photo({
required this.assetName,
required this.title,
required this.subtitle,
});
final String assetName;
final String title;
final String subtitle;
}
/// Allow the text size to shrink to fit in the space
class _GridTitleText extends StatelessWidget {
const _GridTitleText(this.text);
final String text;
@override
Widget build(BuildContext context) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: AlignmentDirectional.centerStart,
child: Text(text),
);
}
}
class _GridDemoPhotoItem extends StatelessWidget {
const _GridDemoPhotoItem({
required this.photo,
required this.tileStyle,
});
final _Photo photo;
final GridListDemoType tileStyle;
@override
Widget build(BuildContext context) {
final Widget image = Semantics(
label: '${photo.title} ${photo.subtitle}',
child: Material(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
clipBehavior: Clip.antiAlias,
child: Image.asset(
photo.assetName,
package: 'flutter_gallery_assets',
fit: BoxFit.cover,
),
),
);
switch (tileStyle) {
case GridListDemoType.imageOnly:
return image;
case GridListDemoType.header:
return GridTile(
header: Material(
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(4)),
),
clipBehavior: Clip.antiAlias,
child: GridTileBar(
title: _GridTitleText(photo.title),
backgroundColor: Colors.black45,
),
),
child: image,
);
case GridListDemoType.footer:
return GridTile(
footer: Material(
color: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(4)),
),
clipBehavior: Clip.antiAlias,
child: GridTileBar(
backgroundColor: Colors.black45,
title: _GridTitleText(photo.title),
subtitle: _GridTitleText(photo.subtitle),
),
),
child: image,
);
}
}
}
// END

View File

@ -0,0 +1,49 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN listDemo
class ListDemo extends StatelessWidget {
const ListDemo({super.key, required this.type});
final ListDemoType type;
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoListsTitle),
),
body: Scrollbar(
child: ListView(
restorationId: 'list_demo_list_view',
padding: const EdgeInsets.symmetric(vertical: 8),
children: <Widget>[
for (int index = 1; index < 21; index++)
ListTile(
leading: ExcludeSemantics(
child: CircleAvatar(child: Text('$index')),
),
title: Text(
localizations.demoBottomSheetItem(index),
),
subtitle: type == ListDemoType.twoLine
? Text(localizations.demoListsSecondary)
: null,
),
],
),
),
);
}
}
// END

View File

@ -0,0 +1,86 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
enum BottomNavigationDemoType {
withLabels,
withoutLabels,
}
enum BottomSheetDemoType {
persistent,
modal,
}
enum ButtonDemoType {
text,
elevated,
outlined,
toggle,
floating,
}
enum ChipDemoType {
action,
choice,
filter,
input,
}
enum DialogDemoType {
alert,
alertTitle,
simple,
fullscreen,
}
enum GridListDemoType {
imageOnly,
header,
footer,
}
enum ListDemoType {
oneLine,
twoLine,
}
enum MenuDemoType {
contextMenu,
sectionedMenu,
simpleMenu,
checklistMenu,
}
enum PickerDemoType {
date,
time,
range,
}
enum ProgressIndicatorDemoType {
circular,
linear,
}
enum SelectionControlsDemoType {
checkbox,
radio,
switches,
}
enum SlidersDemoType {
sliders,
rangeSliders,
customSliders,
}
enum TabsDemoType {
scrollable,
nonScrollable,
}
enum DividerDemoType {
horizontal,
vertical,
}

View File

@ -0,0 +1,28 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export 'package:gallery/demos/material/app_bar_demo.dart';
export 'package:gallery/demos/material/banner_demo.dart';
export 'package:gallery/demos/material/bottom_app_bar_demo.dart';
export 'package:gallery/demos/material/bottom_navigation_demo.dart';
export 'package:gallery/demos/material/bottom_sheet_demo.dart';
export 'package:gallery/demos/material/button_demo.dart';
export 'package:gallery/demos/material/cards_demo.dart';
export 'package:gallery/demos/material/chip_demo.dart';
export 'package:gallery/demos/material/data_table_demo.dart';
export 'package:gallery/demos/material/dialog_demo.dart';
export 'package:gallery/demos/material/divider_demo.dart';
export 'package:gallery/demos/material/grid_list_demo.dart';
export 'package:gallery/demos/material/list_demo.dart';
export 'package:gallery/demos/material/menu_demo.dart';
export 'package:gallery/demos/material/navigation_drawer.dart';
export 'package:gallery/demos/material/navigation_rail_demo.dart';
export 'package:gallery/demos/material/picker_demo.dart';
export 'package:gallery/demos/material/progress_indicator_demo.dart';
export 'package:gallery/demos/material/selection_controls_demo.dart';
export 'package:gallery/demos/material/sliders_demo.dart';
export 'package:gallery/demos/material/snackbar_demo.dart';
export 'package:gallery/demos/material/tabs_demo.dart';
export 'package:gallery/demos/material/text_field_demo.dart';
export 'package:gallery/demos/material/tooltip_demo.dart';

View File

@ -0,0 +1,416 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
enum SimpleValue {
one,
two,
three,
}
enum CheckedValue {
one,
two,
three,
four,
}
class MenuDemo extends StatefulWidget {
const MenuDemo({super.key, required this.type});
final MenuDemoType type;
@override
State<MenuDemo> createState() => _MenuDemoState();
}
class _MenuDemoState extends State<MenuDemo> {
void showInSnackBar(String value) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(value),
));
}
@override
Widget build(BuildContext context) {
Widget demo;
switch (widget.type) {
case MenuDemoType.contextMenu:
demo = _ContextMenuDemo(showInSnackBar: showInSnackBar);
case MenuDemoType.sectionedMenu:
demo = _SectionedMenuDemo(showInSnackBar: showInSnackBar);
case MenuDemoType.simpleMenu:
demo = _SimpleMenuDemo(showInSnackBar: showInSnackBar);
case MenuDemoType.checklistMenu:
demo = _ChecklistMenuDemo(showInSnackBar: showInSnackBar);
}
return Scaffold(
appBar: AppBar(
title: Text(GalleryLocalizations.of(context)!.demoMenuTitle),
automaticallyImplyLeading: false,
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Center(
child: demo,
),
),
);
}
}
// BEGIN menuDemoContext
// Pressing the PopupMenuButton on the right of this item shows
// a simple menu with one disabled item. Typically the contents
// of this "contextual menu" would reflect the app's state.
class _ContextMenuDemo extends StatelessWidget {
const _ContextMenuDemo({required this.showInSnackBar});
final void Function(String value) showInSnackBar;
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return ListTile(
title: Text(localizations.demoMenuAnItemWithAContextMenuButton),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: (String value) => showInSnackBar(
localizations.demoMenuSelected(value),
),
itemBuilder: (BuildContext context) => <PopupMenuItem<String>>[
PopupMenuItem<String>(
value: localizations.demoMenuContextMenuItemOne,
child: Text(
localizations.demoMenuContextMenuItemOne,
),
),
PopupMenuItem<String>(
enabled: false,
child: Text(
localizations.demoMenuADisabledMenuItem,
),
),
PopupMenuItem<String>(
value: localizations.demoMenuContextMenuItemThree,
child: Text(
localizations.demoMenuContextMenuItemThree,
),
),
],
),
);
}
}
// END
// BEGIN menuDemoSectioned
// Pressing the PopupMenuButton on the right of this item shows
// a menu whose items have text labels and icons and a divider
// That separates the first three items from the last one.
class _SectionedMenuDemo extends StatelessWidget {
const _SectionedMenuDemo({required this.showInSnackBar});
final void Function(String value) showInSnackBar;
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return ListTile(
title: Text(localizations.demoMenuAnItemWithASectionedMenu),
trailing: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: (String value) =>
showInSnackBar(localizations.demoMenuSelected(value)),
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: localizations.demoMenuPreview,
child: ListTile(
leading: const Icon(Icons.visibility),
title: Text(
localizations.demoMenuPreview,
),
),
),
PopupMenuItem<String>(
value: localizations.demoMenuShare,
child: ListTile(
leading: const Icon(Icons.person_add),
title: Text(
localizations.demoMenuShare,
),
),
),
PopupMenuItem<String>(
value: localizations.demoMenuGetLink,
child: ListTile(
leading: const Icon(Icons.link),
title: Text(
localizations.demoMenuGetLink,
),
),
),
const PopupMenuDivider(),
PopupMenuItem<String>(
value: localizations.demoMenuRemove,
child: ListTile(
leading: const Icon(Icons.delete),
title: Text(
localizations.demoMenuRemove,
),
),
),
],
),
);
}
}
// END
// BEGIN menuDemoSimple
// This entire list item is a PopupMenuButton. Tapping anywhere shows
// a menu whose current value is highlighted and aligned over the
// list item's center line.
class _SimpleMenuDemo extends StatefulWidget {
const _SimpleMenuDemo({required this.showInSnackBar});
final void Function(String value) showInSnackBar;
@override
_SimpleMenuDemoState createState() => _SimpleMenuDemoState();
}
class _SimpleMenuDemoState extends State<_SimpleMenuDemo> {
late SimpleValue _simpleValue;
void showAndSetMenuSelection(BuildContext context, SimpleValue value) {
setState(() {
_simpleValue = value;
});
widget.showInSnackBar(
GalleryLocalizations.of(context)!
.demoMenuSelected(simpleValueToString(context, value)),
);
}
String simpleValueToString(BuildContext context, SimpleValue value) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <SimpleValue, String>{
SimpleValue.one: localizations.demoMenuItemValueOne,
SimpleValue.two: localizations.demoMenuItemValueTwo,
SimpleValue.three: localizations.demoMenuItemValueThree,
}[value]!;
}
@override
void initState() {
super.initState();
_simpleValue = SimpleValue.two;
}
@override
Widget build(BuildContext context) {
return PopupMenuButton<SimpleValue>(
padding: EdgeInsets.zero,
initialValue: _simpleValue,
onSelected: (SimpleValue value) => showAndSetMenuSelection(context, value),
itemBuilder: (BuildContext context) => <PopupMenuItem<SimpleValue>>[
PopupMenuItem<SimpleValue>(
value: SimpleValue.one,
child: Text(simpleValueToString(
context,
SimpleValue.one,
)),
),
PopupMenuItem<SimpleValue>(
value: SimpleValue.two,
child: Text(simpleValueToString(
context,
SimpleValue.two,
)),
),
PopupMenuItem<SimpleValue>(
value: SimpleValue.three,
child: Text(simpleValueToString(
context,
SimpleValue.three,
)),
),
],
child: ListTile(
title: Text(
GalleryLocalizations.of(context)!.demoMenuAnItemWithASimpleMenu),
subtitle: Text(simpleValueToString(context, _simpleValue)),
),
);
}
}
// END
// BEGIN menuDemoChecklist
// Pressing the PopupMenuButton on the right of this item shows a menu
// whose items have checked icons that reflect this app's state.
class _ChecklistMenuDemo extends StatefulWidget {
const _ChecklistMenuDemo({required this.showInSnackBar});
final void Function(String value) showInSnackBar;
@override
_ChecklistMenuDemoState createState() => _ChecklistMenuDemoState();
}
class _RestorableCheckedValues extends RestorableProperty<Set<CheckedValue>> {
Set<CheckedValue> _checked = <CheckedValue>{};
void check(CheckedValue value) {
_checked.add(value);
notifyListeners();
}
void uncheck(CheckedValue value) {
_checked.remove(value);
notifyListeners();
}
bool isChecked(CheckedValue value) => _checked.contains(value);
Iterable<String> checkedValuesToString(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return _checked.map((CheckedValue value) {
return <CheckedValue, String>{
CheckedValue.one: localizations.demoMenuOne,
CheckedValue.two: localizations.demoMenuTwo,
CheckedValue.three: localizations.demoMenuThree,
CheckedValue.four: localizations.demoMenuFour,
}[value]!;
});
}
@override
Set<CheckedValue> createDefaultValue() => _checked;
@override
Set<CheckedValue> initWithValue(Set<CheckedValue> a) {
_checked = a;
return _checked;
}
@override
Object toPrimitives() => _checked.map((CheckedValue value) => value.index).toList();
@override
Set<CheckedValue> fromPrimitives(Object? data) {
final List<dynamic> checkedValues = data! as List<dynamic>;
return Set<CheckedValue>.from(checkedValues.map<CheckedValue>((dynamic id) {
return CheckedValue.values[id as int];
}));
}
}
class _ChecklistMenuDemoState extends State<_ChecklistMenuDemo>
with RestorationMixin {
final _RestorableCheckedValues _checkedValues = _RestorableCheckedValues()
..check(CheckedValue.three);
@override
String get restorationId => 'checklist_menu_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_checkedValues, 'checked_values');
}
void showCheckedMenuSelections(BuildContext context, CheckedValue value) {
if (_checkedValues.isChecked(value)) {
setState(() {
_checkedValues.uncheck(value);
});
} else {
setState(() {
_checkedValues.check(value);
});
}
widget.showInSnackBar(
GalleryLocalizations.of(context)!.demoMenuChecked(
_checkedValues.checkedValuesToString(context),
),
);
}
String checkedValueToString(BuildContext context, CheckedValue value) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <CheckedValue, String>{
CheckedValue.one: localizations.demoMenuOne,
CheckedValue.two: localizations.demoMenuTwo,
CheckedValue.three: localizations.demoMenuThree,
CheckedValue.four: localizations.demoMenuFour,
}[value]!;
}
@override
void dispose() {
_checkedValues.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListTile(
title: Text(
GalleryLocalizations.of(context)!.demoMenuAnItemWithAChecklistMenu,
),
trailing: PopupMenuButton<CheckedValue>(
padding: EdgeInsets.zero,
onSelected: (CheckedValue value) => showCheckedMenuSelections(context, value),
itemBuilder: (BuildContext context) => <PopupMenuItem<CheckedValue>>[
CheckedPopupMenuItem<CheckedValue>(
value: CheckedValue.one,
checked: _checkedValues.isChecked(CheckedValue.one),
child: Text(
checkedValueToString(context, CheckedValue.one),
),
),
CheckedPopupMenuItem<CheckedValue>(
value: CheckedValue.two,
enabled: false,
checked: _checkedValues.isChecked(CheckedValue.two),
child: Text(
checkedValueToString(context, CheckedValue.two),
),
),
CheckedPopupMenuItem<CheckedValue>(
value: CheckedValue.three,
checked: _checkedValues.isChecked(CheckedValue.three),
child: Text(
checkedValueToString(context, CheckedValue.three),
),
),
CheckedPopupMenuItem<CheckedValue>(
value: CheckedValue.four,
checked: _checkedValues.isChecked(CheckedValue.four),
child: Text(
checkedValueToString(context, CheckedValue.four),
),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,76 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN navDrawerDemo
// Press the Navigation Drawer button to the left of AppBar to show
// a simple Drawer with two items.
class NavDrawerDemo extends StatelessWidget {
const NavDrawerDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localization = GalleryLocalizations.of(context)!;
final UserAccountsDrawerHeader drawerHeader = UserAccountsDrawerHeader(
accountName: Text(
localization.demoNavigationDrawerUserName,
),
accountEmail: Text(
localization.demoNavigationDrawerUserEmail,
),
currentAccountPicture: const CircleAvatar(
child: FlutterLogo(size: 42.0),
),
);
final ListView drawerItems = ListView(
children: <Widget>[
drawerHeader,
ListTile(
title: Text(
localization.demoNavigationDrawerToPageOne,
),
leading: const Icon(Icons.favorite),
onTap: () {
Navigator.pop(context);
},
),
ListTile(
title: Text(
localization.demoNavigationDrawerToPageTwo,
),
leading: const Icon(Icons.comment),
onTap: () {
Navigator.pop(context);
},
),
],
);
return Scaffold(
appBar: AppBar(
title: Text(
localization.demoNavigationDrawerTitle,
),
),
body: Semantics(
container: true,
child: Center(
child: Padding(
padding: const EdgeInsets.all(50.0),
child: Text(
localization.demoNavigationDrawerText,
),
),
),
),
drawer: Drawer(
child: drawerItems,
),
);
}
}
// END

View File

@ -0,0 +1,116 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN navRailDemo
class NavRailDemo extends StatefulWidget {
const NavRailDemo({super.key});
@override
State<NavRailDemo> createState() => _NavRailDemoState();
}
class _NavRailDemoState extends State<NavRailDemo> with RestorationMixin {
final RestorableInt _selectedIndex = RestorableInt(0);
@override
String get restorationId => 'nav_rail_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_selectedIndex, 'selected_index');
}
@override
void dispose() {
_selectedIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localization = GalleryLocalizations.of(context)!;
final String destinationFirst = localization.demoNavigationRailFirst;
final String destinationSecond = localization.demoNavigationRailSecond;
final String destinationThird = localization.demoNavigationRailThird;
final List<String> selectedItem = <String>[
destinationFirst,
destinationSecond,
destinationThird
];
return Scaffold(
appBar: AppBar(
title: Text(
localization.demoNavigationRailTitle,
),
),
body: Row(
children: <Widget>[
NavigationRail(
leading: FloatingActionButton(
onPressed: () {},
tooltip: localization.buttonTextCreate,
child: const Icon(Icons.add),
),
selectedIndex: _selectedIndex.value,
onDestinationSelected: (int index) {
setState(() {
_selectedIndex.value = index;
});
},
labelType: NavigationRailLabelType.selected,
destinations: <NavigationRailDestination>[
NavigationRailDestination(
icon: const Icon(
Icons.favorite_border,
),
selectedIcon: const Icon(
Icons.favorite,
),
label: Text(
destinationFirst,
),
),
NavigationRailDestination(
icon: const Icon(
Icons.bookmark_border,
),
selectedIcon: const Icon(
Icons.book,
),
label: Text(
destinationSecond,
),
),
NavigationRailDestination(
icon: const Icon(
Icons.star_border,
),
selectedIcon: const Icon(
Icons.star,
),
label: Text(
destinationThird,
),
),
],
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: Center(
child: Text(
selectedItem[_selectedIndex.value],
),
),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,229 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN pickerDemo
class PickerDemo extends StatefulWidget {
const PickerDemo({super.key, required this.type});
final PickerDemoType type;
@override
State<PickerDemo> createState() => _PickerDemoState();
}
class _PickerDemoState extends State<PickerDemo> with RestorationMixin {
final RestorableDateTime _fromDate = RestorableDateTime(DateTime.now());
final RestorableTimeOfDay _fromTime = RestorableTimeOfDay(
TimeOfDay.fromDateTime(DateTime.now()),
);
final RestorableDateTime _startDate = RestorableDateTime(DateTime.now());
final RestorableDateTime _endDate = RestorableDateTime(DateTime.now());
late RestorableRouteFuture<DateTime?> _restorableDatePickerRouteFuture;
late RestorableRouteFuture<DateTimeRange?>
_restorableDateRangePickerRouteFuture;
late RestorableRouteFuture<TimeOfDay?> _restorableTimePickerRouteFuture;
void _selectDate(DateTime? selectedDate) {
if (selectedDate != null && selectedDate != _fromDate.value) {
setState(() {
_fromDate.value = selectedDate;
});
}
}
void _selectDateRange(DateTimeRange? newSelectedDate) {
if (newSelectedDate != null) {
setState(() {
_startDate.value = newSelectedDate.start;
_endDate.value = newSelectedDate.end;
});
}
}
void _selectTime(TimeOfDay? selectedTime) {
if (selectedTime != null && selectedTime != _fromTime.value) {
setState(() {
_fromTime.value = selectedTime;
});
}
}
static Route<DateTime> _datePickerRoute(
BuildContext context,
Object? arguments,
) {
return DialogRoute<DateTime>(
context: context,
builder: (BuildContext context) {
return DatePickerDialog(
restorationId: 'date_picker_dialog',
initialDate: DateTime.fromMillisecondsSinceEpoch(arguments! as int),
firstDate: DateTime(2015),
lastDate: DateTime(2100),
);
},
);
}
static Route<TimeOfDay> _timePickerRoute(
BuildContext context,
Object? arguments,
) {
final List<Object> args = arguments! as List<Object>;
final TimeOfDay initialTime = TimeOfDay(
hour: args[0] as int,
minute: args[1] as int,
);
return DialogRoute<TimeOfDay>(
context: context,
builder: (BuildContext context) {
return TimePickerDialog(
restorationId: 'time_picker_dialog',
initialTime: initialTime,
);
},
);
}
static Route<DateTimeRange> _dateRangePickerRoute(
BuildContext context,
Object? arguments,
) {
return DialogRoute<DateTimeRange>(
context: context,
builder: (BuildContext context) {
return DateRangePickerDialog(
restorationId: 'date_rage_picker_dialog',
firstDate: DateTime(DateTime.now().year - 5),
lastDate: DateTime(DateTime.now().year + 5),
);
},
);
}
@override
void initState() {
super.initState();
_restorableDatePickerRouteFuture = RestorableRouteFuture<DateTime?>(
onComplete: _selectDate,
onPresent: (NavigatorState navigator, Object? arguments) {
return navigator.restorablePush(
_datePickerRoute,
arguments: _fromDate.value.millisecondsSinceEpoch,
);
},
);
_restorableDateRangePickerRouteFuture =
RestorableRouteFuture<DateTimeRange?>(
onComplete: _selectDateRange,
onPresent: (NavigatorState navigator, Object? arguments) =>
navigator.restorablePush(_dateRangePickerRoute),
);
_restorableTimePickerRouteFuture = RestorableRouteFuture<TimeOfDay?>(
onComplete: _selectTime,
onPresent: (NavigatorState navigator, Object? arguments) => navigator.restorablePush(
_timePickerRoute,
arguments: <int>[_fromTime.value.hour, _fromTime.value.minute],
),
);
}
@override
String get restorationId => 'picker_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_fromDate, 'from_date');
registerForRestoration(_fromTime, 'from_time');
registerForRestoration(_startDate, 'start_date');
registerForRestoration(_endDate, 'end_date');
registerForRestoration(
_restorableDatePickerRouteFuture,
'date_picker_route',
);
registerForRestoration(
_restorableDateRangePickerRouteFuture,
'date_range_picker_route',
);
registerForRestoration(
_restorableTimePickerRouteFuture,
'time_picker_route',
);
}
String get _title {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (widget.type) {
case PickerDemoType.date:
return localizations.demoDatePickerTitle;
case PickerDemoType.time:
return localizations.demoTimePickerTitle;
case PickerDemoType.range:
return localizations.demoDateRangePickerTitle;
}
}
String get _labelText {
switch (widget.type) {
case PickerDemoType.date:
return DateFormat.yMMMd().format(_fromDate.value);
case PickerDemoType.time:
return _fromTime.value.format(context);
case PickerDemoType.range:
return '${DateFormat.yMMMd().format(_startDate.value)} - ${DateFormat.yMMMd().format(_endDate.value)}';
}
}
@override
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title),
),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(_labelText),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
switch (widget.type) {
case PickerDemoType.date:
_restorableDatePickerRouteFuture.present();
case PickerDemoType.time:
_restorableTimePickerRouteFuture.present();
case PickerDemoType.range:
_restorableDateRangePickerRouteFuture.present();
}
},
child: Text(
GalleryLocalizations.of(context)!.demoPickersShowPicker,
),
)
],
),
),
),
);
},
);
}
}
// END

View File

@ -0,0 +1,109 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
// BEGIN progressIndicatorsDemo
class ProgressIndicatorDemo extends StatefulWidget {
const ProgressIndicatorDemo({super.key, required this.type});
final ProgressIndicatorDemoType type;
@override
State<ProgressIndicatorDemo> createState() => _ProgressIndicatorDemoState();
}
class _ProgressIndicatorDemoState extends State<ProgressIndicatorDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
animationBehavior: AnimationBehavior.preserve,
)..forward();
_animation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.9, curve: Curves.fastOutSlowIn),
reverseCurve: Curves.fastOutSlowIn,
)..addStatusListener((AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
_controller.forward();
} else if (status == AnimationStatus.completed) {
_controller.reverse();
}
});
}
@override
void dispose() {
_controller.stop();
super.dispose();
}
String get _title {
switch (widget.type) {
case ProgressIndicatorDemoType.circular:
return GalleryLocalizations.of(context)!
.demoCircularProgressIndicatorTitle;
case ProgressIndicatorDemoType.linear:
return GalleryLocalizations.of(context)!
.demoLinearProgressIndicatorTitle;
}
}
Widget _buildIndicators(BuildContext context, Widget? child) {
switch (widget.type) {
case ProgressIndicatorDemoType.circular:
return Column(
children: <Widget>[
CircularProgressIndicator(
semanticsLabel: GalleryLocalizations.of(context)!.loading,
),
const SizedBox(height: 32),
CircularProgressIndicator(value: _animation.value),
],
);
case ProgressIndicatorDemoType.linear:
return Column(
children: <Widget>[
const LinearProgressIndicator(),
const SizedBox(height: 32),
LinearProgressIndicator(value: _animation.value),
],
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title),
),
body: Center(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.all(8),
child: AnimatedBuilder(
animation: _animation,
builder: _buildIndicators,
),
),
),
),
);
}
}
// END

View File

@ -0,0 +1,278 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class SelectionControlsDemo extends StatelessWidget {
const SelectionControlsDemo({super.key, required this.type});
final SelectionControlsDemoType type;
String _title(BuildContext context) {
switch (type) {
case SelectionControlsDemoType.checkbox:
return GalleryLocalizations.of(context)!
.demoSelectionControlsCheckboxTitle;
case SelectionControlsDemoType.radio:
return GalleryLocalizations.of(context)!
.demoSelectionControlsRadioTitle;
case SelectionControlsDemoType.switches:
return GalleryLocalizations.of(context)!
.demoSelectionControlsSwitchTitle;
}
}
@override
Widget build(BuildContext context) {
Widget? controls;
switch (type) {
case SelectionControlsDemoType.checkbox:
controls = _CheckboxDemo();
case SelectionControlsDemoType.radio:
controls = _RadioDemo();
case SelectionControlsDemoType.switches:
controls = _SwitchDemo();
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: controls,
);
}
}
// BEGIN selectionControlsDemoCheckbox
class _CheckboxDemo extends StatefulWidget {
@override
_CheckboxDemoState createState() => _CheckboxDemoState();
}
class _CheckboxDemoState extends State<_CheckboxDemo> with RestorationMixin {
RestorableBoolN checkboxValueA = RestorableBoolN(true);
RestorableBoolN checkboxValueB = RestorableBoolN(false);
RestorableBoolN checkboxValueC = RestorableBoolN(null);
@override
String get restorationId => 'checkbox_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(checkboxValueA, 'checkbox_a');
registerForRestoration(checkboxValueB, 'checkbox_b');
registerForRestoration(checkboxValueC, 'checkbox_c');
}
@override
void dispose() {
checkboxValueA.dispose();
checkboxValueB.dispose();
checkboxValueC.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
value: checkboxValueA.value,
onChanged: (bool? value) {
setState(() {
checkboxValueA.value = value;
});
},
),
Checkbox(
value: checkboxValueB.value,
onChanged: (bool? value) {
setState(() {
checkboxValueB.value = value;
});
},
),
Checkbox(
value: checkboxValueC.value,
tristate: true,
onChanged: (bool? value) {
setState(() {
checkboxValueC.value = value;
});
},
),
],
),
// Disabled checkboxes
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Checkbox(
value: checkboxValueA.value,
onChanged: null,
),
Checkbox(
value: checkboxValueB.value,
onChanged: null,
),
Checkbox(
value: checkboxValueC.value,
tristate: true,
onChanged: null,
),
],
),
],
);
}
}
// END
// BEGIN selectionControlsDemoRadio
class _RadioDemo extends StatefulWidget {
@override
_RadioDemoState createState() => _RadioDemoState();
}
class _RadioDemoState extends State<_RadioDemo> with RestorationMixin {
final RestorableInt radioValue = RestorableInt(0);
@override
String get restorationId => 'radio_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(radioValue, 'radio_value');
}
void handleRadioValueChanged(int? value) {
setState(() {
radioValue.value = value!;
});
}
@override
void dispose() {
radioValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int index = 0; index < 2; ++index)
Radio<int>(
value: index,
groupValue: radioValue.value,
onChanged: handleRadioValueChanged,
),
],
),
// Disabled radio buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
for (int index = 0; index < 2; ++index)
Radio<int>(
value: index,
groupValue: radioValue.value,
onChanged: null,
),
],
),
],
);
}
}
// END
// BEGIN selectionControlsDemoSwitches
class _SwitchDemo extends StatefulWidget {
@override
_SwitchDemoState createState() => _SwitchDemoState();
}
class _SwitchDemoState extends State<_SwitchDemo> with RestorationMixin {
RestorableBool switchValueA = RestorableBool(true);
RestorableBool switchValueB = RestorableBool(false);
@override
String get restorationId => 'switch_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(switchValueA, 'switch_value1');
registerForRestoration(switchValueB, 'switch_value2');
}
@override
void dispose() {
switchValueA.dispose();
switchValueB.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Switch(
value: switchValueA.value,
onChanged: (bool value) {
setState(() {
switchValueA.value = value;
});
},
),
Switch(
value: switchValueB.value,
onChanged: (bool value) {
setState(() {
switchValueB.value = value;
});
},
),
],
),
// Disabled switches
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Switch(
value: switchValueA.value,
onChanged: null,
),
Switch(
value: switchValueB.value,
onChanged: null,
),
],
),
],
);
}
}
// END

View File

@ -0,0 +1,605 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class SlidersDemo extends StatelessWidget {
const SlidersDemo({super.key, required this.type});
final SlidersDemoType type;
String _title(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
switch (type) {
case SlidersDemoType.sliders:
return localizations.demoSlidersTitle;
case SlidersDemoType.rangeSliders:
return localizations.demoRangeSlidersTitle;
case SlidersDemoType.customSliders:
return localizations.demoCustomSlidersTitle;
}
}
@override
Widget build(BuildContext context) {
Widget sliders;
switch (type) {
case SlidersDemoType.sliders:
sliders = _Sliders();
case SlidersDemoType.rangeSliders:
sliders = _RangeSliders();
case SlidersDemoType.customSliders:
sliders = _CustomSliders();
}
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(_title(context)),
),
body: sliders,
);
}
}
// BEGIN slidersDemo
class _Sliders extends StatefulWidget {
@override
_SlidersState createState() => _SlidersState();
}
class _SlidersState extends State<_Sliders> with RestorationMixin {
final RestorableDouble _continuousValue = RestorableDouble(25);
final RestorableDouble _discreteValue = RestorableDouble(20);
@override
String get restorationId => 'slider_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_continuousValue, 'continuous_value');
registerForRestoration(_discreteValue, 'discrete_value');
}
@override
void dispose() {
_continuousValue.dispose();
_discreteValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Semantics(
label: localizations.demoSlidersEditableNumericalValue,
child: SizedBox(
width: 64,
height: 48,
child: TextField(
textAlign: TextAlign.center,
onSubmitted: (String value) {
final double? newValue = double.tryParse(value);
if (newValue != null &&
newValue != _continuousValue.value) {
setState(() {
_continuousValue.value =
newValue.clamp(0, 100) as double;
});
}
},
keyboardType: TextInputType.number,
controller: TextEditingController(
text: _continuousValue.value.toStringAsFixed(0),
),
),
),
),
Slider(
value: _continuousValue.value,
max: 100,
onChanged: (double value) {
setState(() {
_continuousValue.value = value;
});
},
),
// Disabled slider
Slider(
value: _continuousValue.value,
max: 100,
onChanged: null,
),
Text(localizations
.demoSlidersContinuousWithEditableNumericalValue),
],
),
const SizedBox(height: 80),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Slider(
value: _discreteValue.value,
max: 200,
divisions: 5,
label: _discreteValue.value.round().toString(),
onChanged: (double value) {
setState(() {
_discreteValue.value = value;
});
},
),
// Disabled slider
Slider(
value: _discreteValue.value,
max: 200,
divisions: 5,
label: _discreteValue.value.round().toString(),
onChanged: null,
),
Text(localizations.demoSlidersDiscrete),
],
),
],
),
);
}
}
// END
// BEGIN rangeSlidersDemo
class _RangeSliders extends StatefulWidget {
@override
_RangeSlidersState createState() => _RangeSlidersState();
}
class _RangeSlidersState extends State<_RangeSliders> with RestorationMixin {
final RestorableDouble _continuousStartValue = RestorableDouble(25);
final RestorableDouble _continuousEndValue = RestorableDouble(75);
final RestorableDouble _discreteStartValue = RestorableDouble(40);
final RestorableDouble _discreteEndValue = RestorableDouble(120);
@override
String get restorationId => 'range_sliders_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_continuousStartValue, 'continuous_start_value');
registerForRestoration(_continuousEndValue, 'continuous_end_value');
registerForRestoration(_discreteStartValue, 'discrete_start_value');
registerForRestoration(_discreteEndValue, 'discrete_end_value');
}
@override
void dispose() {
_continuousStartValue.dispose();
_continuousEndValue.dispose();
_discreteStartValue.dispose();
_discreteEndValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final RangeValues continuousValues = RangeValues(
_continuousStartValue.value,
_continuousEndValue.value,
);
final RangeValues discreteValues = RangeValues(
_discreteStartValue.value,
_discreteEndValue.value,
);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RangeSlider(
values: continuousValues,
max: 100,
onChanged: (RangeValues values) {
setState(() {
_continuousStartValue.value = values.start;
_continuousEndValue.value = values.end;
});
},
),
// Disabled range slider
RangeSlider(
values: continuousValues,
max: 100,
onChanged: null,
),
Text(GalleryLocalizations.of(context)!.demoSlidersContinuous),
],
),
const SizedBox(height: 80),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RangeSlider(
values: discreteValues,
max: 200,
divisions: 5,
labels: RangeLabels(
discreteValues.start.round().toString(),
discreteValues.end.round().toString(),
),
onChanged: (RangeValues values) {
setState(() {
_discreteStartValue.value = values.start;
_discreteEndValue.value = values.end;
});
},
),
// Disabled range slider
RangeSlider(
values: discreteValues,
max: 200,
divisions: 5,
labels: RangeLabels(
discreteValues.start.round().toString(),
discreteValues.end.round().toString(),
),
onChanged: null,
),
Text(GalleryLocalizations.of(context)!.demoSlidersDiscrete),
],
),
],
),
);
}
}
// END
// BEGIN customSlidersDemo
Path _downTriangle(double size, Offset thumbCenter, {bool invert = false}) {
final Path thumbPath = Path();
final double height = math.sqrt(3) / 2;
final double centerHeight = size * height / 3;
final double halfSize = size / 2;
final int sign = invert ? -1 : 1;
thumbPath.moveTo(
thumbCenter.dx - halfSize, thumbCenter.dy + sign * centerHeight);
thumbPath.lineTo(thumbCenter.dx, thumbCenter.dy - 2 * sign * centerHeight);
thumbPath.lineTo(
thumbCenter.dx + halfSize, thumbCenter.dy + sign * centerHeight);
thumbPath.close();
return thumbPath;
}
Path _rightTriangle(double size, Offset thumbCenter, {bool invert = false}) {
final Path thumbPath = Path();
final double halfSize = size / 2;
final int sign = invert ? -1 : 1;
thumbPath.moveTo(thumbCenter.dx + halfSize * sign, thumbCenter.dy);
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy - size);
thumbPath.lineTo(thumbCenter.dx - halfSize * sign, thumbCenter.dy + size);
thumbPath.close();
return thumbPath;
}
Path _upTriangle(double size, Offset thumbCenter) =>
_downTriangle(size, thumbCenter, invert: true);
Path _leftTriangle(double size, Offset thumbCenter) =>
_rightTriangle(size, thumbCenter, invert: true);
class _CustomRangeThumbShape extends RangeSliderThumbShape {
const _CustomRangeThumbShape();
static const double _thumbSize = 4;
static const double _disabledThumbSize = 3;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return isEnabled
? const Size.fromRadius(_thumbSize)
: const Size.fromRadius(_disabledThumbSize);
}
static final Animatable<double> sizeTween = Tween<double>(
begin: _disabledThumbSize,
end: _thumbSize,
);
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool isDiscrete = false,
bool isEnabled = false,
bool? isOnTop,
TextDirection? textDirection,
required SliderThemeData sliderTheme,
Thumb? thumb,
bool? isPressed,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
Path thumbPath;
switch (textDirection!) {
case TextDirection.rtl:
switch (thumb!) {
case Thumb.start:
thumbPath = _rightTriangle(size, center);
case Thumb.end:
thumbPath = _leftTriangle(size, center);
}
case TextDirection.ltr:
switch (thumb!) {
case Thumb.start:
thumbPath = _leftTriangle(size, center);
case Thumb.end:
thumbPath = _rightTriangle(size, center);
}
}
canvas.drawPath(
thumbPath,
Paint()..color = colorTween.evaluate(enableAnimation)!,
);
}
}
class _CustomThumbShape extends SliderComponentShape {
const _CustomThumbShape();
static const double _thumbSize = 4;
static const double _disabledThumbSize = 3;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return isEnabled
? const Size.fromRadius(_thumbSize)
: const Size.fromRadius(_disabledThumbSize);
}
static final Animatable<double> sizeTween = Tween<double>(
begin: _disabledThumbSize,
end: _thumbSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
Animation<double>? activationAnimation,
required Animation<double> enableAnimation,
bool? isDiscrete,
TextPainter? labelPainter,
RenderBox? parentBox,
required SliderThemeData sliderTheme,
TextDirection? textDirection,
double? value,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final ColorTween colorTween = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.thumbColor,
);
final double size = _thumbSize * sizeTween.evaluate(enableAnimation);
final Path thumbPath = _downTriangle(size, thumbCenter);
canvas.drawPath(
thumbPath,
Paint()..color = colorTween.evaluate(enableAnimation)!,
);
}
}
class _CustomValueIndicatorShape extends SliderComponentShape {
const _CustomValueIndicatorShape();
static const double _indicatorSize = 4;
static const double _disabledIndicatorSize = 3;
static const double _slideUpHeight = 40;
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) {
return Size.fromRadius(isEnabled ? _indicatorSize : _disabledIndicatorSize);
}
static final Animatable<double> sizeTween = Tween<double>(
begin: _disabledIndicatorSize,
end: _indicatorSize,
);
@override
void paint(
PaintingContext context,
Offset thumbCenter, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
bool? isDiscrete,
required TextPainter labelPainter,
RenderBox? parentBox,
required SliderThemeData sliderTheme,
TextDirection? textDirection,
double? value,
double? textScaleFactor,
Size? sizeWithOverflow,
}) {
final Canvas canvas = context.canvas;
final ColorTween enableColor = ColorTween(
begin: sliderTheme.disabledThumbColor,
end: sliderTheme.valueIndicatorColor,
);
final Tween<double> slideUpTween = Tween<double>(
begin: 0,
end: _slideUpHeight,
);
final double size = _indicatorSize * sizeTween.evaluate(enableAnimation);
final Offset slideUpOffset =
Offset(0, -slideUpTween.evaluate(activationAnimation));
final Path thumbPath = _upTriangle(size, thumbCenter + slideUpOffset);
final Color paintColor = enableColor
.evaluate(enableAnimation)!
.withAlpha((255 * activationAnimation.value).round());
canvas.drawPath(
thumbPath,
Paint()..color = paintColor,
);
canvas.drawLine(
thumbCenter,
thumbCenter + slideUpOffset,
Paint()
..color = paintColor
..style = PaintingStyle.stroke
..strokeWidth = 2);
labelPainter.paint(
canvas,
thumbCenter +
slideUpOffset +
Offset(-labelPainter.width / 2, -labelPainter.height - 4),
);
}
}
class _CustomSliders extends StatefulWidget {
@override
_CustomSlidersState createState() => _CustomSlidersState();
}
class _CustomSlidersState extends State<_CustomSliders> with RestorationMixin {
final RestorableDouble _continuousStartCustomValue = RestorableDouble(40);
final RestorableDouble _continuousEndCustomValue = RestorableDouble(160);
final RestorableDouble _discreteCustomValue = RestorableDouble(25);
@override
String get restorationId => 'custom_sliders_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(
_continuousStartCustomValue, 'continuous_start_custom_value');
registerForRestoration(
_continuousEndCustomValue, 'continuous_end_custom_value');
registerForRestoration(_discreteCustomValue, 'discrete_custom_value');
}
@override
void dispose() {
_continuousStartCustomValue.dispose();
_continuousEndCustomValue.dispose();
_discreteCustomValue.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final RangeValues customRangeValue = RangeValues(
_continuousStartCustomValue.value,
_continuousEndCustomValue.value,
);
final ThemeData theme = Theme.of(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SliderTheme(
data: theme.sliderTheme.copyWith(
trackHeight: 2,
activeTrackColor: Colors.deepPurple,
inactiveTrackColor:
theme.colorScheme.onSurface.withOpacity(0.5),
activeTickMarkColor:
theme.colorScheme.onSurface.withOpacity(0.7),
inactiveTickMarkColor:
theme.colorScheme.surface.withOpacity(0.7),
overlayColor: theme.colorScheme.onSurface.withOpacity(0.12),
thumbColor: Colors.deepPurple,
valueIndicatorColor: Colors.deepPurpleAccent,
thumbShape: const _CustomThumbShape(),
valueIndicatorShape: const _CustomValueIndicatorShape(),
valueIndicatorTextStyle: theme.textTheme.bodyLarge!
.copyWith(color: theme.colorScheme.onSurface),
),
child: Slider(
value: _discreteCustomValue.value,
max: 200,
divisions: 5,
semanticFormatterCallback: (double value) =>
value.round().toString(),
label: '${_discreteCustomValue.value.round()}',
onChanged: (double value) {
setState(() {
_discreteCustomValue.value = value;
});
},
),
),
Text(localizations.demoSlidersDiscreteSliderWithCustomTheme),
],
),
const SizedBox(height: 80),
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SliderTheme(
data: const SliderThemeData(
trackHeight: 2,
activeTrackColor: Colors.deepPurple,
inactiveTrackColor: Colors.black26,
activeTickMarkColor: Colors.white70,
inactiveTickMarkColor: Colors.black,
overlayColor: Colors.black12,
thumbColor: Colors.deepPurple,
rangeThumbShape: _CustomRangeThumbShape(),
showValueIndicator: ShowValueIndicator.never,
),
child: RangeSlider(
values: customRangeValue,
max: 200,
onChanged: (RangeValues values) {
setState(() {
_continuousStartCustomValue.value = values.start;
_continuousEndCustomValue.value = values.end;
});
},
),
),
Text(localizations
.demoSlidersContinuousRangeSliderWithCustomTheme),
],
),
],
),
);
}
}
// END

View File

@ -0,0 +1,46 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN snackbarsDemo
class SnackbarsDemo extends StatelessWidget {
const SnackbarsDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoSnackbarsTitle),
),
body: Center(
child: ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(localizations.demoSnackbarsText),
action: SnackBarAction(
label: localizations.demoSnackbarsActionButtonLabel,
onPressed: () {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(
localizations.demoSnackbarsAction,
)));
},
),
));
},
child: Text(localizations.demoSnackbarsButtonLabel),
),
),
);
}
}
// END

View File

@ -0,0 +1,197 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'material_demo_types.dart';
class TabsDemo extends StatelessWidget {
const TabsDemo({super.key, required this.type});
final TabsDemoType type;
@override
Widget build(BuildContext context) {
Widget tabs;
switch (type) {
case TabsDemoType.scrollable:
tabs = _TabsScrollableDemo();
case TabsDemoType.nonScrollable:
tabs = _TabsNonScrollableDemo();
}
return tabs;
}
}
// BEGIN tabsScrollableDemo
class _TabsScrollableDemo extends StatefulWidget {
@override
__TabsScrollableDemoState createState() => __TabsScrollableDemoState();
}
class __TabsScrollableDemoState extends State<_TabsScrollableDemo>
with SingleTickerProviderStateMixin, RestorationMixin {
TabController? _tabController;
final RestorableInt tabIndex = RestorableInt(0);
@override
String get restorationId => 'tab_scrollable_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(tabIndex, 'tab_index');
_tabController!.index = tabIndex.value;
}
@override
void initState() {
_tabController = TabController(
length: 12,
vsync: this,
);
_tabController!.addListener(() {
// When the tab controller's value is updated, make sure to update the
// tab index value, which is state restorable.
setState(() {
tabIndex.value = _tabController!.index;
});
});
super.initState();
}
@override
void dispose() {
_tabController!.dispose();
tabIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<String> tabs = <String>[
localizations.colorsRed,
localizations.colorsOrange,
localizations.colorsGreen,
localizations.colorsBlue,
localizations.colorsIndigo,
localizations.colorsPurple,
localizations.colorsRed,
localizations.colorsOrange,
localizations.colorsGreen,
localizations.colorsBlue,
localizations.colorsIndigo,
localizations.colorsPurple,
];
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoTabsScrollingTitle),
bottom: TabBar(
controller: _tabController,
isScrollable: true,
tabs: <Widget>[
for (final String tab in tabs) Tab(text: tab),
],
),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
for (final String tab in tabs)
Center(
child: Text(tab),
),
],
),
);
}
}
// END
// BEGIN tabsNonScrollableDemo
class _TabsNonScrollableDemo extends StatefulWidget {
@override
__TabsNonScrollableDemoState createState() => __TabsNonScrollableDemoState();
}
class __TabsNonScrollableDemoState extends State<_TabsNonScrollableDemo>
with SingleTickerProviderStateMixin, RestorationMixin {
late TabController _tabController;
final RestorableInt tabIndex = RestorableInt(0);
@override
String get restorationId => 'tab_non_scrollable_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(tabIndex, 'tab_index');
_tabController.index = tabIndex.value;
}
@override
void initState() {
super.initState();
_tabController = TabController(
length: 3,
vsync: this,
);
_tabController.addListener(() {
// When the tab controller's value is updated, make sure to update the
// tab index value, which is state restorable.
setState(() {
tabIndex.value = _tabController.index;
});
});
}
@override
void dispose() {
_tabController.dispose();
tabIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<String> tabs = <String>[
localizations.colorsRed,
localizations.colorsOrange,
localizations.colorsGreen,
];
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(
localizations.demoTabsNonScrollingTitle,
),
bottom: TabBar(
controller: _tabController,
tabs: <Widget>[
for (final String tab in tabs) Tab(text: tab),
],
),
),
body: TabBarView(
controller: _tabController,
children: <Widget>[
for (final String tab in tabs)
Center(
child: Text(tab),
),
],
),
);
}
}
// END

View File

@ -0,0 +1,422 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../gallery_localizations.dart';
// BEGIN textFieldDemo
class TextFieldDemo extends StatelessWidget {
const TextFieldDemo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoTextFieldTitle),
),
body: const TextFormFieldDemo(),
);
}
}
class TextFormFieldDemo extends StatefulWidget {
const TextFormFieldDemo({super.key});
@override
TextFormFieldDemoState createState() => TextFormFieldDemoState();
}
class PersonData {
String? name = '';
String? phoneNumber = '';
String? email = '';
String password = '';
}
class PasswordField extends StatefulWidget {
const PasswordField({
super.key,
this.restorationId,
this.fieldKey,
this.hintText,
this.labelText,
this.helperText,
this.onSaved,
this.validator,
this.onFieldSubmitted,
this.focusNode,
this.textInputAction,
});
final String? restorationId;
final Key? fieldKey;
final String? hintText;
final String? labelText;
final String? helperText;
final FormFieldSetter<String>? onSaved;
final FormFieldValidator<String>? validator;
final ValueChanged<String>? onFieldSubmitted;
final FocusNode? focusNode;
final TextInputAction? textInputAction;
@override
State<PasswordField> createState() => _PasswordFieldState();
}
class _PasswordFieldState extends State<PasswordField> with RestorationMixin {
final RestorableBool _obscureText = RestorableBool(true);
@override
String? get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_obscureText, 'obscure_text');
}
@override
Widget build(BuildContext context) {
return TextFormField(
key: widget.fieldKey,
restorationId: 'password_text_field',
obscureText: _obscureText.value,
maxLength: 8,
onSaved: widget.onSaved,
validator: widget.validator,
onFieldSubmitted: widget.onFieldSubmitted,
decoration: InputDecoration(
filled: true,
hintText: widget.hintText,
labelText: widget.labelText,
helperText: widget.helperText,
suffixIcon: IconButton(
onPressed: () {
setState(() {
_obscureText.value = !_obscureText.value;
});
},
hoverColor: Colors.transparent,
icon: Icon(
_obscureText.value ? Icons.visibility : Icons.visibility_off,
semanticLabel: _obscureText.value
? GalleryLocalizations.of(context)!
.demoTextFieldShowPasswordLabel
: GalleryLocalizations.of(context)!
.demoTextFieldHidePasswordLabel,
),
),
),
);
}
}
class TextFormFieldDemoState extends State<TextFormFieldDemo>
with RestorationMixin {
PersonData person = PersonData();
late FocusNode _phoneNumber, _email, _lifeStory, _password, _retypePassword;
@override
void initState() {
super.initState();
_phoneNumber = FocusNode();
_email = FocusNode();
_lifeStory = FocusNode();
_password = FocusNode();
_retypePassword = FocusNode();
}
@override
void dispose() {
_phoneNumber.dispose();
_email.dispose();
_lifeStory.dispose();
_password.dispose();
_retypePassword.dispose();
super.dispose();
}
void showInSnackBar(String value) {
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(value),
));
}
@override
String get restorationId => 'text_field_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_autoValidateModeIndex, 'autovalidate_mode');
}
final RestorableInt _autoValidateModeIndex =
RestorableInt(AutovalidateMode.disabled.index);
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final GlobalKey<FormFieldState<String>> _passwordFieldKey =
GlobalKey<FormFieldState<String>>();
final _UsNumberTextInputFormatter _phoneNumberFormatter =
_UsNumberTextInputFormatter();
void _handleSubmitted() {
final FormState form = _formKey.currentState!;
if (!form.validate()) {
_autoValidateModeIndex.value =
AutovalidateMode.always.index; // Start validating on every change.
showInSnackBar(
GalleryLocalizations.of(context)!.demoTextFieldFormErrors,
);
} else {
form.save();
showInSnackBar(GalleryLocalizations.of(context)!
.demoTextFieldNameHasPhoneNumber(person.name!, person.phoneNumber!));
}
}
String? _validateName(String? value) {
if (value == null || value.isEmpty) {
return GalleryLocalizations.of(context)!.demoTextFieldNameRequired;
}
final RegExp nameExp = RegExp(r'^[A-Za-z ]+$');
if (!nameExp.hasMatch(value)) {
return GalleryLocalizations.of(context)!
.demoTextFieldOnlyAlphabeticalChars;
}
return null;
}
String? _validatePhoneNumber(String? value) {
final RegExp phoneExp = RegExp(r'^\(\d\d\d\) \d\d\d\-\d\d\d\d$');
if (!phoneExp.hasMatch(value!)) {
return GalleryLocalizations.of(context)!.demoTextFieldEnterUSPhoneNumber;
}
return null;
}
String? _validatePassword(String? value) {
final FormFieldState<String> passwordField = _passwordFieldKey.currentState!;
if (passwordField.value == null || passwordField.value!.isEmpty) {
return GalleryLocalizations.of(context)!.demoTextFieldEnterPassword;
}
if (passwordField.value != value) {
return GalleryLocalizations.of(context)!.demoTextFieldPasswordsDoNotMatch;
}
return null;
}
@override
Widget build(BuildContext context) {
const SizedBox sizedBoxSpace = SizedBox(height: 24);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.values[_autoValidateModeIndex.value],
child: Scrollbar(
child: SingleChildScrollView(
restorationId: 'text_field_demo_scroll_view',
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: <Widget>[
sizedBoxSpace,
TextFormField(
restorationId: 'name_field',
textInputAction: TextInputAction.next,
textCapitalization: TextCapitalization.words,
decoration: InputDecoration(
filled: true,
icon: const Icon(Icons.person),
hintText: localizations.demoTextFieldWhatDoPeopleCallYou,
labelText: localizations.demoTextFieldNameField,
),
onSaved: (String? value) {
person.name = value;
_phoneNumber.requestFocus();
},
validator: _validateName,
),
sizedBoxSpace,
TextFormField(
restorationId: 'phone_number_field',
textInputAction: TextInputAction.next,
focusNode: _phoneNumber,
decoration: InputDecoration(
filled: true,
icon: const Icon(Icons.phone),
hintText: localizations.demoTextFieldWhereCanWeReachYou,
labelText: localizations.demoTextFieldPhoneNumber,
prefixText: '+1 ',
),
keyboardType: TextInputType.phone,
onSaved: (String? value) {
person.phoneNumber = value;
_email.requestFocus();
},
maxLength: 14,
maxLengthEnforcement: MaxLengthEnforcement.none,
validator: _validatePhoneNumber,
// TextInputFormatters are applied in sequence.
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly,
// Fit the validating format.
_phoneNumberFormatter,
],
),
sizedBoxSpace,
TextFormField(
restorationId: 'email_field',
textInputAction: TextInputAction.next,
focusNode: _email,
decoration: InputDecoration(
filled: true,
icon: const Icon(Icons.email),
hintText: localizations.demoTextFieldYourEmailAddress,
labelText: localizations.demoTextFieldEmail,
),
keyboardType: TextInputType.emailAddress,
onSaved: (String? value) {
person.email = value;
_lifeStory.requestFocus();
},
),
sizedBoxSpace,
// Disabled text field
TextFormField(
enabled: false,
restorationId: 'disabled_email_field',
textInputAction: TextInputAction.next,
decoration: InputDecoration(
filled: true,
icon: const Icon(Icons.email),
hintText: localizations.demoTextFieldYourEmailAddress,
labelText: localizations.demoTextFieldEmail,
),
keyboardType: TextInputType.emailAddress,
),
sizedBoxSpace,
TextFormField(
restorationId: 'life_story_field',
focusNode: _lifeStory,
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: localizations.demoTextFieldTellUsAboutYourself,
helperText: localizations.demoTextFieldKeepItShort,
labelText: localizations.demoTextFieldLifeStory,
),
maxLines: 3,
),
sizedBoxSpace,
TextFormField(
restorationId: 'salary_field',
textInputAction: TextInputAction.next,
keyboardType: TextInputType.number,
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: localizations.demoTextFieldSalary,
suffixText: localizations.demoTextFieldUSD,
),
),
sizedBoxSpace,
PasswordField(
restorationId: 'password_field',
textInputAction: TextInputAction.next,
focusNode: _password,
fieldKey: _passwordFieldKey,
helperText: localizations.demoTextFieldNoMoreThan,
labelText: localizations.demoTextFieldPassword,
onFieldSubmitted: (String value) {
setState(() {
person.password = value;
_retypePassword.requestFocus();
});
},
),
sizedBoxSpace,
TextFormField(
restorationId: 'retype_password_field',
focusNode: _retypePassword,
decoration: InputDecoration(
filled: true,
labelText: localizations.demoTextFieldRetypePassword,
),
maxLength: 8,
obscureText: true,
validator: _validatePassword,
onFieldSubmitted: (String value) {
_handleSubmitted();
},
),
sizedBoxSpace,
Center(
child: ElevatedButton(
onPressed: _handleSubmitted,
child: Text(localizations.demoTextFieldSubmit),
),
),
sizedBoxSpace,
Text(
localizations.demoTextFieldRequiredField,
style: Theme.of(context).textTheme.bodySmall,
),
sizedBoxSpace,
],
),
),
),
);
}
}
/// Format incoming numeric text to fit the format of (###) ###-#### ##
class _UsNumberTextInputFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
final int newTextLength = newValue.text.length;
final StringBuffer newText = StringBuffer();
int selectionIndex = newValue.selection.end;
int usedSubstringIndex = 0;
if (newTextLength >= 1) {
newText.write('(');
if (newValue.selection.end >= 1) {
selectionIndex++;
}
}
if (newTextLength >= 4) {
newText.write('${newValue.text.substring(0, usedSubstringIndex = 3)}) ');
if (newValue.selection.end >= 3) {
selectionIndex += 2;
}
}
if (newTextLength >= 7) {
newText.write('${newValue.text.substring(3, usedSubstringIndex = 6)}-');
if (newValue.selection.end >= 6) {
selectionIndex++;
}
}
if (newTextLength >= 11) {
newText.write('${newValue.text.substring(6, usedSubstringIndex = 10)} ');
if (newValue.selection.end >= 10) {
selectionIndex++;
}
}
// Dump the rest.
if (newTextLength >= usedSubstringIndex) {
newText.write(newValue.text.substring(usedSubstringIndex));
}
return TextEditingValue(
text: newText.toString(),
selection: TextSelection.collapsed(offset: selectionIndex),
);
}
}
// END

View File

@ -0,0 +1,48 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN tooltipDemo
class TooltipDemo extends StatelessWidget {
const TooltipDemo({super.key});
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(localizations.demoTooltipTitle),
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(8),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
localizations.demoTooltipInstructions,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
Tooltip(
message: localizations.starterAppTooltipSearch,
child: IconButton(
color: Theme.of(context).colorScheme.primary,
onPressed: () {},
icon: const Icon(Icons.search),
),
),
],
),
),
),
);
}
}
// END

View File

@ -0,0 +1,260 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN colorsDemo
const double kColorItemHeight = 48;
class _Palette {
_Palette({
required this.name,
required this.primary,
this.accent,
this.threshold = 900,
});
final String name;
final MaterialColor primary;
final MaterialAccentColor? accent;
// Titles for indices > threshold are white, otherwise black.
final int threshold;
}
List<_Palette> _allPalettes(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <_Palette>[
_Palette(
name: localizations.colorsRed,
primary: Colors.red,
accent: Colors.redAccent,
threshold: 300,
),
_Palette(
name: localizations.colorsPink,
primary: Colors.pink,
accent: Colors.pinkAccent,
threshold: 200,
),
_Palette(
name: localizations.colorsPurple,
primary: Colors.purple,
accent: Colors.purpleAccent,
threshold: 200,
),
_Palette(
name: localizations.colorsDeepPurple,
primary: Colors.deepPurple,
accent: Colors.deepPurpleAccent,
threshold: 200,
),
_Palette(
name: localizations.colorsIndigo,
primary: Colors.indigo,
accent: Colors.indigoAccent,
threshold: 200,
),
_Palette(
name: localizations.colorsBlue,
primary: Colors.blue,
accent: Colors.blueAccent,
threshold: 400,
),
_Palette(
name: localizations.colorsLightBlue,
primary: Colors.lightBlue,
accent: Colors.lightBlueAccent,
threshold: 500,
),
_Palette(
name: localizations.colorsCyan,
primary: Colors.cyan,
accent: Colors.cyanAccent,
threshold: 600,
),
_Palette(
name: localizations.colorsTeal,
primary: Colors.teal,
accent: Colors.tealAccent,
threshold: 400,
),
_Palette(
name: localizations.colorsGreen,
primary: Colors.green,
accent: Colors.greenAccent,
threshold: 500,
),
_Palette(
name: localizations.colorsLightGreen,
primary: Colors.lightGreen,
accent: Colors.lightGreenAccent,
threshold: 600,
),
_Palette(
name: localizations.colorsLime,
primary: Colors.lime,
accent: Colors.limeAccent,
threshold: 800,
),
_Palette(
name: localizations.colorsYellow,
primary: Colors.yellow,
accent: Colors.yellowAccent,
),
_Palette(
name: localizations.colorsAmber,
primary: Colors.amber,
accent: Colors.amberAccent,
),
_Palette(
name: localizations.colorsOrange,
primary: Colors.orange,
accent: Colors.orangeAccent,
threshold: 700,
),
_Palette(
name: localizations.colorsDeepOrange,
primary: Colors.deepOrange,
accent: Colors.deepOrangeAccent,
threshold: 400,
),
_Palette(
name: localizations.colorsBrown,
primary: Colors.brown,
threshold: 200,
),
_Palette(
name: localizations.colorsGrey,
primary: Colors.grey,
threshold: 500,
),
_Palette(
name: localizations.colorsBlueGrey,
primary: Colors.blueGrey,
threshold: 500,
),
];
}
class _ColorItem extends StatelessWidget {
const _ColorItem({
required this.index,
required this.color,
this.prefix = '',
});
final int index;
final Color color;
final String prefix;
String get _colorString =>
"#${color.value.toRadixString(16).padLeft(8, '0').toUpperCase()}";
@override
Widget build(BuildContext context) {
return Semantics(
container: true,
child: Container(
height: kColorItemHeight,
padding: const EdgeInsets.symmetric(horizontal: 16),
color: color,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text('$prefix$index'),
Flexible(child: Text(_colorString)),
],
),
),
);
}
}
class _PaletteTabView extends StatelessWidget {
const _PaletteTabView({required this.colors});
final _Palette colors;
static const List<int> primaryKeys = <int>[
50,
100,
200,
300,
400,
500,
600,
700,
800,
900
];
static const List<int> accentKeys = <int>[100, 200, 400, 700];
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle whiteTextStyle = textTheme.bodyMedium!.copyWith(
color: Colors.white,
);
final TextStyle blackTextStyle = textTheme.bodyMedium!.copyWith(
color: Colors.black,
);
return Scrollbar(
child: ListView(
itemExtent: kColorItemHeight,
children: <Widget>[
for (final int key in primaryKeys)
DefaultTextStyle(
style: key > colors.threshold ? whiteTextStyle : blackTextStyle,
child: _ColorItem(index: key, color: colors.primary[key]!),
),
if (colors.accent != null)
for (final int key in accentKeys)
DefaultTextStyle(
style: key > colors.threshold ? whiteTextStyle : blackTextStyle,
child: _ColorItem(
index: key,
color: colors.accent![key]!,
prefix: 'A',
),
),
],
),
);
}
}
class ColorsDemo extends StatelessWidget {
const ColorsDemo({super.key});
@override
Widget build(BuildContext context) {
final List<_Palette> palettes = _allPalettes(context);
return DefaultTabController(
length: palettes.length,
child: Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoColorsTitle),
bottom: TabBar(
isScrollable: true,
tabs: <Widget>[
for (final _Palette palette in palettes) Tab(text: palette.name),
],
labelColor: Theme.of(context).colorScheme.onPrimary,
),
),
body: TabBarView(
children: <Widget>[
for (final _Palette palette in palettes) _PaletteTabView(colors: palette),
],
),
),
);
}
}
// END

View File

@ -0,0 +1,590 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN openContainerTransformDemo
const String _loremIpsumParagraph =
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
'vitae.\n'
'\n'
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod '
'tempor incididunt ut labore et dolore magna aliqua. Vulputate dignissim '
'suspendisse in est. Ut ornare lectus sit amet. Eget nunc lobortis mattis '
'aliquam faucibus purus in. Hendrerit gravida rutrum quisque non tellus '
'orci ac auctor. Mattis aliquam faucibus purus in massa. Tellus rutrum '
'tellus pellentesque eu tincidunt tortor. Nunc eget lorem dolor sed. Nulla '
'at volutpat diam ut venenatis tellus in metus. Tellus cras adipiscing enim '
'eu turpis. Pretium fusce id velit ut tortor. Adipiscing enim eu turpis '
'egestas pretium. Quis varius quam quisque id. Blandit aliquam etiam erat '
'velit scelerisque. In nisl nisi scelerisque eu. Semper risus in hendrerit '
'gravida rutrum quisque. Suspendisse in est ante in nibh mauris cursus '
'mattis molestie. Adipiscing elit duis tristique sollicitudin nibh sit '
'amet commodo nulla. Pretium viverra suspendisse potenti nullam ac tortor '
'vitae';
const double _fabDimension = 56;
class OpenContainerTransformDemo extends StatefulWidget {
const OpenContainerTransformDemo({super.key});
@override
State<OpenContainerTransformDemo> createState() =>
_OpenContainerTransformDemoState();
}
class _OpenContainerTransformDemoState
extends State<OpenContainerTransformDemo> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
ContainerTransitionType _transitionType = ContainerTransitionType.fade;
void _showSettingsBottomModalSheet(BuildContext context) {
final GalleryLocalizations? localizations = GalleryLocalizations.of(context);
showModalBottomSheet<void>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, void Function(void Function()) setModalState) {
return Container(
height: 125,
padding: const EdgeInsets.all(15),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
localizations!.demoContainerTransformModalBottomSheetTitle,
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(
height: 12,
),
ToggleButtons(
borderRadius: BorderRadius.circular(2),
selectedBorderColor: Theme.of(context).colorScheme.primary,
onPressed: (int index) {
setModalState(() {
setState(() {
_transitionType = index == 0
? ContainerTransitionType.fade
: ContainerTransitionType.fadeThrough;
});
});
},
isSelected: <bool>[
_transitionType == ContainerTransitionType.fade,
_transitionType == ContainerTransitionType.fadeThrough,
],
children: <Widget>[
Text(
localizations.demoContainerTransformTypeFade,
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10,
),
child: Text(
localizations.demoContainerTransformTypeFadeThrough,
),
),
],
),
],
),
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations? localizations = GalleryLocalizations.of(context);
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Navigator(
// Adding [ValueKey] to make sure that the widget gets rebuilt when
// changing type.
key: ValueKey<ContainerTransitionType>(_transitionType),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
builder: (BuildContext context) => Scaffold(
key: _scaffoldKey,
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(
localizations!.demoContainerTransformTitle,
),
Text(
'(${localizations.demoContainerTransformDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
actions: <Widget>[
IconButton(
icon: const Icon(
Icons.settings,
),
onPressed: () {
_showSettingsBottomModalSheet(context);
},
),
],
),
body: ListView(
padding: const EdgeInsets.all(8),
children: <Widget>[
_OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _DetailsCard(openContainer: openContainer);
},
),
const SizedBox(height: 16),
_OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _DetailsListTile(openContainer: openContainer);
},
),
const SizedBox(
height: 16,
),
Row(
children: <Widget>[
Expanded(
child: _OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _SmallDetailsCard(
openContainer: openContainer,
subtitle:
localizations.demoMotionPlaceholderSubtitle,
);
},
),
),
const SizedBox(
width: 8,
),
Expanded(
child: _OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _SmallDetailsCard(
openContainer: openContainer,
subtitle:
localizations.demoMotionPlaceholderSubtitle,
);
},
),
),
],
),
const SizedBox(
height: 16,
),
Row(
children: <Widget>[
Expanded(
child: _OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _SmallDetailsCard(
openContainer: openContainer,
subtitle: localizations
.demoMotionSmallPlaceholderSubtitle,
);
},
),
),
const SizedBox(
width: 8,
),
Expanded(
child: _OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _SmallDetailsCard(
openContainer: openContainer,
subtitle: localizations
.demoMotionSmallPlaceholderSubtitle,
);
},
),
),
const SizedBox(
width: 8,
),
Expanded(
child: _OpenContainerWrapper(
transitionType: _transitionType,
closedBuilder: (BuildContext context, void Function() openContainer) {
return _SmallDetailsCard(
openContainer: openContainer,
subtitle: localizations
.demoMotionSmallPlaceholderSubtitle,
);
},
),
),
],
),
const SizedBox(
height: 16,
),
...List<OpenContainer<bool>>.generate(10, (int index) {
return OpenContainer<bool>(
transitionType: _transitionType,
openBuilder: (BuildContext context, void Function() openContainer) =>
const _DetailsPage(),
tappable: false,
closedShape: const RoundedRectangleBorder(),
closedElevation: 0,
closedBuilder: (BuildContext context, void Function() openContainer) {
return ListTile(
leading: Image.asset(
'placeholders/avatar_logo.png',
package: 'flutter_gallery_assets',
width: 40,
),
onTap: openContainer,
title: Text(
'${localizations.demoMotionListTileTitle} ${index + 1}',
),
subtitle: Text(
localizations.demoMotionPlaceholderSubtitle,
),
);
},
);
}),
],
),
floatingActionButton: OpenContainer(
transitionType: _transitionType,
openBuilder: (BuildContext context, void Function() openContainer) => const _DetailsPage(),
closedElevation: 6,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(_fabDimension / 2),
),
),
closedColor: colorScheme.secondary,
closedBuilder: (BuildContext context, void Function() openContainer) {
return SizedBox(
height: _fabDimension,
width: _fabDimension,
child: Center(
child: Icon(
Icons.add,
color: colorScheme.onSecondary,
),
),
);
},
),
),
);
},
);
}
}
class _OpenContainerWrapper extends StatelessWidget {
const _OpenContainerWrapper({
required this.closedBuilder,
required this.transitionType,
});
final CloseContainerBuilder closedBuilder;
final ContainerTransitionType transitionType;
@override
Widget build(BuildContext context) {
return OpenContainer<bool>(
transitionType: transitionType,
openBuilder: (BuildContext context, void Function() openContainer) => const _DetailsPage(),
tappable: false,
closedBuilder: closedBuilder,
);
}
}
class _DetailsCard extends StatelessWidget {
const _DetailsCard({required this.openContainer});
final VoidCallback openContainer;
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return _InkWellOverlay(
openContainer: openContainer,
height: 300,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: ColoredBox(
color: Colors.black38,
child: Center(
child: Image.asset(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
width: 100,
),
),
),
),
ListTile(
title: Text(
localizations.demoMotionPlaceholderTitle,
),
subtitle: Text(
localizations.demoMotionPlaceholderSubtitle,
),
),
Padding(
padding: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 16,
),
child: Text(
'Lorem ipsum dolor sit amet, consectetur '
'adipiscing elit, sed do eiusmod tempor.',
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
color: Colors.black54,
inherit: false,
),
),
),
],
),
);
}
}
class _SmallDetailsCard extends StatelessWidget {
const _SmallDetailsCard({
required this.openContainer,
required this.subtitle,
});
final VoidCallback openContainer;
final String subtitle;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
return _InkWellOverlay(
openContainer: openContainer,
height: 225,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
color: Colors.black38,
height: 150,
child: Center(
child: Image.asset(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
width: 80,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
GalleryLocalizations.of(context)!
.demoMotionPlaceholderTitle,
style: textTheme.titleLarge,
),
const SizedBox(
height: 4,
),
Text(
subtitle,
style: textTheme.bodySmall,
),
],
),
),
),
],
),
);
}
}
class _DetailsListTile extends StatelessWidget {
const _DetailsListTile({required this.openContainer});
final VoidCallback openContainer;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
const double height = 120.0;
return _InkWellOverlay(
openContainer: openContainer,
height: height,
child: Row(
children: <Widget>[
Container(
color: Colors.black38,
height: height,
width: height,
child: Center(
child: Image.asset(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
width: 60,
),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
GalleryLocalizations.of(context)!
.demoMotionPlaceholderTitle,
style: textTheme.titleMedium,
),
const SizedBox(
height: 8,
),
Text(
'Lorem ipsum dolor sit amet, consectetur '
'adipiscing elit,',
style: textTheme.bodySmall,
),
],
),
),
),
],
),
);
}
}
class _InkWellOverlay extends StatelessWidget {
const _InkWellOverlay({
required this.openContainer,
required this.height,
required this.child,
});
final VoidCallback openContainer;
final double height;
final Widget child;
@override
Widget build(BuildContext context) {
return SizedBox(
height: height,
child: InkWell(
onTap: openContainer,
child: child,
),
);
}
}
class _DetailsPage extends StatelessWidget {
const _DetailsPage();
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final TextTheme textTheme = Theme.of(context).textTheme;
return Scaffold(
appBar: AppBar(
title: Text(
localizations.demoMotionDetailsPageTitle,
),
),
body: ListView(
children: <Widget>[
Container(
color: Colors.black38,
height: 250,
child: Padding(
padding: const EdgeInsets.all(70),
child: Image.asset(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
),
),
),
Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
localizations.demoMotionPlaceholderTitle,
style: textTheme.headlineSmall!.copyWith(
color: Colors.black54,
fontSize: 30,
),
),
const SizedBox(
height: 10,
),
Text(
_loremIpsumParagraph,
style: textTheme.bodyMedium!.copyWith(
color: Colors.black54,
height: 1.5,
fontSize: 16,
),
),
],
),
),
],
),
);
}
}
// END openContainerTransformDemo

View File

@ -0,0 +1,166 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN fadeScaleTransitionDemo
class FadeScaleTransitionDemo extends StatefulWidget {
const FadeScaleTransitionDemo({super.key});
@override
State<FadeScaleTransitionDemo> createState() =>
_FadeScaleTransitionDemoState();
}
class _FadeScaleTransitionDemoState extends State<FadeScaleTransitionDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
value: 0,
duration: const Duration(milliseconds: 150),
reverseDuration: const Duration(milliseconds: 75),
vsync: this,
)..addStatusListener((AnimationStatus status) {
setState(() {
// setState needs to be called to trigger a rebuild because
// the 'HIDE FAB'/'SHOW FAB' button needs to be updated based
// the latest value of [_controller.status].
});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool get _isAnimationRunningForwardsOrComplete {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
return true;
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
return false;
}
}
Widget _showExampleAlertDialog() {
return Theme(
data: Theme.of(context),
child: _ExampleAlertDialog(),
);
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(localizations.demoFadeScaleTitle),
Text(
'(${localizations.demoFadeScaleDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
),
floatingActionButton: AnimatedBuilder(
animation: _controller,
builder: (BuildContext context, Widget? child) {
return FadeScaleTransition(
animation: _controller,
child: child,
);
},
child: Visibility(
visible: _controller.status != AnimationStatus.dismissed,
child: FloatingActionButton(
onPressed: () {},
child: const Icon(Icons.add),
),
),
),
bottomNavigationBar: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Divider(height: 0),
Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: () {
showModal<void>(
context: context,
builder: (BuildContext context) => _showExampleAlertDialog());
},
child: Text(localizations.demoFadeScaleShowAlertDialogButton),
),
const SizedBox(width: 10),
ElevatedButton(
onPressed: () {
if (_isAnimationRunningForwardsOrComplete) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text(
_isAnimationRunningForwardsOrComplete
? localizations.demoFadeScaleHideFabButton
: localizations.demoFadeScaleShowFabButton,
),
),
],
),
),
],
),
);
}
}
class _ExampleAlertDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return AlertDialog(
content: Text(localizations.demoFadeScaleAlertDialogHeader),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(localizations.demoFadeScaleAlertDialogCancelButton),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(localizations.demoFadeScaleAlertDialogDiscardButton),
),
],
);
}
}
// END fadeScaleTransitionDemo

View File

@ -0,0 +1,200 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN fadeThroughTransitionDemo
class FadeThroughTransitionDemo extends StatefulWidget {
const FadeThroughTransitionDemo({super.key});
@override
State<FadeThroughTransitionDemo> createState() =>
_FadeThroughTransitionDemoState();
}
class _FadeThroughTransitionDemoState extends State<FadeThroughTransitionDemo> {
int _pageIndex = 0;
final List<Widget> _pageList = <Widget>[
_AlbumsPage(),
_PhotosPage(),
_SearchPage(),
];
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(localizations.demoFadeThroughTitle),
Text(
'(${localizations.demoFadeThroughDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
),
body: PageTransitionSwitcher(
transitionBuilder: (
Widget child,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return FadeThroughTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
child: _pageList[_pageIndex],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _pageIndex,
onTap: (int selectedIndex) {
setState(() {
_pageIndex = selectedIndex;
});
},
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.photo_library),
label: localizations.demoFadeThroughAlbumsDestination,
),
BottomNavigationBarItem(
icon: const Icon(Icons.photo),
label: localizations.demoFadeThroughPhotosDestination,
),
BottomNavigationBarItem(
icon: const Icon(Icons.search),
label: localizations.demoFadeThroughSearchDestination,
),
],
),
);
}
}
class _ExampleCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final TextTheme textTheme = Theme.of(context).textTheme;
return Expanded(
child: Card(
child: Stack(
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Expanded(
child: ColoredBox(
color: Colors.black26,
child: Padding(
padding: const EdgeInsets.all(30),
child: Ink.image(
image: const AssetImage(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
),
),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
localizations.demoFadeThroughTextPlaceholder,
style: textTheme.bodyLarge,
),
Text(
localizations.demoFadeThroughTextPlaceholder,
style: textTheme.bodySmall,
),
],
),
),
],
),
InkWell(
splashColor: Colors.black38,
onTap: () {},
),
],
),
),
);
}
}
class _AlbumsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
...List<Widget>.generate(
3,
(int index) => Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
_ExampleCard(),
_ExampleCard(),
],
),
),
),
],
);
}
}
class _PhotosPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
_ExampleCard(),
_ExampleCard(),
],
);
}
}
class _SearchPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final GalleryLocalizations? localizations = GalleryLocalizations.of(context);
return ListView.builder(
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: Image.asset(
'placeholders/avatar_logo.png',
package: 'flutter_gallery_assets',
width: 40,
),
title: Text('${localizations!.demoMotionListTileTitle} ${index + 1}'),
subtitle: Text(localizations.demoMotionPlaceholderSubtitle),
);
},
itemCount: 10,
);
}
}
// END fadeThroughTransitionDemo

View File

@ -0,0 +1,247 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN sharedXAxisTransitionDemo
class SharedXAxisTransitionDemo extends StatefulWidget {
const SharedXAxisTransitionDemo({super.key});
@override
State<SharedXAxisTransitionDemo> createState() =>
_SharedXAxisTransitionDemoState();
}
class _SharedXAxisTransitionDemoState extends State<SharedXAxisTransitionDemo> {
bool _isLoggedIn = false;
void _toggleLoginStatus() {
setState(() {
_isLoggedIn = !_isLoggedIn;
});
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(localizations.demoSharedXAxisTitle),
Text(
'(${localizations.demoSharedXAxisDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
),
body: SafeArea(
child: Column(
children: <Widget>[
Expanded(
child: PageTransitionSwitcher(
reverse: !_isLoggedIn,
transitionBuilder: (
Widget child,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
);
},
child: _isLoggedIn ? const _CoursePage() : const _SignInPage(),
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
TextButton(
onPressed: _isLoggedIn ? _toggleLoginStatus : null,
child: Text(localizations.demoSharedXAxisBackButtonText),
),
ElevatedButton(
onPressed: _isLoggedIn ? null : _toggleLoginStatus,
child: Text(localizations.demoSharedXAxisNextButtonText),
),
],
),
),
],
),
),
);
}
}
class _CoursePage extends StatelessWidget {
const _CoursePage();
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return ListView(
children: <Widget>[
const SizedBox(height: 16),
Text(
localizations.demoSharedXAxisCoursePageTitle,
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 10),
child: Text(
localizations.demoSharedXAxisCoursePageSubtitle,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
),
_CourseSwitch(
course: localizations.demoSharedXAxisArtsAndCraftsCourseTitle),
_CourseSwitch(course: localizations.demoSharedXAxisBusinessCourseTitle),
_CourseSwitch(
course: localizations.demoSharedXAxisIllustrationCourseTitle),
_CourseSwitch(course: localizations.demoSharedXAxisDesignCourseTitle),
_CourseSwitch(course: localizations.demoSharedXAxisCulinaryCourseTitle),
],
);
}
}
class _CourseSwitch extends StatefulWidget {
const _CourseSwitch({
this.course,
});
final String? course;
@override
_CourseSwitchState createState() => _CourseSwitchState();
}
class _CourseSwitchState extends State<_CourseSwitch> {
bool _isCourseBundled = true;
@override
Widget build(BuildContext context) {
final GalleryLocalizations? localizations = GalleryLocalizations.of(context);
final String subtitle = _isCourseBundled
? localizations!.demoSharedXAxisBundledCourseSubtitle
: localizations!.demoSharedXAxisIndividualCourseSubtitle;
return SwitchListTile(
title: Text(widget.course!),
subtitle: Text(subtitle),
value: _isCourseBundled,
onChanged: (bool newValue) {
setState(() {
_isCourseBundled = newValue;
});
},
);
}
}
class _SignInPage extends StatelessWidget {
const _SignInPage();
@override
Widget build(BuildContext context) {
final GalleryLocalizations? localizations = GalleryLocalizations.of(context);
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double maxHeight = constraints.maxHeight;
const SizedBox spacing = SizedBox(height: 10);
return Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
children: <Widget>[
SizedBox(height: maxHeight / 10),
Image.asset(
'placeholders/avatar_logo.png',
package: 'flutter_gallery_assets',
width: 80,
height: 80,
),
spacing,
Text(
localizations!.demoSharedXAxisSignInWelcomeText,
style: Theme.of(context).textTheme.headlineSmall,
),
spacing,
Text(
localizations.demoSharedXAxisSignInSubtitleText,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(
top: 40,
start: 10,
end: 10,
bottom: 10,
),
child: TextField(
decoration: InputDecoration(
suffixIcon: const Icon(
Icons.visibility,
size: 20,
color: Colors.black54,
),
labelText:
localizations.demoSharedXAxisSignInTextFieldLabel,
border: const OutlineInputBorder(),
),
),
),
TextButton(
onPressed: () {},
child: Text(
localizations.demoSharedXAxisForgotEmailButtonText,
),
),
spacing,
TextButton(
onPressed: () {},
child: Text(
localizations.demoSharedXAxisCreateAccountButtonText,
),
),
],
),
],
),
);
},
);
}
}
// END sharedXAxisTransitionDemo

View File

@ -0,0 +1,201 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN sharedYAxisTransitionDemo
class SharedYAxisTransitionDemo extends StatefulWidget {
const SharedYAxisTransitionDemo({super.key});
@override
State<SharedYAxisTransitionDemo> createState() =>
_SharedYAxisTransitionDemoState();
}
class _SharedYAxisTransitionDemoState extends State<SharedYAxisTransitionDemo>
with SingleTickerProviderStateMixin {
bool _isAlphabetical = false;
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
}
final ListView _recentList = ListView(
// Adding [UniqueKey] to make sure the widget rebuilds when transitioning.
key: UniqueKey(),
children: <Widget>[
for (int i = 0; i < 10; i++) _AlbumTile((i + 1).toString()),
],
);
final ListView _alphabeticalList = ListView(
// Adding [UniqueKey] to make sure the widget rebuilds when transitioning.
key: UniqueKey(),
children: <Widget>[
for (final String letter in _alphabet) _AlbumTile(letter),
],
);
static const List<String> _alphabet = <String>[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
];
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(localizations.demoSharedYAxisTitle),
Text(
'(${localizations.demoSharedYAxisDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
),
body: Column(
children: <Widget>[
const SizedBox(height: 5),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Padding(
padding: const EdgeInsets.only(left: 15),
child: Text(localizations.demoSharedYAxisAlbumCount),
),
Padding(
padding: const EdgeInsets.only(right: 7),
child: InkWell(
customBorder: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
onTap: () {
if (!_isAlphabetical) {
_controller.reset();
_controller.animateTo(0.5);
} else {
_controller.animateTo(1);
}
setState(() {
_isAlphabetical = !_isAlphabetical;
});
},
child: Row(
children: <Widget>[
Text(_isAlphabetical
? localizations.demoSharedYAxisAlphabeticalSortTitle
: localizations.demoSharedYAxisRecentSortTitle),
RotationTransition(
turns: Tween<double>(begin: 0.0, end: 1.0)
.animate(_controller.view),
child: const Icon(Icons.arrow_drop_down),
),
],
),
),
),
],
),
const SizedBox(height: 10),
Expanded(
child: PageTransitionSwitcher(
reverse: _isAlphabetical,
transitionBuilder: (Widget child, Animation<double> animation, Animation<double> secondaryAnimation) {
return SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.vertical,
child: child,
);
},
child: _isAlphabetical ? _alphabeticalList : _recentList,
),
),
],
),
);
}
}
class _AlbumTile extends StatelessWidget {
const _AlbumTile(this._title);
final String _title;
@override
Widget build(BuildContext context) {
final Random randomNumberGenerator = Random();
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Column(
children: <Widget>[
ListTile(
leading: Container(
height: 60,
width: 60,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(4),
),
color: Colors.grey,
),
child: Padding(
padding: const EdgeInsets.all(6),
child: Image.asset(
'placeholders/placeholder_image.png',
package: 'flutter_gallery_assets',
),
),
),
title: Text(
'${localizations.demoSharedYAxisAlbumTileTitle} $_title',
),
subtitle: Text(
localizations.demoSharedYAxisAlbumTileSubtitle,
),
trailing: Text(
'${randomNumberGenerator.nextInt(50) + 10} '
'${localizations.demoSharedYAxisAlbumTileDurationUnit}',
),
),
const Divider(height: 20, thickness: 1),
],
);
}
}
// END sharedYAxisTransitionDemo

View File

@ -0,0 +1,260 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN sharedZAxisTransitionDemo
class SharedZAxisTransitionDemo extends StatelessWidget {
const SharedZAxisTransitionDemo({super.key});
@override
Widget build(BuildContext context) {
return Navigator(
onGenerateRoute: (RouteSettings settings) {
return _createHomeRoute();
},
);
}
Route<void> _createHomeRoute() {
return PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Column(
children: <Widget>[
Text(localizations.demoSharedZAxisTitle),
Text(
'(${localizations.demoSharedZAxisDemoInstructions})',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Colors.white),
),
],
),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.of(context).push<void>(_createSettingsRoute());
},
),
],
),
body: const _RecipePage(),
);
},
transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SharedAxisTransition(
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.scaled,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
);
}
Route<void> _createSettingsRoute() {
return PageRouteBuilder<void>(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) =>
const _SettingsPage(),
transitionsBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
return SharedAxisTransition(
fillColor: Colors.transparent,
transitionType: SharedAxisTransitionType.scaled,
animation: animation,
secondaryAnimation: secondaryAnimation,
child: child,
);
},
);
}
}
class _SettingsPage extends StatelessWidget {
const _SettingsPage();
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<_SettingsInfo> settingsList = <_SettingsInfo>[
_SettingsInfo(
Icons.person,
localizations.demoSharedZAxisProfileSettingLabel,
),
_SettingsInfo(
Icons.notifications,
localizations.demoSharedZAxisNotificationSettingLabel,
),
_SettingsInfo(
Icons.security,
localizations.demoSharedZAxisPrivacySettingLabel,
),
_SettingsInfo(
Icons.help,
localizations.demoSharedZAxisHelpSettingLabel,
),
];
return Scaffold(
appBar: AppBar(
title: Text(
localizations.demoSharedZAxisSettingsPageTitle,
),
),
body: ListView(
children: <Widget>[
for (final _SettingsInfo setting in settingsList) _SettingsTile(setting),
],
),
);
}
}
class _SettingsTile extends StatelessWidget {
const _SettingsTile(this.settingData);
final _SettingsInfo settingData;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
ListTile(
leading: Icon(settingData.settingIcon),
title: Text(settingData.settingsLabel),
),
const Divider(thickness: 2),
],
);
}
}
class _SettingsInfo {
const _SettingsInfo(this.settingIcon, this.settingsLabel);
final IconData settingIcon;
final String settingsLabel;
}
class _RecipePage extends StatelessWidget {
const _RecipePage();
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<_RecipeInfo> savedRecipes = <_RecipeInfo>[
_RecipeInfo(
localizations.demoSharedZAxisBurgerRecipeTitle,
localizations.demoSharedZAxisBurgerRecipeDescription,
'crane/destinations/eat_2.jpg',
),
_RecipeInfo(
localizations.demoSharedZAxisSandwichRecipeTitle,
localizations.demoSharedZAxisSandwichRecipeDescription,
'crane/destinations/eat_3.jpg',
),
_RecipeInfo(
localizations.demoSharedZAxisDessertRecipeTitle,
localizations.demoSharedZAxisDessertRecipeDescription,
'crane/destinations/eat_4.jpg',
),
_RecipeInfo(
localizations.demoSharedZAxisShrimpPlateRecipeTitle,
localizations.demoSharedZAxisShrimpPlateRecipeDescription,
'crane/destinations/eat_6.jpg',
),
_RecipeInfo(
localizations.demoSharedZAxisCrabPlateRecipeTitle,
localizations.demoSharedZAxisCrabPlateRecipeDescription,
'crane/destinations/eat_8.jpg',
),
_RecipeInfo(
localizations.demoSharedZAxisBeefSandwichRecipeTitle,
localizations.demoSharedZAxisBeefSandwichRecipeDescription,
'crane/destinations/eat_10.jpg',
),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsetsDirectional.only(start: 8.0),
child: Text(localizations.demoSharedZAxisSavedRecipesListTitle),
),
const SizedBox(height: 4),
Expanded(
child: ListView(
padding: const EdgeInsets.all(8),
children: <Widget>[
for (final _RecipeInfo recipe in savedRecipes)
_RecipeTile(recipe, savedRecipes.indexOf(recipe))
],
),
),
],
);
}
}
class _RecipeInfo {
const _RecipeInfo(this.recipeName, this.recipeDescription, this.recipeImage);
final String recipeName;
final String recipeDescription;
final String recipeImage;
}
class _RecipeTile extends StatelessWidget {
const _RecipeTile(this._recipe, this._index);
final _RecipeInfo _recipe;
final int _index;
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
SizedBox(
height: 70,
width: 100,
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Image.asset(
_recipe.recipeImage,
package: 'flutter_gallery_assets',
fit: BoxFit.fill,
),
),
),
const SizedBox(width: 24),
Expanded(
child: Column(
children: <Widget>[
ListTile(
title: Text(_recipe.recipeName),
subtitle: Text(_recipe.recipeDescription),
trailing: Text('0${_index + 1}'),
),
const Divider(thickness: 2),
],
),
),
],
);
}
}
// END sharedZAxisTransitionDemo

View File

@ -0,0 +1,247 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'transformations_demo_board.dart';
import 'transformations_demo_edit_board_point.dart';
// BEGIN transformationsDemo#1
class TransformationsDemo extends StatefulWidget {
const TransformationsDemo({super.key});
@override
State<TransformationsDemo> createState() => _TransformationsDemoState();
}
class _TransformationsDemoState extends State<TransformationsDemo>
with TickerProviderStateMixin {
final GlobalKey _targetKey = GlobalKey();
// The radius of a hexagon tile in pixels.
static const double _kHexagonRadius = 16.0;
// The margin between hexagons.
static const double _kHexagonMargin = 1.0;
// The radius of the entire board in hexagons, not including the center.
static const int _kBoardRadius = 8;
Board _board = Board(
boardRadius: _kBoardRadius,
hexagonRadius: _kHexagonRadius,
hexagonMargin: _kHexagonMargin,
);
final TransformationController _transformationController =
TransformationController();
Animation<Matrix4>? _animationReset;
late AnimationController _controllerReset;
Matrix4? _homeMatrix;
// Handle reset to home transform animation.
void _onAnimateReset() {
_transformationController.value = _animationReset!.value;
if (!_controllerReset.isAnimating) {
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
}
// Initialize the reset to home transform animation.
void _animateResetInitialize() {
_controllerReset.reset();
_animationReset = Matrix4Tween(
begin: _transformationController.value,
end: _homeMatrix,
).animate(_controllerReset);
_controllerReset.duration = const Duration(milliseconds: 400);
_animationReset!.addListener(_onAnimateReset);
_controllerReset.forward();
}
// Stop a running reset to home transform animation.
void _animateResetStop() {
_controllerReset.stop();
_animationReset?.removeListener(_onAnimateReset);
_animationReset = null;
_controllerReset.reset();
}
void _onScaleStart(ScaleStartDetails details) {
// If the user tries to cause a transformation while the reset animation is
// running, cancel the reset animation.
if (_controllerReset.status == AnimationStatus.forward) {
_animateResetStop();
}
}
void _onTapUp(TapUpDetails details) {
final RenderBox renderBox =
_targetKey.currentContext!.findRenderObject()! as RenderBox;
final Offset offset =
details.globalPosition - renderBox.localToGlobal(Offset.zero);
final Offset scenePoint = _transformationController.toScene(offset);
final BoardPoint? boardPoint = _board.pointToBoardPoint(scenePoint);
setState(() {
_board = _board.copyWithSelected(boardPoint);
});
}
@override
void initState() {
super.initState();
_controllerReset = AnimationController(
vsync: this,
);
}
@override
Widget build(BuildContext context) {
// The scene is drawn by a CustomPaint, but user interaction is handled by
// the InteractiveViewer parent widget.
return Scaffold(
backgroundColor: Theme.of(context).colorScheme.primary,
appBar: AppBar(
automaticallyImplyLeading: false,
title:
Text(GalleryLocalizations.of(context)!.demo2dTransformationsTitle),
),
body: ColoredBox(
color: backgroundColor,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
// Draw the scene as big as is available, but allow the user to
// translate beyond that to a visibleSize that's a bit bigger.
final Size viewportSize = Size(
constraints.maxWidth,
constraints.maxHeight,
);
// Start the first render, start the scene centered in the viewport.
if (_homeMatrix == null) {
_homeMatrix = Matrix4.identity()
..translate(
viewportSize.width / 2 - _board.size.width / 2,
viewportSize.height / 2 - _board.size.height / 2,
);
_transformationController.value = _homeMatrix!;
}
return ClipRect(
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTapUp: _onTapUp,
child: InteractiveViewer(
key: _targetKey,
transformationController: _transformationController,
boundaryMargin: EdgeInsets.symmetric(
horizontal: viewportSize.width,
vertical: viewportSize.height,
),
minScale: 0.01,
onInteractionStart: _onScaleStart,
child: SizedBox.expand(
child: CustomPaint(
size: _board.size,
painter: _BoardPainter(
board: _board,
),
),
),
),
),
),
);
},
),
),
persistentFooterButtons: <Widget>[resetButton, editButton],
);
}
IconButton get resetButton {
return IconButton(
onPressed: () {
setState(() {
_animateResetInitialize();
});
},
tooltip: 'Reset',
color: Theme.of(context).colorScheme.surface,
icon: const Icon(Icons.replay),
);
}
IconButton get editButton {
return IconButton(
onPressed: () {
if (_board.selected == null) {
return;
}
showModalBottomSheet<Widget>(
context: context,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 150,
padding: const EdgeInsets.all(12),
child: EditBoardPoint(
boardPoint: _board.selected!,
onColorSelection: (Color color) {
setState(() {
_board = _board.copyWithBoardPointColor(
_board.selected!, color);
Navigator.pop(context);
});
},
),
);
});
},
tooltip: 'Edit',
color: Theme.of(context).colorScheme.surface,
icon: const Icon(Icons.edit),
);
}
@override
void dispose() {
_controllerReset.dispose();
super.dispose();
}
}
// CustomPainter is what is passed to CustomPaint and actually draws the scene
// when its `paint` method is called.
class _BoardPainter extends CustomPainter {
const _BoardPainter({required this.board});
final Board board;
@override
void paint(Canvas canvas, Size size) {
void drawBoardPoint(BoardPoint? boardPoint) {
final Color color = boardPoint!.color.withOpacity(
board.selected == boardPoint ? 0.7 : 1,
);
final Vertices vertices = board.getVerticesForBoardPoint(boardPoint, color);
canvas.drawVertices(vertices, BlendMode.color, Paint());
}
board.forEach(drawBoardPoint);
}
// We should repaint whenever the board changes, such as board.selected.
@override
bool shouldRepaint(_BoardPainter oldDelegate) {
return oldDelegate.board != board;
}
}
// END

View File

@ -0,0 +1,301 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection' show IterableMixin;
import 'dart:math';
import 'dart:ui' show Vertices;
import 'package:flutter/material.dart';
import 'package:vector_math/vector_math_64.dart' show Vector3;
// BEGIN transformationsDemo#2
// The entire state of the hex board and abstraction to get information about
// it. Iterable so that all BoardPoints on the board can be iterated over.
@immutable
class Board extends Object with IterableMixin<BoardPoint?> {
Board({
required this.boardRadius,
required this.hexagonRadius,
required this.hexagonMargin,
this.selected,
List<BoardPoint>? boardPoints,
}) : assert(boardRadius > 0),
assert(hexagonRadius > 0),
assert(hexagonMargin >= 0) {
// Set up the positions for the center hexagon where the entire board is
// centered on the origin.
// Start point of hexagon (top vertex).
final Point<double> hexStart = Point<double>(0, -hexagonRadius);
final double hexagonRadiusPadded = hexagonRadius - hexagonMargin;
final double centerToFlat = sqrt(3) / 2 * hexagonRadiusPadded;
positionsForHexagonAtOrigin.addAll(<Offset>[
Offset(hexStart.x, hexStart.y),
Offset(hexStart.x + centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x + centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
Offset(hexStart.x, hexStart.y + 2 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 1.5 * hexagonRadiusPadded),
Offset(hexStart.x - centerToFlat, hexStart.y + 0.5 * hexagonRadiusPadded),
]);
if (boardPoints != null) {
_boardPoints.addAll(boardPoints);
} else {
// Generate boardPoints for a fresh board.
BoardPoint? boardPoint = _getNextBoardPoint(null);
while (boardPoint != null) {
_boardPoints.add(boardPoint);
boardPoint = _getNextBoardPoint(boardPoint);
}
}
}
final int boardRadius; // Number of hexagons from center to edge.
final double hexagonRadius; // Pixel radius of a hexagon (center to vertex).
final double hexagonMargin; // Margin between hexagons.
final List<Offset> positionsForHexagonAtOrigin = <Offset>[];
final BoardPoint? selected;
final List<BoardPoint> _boardPoints = <BoardPoint>[];
@override
Iterator<BoardPoint?> get iterator => _BoardIterator(_boardPoints);
// For a given q axial coordinate, get the range of possible r values
// See the definition of BoardPoint for more information about hex grids and
// axial coordinates.
_Range _getRRangeForQ(int q) {
int rStart;
int rEnd;
if (q <= 0) {
rStart = -boardRadius - q;
rEnd = boardRadius;
} else {
rEnd = boardRadius - q;
rStart = -boardRadius;
}
return _Range(rStart, rEnd);
}
// Get the BoardPoint that comes after the given BoardPoint. If given null,
// returns the origin BoardPoint. If given BoardPoint is the last, returns
// null.
BoardPoint? _getNextBoardPoint(BoardPoint? boardPoint) {
// If before the first element.
if (boardPoint == null) {
return BoardPoint(-boardRadius, 0);
}
final _Range rRange = _getRRangeForQ(boardPoint.q);
// If at or after the last element.
if (boardPoint.q >= boardRadius && boardPoint.r >= rRange.max) {
return null;
}
// If wrapping from one q to the next.
if (boardPoint.r >= rRange.max) {
return BoardPoint(boardPoint.q + 1, _getRRangeForQ(boardPoint.q + 1).min);
}
// Otherwise we're just incrementing r.
return BoardPoint(boardPoint.q, boardPoint.r + 1);
}
// Check if the board point is actually on the board.
bool _validateBoardPoint(BoardPoint boardPoint) {
const BoardPoint center = BoardPoint(0, 0);
final int distanceFromCenter = getDistance(center, boardPoint);
return distanceFromCenter <= boardRadius;
}
// Get the size in pixels of the entire board.
Size get size {
final double centerToFlat = sqrt(3) / 2 * hexagonRadius;
return Size(
(boardRadius * 2 + 1) * centerToFlat * 2,
2 * (hexagonRadius + boardRadius * 1.5 * hexagonRadius),
);
}
// Get the distance between two BoardPoints.
static int getDistance(BoardPoint a, BoardPoint b) {
final Vector3 a3 = a.cubeCoordinates;
final Vector3 b3 = b.cubeCoordinates;
return ((a3.x - b3.x).abs() + (a3.y - b3.y).abs() + (a3.z - b3.z).abs()) ~/
2;
}
// Return the q,r BoardPoint for a point in the scene, where the origin is in
// the center of the board in both coordinate systems. If no BoardPoint at the
// location, return null.
BoardPoint? pointToBoardPoint(Offset point) {
final Offset pointCentered = Offset(
point.dx - size.width / 2,
point.dy - size.height / 2,
);
final BoardPoint boardPoint = BoardPoint(
((sqrt(3) / 3 * pointCentered.dx - 1 / 3 * pointCentered.dy) /
hexagonRadius)
.round(),
((2 / 3 * pointCentered.dy) / hexagonRadius).round(),
);
if (!_validateBoardPoint(boardPoint)) {
return null;
}
return _boardPoints.firstWhere((BoardPoint boardPointI) {
return boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r;
});
}
// Return a scene point for the center of a hexagon given its q,r point.
Point<double> boardPointToPoint(BoardPoint boardPoint) {
return Point<double>(
sqrt(3) * hexagonRadius * boardPoint.q +
sqrt(3) / 2 * hexagonRadius * boardPoint.r +
size.width / 2,
1.5 * hexagonRadius * boardPoint.r + size.height / 2,
);
}
// Get Vertices that can be drawn to a Canvas for the given BoardPoint.
Vertices getVerticesForBoardPoint(BoardPoint boardPoint, Color color) {
final Point<double> centerOfHexZeroCenter = boardPointToPoint(boardPoint);
final List<Offset> positions = positionsForHexagonAtOrigin.map((Offset offset) {
return offset.translate(centerOfHexZeroCenter.x, centerOfHexZeroCenter.y);
}).toList();
return Vertices(
VertexMode.triangleFan,
positions,
colors: List<Color>.filled(positions.length, color),
);
}
// Return a new board with the given BoardPoint selected.
Board copyWithSelected(BoardPoint? boardPoint) {
if (selected == boardPoint) {
return this;
}
final Board nextBoard = Board(
boardRadius: boardRadius,
hexagonRadius: hexagonRadius,
hexagonMargin: hexagonMargin,
selected: boardPoint,
boardPoints: _boardPoints,
);
return nextBoard;
}
// Return a new board where boardPoint has the given color.
Board copyWithBoardPointColor(BoardPoint boardPoint, Color color) {
final BoardPoint nextBoardPoint = boardPoint.copyWithColor(color);
final int boardPointIndex = _boardPoints.indexWhere((BoardPoint boardPointI) =>
boardPointI.q == boardPoint.q && boardPointI.r == boardPoint.r);
if (elementAt(boardPointIndex) == boardPoint && boardPoint.color == color) {
return this;
}
final List<BoardPoint> nextBoardPoints = List<BoardPoint>.from(_boardPoints);
nextBoardPoints[boardPointIndex] = nextBoardPoint;
final BoardPoint? selectedBoardPoint =
boardPoint == selected ? nextBoardPoint : selected;
return Board(
boardRadius: boardRadius,
hexagonRadius: hexagonRadius,
hexagonMargin: hexagonMargin,
selected: selectedBoardPoint,
boardPoints: nextBoardPoints,
);
}
}
class _BoardIterator implements Iterator<BoardPoint?> {
_BoardIterator(this.boardPoints);
final List<BoardPoint> boardPoints;
int? currentIndex;
@override
BoardPoint? current;
@override
bool moveNext() {
if (currentIndex == null) {
currentIndex = 0;
} else {
currentIndex = currentIndex! + 1;
}
if (currentIndex! >= boardPoints.length) {
current = null;
return false;
}
current = boardPoints[currentIndex!];
return true;
}
}
// A range of q/r board coordinate values.
@immutable
class _Range {
const _Range(this.min, this.max) : assert(min <= max);
final int min;
final int max;
}
// A location on the board in axial coordinates.
// Axial coordinates use two integers, q and r, to locate a hexagon on a grid.
// https://www.redblobgames.com/grids/hexagons/#coordinates-axial
@immutable
class BoardPoint {
const BoardPoint(
this.q,
this.r, {
this.color = const Color(0xFFCDCDCD),
});
final int q;
final int r;
final Color color;
@override
String toString() {
return 'BoardPoint($q, $r, $color)';
}
// Only compares by location.
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is BoardPoint && other.q == q && other.r == r;
}
@override
int get hashCode => Object.hash(q, r);
BoardPoint copyWithColor(Color nextColor) =>
BoardPoint(q, r, color: nextColor);
// Convert from q,r axial coords to x,y,z cube coords.
Vector3 get cubeCoordinates {
return Vector3(
q.toDouble(),
r.toDouble(),
(-q - r).toDouble(),
);
}
}
// END

View File

@ -0,0 +1,75 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
// A generic widget for a list of selectable colors.
@immutable
class ColorPicker extends StatelessWidget {
const ColorPicker({
super.key,
required this.colors,
required this.selectedColor,
this.onColorSelection,
});
final Set<Color> colors;
final Color selectedColor;
final ValueChanged<Color>? onColorSelection;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: colors.map((Color color) {
return _ColorPickerSwatch(
color: color,
selected: color == selectedColor,
onTap: () {
if (onColorSelection != null) {
onColorSelection!(color);
}
},
);
}).toList(),
);
}
}
// A single selectable color widget in the ColorPicker.
@immutable
class _ColorPickerSwatch extends StatelessWidget {
const _ColorPickerSwatch({
required this.color,
required this.selected,
this.onTap,
});
final Color color;
final bool selected;
final void Function()? onTap;
@override
Widget build(BuildContext context) {
return Container(
width: 60,
height: 60,
padding: const EdgeInsets.fromLTRB(2, 0, 2, 0),
child: RawMaterialButton(
fillColor: color,
onPressed: () {
if (onTap != null) {
onTap!();
}
},
child: !selected
? null
: const Icon(
Icons.check,
color: Colors.white,
),
),
);
}
}

View File

@ -0,0 +1,50 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../themes/gallery_theme_data.dart';
import 'transformations_demo_board.dart';
import 'transformations_demo_color_picker.dart';
const Color backgroundColor = Color(0xFF272727);
// The panel for editing a board point.
@immutable
class EditBoardPoint extends StatelessWidget {
const EditBoardPoint({
super.key,
required this.boardPoint,
this.onColorSelection,
});
final BoardPoint boardPoint;
final ValueChanged<Color>? onColorSelection;
@override
Widget build(BuildContext context) {
final Set<Color> boardPointColors = <Color>{
Colors.white,
GalleryThemeData.darkColorScheme.primary,
GalleryThemeData.darkColorScheme.primaryContainer,
GalleryThemeData.darkColorScheme.secondary,
backgroundColor,
};
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'${boardPoint.q}, ${boardPoint.r}',
textAlign: TextAlign.right,
style: const TextStyle(fontWeight: FontWeight.bold),
),
ColorPicker(
colors: boardPointColors,
selectedColor: boardPoint.color,
onColorSelection: onColorSelection,
),
],
);
}
}

View File

@ -0,0 +1,229 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN twoPaneDemo
enum TwoPaneDemoType {
foldable,
tablet,
smallScreen,
}
class TwoPaneDemo extends StatefulWidget {
const TwoPaneDemo({
super.key,
required this.restorationId,
required this.type,
});
final String restorationId;
final TwoPaneDemoType type;
@override
TwoPaneDemoState createState() => TwoPaneDemoState();
}
class TwoPaneDemoState extends State<TwoPaneDemo> with RestorationMixin {
final RestorableInt _currentIndex = RestorableInt(-1);
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_currentIndex, 'two_pane_selected_item');
}
@override
void dispose() {
_currentIndex.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
TwoPanePriority panePriority = TwoPanePriority.both;
if (widget.type == TwoPaneDemoType.smallScreen) {
panePriority = _currentIndex.value == -1
? TwoPanePriority.start
: TwoPanePriority.end;
}
return SimulateScreen(
type: widget.type,
child: TwoPane(
paneProportion: 0.3,
panePriority: panePriority,
startPane: ListPane(
selectedIndex: _currentIndex.value,
onSelect: (int index) {
setState(() {
_currentIndex.value = index;
});
},
),
endPane: DetailsPane(
selectedIndex: _currentIndex.value,
onClose: widget.type == TwoPaneDemoType.smallScreen
? () {
setState(() {
_currentIndex.value = -1;
});
}
: null,
),
),
);
}
}
class ListPane extends StatelessWidget {
const ListPane({
super.key,
required this.onSelect,
required this.selectedIndex,
});
final ValueChanged<int> onSelect;
final int selectedIndex;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoTwoPaneList),
),
body: Scrollbar(
child: ListView(
restorationId: 'list_demo_list_view',
padding: const EdgeInsets.symmetric(vertical: 8),
children: <Widget>[
for (int index = 1; index < 21; index++)
ListTile(
onTap: () {
onSelect(index);
},
selected: selectedIndex == index,
leading: ExcludeSemantics(
child: CircleAvatar(child: Text('$index')),
),
title: Text(
GalleryLocalizations.of(context)!.demoTwoPaneItem(index),
),
),
],
),
),
);
}
}
class DetailsPane extends StatelessWidget {
const DetailsPane({
super.key,
required this.selectedIndex,
this.onClose,
});
final VoidCallback? onClose;
final int selectedIndex;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
leading: onClose == null
? null
: IconButton(icon: const Icon(Icons.close), onPressed: onClose),
title: Text(
GalleryLocalizations.of(context)!.demoTwoPaneDetails,
),
),
body: ColoredBox(
color: const Color(0xfffafafa),
child: Center(
child: Text(
selectedIndex == -1
? GalleryLocalizations.of(context)!.demoTwoPaneSelectItem
: GalleryLocalizations.of(context)!
.demoTwoPaneItemDetails(selectedIndex),
),
),
),
);
}
}
class SimulateScreen extends StatelessWidget {
const SimulateScreen({
super.key,
required this.type,
required this.child,
});
final TwoPaneDemoType type;
final TwoPane child;
// An approximation of a real foldable
static const double foldableAspectRatio = 20 / 18;
// 16x9 candy bar phone
static const double singleScreenAspectRatio = 9 / 16;
// Taller desktop / tablet
static const double tabletAspectRatio = 4 / 3;
// How wide should the hinge be, as a proportion of total width
static const double hingeProportion = 1 / 35;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(16),
),
padding: const EdgeInsets.all(14),
child: AspectRatio(
aspectRatio: type == TwoPaneDemoType.foldable
? foldableAspectRatio
: type == TwoPaneDemoType.tablet
? tabletAspectRatio
: singleScreenAspectRatio,
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
final Size size = Size(constraints.maxWidth, constraints.maxHeight);
final Size hingeSize = Size(size.width * hingeProportion, size.height);
// Position the hinge in the middle of the display
final Rect hingeBounds = Rect.fromLTWH(
(size.width - hingeSize.width) / 2,
0,
hingeSize.width,
hingeSize.height,
);
return MediaQuery(
data: MediaQueryData(
size: size,
displayFeatures: <DisplayFeature>[
if (type == TwoPaneDemoType.foldable)
DisplayFeature(
bounds: hingeBounds,
type: DisplayFeatureType.hinge,
state: DisplayFeatureState.postureFlat,
),
],
),
child: child,
);
}),
),
),
);
}
}
// END

View File

@ -0,0 +1,125 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
// BEGIN typographyDemo
class _TextStyleItem extends StatelessWidget {
const _TextStyleItem({
required this.name,
required this.style,
required this.text,
});
final String name;
final TextStyle style;
final String text;
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
child: Row(
children: <Widget>[
SizedBox(
width: 72,
child: Text(name, style: Theme.of(context).textTheme.bodySmall),
),
Expanded(
child: Text(text, style: style),
),
],
),
);
}
}
class TypographyDemo extends StatelessWidget {
const TypographyDemo({super.key});
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final List<_TextStyleItem> styleItems = <_TextStyleItem>[
_TextStyleItem(
name: 'Headline 1',
style: textTheme.displayLarge!,
text: 'Light 96sp',
),
_TextStyleItem(
name: 'Headline 2',
style: textTheme.displayMedium!,
text: 'Light 60sp',
),
_TextStyleItem(
name: 'Headline 3',
style: textTheme.displaySmall!,
text: 'Regular 48sp',
),
_TextStyleItem(
name: 'Headline 4',
style: textTheme.headlineMedium!,
text: 'Regular 34sp',
),
_TextStyleItem(
name: 'Headline 5',
style: textTheme.headlineSmall!,
text: 'Regular 24sp',
),
_TextStyleItem(
name: 'Headline 6',
style: textTheme.titleLarge!,
text: 'Medium 20sp',
),
_TextStyleItem(
name: 'Subtitle 1',
style: textTheme.titleMedium!,
text: 'Regular 16sp',
),
_TextStyleItem(
name: 'Subtitle 2',
style: textTheme.titleSmall!,
text: 'Medium 14sp',
),
_TextStyleItem(
name: 'Body Text 1',
style: textTheme.bodyLarge!,
text: 'Regular 16sp',
),
_TextStyleItem(
name: 'Body Text 2',
style: textTheme.bodyMedium!,
text: 'Regular 14sp',
),
_TextStyleItem(
name: 'Button',
style: textTheme.labelLarge!,
text: 'MEDIUM (ALL CAPS) 14sp',
),
_TextStyleItem(
name: 'Caption',
style: textTheme.bodySmall!,
text: 'Regular 12sp',
),
_TextStyleItem(
name: 'Overline',
style: textTheme.labelSmall!,
text: 'REGULAR (ALL CAPS) 10sp',
),
];
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(GalleryLocalizations.of(context)!.demoTypographyTitle),
),
body: Scrollbar(child: ListView(children: styleItems)),
);
}
}
// END

View File

@ -0,0 +1,251 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// Animations class to compute animation values for overlay widgets.
///
/// Values are loosely based on Material Design specs, which are minimal.
class Animations {
Animations(
this.openController,
this.tapController,
this.rippleController,
this.dismissController,
);
final AnimationController openController;
final AnimationController tapController;
final AnimationController rippleController;
final AnimationController dismissController;
static const double backgroundMaxOpacity = 0.96;
static const double backgroundTapRadius = 20.0;
static const double rippleMaxOpacity = 0.75;
static const double tapTargetToContentDistance = 20.0;
static const double tapTargetMaxRadius = 44.0;
static const double tapTargetMinRadius = 20.0;
static const double tapTargetRippleRadius = 64.0;
Animation<double> backgroundOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return const AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: backgroundMaxOpacity)
.animate(CurvedAnimation(
parent: openController,
curve: const Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: backgroundMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: backgroundMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: dismissController,
curve: const Interval(0.2, 1.0, curve: Curves.ease),
));
case FeatureDiscoveryStatus.ripple:
return const AlwaysStoppedAnimation<double>(backgroundMaxOpacity);
}
}
Animation<double> backgroundRadius(
FeatureDiscoveryStatus status,
double backgroundRadiusMax,
) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return const AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: backgroundRadiusMax)
.animate(CurvedAnimation(
parent: openController,
curve: const Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(
begin: backgroundRadiusMax,
end: backgroundRadiusMax + backgroundTapRadius)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: backgroundRadiusMax, end: 0)
.animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.ripple:
return AlwaysStoppedAnimation<double>(backgroundRadiusMax);
}
}
Animation<Offset> backgroundCenter(
FeatureDiscoveryStatus status,
Offset start,
Offset end,
) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return AlwaysStoppedAnimation<Offset>(start);
case FeatureDiscoveryStatus.open:
return Tween<Offset>(begin: start, end: end).animate(CurvedAnimation(
parent: openController,
curve: const Interval(0, 0.5, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<Offset>(begin: end, end: start).animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<Offset>(begin: end, end: start).animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.ripple:
return AlwaysStoppedAnimation<Offset>(end);
}
}
Animation<double> contentOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return const AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: openController,
curve: const Interval(0.4, 0.7, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: tapController,
curve: const Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: dismissController,
curve: const Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.ripple:
return const AlwaysStoppedAnimation<double>(1.0);
}
}
Animation<double> rippleOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.ripple:
return Tween<double>(begin: rippleMaxOpacity, end: 0)
.animate(CurvedAnimation(
parent: rippleController,
curve: const Interval(0.3, 0.8, curve: Curves.ease),
));
case FeatureDiscoveryStatus.closed:
case FeatureDiscoveryStatus.open:
case FeatureDiscoveryStatus.tap:
case FeatureDiscoveryStatus.dismiss:
return const AlwaysStoppedAnimation<double>(0);
}
}
Animation<double> rippleRadius(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.ripple:
if (rippleController.value >= 0.3 && rippleController.value <= 0.8) {
return Tween<double>(begin: tapTargetMaxRadius, end: 79.0)
.animate(CurvedAnimation(
parent: rippleController,
curve: const Interval(0.3, 0.8, curve: Curves.ease),
));
}
return const AlwaysStoppedAnimation<double>(tapTargetMaxRadius);
case FeatureDiscoveryStatus.closed:
case FeatureDiscoveryStatus.open:
case FeatureDiscoveryStatus.tap:
case FeatureDiscoveryStatus.dismiss:
return const AlwaysStoppedAnimation<double>(0);
}
}
Animation<double> tapTargetOpacity(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return const AlwaysStoppedAnimation<double>(0);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: 0, end: 1.0).animate(CurvedAnimation(
parent: openController,
curve: const Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: tapController,
curve: const Interval(0.1, 0.6, curve: Curves.ease),
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: 1.0, end: 0).animate(CurvedAnimation(
parent: dismissController,
curve: const Interval(0.2, 0.8, curve: Curves.ease),
));
case FeatureDiscoveryStatus.ripple:
return const AlwaysStoppedAnimation<double>(1.0);
}
}
Animation<double> tapTargetRadius(FeatureDiscoveryStatus status) {
switch (status) {
case FeatureDiscoveryStatus.closed:
return const AlwaysStoppedAnimation<double>(tapTargetMinRadius);
case FeatureDiscoveryStatus.open:
return Tween<double>(begin: tapTargetMinRadius, end: tapTargetMaxRadius)
.animate(CurvedAnimation(
parent: openController,
curve: const Interval(0, 0.4, curve: Curves.ease),
));
case FeatureDiscoveryStatus.ripple:
if (rippleController.value < 0.3) {
return Tween<double>(
begin: tapTargetMaxRadius, end: tapTargetRippleRadius)
.animate(CurvedAnimation(
parent: rippleController,
curve: const Interval(0, 0.3, curve: Curves.ease),
));
} else if (rippleController.value < 0.6) {
return Tween<double>(
begin: tapTargetRippleRadius, end: tapTargetMaxRadius)
.animate(CurvedAnimation(
parent: rippleController,
curve: const Interval(0.3, 0.6, curve: Curves.ease),
));
}
return const AlwaysStoppedAnimation<double>(tapTargetMaxRadius);
case FeatureDiscoveryStatus.tap:
return Tween<double>(begin: tapTargetMaxRadius, end: tapTargetMinRadius)
.animate(CurvedAnimation(
parent: tapController,
curve: Curves.ease,
));
case FeatureDiscoveryStatus.dismiss:
return Tween<double>(begin: tapTargetMaxRadius, end: tapTargetMinRadius)
.animate(CurvedAnimation(
parent: dismissController,
curve: Curves.ease,
));
}
}
}
/// Enum to indicate the current status of a [FeatureDiscovery] widget.
enum FeatureDiscoveryStatus {
closed, // Overlay is closed.
open, // Overlay is opening.
ripple, // Overlay is rippling.
tap, // Overlay is tapped.
dismiss, // Overlay is being dismissed.
}

View File

@ -0,0 +1,384 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'animation.dart';
import 'overlay.dart';
/// [Widget] to enforce a global lock system for [FeatureDiscovery] widgets.
///
/// This widget enforces that at most one [FeatureDiscovery] widget in its
/// widget tree is shown at a time.
///
/// Users wanting to use [FeatureDiscovery] need to put this controller
/// above [FeatureDiscovery] widgets in the widget tree.
class FeatureDiscoveryController extends StatefulWidget {
const FeatureDiscoveryController(this.child, {super.key});
final Widget child;
static _FeatureDiscoveryControllerState _of(BuildContext context) {
final _FeatureDiscoveryControllerState? matchResult =
context.findAncestorStateOfType<_FeatureDiscoveryControllerState>();
if (matchResult != null) {
return matchResult;
}
throw FlutterError(
'FeatureDiscoveryController.of() called with a context that does not '
'contain a FeatureDiscoveryController.\n The context used was:\n '
'$context');
}
@override
State<FeatureDiscoveryController> createState() =>
_FeatureDiscoveryControllerState();
}
class _FeatureDiscoveryControllerState
extends State<FeatureDiscoveryController> {
bool _isLocked = false;
/// Flag to indicate whether a [FeatureDiscovery] widget descendant is
/// currently showing its overlay or not.
///
/// If true, then no other [FeatureDiscovery] widget should display its
/// overlay.
bool get isLocked => _isLocked;
/// Lock the controller.
///
/// Note we do not [setState] here because this function will be called
/// by the first [FeatureDiscovery] ready to show its overlay, and any
/// additional [FeatureDiscovery] widgets wanting to show their overlays
/// will already be scheduled to be built, so the lock change will be caught
/// in their builds.
void lock() => _isLocked = true;
/// Unlock the controller.
void unlock() => setState(() => _isLocked = false);
@override
void didChangeDependencies() {
super.didChangeDependencies();
assert(
context.findAncestorStateOfType<_FeatureDiscoveryControllerState>() ==
null,
'There should not be another ancestor of type '
'FeatureDiscoveryController in the widget tree.',
);
}
@override
Widget build(BuildContext context) => widget.child;
}
/// Widget that highlights the [child] with an overlay.
///
/// This widget loosely follows the guidelines set forth in the Material Specs:
/// https://material.io/archive/guidelines/growth-communications/feature-discovery.html.
class FeatureDiscovery extends StatefulWidget {
const FeatureDiscovery({
super.key,
required this.title,
required this.description,
required this.child,
required this.showOverlay,
this.onDismiss,
this.onTap,
this.color,
});
/// Title to be displayed in the overlay.
final String title;
/// Description to be displayed in the overlay.
final String description;
/// Icon to be promoted.
final Icon child;
/// Flag to indicate whether to show the overlay or not anchored to the
/// [child].
final bool showOverlay;
/// Callback invoked when the user dismisses an overlay.
final void Function()? onDismiss;
/// Callback invoked when the user taps on the tap target of an overlay.
final void Function()? onTap;
/// Color with which to fill the outer circle.
final Color? color;
@visibleForTesting
static const Key overlayKey = Key('overlay key');
@visibleForTesting
static const Key gestureDetectorKey = Key('gesture detector key');
@override
State<FeatureDiscovery> createState() => _FeatureDiscoveryState();
}
class _FeatureDiscoveryState extends State<FeatureDiscovery>
with TickerProviderStateMixin {
bool showOverlay = false;
FeatureDiscoveryStatus status = FeatureDiscoveryStatus.closed;
late AnimationController openController;
late AnimationController rippleController;
late AnimationController tapController;
late AnimationController dismissController;
late Animations animations;
OverlayEntry? overlay;
Widget buildOverlay(BuildContext ctx, Offset center) {
debugCheckHasMediaQuery(ctx);
debugCheckHasDirectionality(ctx);
final Size deviceSize = MediaQuery.of(ctx).size;
final Color color = widget.color ?? Theme.of(ctx).colorScheme.primary;
// Wrap in transparent [Material] to enable widgets that require one.
return Material(
key: FeatureDiscovery.overlayKey,
type: MaterialType.transparency,
child: Stack(
children: <Widget>[
MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
key: FeatureDiscovery.gestureDetectorKey,
onTap: dismiss,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.transparent,
),
),
),
Background(
animations: animations,
status: status,
color: color,
center: center,
deviceSize: deviceSize,
textDirection: Directionality.of(ctx),
),
Content(
animations: animations,
status: status,
center: center,
deviceSize: deviceSize,
title: widget.title,
description: widget.description,
textTheme: Theme.of(ctx).textTheme,
),
Ripple(
animations: animations,
status: status,
center: center,
),
TapTarget(
animations: animations,
status: status,
center: center,
onTap: tap,
child: widget.child,
),
],
),
);
}
/// Method to handle user tap on [TapTarget].
///
/// Tapping will stop any active controller and start the [tapController].
void tap() {
openController.stop();
rippleController.stop();
dismissController.stop();
tapController.forward(from: 0.0);
}
/// Method to handle user dismissal.
///
/// Dismissal will stop any active controller and start the
/// [dismissController].
void dismiss() {
openController.stop();
rippleController.stop();
tapController.stop();
dismissController.forward(from: 0.0);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(builder: (BuildContext ctx, _) {
if (overlay != null) {
SchedulerBinding.instance.addPostFrameCallback((_) {
// [OverlayEntry] needs to be explicitly rebuilt when necessary.
overlay!.markNeedsBuild();
});
} else {
if (showOverlay && !FeatureDiscoveryController._of(ctx).isLocked) {
final OverlayEntry entry = OverlayEntry(
builder: (_) => buildOverlay(ctx, getOverlayCenter(ctx)),
);
// Lock [FeatureDiscoveryController] early in order to prevent
// another [FeatureDiscovery] widget from trying to show its
// overlay while the post frame callback and set state are not
// complete.
FeatureDiscoveryController._of(ctx).lock();
SchedulerBinding.instance.addPostFrameCallback((_) {
setState(() {
overlay = entry;
status = FeatureDiscoveryStatus.closed;
openController.forward(from: 0.0);
});
Overlay.of(context).insert(entry);
});
}
}
return widget.child;
});
}
/// Compute the center position of the overlay.
Offset getOverlayCenter(BuildContext parentCtx) {
final RenderBox box = parentCtx.findRenderObject()! as RenderBox;
final Size size = box.size;
final Offset topLeftPosition = box.localToGlobal(Offset.zero);
final Offset centerPosition = Offset(
topLeftPosition.dx + size.width / 2,
topLeftPosition.dy + size.height / 2,
);
return centerPosition;
}
static bool _featureHighlightShown = false;
@override
void initState() {
super.initState();
initAnimationControllers();
initAnimations();
showOverlay = widget.showOverlay && !_featureHighlightShown;
if (showOverlay) {
_featureHighlightShown = true;
}
}
void initAnimationControllers() {
openController = AnimationController(
duration: const Duration(milliseconds: 500),
vsync: this,
)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus animationStatus) {
if (animationStatus == AnimationStatus.forward) {
setState(() => status = FeatureDiscoveryStatus.open);
} else if (animationStatus == AnimationStatus.completed) {
rippleController.forward(from: 0.0);
}
});
rippleController = AnimationController(
duration: const Duration(milliseconds: 1000),
vsync: this,
)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus animationStatus) {
if (animationStatus == AnimationStatus.forward) {
setState(() => status = FeatureDiscoveryStatus.ripple);
} else if (animationStatus == AnimationStatus.completed) {
rippleController.forward(from: 0.0);
}
});
tapController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus animationStatus) {
if (animationStatus == AnimationStatus.forward) {
setState(() => status = FeatureDiscoveryStatus.tap);
} else if (animationStatus == AnimationStatus.completed) {
widget.onTap?.call();
cleanUponOverlayClose();
}
});
dismissController = AnimationController(
duration: const Duration(milliseconds: 250),
vsync: this,
)
..addListener(() {
setState(() {});
})
..addStatusListener((AnimationStatus animationStatus) {
if (animationStatus == AnimationStatus.forward) {
setState(() => status = FeatureDiscoveryStatus.dismiss);
} else if (animationStatus == AnimationStatus.completed) {
widget.onDismiss?.call();
cleanUponOverlayClose();
}
});
}
void initAnimations() {
animations = Animations(
openController,
tapController,
rippleController,
dismissController,
);
}
/// Clean up once overlay has been dismissed or tap target has been tapped.
///
/// This is called upon [tapController] and [dismissController] end.
void cleanUponOverlayClose() {
FeatureDiscoveryController._of(context).unlock();
setState(() {
status = FeatureDiscoveryStatus.closed;
showOverlay = false;
overlay?.remove();
overlay = null;
});
}
@override
void didUpdateWidget(FeatureDiscovery oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.showOverlay != oldWidget.showOverlay) {
showOverlay = widget.showOverlay;
}
}
@override
void dispose() {
overlay?.remove();
openController.dispose();
rippleController.dispose();
tapController.dispose();
dismissController.dispose();
super.dispose();
}
}

View File

@ -0,0 +1,382 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
import 'animation.dart';
const double contentHeight = 80.0;
const double contentWidth = 300.0;
const double contentHorizontalPadding = 40.0;
const double tapTargetRadius = 44.0;
const double tapTargetToContentDistance = 20.0;
const double gutterHeight = 88.0;
/// Background of the overlay.
class Background extends StatelessWidget {
const Background({
super.key,
required this.animations,
required this.center,
required this.color,
required this.deviceSize,
required this.status,
required this.textDirection,
});
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Color of the background.
final Color color;
/// Device size.
final Size deviceSize;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Directionality of content.
final TextDirection textDirection;
static const double horizontalShift = 20.0;
static const double padding = 40.0;
/// Compute the center position of the background.
///
/// If [center] is near the top or bottom edges of the screen, then
/// background is centered there.
/// Otherwise, background center is calculated and upon opening, animated
/// from [center] to the new calculated position.
Offset get centerPosition {
if (_isNearTopOrBottomEdges(center, deviceSize)) {
return center;
} else {
final Offset start = center;
// dy of centerPosition is calculated to be the furthest point in
// [Content] from the [center].
double endY;
if (_isOnTopHalfOfScreen(center, deviceSize)) {
endY = center.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
if (endY < 0.0) {
endY = center.dy + tapTargetRadius + tapTargetToContentDistance;
}
} else {
endY = center.dy + tapTargetRadius + tapTargetToContentDistance;
if (endY + contentHeight > deviceSize.height) {
endY = center.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
}
}
// Horizontal background center shift based on whether the tap target is
// on the left, center, or right side of the screen.
double shift;
if (_isOnLeftHalfOfScreen(center, deviceSize)) {
shift = horizontalShift;
} else if (center.dx == deviceSize.width / 2) {
shift = textDirection == TextDirection.ltr
? -horizontalShift
: horizontalShift;
} else {
shift = -horizontalShift;
}
// dx of centerPosition is calculated to be the middle point of the
// [Content] bounds shifted by [horizontalShift].
final Rect textBounds = _getContentBounds(deviceSize, center);
final double left = min(textBounds.left, center.dx - 88.0);
final double right = max(textBounds.right, center.dx + 88.0);
final double endX = (left + right) / 2 + shift;
final Offset end = Offset(endX, endY);
return animations.backgroundCenter(status, start, end).value;
}
}
/// Compute the radius.
///
/// Radius is a function of the greatest distance from [center] to one of
/// the corners of [Content].
double get radius {
final Rect textBounds = _getContentBounds(deviceSize, center);
final double textRadius = _maxDistance(center, textBounds) + padding;
if (_isNearTopOrBottomEdges(center, deviceSize)) {
return animations.backgroundRadius(status, textRadius).value;
} else {
// Scale down radius if icon is towards the middle of the screen.
return animations.backgroundRadius(status, textRadius).value * 0.8;
}
}
double get opacity => animations.backgroundOpacity(status).value;
@override
Widget build(BuildContext context) {
return Positioned(
left: centerPosition.dx,
top: centerPosition.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: color,
),
),
),
));
}
/// Compute the maximum distance from [point] to the four corners of [bounds].
double _maxDistance(Offset point, Rect bounds) {
double distance(double x1, double y1, double x2, double y2) {
return sqrt(pow(x2 - x1, 2) + pow(y2 - y1, 2));
}
final double tl = distance(point.dx, point.dy, bounds.left, bounds.top);
final double tr = distance(point.dx, point.dy, bounds.right, bounds.top);
final double bl = distance(point.dx, point.dy, bounds.left, bounds.bottom);
final double br = distance(point.dx, point.dy, bounds.right, bounds.bottom);
return max(tl, max(tr, max(bl, br)));
}
}
/// Widget that represents the text to show in the overlay.
class Content extends StatelessWidget {
const Content({
super.key,
required this.animations,
required this.center,
required this.description,
required this.deviceSize,
required this.status,
required this.title,
required this.textTheme,
});
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Description.
final String description;
/// Device size.
final Size deviceSize;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Title.
final String title;
/// [TextTheme] to use for drawing the [title] and the [description].
final TextTheme textTheme;
double get opacity => animations.contentOpacity(status).value;
@override
Widget build(BuildContext context) {
final Rect position = _getContentBounds(deviceSize, center);
return Positioned(
left: position.left,
height: position.bottom - position.top,
width: position.right - position.left,
top: position.top,
child: Opacity(
opacity: opacity,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_buildTitle(textTheme),
const SizedBox(height: 12.0),
_buildDescription(textTheme),
],
),
),
);
}
Widget _buildTitle(TextTheme theme) {
return Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: theme.titleLarge?.copyWith(color: Colors.white),
);
}
Widget _buildDescription(TextTheme theme) {
return Text(
description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: theme.titleMedium?.copyWith(color: Colors.white70),
);
}
}
/// Widget that represents the ripple effect of [TapTarget].
class Ripple extends StatelessWidget {
const Ripple({
super.key,
required this.animations,
required this.center,
required this.status,
});
/// Animations.
final Animations animations;
/// Overlay center position.
final Offset center;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
double get radius => animations.rippleRadius(status).value;
double get opacity => animations.rippleOpacity(status).value;
@override
Widget build(BuildContext context) {
return Positioned(
left: center.dx,
top: center.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
),
),
);
}
}
/// Wrapper widget around [child] representing the anchor of the overlay.
class TapTarget extends StatelessWidget {
const TapTarget({
super.key,
required this.animations,
required this.center,
required this.status,
required this.onTap,
required this.child,
});
/// Animations.
final Animations animations;
/// Device size.
final Offset center;
/// Status of the parent overlay.
final FeatureDiscoveryStatus status;
/// Callback invoked when the user taps on the [TapTarget].
final void Function() onTap;
/// Child widget that will be promoted by the overlay.
final Icon child;
double get radius => animations.tapTargetRadius(status).value;
double get opacity => animations.tapTargetOpacity(status).value;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
return Positioned(
left: center.dx,
top: center.dy,
child: FractionalTranslation(
translation: const Offset(-0.5, -0.5),
child: InkWell(
onTap: onTap,
child: Opacity(
opacity: opacity,
child: Container(
height: radius * 2,
width: radius * 2,
decoration: BoxDecoration(
color: theme.brightness == Brightness.dark
? theme.colorScheme.primary
: Colors.white,
shape: BoxShape.circle,
),
child: child,
),
),
),
),
);
}
}
/// Method to compute the bounds of the content.
///
/// This is exposed so it can be used for calculating the background radius
/// and center and for laying out the content.
Rect _getContentBounds(Size deviceSize, Offset overlayCenter) {
double top;
if (_isOnTopHalfOfScreen(overlayCenter, deviceSize)) {
top = overlayCenter.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
if (top < 0) {
top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance;
}
} else {
top = overlayCenter.dy + tapTargetRadius + tapTargetToContentDistance;
if (top + contentHeight > deviceSize.height) {
top = overlayCenter.dy -
tapTargetRadius -
tapTargetToContentDistance -
contentHeight;
}
}
final double left = max(contentHorizontalPadding, overlayCenter.dx - contentWidth);
final double right =
min(deviceSize.width - contentHorizontalPadding, left + contentWidth);
return Rect.fromLTRB(left, top, right, top + contentHeight);
}
bool _isNearTopOrBottomEdges(Offset position, Size deviceSize) {
return position.dy <= gutterHeight ||
(deviceSize.height - position.dy) <= gutterHeight;
}
bool _isOnTopHalfOfScreen(Offset position, Size deviceSize) {
return position.dy < (deviceSize.height / 2.0);
}
bool _isOnLeftHalfOfScreen(Offset position, Size deviceSize) {
return position.dx < (deviceSize.width / 2.0);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,828 @@
{
"loading": "Loading",
"deselect": "Deselect",
"select": "Select",
"selectable": "Selectable (long press)",
"selected": "Selected",
"demo": "Demo",
"bottomAppBar": "Bottom app bar",
"notSelected": "Not selected",
"demoCupertinoSearchTextFieldTitle": "Search text field",
"demoCupertinoPicker": "Picker",
"demoCupertinoSearchTextFieldSubtitle": "iOS-style search text field",
"demoCupertinoSearchTextFieldDescription": "A search text field that lets the user search by entering text and that can offer and filter suggestions.",
"demoCupertinoSearchTextFieldPlaceholder": "Enter some text",
"demoCupertinoScrollbarTitle": "Scrollbar",
"demoCupertinoScrollbarSubtitle": "iOS-style scrollbar",
"demoCupertinoScrollbarDescription": "A scrollbar that wraps the given child",
"demoTwoPaneItem": "Item {value}",
"demoTwoPaneList": "List",
"demoTwoPaneFoldableLabel": "Foldable",
"demoTwoPaneSmallScreenLabel": "Small screen",
"demoTwoPaneSmallScreenDescription": "This is how TwoPane behaves on a small screen device.",
"demoTwoPaneTabletLabel": "Tablet/Desktop",
"demoTwoPaneTabletDescription": "This is how TwoPane behaves on a larger screen like a tablet or desktop.",
"demoTwoPaneTitle": "TwoPane",
"demoTwoPaneSubtitle": "Responsive layouts on foldable, large and small screens",
"splashSelectDemo": "Select a demo",
"demoTwoPaneFoldableDescription": "This is how TwoPane behaves on a foldable device.",
"demoTwoPaneDetails": "Details",
"demoTwoPaneSelectItem": "Select an item",
"demoTwoPaneItemDetails": "Item {value} details",
"demoCupertinoContextMenuActionText": "Tap and hold the Flutter logo to see the context menu.",
"demoCupertinoContextMenuDescription": "An iOS-style full screen contextual menu that appears when an element is long-pressed.",
"demoAppBarTitle": "App bar",
"demoAppBarDescription": "The app bar provides content and actions related to the current screen. It's used for branding, screen titles, navigation and actions",
"demoDividerTitle": "Divider",
"demoDividerSubtitle": "A divider is a thin line that groups content in lists and layouts.",
"demoDividerDescription": "Dividers can be used in lists, drawers and elsewhere to separate content.",
"demoVerticalDividerTitle": "Vertical divider",
"demoCupertinoContextMenuTitle": "Context menu",
"demoCupertinoContextMenuSubtitle": "iOS-style context menu",
"demoAppBarSubtitle": "Displays information and actions relating to the current screen",
"demoCupertinoContextMenuActionOne": "Action one",
"demoCupertinoContextMenuActionTwo": "Action two",
"demoDateRangePickerDescription": "Shows a dialogue containing a Material Design date range picker.",
"demoDateRangePickerTitle": "Date range picker",
"demoNavigationDrawerUserName": "User name",
"demoNavigationDrawerUserEmail": "user.name@example.com",
"demoNavigationDrawerText": "Swipe from the edge or tap the upper-left icon to see the drawer",
"demoNavigationRailTitle": "Navigation rail",
"demoNavigationRailSubtitle": "Displaying a navigation rail within an app",
"demoNavigationRailDescription": "A material widget that is meant to be displayed at the left or right of an app to navigate between a small number of views, typically between three and five.",
"demoNavigationRailFirst": "First",
"demoNavigationDrawerTitle": "Navigation drawer",
"demoNavigationRailThird": "Third",
"replyStarredLabel": "Starred",
"demoTextButtonDescription": "A text button displays an ink splash on press but does not lift. Use text buttons on toolbars, in dialogues and inline with padding",
"demoElevatedButtonTitle": "Elevated button",
"demoElevatedButtonDescription": "Elevated buttons add dimension to mostly flat layouts. They emphasise functions on busy or wide spaces.",
"demoOutlinedButtonTitle": "Outlined button",
"demoOutlinedButtonDescription": "Outlined buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.",
"demoContainerTransformDemoInstructions": "Cards, lists and FAB",
"demoNavigationDrawerSubtitle": "Displaying a drawer within app bar",
"replyDescription": "An efficient, focused email app",
"demoNavigationDrawerDescription": "A Material Design panel that slides in horizontally from the edge of the screen to show navigation links in an application.",
"replyDraftsLabel": "Drafts",
"demoNavigationDrawerToPageOne": "Item one",
"replyInboxLabel": "Inbox",
"demoSharedXAxisDemoInstructions": "Next and back buttons",
"replySpamLabel": "Spam",
"replyTrashLabel": "Bin",
"replySentLabel": "Sent",
"demoNavigationRailSecond": "Second",
"demoNavigationDrawerToPageTwo": "Item two",
"demoFadeScaleDemoInstructions": "Modal and FAB",
"demoFadeThroughDemoInstructions": "Bottom navigation",
"demoSharedZAxisDemoInstructions": "Settings icon button",
"demoSharedYAxisDemoInstructions": "Sort by 'Recently played'",
"demoTextButtonTitle": "Text button",
"demoSharedZAxisBeefSandwichRecipeTitle": "Beef sandwich",
"demoSharedZAxisDessertRecipeDescription": "Dessert recipe",
"demoSharedYAxisAlbumTileSubtitle": "Artist",
"demoSharedYAxisAlbumTileTitle": "Album",
"demoSharedYAxisRecentSortTitle": "Recently played",
"demoSharedYAxisAlphabeticalSortTitle": "AZ",
"demoSharedYAxisAlbumCount": "268 albums",
"demoSharedYAxisTitle": "Shared y-axis",
"demoSharedXAxisCreateAccountButtonText": "CREATE ACCOUNT",
"demoFadeScaleAlertDialogDiscardButton": "DISCARD",
"demoSharedXAxisSignInTextFieldLabel": "Email or phone number",
"demoSharedXAxisSignInSubtitleText": "Sign in with your account",
"demoSharedXAxisSignInWelcomeText": "Hi David Park",
"demoSharedXAxisIndividualCourseSubtitle": "Shown individually",
"demoSharedXAxisBundledCourseSubtitle": "Bundled",
"demoFadeThroughAlbumsDestination": "Albums",
"demoSharedXAxisDesignCourseTitle": "Design",
"demoSharedXAxisIllustrationCourseTitle": "Illustration",
"demoSharedXAxisBusinessCourseTitle": "Business",
"demoSharedXAxisArtsAndCraftsCourseTitle": "Arts and crafts",
"demoMotionPlaceholderSubtitle": "Secondary text",
"demoFadeScaleAlertDialogCancelButton": "CANCEL",
"demoFadeScaleAlertDialogHeader": "Alert dialogue",
"demoFadeScaleHideFabButton": "HIDE FAB",
"demoFadeScaleShowFabButton": "SHOW FAB",
"demoFadeScaleShowAlertDialogButton": "SHOW MODAL",
"demoFadeScaleDescription": "The fade pattern is used for UI elements that enter or exit within the bounds of the screen, such as a dialogue that fades in the centre of the screen.",
"demoFadeScaleTitle": "Fade",
"demoFadeThroughTextPlaceholder": "123 photos",
"demoFadeThroughSearchDestination": "Search",
"demoFadeThroughPhotosDestination": "Photos",
"demoSharedXAxisCoursePageSubtitle": "Bundled categories appear as groups in your feed. You can always change this later.",
"demoFadeThroughDescription": "The fade-through pattern is used for transitions between UI elements that do not have a strong relationship to each other.",
"demoFadeThroughTitle": "Fade through",
"demoSharedZAxisHelpSettingLabel": "Help",
"demoMotionSubtitle": "All of the predefined transition patterns",
"demoSharedZAxisNotificationSettingLabel": "Notifications",
"demoSharedZAxisProfileSettingLabel": "Profile",
"demoSharedZAxisSavedRecipesListTitle": "Saved recipes",
"demoSharedZAxisBeefSandwichRecipeDescription": "Beef sandwich recipe",
"demoSharedZAxisCrabPlateRecipeDescription": "Crab plate recipe",
"demoSharedXAxisCoursePageTitle": "Streamline your courses",
"demoSharedZAxisCrabPlateRecipeTitle": "Crab",
"demoSharedZAxisShrimpPlateRecipeDescription": "Shrimp plate recipe",
"demoSharedZAxisShrimpPlateRecipeTitle": "Shrimp",
"demoContainerTransformTypeFadeThrough": "FADE THROUGH",
"demoSharedZAxisDessertRecipeTitle": "Dessert",
"demoSharedZAxisSandwichRecipeDescription": "Sandwich recipe",
"demoSharedZAxisSandwichRecipeTitle": "Sandwich",
"demoSharedZAxisBurgerRecipeDescription": "Burger recipe",
"demoSharedZAxisBurgerRecipeTitle": "Burger",
"demoSharedZAxisSettingsPageTitle": "Settings",
"demoSharedZAxisTitle": "Shared z-axis",
"demoSharedZAxisPrivacySettingLabel": "Privacy",
"demoMotionTitle": "Motion",
"demoContainerTransformTitle": "Container transform",
"demoContainerTransformDescription": "The container transform pattern is designed for transitions between UI elements that include a container. This pattern creates a visible connection between two UI elements",
"demoContainerTransformModalBottomSheetTitle": "Fade mode",
"demoContainerTransformTypeFade": "FADE",
"demoSharedYAxisAlbumTileDurationUnit": "min",
"demoMotionPlaceholderTitle": "Title",
"demoSharedXAxisForgotEmailButtonText": "FORGOT EMAIL?",
"demoMotionSmallPlaceholderSubtitle": "Secondary",
"demoMotionDetailsPageTitle": "Details page",
"demoMotionListTileTitle": "List item",
"demoSharedAxisDescription": "The shared axis pattern is used for transitions between the UI elements that have a spatial or navigational relationship. This pattern uses a shared transformation on the x, y or z axis to reinforce the relationship between elements.",
"demoSharedXAxisTitle": "Shared x-axis",
"demoSharedXAxisBackButtonText": "BACK",
"demoSharedXAxisNextButtonText": "NEXT",
"demoSharedXAxisCulinaryCourseTitle": "Culinary",
"githubRepo": "{repoName} GitHub repository",
"fortnightlyMenuUS": "US",
"fortnightlyMenuBusiness": "Business",
"fortnightlyMenuScience": "Science",
"fortnightlyMenuSports": "Sport",
"fortnightlyMenuTravel": "Travel",
"fortnightlyMenuCulture": "Culture",
"fortnightlyTrendingTechDesign": "TechDesign",
"rallyBudgetDetailAmountLeft": "Amount left",
"fortnightlyHeadlineArmy": "Reforming The Green Army from Within",
"fortnightlyDescription": "A content-focused news app",
"rallyBillDetailAmountDue": "Amount due",
"rallyBudgetDetailTotalCap": "Total cap",
"rallyBudgetDetailAmountUsed": "Amount used",
"fortnightlyTrendingHealthcareRevolution": "HealthcareRevolution",
"fortnightlyMenuFrontPage": "Front page",
"fortnightlyMenuWorld": "World",
"rallyBillDetailAmountPaid": "Amount paid",
"fortnightlyMenuPolitics": "Politics",
"fortnightlyHeadlineBees": "Farmland Bees in Short Supply",
"fortnightlyHeadlineGasoline": "The Future of Petrol",
"fortnightlyTrendingGreenArmy": "GreenArmy",
"fortnightlyHeadlineFeminists": "Feminists take on Partisanship",
"fortnightlyHeadlineFabrics": "Designers use Tech to make Futuristic Fabrics",
"fortnightlyHeadlineStocks": "As Stocks Stagnate, many Look to Currency",
"fortnightlyTrendingReform": "Reform",
"fortnightlyMenuTech": "Tech",
"fortnightlyHeadlineWar": "Divided American Lives During War",
"fortnightlyHeadlineHealthcare": "The Quiet, yet Powerful Healthcare Revolution",
"fortnightlyLatestUpdates": "Latest updates",
"fortnightlyTrendingStocks": "Stocks",
"rallyBillDetailTotalAmount": "Total amount",
"demoCupertinoPickerDateTime": "Date and time",
"signIn": "SIGN IN",
"dataTableRowWithSugar": "{value} with sugar",
"dataTableRowApplePie": "Apple pie",
"dataTableRowDonut": "Doughnut",
"dataTableRowHoneycomb": "Honeycomb",
"dataTableRowLollipop": "Lollipop",
"dataTableRowJellyBean": "Jelly bean",
"dataTableRowGingerbread": "Gingerbread",
"dataTableRowCupcake": "Cupcake",
"dataTableRowEclair": "Eclair",
"dataTableRowIceCreamSandwich": "Ice cream sandwich",
"dataTableRowFrozenYogurt": "Frozen yogurt",
"dataTableColumnIron": "Iron (%)",
"dataTableColumnCalcium": "Calcium (%)",
"dataTableColumnSodium": "Sodium (mg)",
"demoTimePickerTitle": "Time picker",
"demo2dTransformationsResetTooltip": "Reset transformations",
"dataTableColumnFat": "Fat (gm)",
"dataTableColumnCalories": "Calories",
"dataTableColumnDessert": "Dessert (1 serving)",
"cardsDemoTravelDestinationLocation1": "Thanjavur, Tamil Nadu",
"demoTimePickerDescription": "Shows a dialogue containing a material design time picker.",
"demoPickersShowPicker": "SHOW PICKER",
"demoTabsScrollingTitle": "Scrolling",
"demoTabsNonScrollingTitle": "Non-scrolling",
"craneHours": "{hours,plural,=1{1 h}other{{hours}h}}",
"craneMinutes": "{minutes,plural,=1{1 m}other{{minutes}m}}",
"craneFlightDuration": "{hoursShortForm} {minutesShortForm}",
"dataTableHeader": "Nutrition",
"demoDatePickerTitle": "Date picker",
"demoPickersSubtitle": "Date and time selection",
"demoPickersTitle": "Pickers",
"demo2dTransformationsEditTooltip": "Edit tile",
"demoDataTableDescription": "Data tables display information in a grid-like format of rows and columns. They organise information in a way that's easy to scan, so that users can look for patterns and insights.",
"demo2dTransformationsDescription": "Tap to edit tiles, and use gestures to move around the scene. Drag to pan, pinch to zoom, rotate with two fingers. Press the reset button to return to the starting orientation.",
"demo2dTransformationsSubtitle": "Pan, zoom, rotate",
"demo2dTransformationsTitle": "2D transformations",
"demoCupertinoTextFieldPIN": "PIN",
"demoCupertinoTextFieldDescription": "A text field allows the user to enter text, either with a hardware keyboard or with an on-screen keyboard.",
"demoCupertinoTextFieldSubtitle": "iOS-style text fields",
"demoCupertinoTextFieldTitle": "Text fields",
"demoDatePickerDescription": "Shows a dialogue containing a material design date picker.",
"demoCupertinoPickerTime": "Time",
"demoCupertinoPickerDate": "Date",
"demoCupertinoPickerTimer": "Timer",
"demoCupertinoPickerDescription": "An iOS-style picker widget that can be used to select strings, dates, times or both date and time.",
"demoCupertinoPickerSubtitle": "iOS-style pickers",
"demoCupertinoPickerTitle": "Pickers",
"dataTableRowWithHoney": "{value} with honey",
"cardsDemoTravelDestinationCity2": "Chettinad",
"bannerDemoResetText": "Reset the banner",
"bannerDemoMultipleText": "Multiple actions",
"bannerDemoLeadingText": "Leading icon",
"dismiss": "DISMISS",
"cardsDemoTappable": "Tappable",
"cardsDemoSelectable": "Selectable (long press)",
"cardsDemoExplore": "Explore",
"cardsDemoExploreSemantics": "Explore {destinationName}",
"cardsDemoShareSemantics": "Share {destinationName}",
"cardsDemoTravelDestinationTitle1": "Top 10 cities to visit in Tamil Nadu",
"cardsDemoTravelDestinationDescription1": "Number 10",
"cardsDemoTravelDestinationCity1": "Thanjavur",
"dataTableColumnProtein": "Protein (gm)",
"cardsDemoTravelDestinationTitle2": "Artisans of Southern India",
"cardsDemoTravelDestinationDescription2": "Silk spinners",
"bannerDemoText": "Your password was updated on your other device. Please sign in again.",
"cardsDemoTravelDestinationLocation2": "Sivaganga, Tamil Nadu",
"cardsDemoTravelDestinationTitle3": "Brihadisvara Temple",
"cardsDemoTravelDestinationDescription3": "Temples",
"demoBannerTitle": "Banner",
"demoBannerSubtitle": "Displaying a banner within a list",
"demoBannerDescription": "A banner displays an important, succinct message, and provides actions for users to address (or dismiss the banner). A user action is required for it to be dismissed.",
"demoCardTitle": "Cards",
"demoCardSubtitle": "Baseline cards with rounded corners",
"demoCardDescription": "A card is a sheet of material used to represent some related information, for example, an album, a geographical location, a meal, contact details, etc.",
"demoDataTableTitle": "Data tables",
"demoDataTableSubtitle": "Rows and columns of information",
"dataTableColumnCarbs": "Carbs (gm)",
"placeTanjore": "Tanjore",
"demoGridListsTitle": "Grid lists",
"placeFlowerMarket": "Flower market",
"placeBronzeWorks": "Bronze works",
"placeMarket": "Market",
"placeThanjavurTemple": "Thanjavur Temple",
"placeSaltFarm": "Salt farm",
"placeScooters": "Scooters",
"placeSilkMaker": "Silk maker",
"placeLunchPrep": "Lunch prep",
"placeBeach": "Beach",
"placeFisherman": "Fisherman",
"demoMenuSelected": "Selected: {value}",
"demoMenuRemove": "Remove",
"demoMenuGetLink": "Get link",
"demoMenuShare": "Share",
"demoBottomAppBarSubtitle": "Displays navigation and actions at the bottom",
"demoMenuAnItemWithASectionedMenu": "An item with a sectioned menu",
"demoMenuADisabledMenuItem": "Disabled menu item",
"demoLinearProgressIndicatorTitle": "Linear progress indicator",
"demoMenuContextMenuItemOne": "Context menu item one",
"demoMenuAnItemWithASimpleMenu": "An item with a simple menu",
"demoCustomSlidersTitle": "Custom sliders",
"demoMenuAnItemWithAChecklistMenu": "An item with a checklist menu",
"demoCupertinoActivityIndicatorTitle": "Activity indicator",
"demoCupertinoActivityIndicatorSubtitle": "iOS-style activity indicators",
"demoCupertinoActivityIndicatorDescription": "An iOS-style activity indicator that spins clockwise.",
"demoCupertinoNavigationBarTitle": "Navigation bar",
"demoCupertinoNavigationBarSubtitle": "iOS-style navigation bar",
"demoCupertinoNavigationBarDescription": "An iOS-styled navigation bar. The navigation bar is a toolbar that minimally consists of a page title, in the middle of the toolbar.",
"demoCupertinoPullToRefreshTitle": "Pull to refresh",
"demoCupertinoPullToRefreshSubtitle": "iOS-style pull to refresh control",
"demoCupertinoPullToRefreshDescription": "A widget implementing the iOS-style pull to refresh content control.",
"demoProgressIndicatorTitle": "Progress indicators",
"demoProgressIndicatorSubtitle": "Linear, circular, indeterminate",
"demoCircularProgressIndicatorTitle": "Circular progress indicator",
"demoCircularProgressIndicatorDescription": "A material design circular progress indicator, which spins to indicate that the application is busy.",
"demoMenuFour": "Four",
"demoLinearProgressIndicatorDescription": "A material design linear progress indicator, also known as a progress bar.",
"demoTooltipTitle": "Tooltips",
"demoTooltipSubtitle": "Short message displayed on long press or hover",
"demoTooltipDescription": "Tooltips provide text labels that help to explain the function of a button or other user interface action. Tooltips display informative text when users hover over, focus on or long press an element.",
"demoTooltipInstructions": "Long press or hover to display the tooltip.",
"placeChennai": "Chennai",
"demoMenuChecked": "Checked: {value}",
"placeChettinad": "Chettinad",
"demoMenuPreview": "Preview",
"demoBottomAppBarTitle": "Bottom app bar",
"demoBottomAppBarDescription": "Bottom app bars provide access to a bottom navigation drawer and up to four actions, including the floating action button.",
"bottomAppBarNotch": "Notch",
"bottomAppBarPosition": "Floating action button position",
"bottomAppBarPositionDockedEnd": "Docked - End",
"bottomAppBarPositionDockedCenter": "Docked - Centre",
"bottomAppBarPositionFloatingEnd": "Floating - End",
"bottomAppBarPositionFloatingCenter": "Floating - Centre",
"demoSlidersEditableNumericalValue": "Editable numerical value",
"demoGridListsSubtitle": "Row and column layout",
"demoGridListsDescription": "Grid lists are best suited for presenting homogeneous data, typically images. Each item in a grid list is called a tile.",
"demoGridListsImageOnlyTitle": "Image only",
"demoGridListsHeaderTitle": "With header",
"demoGridListsFooterTitle": "With footer",
"demoSlidersTitle": "Sliders",
"demoSlidersSubtitle": "Widgets for selecting a value by swiping",
"demoSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value. They are ideal for adjusting settings such as volume, brightness or applying image filters.",
"demoRangeSlidersTitle": "Range sliders",
"demoRangeSlidersDescription": "Sliders reflect a range of values along a bar. They can have icons on both ends of the bar that reflect a range of values. They are ideal for adjusting settings such as volume, brightness or applying image filters.",
"demoMenuAnItemWithAContextMenuButton": "An item with a context menu",
"demoCustomSlidersDescription": "Sliders reflect a range of values along a bar, from which users may select a single value or range of values. The sliders can be themed and customised.",
"demoSlidersContinuousWithEditableNumericalValue": "Continuous with editable numerical value",
"demoSlidersDiscrete": "Discrete",
"demoSlidersDiscreteSliderWithCustomTheme": "Discrete slider with custom theme",
"demoSlidersContinuousRangeSliderWithCustomTheme": "Continuous range slider with custom theme",
"demoSlidersContinuous": "Continuous",
"placePondicherry": "Pondicherry",
"demoMenuTitle": "Menu",
"demoContextMenuTitle": "Context menu",
"demoSectionedMenuTitle": "Sectioned menu",
"demoSimpleMenuTitle": "Simple menu",
"demoChecklistMenuTitle": "Checklist menu",
"demoMenuSubtitle": "Menu buttons and simple menus",
"demoMenuDescription": "A menu displays a list of choices on a temporary surface. They appear when users interact with a button, action or other control.",
"demoMenuItemValueOne": "Menu item one",
"demoMenuItemValueTwo": "Menu item two",
"demoMenuItemValueThree": "Menu item three",
"demoMenuOne": "One",
"demoMenuTwo": "Two",
"demoMenuThree": "Three",
"demoMenuContextMenuItemThree": "Context menu item three",
"demoCupertinoSwitchSubtitle": "iOS-style switch",
"demoSnackbarsText": "This is a snackbar.",
"demoCupertinoSliderSubtitle": "iOS-style slider",
"demoCupertinoSliderDescription": "A slider can be used to select from either a continuous or a discrete set of values.",
"demoCupertinoSliderContinuous": "Continuous: {value}",
"demoCupertinoSliderDiscrete": "Discrete: {value}",
"demoSnackbarsAction": "You pressed the snackbar action.",
"backToGallery": "Back to Gallery",
"demoCupertinoTabBarTitle": "Tab bar",
"demoCupertinoSwitchDescription": "A switch is used to toggle the on/off state of a single setting.",
"demoSnackbarsActionButtonLabel": "ACTION",
"cupertinoTabBarProfileTab": "Profile",
"demoSnackbarsButtonLabel": "SHOW A SNACKBAR",
"demoSnackbarsDescription": "Snackbars inform users of a process that an app has performed or will perform. They appear temporarily, towards the bottom of the screen. They shouldn't interrupt the user experience, and they don't require user input to disappear.",
"demoSnackbarsSubtitle": "Snackbars show messages at the bottom of the screen",
"demoSnackbarsTitle": "Snackbars",
"demoCupertinoSliderTitle": "Slider",
"cupertinoTabBarChatTab": "Chat",
"cupertinoTabBarHomeTab": "Home",
"demoCupertinoTabBarDescription": "An iOS-style bottom navigation tab bar. Displays multiple tabs with one tab being active, the first tab by default.",
"demoCupertinoTabBarSubtitle": "iOS-style bottom tab bar",
"demoOptionsFeatureTitle": "View options",
"demoOptionsFeatureDescription": "Tap here to view available options for this demo.",
"demoCodeViewerCopyAll": "COPY ALL",
"shrineScreenReaderRemoveProductButton": "Remove {product}",
"shrineScreenReaderProductAddToCart": "Add to basket",
"shrineScreenReaderCart": "{quantity,plural,=0{Shopping basket, no items}=1{Shopping basket, 1 item}other{Shopping basket, {quantity} items}}",
"demoCodeViewerFailedToCopyToClipboardMessage": "Failed to copy to clipboard: {error}",
"demoCodeViewerCopiedToClipboardMessage": "Copied to clipboard.",
"craneSleep8SemanticLabel": "Mayan ruins on a cliff above a beach",
"craneSleep4SemanticLabel": "Lake-side hotel in front of mountains",
"craneSleep2SemanticLabel": "Machu Picchu citadel",
"craneSleep1SemanticLabel": "Chalet in a snowy landscape with evergreen trees",
"craneSleep0SemanticLabel": "Overwater bungalows",
"craneFly13SemanticLabel": "Seaside pool with palm trees",
"craneFly12SemanticLabel": "Pool with palm trees",
"craneFly11SemanticLabel": "Brick lighthouse at sea",
"craneFly10SemanticLabel": "Al-Azhar Mosque towers during sunset",
"craneFly9SemanticLabel": "Man leaning on an antique blue car",
"craneFly8SemanticLabel": "Supertree Grove",
"craneEat9SemanticLabel": "Café counter with pastries",
"craneEat2SemanticLabel": "Burger",
"craneFly5SemanticLabel": "Lake-side hotel in front of mountains",
"demoSelectionControlsSubtitle": "Tick boxes, radio buttons and switches",
"craneEat10SemanticLabel": "Woman holding huge pastrami sandwich",
"craneFly4SemanticLabel": "Overwater bungalows",
"craneEat7SemanticLabel": "Bakery entrance",
"craneEat6SemanticLabel": "Shrimp dish",
"craneEat5SemanticLabel": "Artsy restaurant seating area",
"craneEat4SemanticLabel": "Chocolate dessert",
"craneEat3SemanticLabel": "Korean taco",
"craneFly3SemanticLabel": "Machu Picchu citadel",
"craneEat1SemanticLabel": "Empty bar with diner-style stools",
"craneEat0SemanticLabel": "Pizza in a wood-fired oven",
"craneSleep11SemanticLabel": "Taipei 101 skyscraper",
"craneSleep10SemanticLabel": "Al-Azhar Mosque towers during sunset",
"craneSleep9SemanticLabel": "Brick lighthouse at sea",
"craneEat8SemanticLabel": "Plate of crawfish",
"craneSleep7SemanticLabel": "Colourful apartments at Ribeira Square",
"craneSleep6SemanticLabel": "Pool with palm trees",
"craneSleep5SemanticLabel": "Tent in a field",
"settingsButtonCloseLabel": "Close settings",
"demoSelectionControlsCheckboxDescription": "Tick boxes allow the user to select multiple options from a set. A normal tick box's value is true or false and a tristate tick box's value can also be null.",
"settingsButtonLabel": "Settings",
"demoListsTitle": "Lists",
"demoListsSubtitle": "Scrolling list layouts",
"demoListsDescription": "A single fixed-height row that typically contains some text as well as a leading or trailing icon.",
"demoOneLineListsTitle": "One line",
"demoTwoLineListsTitle": "Two lines",
"demoListsSecondary": "Secondary text",
"demoSelectionControlsTitle": "Selection controls",
"craneFly7SemanticLabel": "Mount Rushmore",
"demoSelectionControlsCheckboxTitle": "Tick box",
"craneSleep3SemanticLabel": "Man leaning on an antique blue car",
"demoSelectionControlsRadioTitle": "Radio",
"demoSelectionControlsRadioDescription": "Radio buttons allow the user to select one option from a set. Use radio buttons for exclusive selection if you think that the user needs to see all available options side by side.",
"demoSelectionControlsSwitchTitle": "Switch",
"demoSelectionControlsSwitchDescription": "On/off switches toggle the state of a single settings option. The option that the switch controls, as well as the state it's in, should be made clear from the corresponding inline label.",
"craneFly0SemanticLabel": "Chalet in a snowy landscape with evergreen trees",
"craneFly1SemanticLabel": "Tent in a field",
"craneFly2SemanticLabel": "Prayer flags in front of snowy mountain",
"craneFly6SemanticLabel": "Aerial view of Palacio de Bellas Artes",
"rallySeeAllAccounts": "See all accounts",
"rallyBillAmount": "{billName} bill due {date} for {amount}.",
"shrineTooltipCloseCart": "Close basket",
"shrineTooltipCloseMenu": "Close menu",
"shrineTooltipOpenMenu": "Open menu",
"shrineTooltipSettings": "Settings",
"shrineTooltipSearch": "Search",
"demoTabsDescription": "Tabs organise content across different screens, data sets and other interactions.",
"demoTabsSubtitle": "Tabs with independently scrollable views",
"demoTabsTitle": "Tabs",
"rallyBudgetAmount": "{budgetName} budget with {amountUsed} used of {amountTotal}, {amountLeft} left",
"shrineTooltipRemoveItem": "Remove item",
"rallyAccountAmount": "{accountName} account {accountNumber} with {amount}.",
"rallySeeAllBudgets": "See all budgets",
"rallySeeAllBills": "See all bills",
"craneFormDate": "Select date",
"craneFormOrigin": "Choose origin",
"craneFly2": "Khumbu Valley, Nepal",
"craneFly3": "Machu Picchu, Peru",
"craneFly4": "Malé, Maldives",
"craneFly5": "Vitznau, Switzerland",
"craneFly6": "Mexico City, Mexico",
"craneFly7": "Mount Rushmore, United States",
"settingsTextDirectionLocaleBased": "Based on locale",
"craneFly9": "Havana, Cuba",
"craneFly10": "Cairo, Egypt",
"craneFly11": "Lisbon, Portugal",
"craneFly12": "Napa, United States",
"craneFly13": "Bali, Indonesia",
"craneSleep0": "Malé, Maldives",
"craneSleep1": "Aspen, United States",
"craneSleep2": "Machu Picchu, Peru",
"demoCupertinoSegmentedControlTitle": "Segmented control",
"craneSleep4": "Vitznau, Switzerland",
"craneSleep5": "Big Sur, United States",
"craneSleep6": "Napa, United States",
"craneSleep7": "Porto, Portugal",
"craneSleep8": "Tulum, Mexico",
"craneEat5": "Seoul, South Korea",
"demoChipTitle": "Chips",
"demoChipSubtitle": "Compact elements that represent an input, attribute or action",
"demoActionChipTitle": "Action chip",
"demoActionChipDescription": "Action chips are a set of options which trigger an action related to primary content. Action chips should appear dynamically and contextually in a UI.",
"demoChoiceChipTitle": "Choice chip",
"demoChoiceChipDescription": "Choice chips represent a single choice from a set. Choice chips contain related descriptive text or categories.",
"demoFilterChipTitle": "Filter chip",
"demoFilterChipDescription": "Filter chips use tags or descriptive words as a way to filter content.",
"demoInputChipTitle": "Input chip",
"demoInputChipDescription": "Input chips represent a complex piece of information, such as an entity (person, place or thing) or conversational text, in a compact form.",
"craneSleep9": "Lisbon, Portugal",
"craneEat10": "Lisbon, Portugal",
"demoCupertinoSegmentedControlDescription": "Used to select between a number of mutually exclusive options. When one option in the segmented control is selected, the other options in the segmented control cease to be selected.",
"chipTurnOnLights": "Turn on lights",
"chipSmall": "Small",
"chipMedium": "Medium",
"chipLarge": "Large",
"chipElevator": "Lift",
"chipWasher": "Washing machine",
"chipFireplace": "Fireplace",
"chipBiking": "Cycling",
"craneFormDiners": "Diners",
"rallyAlertsMessageUnassignedTransactions": "{count,plural,=1{Increase your potential tax deduction! Assign categories to 1 unassigned transaction.}other{Increase your potential tax deduction! Assign categories to {count} unassigned transactions.}}",
"craneFormTime": "Select time",
"craneFormLocation": "Select location",
"craneFormTravelers": "Travellers",
"craneEat8": "Atlanta, United States",
"craneFormDestination": "Choose destination",
"craneFormDates": "Select dates",
"craneFly": "FLY",
"craneSleep": "SLEEP",
"craneEat": "EAT",
"craneFlySubhead": "Explore flights by destination",
"craneSleepSubhead": "Explore properties by destination",
"craneEatSubhead": "Explore restaurants by destination",
"craneFlyStops": "{numberOfStops,plural,=0{Non-stop}=1{1 stop}other{{numberOfStops} stops}}",
"craneSleepProperties": "{totalProperties,plural,=0{No available properties}=1{1 available property}other{{totalProperties} available properties}}",
"craneEatRestaurants": "{totalRestaurants,plural,=0{No restaurants}=1{1 restaurant}other{{totalRestaurants} restaurants}}",
"craneFly0": "Aspen, United States",
"demoCupertinoSegmentedControlSubtitle": "iOS-style segmented control",
"craneSleep10": "Cairo, Egypt",
"craneEat9": "Madrid, Spain",
"craneFly1": "Big Sur, United States",
"craneEat7": "Nashville, United States",
"craneEat6": "Seattle, United States",
"craneFly8": "Singapore",
"craneEat4": "Paris, France",
"craneEat3": "Portland, United States",
"craneEat2": "Córdoba, Argentina",
"craneEat1": "Dallas, United States",
"craneEat0": "Naples, Italy",
"craneSleep11": "Taipei, Taiwan",
"craneSleep3": "Havana, Cuba",
"shrineLogoutButtonCaption": "LOGOUT",
"rallyTitleBills": "BILLS",
"rallyTitleAccounts": "ACCOUNTS",
"shrineProductVagabondSack": "Vagabond sack",
"rallyAccountDetailDataInterestYtd": "Interest YTD",
"shrineProductWhitneyBelt": "Whitney belt",
"shrineProductGardenStrand": "Garden strand",
"shrineProductStrutEarrings": "Strut earrings",
"shrineProductVarsitySocks": "Varsity socks",
"shrineProductWeaveKeyring": "Weave keyring",
"shrineProductGatsbyHat": "Gatsby hat",
"shrineProductShrugBag": "Shrug bag",
"shrineProductGiltDeskTrio": "Gilt desk trio",
"shrineProductCopperWireRack": "Copper wire rack",
"shrineProductSootheCeramicSet": "Soothe ceramic set",
"shrineProductHurrahsTeaSet": "Hurrahs tea set",
"shrineProductBlueStoneMug": "Blue stone mug",
"shrineProductRainwaterTray": "Rainwater tray",
"shrineProductChambrayNapkins": "Chambray napkins",
"shrineProductSucculentPlanters": "Succulent planters",
"shrineProductQuartetTable": "Quartet table",
"shrineProductKitchenQuattro": "Kitchen quattro",
"shrineProductClaySweater": "Clay sweater",
"shrineProductSeaTunic": "Sea tunic",
"shrineProductPlasterTunic": "Plaster tunic",
"rallyBudgetCategoryRestaurants": "Restaurants",
"shrineProductChambrayShirt": "Chambray shirt",
"shrineProductSeabreezeSweater": "Seabreeze sweater",
"shrineProductGentryJacket": "Gentry jacket",
"shrineProductNavyTrousers": "Navy trousers",
"shrineProductWalterHenleyWhite": "Walter henley (white)",
"shrineProductSurfAndPerfShirt": "Surf and perf shirt",
"shrineProductGingerScarf": "Ginger scarf",
"shrineProductRamonaCrossover": "Ramona crossover",
"shrineProductClassicWhiteCollar": "Classic white collar",
"shrineProductSunshirtDress": "Sunshirt dress",
"rallyAccountDetailDataInterestRate": "Interest rate",
"rallyAccountDetailDataAnnualPercentageYield": "Annual percentage yield",
"rallyAccountDataVacation": "Holiday",
"shrineProductFineLinesTee": "Fine lines tee",
"rallyAccountDataHomeSavings": "Home savings",
"rallyAccountDataChecking": "Current",
"rallyAccountDetailDataInterestPaidLastYear": "Interest paid last year",
"rallyAccountDetailDataNextStatement": "Next statement",
"rallyAccountDetailDataAccountOwner": "Account owner",
"rallyBudgetCategoryCoffeeShops": "Coffee shops",
"rallyBudgetCategoryGroceries": "Groceries",
"shrineProductCeriseScallopTee": "Cerise scallop tee",
"rallyBudgetCategoryClothing": "Clothing",
"rallySettingsManageAccounts": "Manage accounts",
"rallyAccountDataCarSavings": "Car savings",
"rallySettingsTaxDocuments": "Tax documents",
"rallySettingsPasscodeAndTouchId": "Passcode and Touch ID",
"rallySettingsNotifications": "Notifications",
"rallySettingsPersonalInformation": "Personal information",
"rallySettingsPaperlessSettings": "Paperless settings",
"rallySettingsFindAtms": "Find ATMs",
"rallySettingsHelp": "Help",
"rallySettingsSignOut": "Sign out",
"rallyAccountTotal": "Total",
"rallyBillsDue": "Due",
"rallyBudgetLeft": "Left",
"rallyAccounts": "Accounts",
"rallyBills": "Bills",
"rallyBudgets": "Budgets",
"rallyAlerts": "Alerts",
"rallySeeAll": "SEE ALL",
"rallyFinanceLeft": "LEFT",
"rallyTitleOverview": "OVERVIEW",
"shrineProductShoulderRollsTee": "Shoulder rolls tee",
"shrineNextButtonCaption": "NEXT",
"rallyTitleBudgets": "BUDGETS",
"rallyTitleSettings": "SETTINGS",
"rallyLoginLoginToRally": "Log in to Rally",
"rallyLoginNoAccount": "Don't have an account?",
"rallyLoginSignUp": "SIGN UP",
"rallyLoginUsername": "Username",
"rallyLoginPassword": "Password",
"rallyLoginLabelLogin": "Log in",
"rallyLoginRememberMe": "Remember me",
"rallyLoginButtonLogin": "LOGIN",
"rallyAlertsMessageHeadsUpShopping": "Heads up: you've used up {percent} of your shopping budget for this month.",
"rallyAlertsMessageSpentOnRestaurants": "You've spent {amount} on restaurants this week.",
"rallyAlertsMessageATMFees": "You've spent {amount} in ATM fees this month",
"rallyAlertsMessageCheckingAccount": "Good work! Your current account is {percent} higher than last month.",
"shrineMenuCaption": "MENU",
"shrineCategoryNameAll": "ALL",
"shrineCategoryNameAccessories": "ACCESSORIES",
"shrineCategoryNameClothing": "CLOTHING",
"shrineCategoryNameHome": "HOME",
"shrineLoginUsernameLabel": "Username",
"shrineLoginPasswordLabel": "Password",
"shrineCancelButtonCaption": "CANCEL",
"shrineCartTaxCaption": "Tax:",
"shrineCartPageCaption": "BASKET",
"shrineProductQuantity": "Quantity: {quantity}",
"shrineProductPrice": "x {price}",
"shrineCartItemCount": "{quantity,plural,=0{NO ITEMS}=1{1 ITEM}other{{quantity} ITEMS}}",
"shrineCartClearButtonCaption": "CLEAR BASKET",
"shrineCartTotalCaption": "TOTAL",
"shrineCartSubtotalCaption": "Subtotal:",
"shrineCartShippingCaption": "Delivery:",
"shrineProductGreySlouchTank": "Grey slouch tank top",
"shrineProductStellaSunglasses": "Stella sunglasses",
"shrineProductWhitePinstripeShirt": "White pinstripe shirt",
"demoTextFieldWhereCanWeReachYou": "Where can we contact you?",
"settingsTextDirectionLTR": "LTR",
"settingsTextScalingLarge": "Large",
"demoBottomSheetHeader": "Header",
"demoBottomSheetItem": "Item {value}",
"demoBottomTextFieldsTitle": "Text fields",
"demoTextFieldTitle": "Text fields",
"demoTextFieldSubtitle": "Single line of editable text and numbers",
"demoTextFieldDescription": "Text fields allow users to enter text into a UI. They typically appear in forms and dialogues.",
"demoTextFieldShowPasswordLabel": "Show password",
"demoTextFieldHidePasswordLabel": "Hide password",
"demoTextFieldFormErrors": "Please fix the errors in red before submitting.",
"demoTextFieldNameRequired": "Name is required.",
"demoTextFieldOnlyAlphabeticalChars": "Please enter only alphabetical characters.",
"demoTextFieldEnterUSPhoneNumber": "(###) ###-#### Enter a US phone number.",
"demoTextFieldEnterPassword": "Please enter a password.",
"demoTextFieldPasswordsDoNotMatch": "The passwords don't match",
"demoTextFieldWhatDoPeopleCallYou": "What do people call you?",
"demoTextFieldNameField": "Name*",
"demoBottomSheetButtonText": "SHOW BOTTOM SHEET",
"demoTextFieldPhoneNumber": "Phone number*",
"demoBottomSheetTitle": "Bottom sheet",
"demoTextFieldEmail": "Email",
"demoTextFieldTellUsAboutYourself": "Tell us about yourself (e.g. write down what you do or what hobbies you have)",
"demoTextFieldKeepItShort": "Keep it short, this is just a demo.",
"starterAppGenericButton": "BUTTON",
"demoTextFieldLifeStory": "Life story",
"demoTextFieldSalary": "Salary",
"demoTextFieldUSD": "USD",
"demoTextFieldNoMoreThan": "No more than 8 characters.",
"demoTextFieldPassword": "Password*",
"demoTextFieldRetypePassword": "Re-type password*",
"demoTextFieldSubmit": "SUBMIT",
"demoBottomNavigationSubtitle": "Bottom navigation with cross-fading views",
"demoBottomSheetAddLabel": "Add",
"demoBottomSheetModalDescription": "A modal bottom sheet is an alternative to a menu or a dialogue and prevents the user from interacting with the rest of the app.",
"demoBottomSheetModalTitle": "Modal bottom sheet",
"demoBottomSheetPersistentDescription": "A persistent bottom sheet shows information that supplements the primary content of the app. A persistent bottom sheet remains visible even when the user interacts with other parts of the app.",
"demoBottomSheetPersistentTitle": "Persistent bottom sheet",
"demoBottomSheetSubtitle": "Persistent and modal bottom sheets",
"demoTextFieldNameHasPhoneNumber": "{name} phone number is {phoneNumber}",
"buttonText": "BUTTON",
"demoTypographyDescription": "Definitions for the various typographical styles found in Material Design.",
"demoTypographySubtitle": "All of the predefined text styles",
"demoTypographyTitle": "Typography",
"demoFullscreenDialogDescription": "The fullscreenDialog property specifies whether the incoming page is a full-screen modal dialogue",
"demoFlatButtonDescription": "A flat button displays an ink splash on press but does not lift. Use flat buttons on toolbars, in dialogues and inline with padding",
"demoBottomNavigationDescription": "Bottom navigation bars display three to five destinations at the bottom of a screen. Each destination is represented by an icon and an optional text label. When a bottom navigation icon is tapped, the user is taken to the top-level navigation destination associated with that icon.",
"demoBottomNavigationSelectedLabel": "Selected label",
"demoBottomNavigationPersistentLabels": "Persistent labels",
"starterAppDrawerItem": "Item {value}",
"demoTextFieldRequiredField": "* indicates required field",
"demoBottomNavigationTitle": "Bottom navigation",
"settingsLightTheme": "Light",
"settingsTheme": "Theme",
"settingsPlatformIOS": "iOS",
"settingsPlatformAndroid": "Android",
"settingsTextDirectionRTL": "RTL",
"settingsTextScalingHuge": "Huge",
"cupertinoButton": "Button",
"settingsTextScalingNormal": "Normal",
"settingsTextScalingSmall": "Small",
"settingsSystemDefault": "System",
"settingsTitle": "Settings",
"rallyDescription": "A personal finance app",
"aboutDialogDescription": "To see the source code for this app, please visit the {repoLink}.",
"bottomNavigationCommentsTab": "Comments",
"starterAppGenericBody": "Body",
"starterAppGenericHeadline": "Headline",
"starterAppGenericSubtitle": "Subtitle",
"starterAppGenericTitle": "Title",
"starterAppTooltipSearch": "Search",
"starterAppTooltipShare": "Share",
"starterAppTooltipFavorite": "Favourite",
"starterAppTooltipAdd": "Add",
"bottomNavigationCalendarTab": "Calendar",
"starterAppDescription": "A responsive starter layout",
"starterAppTitle": "Starter app",
"aboutFlutterSamplesRepo": "Flutter samples GitHub repo",
"bottomNavigationContentPlaceholder": "Placeholder for {title} tab",
"bottomNavigationCameraTab": "Camera",
"bottomNavigationAlarmTab": "Alarm",
"bottomNavigationAccountTab": "Account",
"demoTextFieldYourEmailAddress": "Your email address",
"demoToggleButtonDescription": "Toggle buttons can be used to group related options. To emphasise groups of related toggle buttons, a group should share a common container",
"colorsGrey": "GREY",
"colorsBrown": "BROWN",
"colorsDeepOrange": "DEEP ORANGE",
"colorsOrange": "ORANGE",
"colorsAmber": "AMBER",
"colorsYellow": "YELLOW",
"colorsLime": "LIME",
"colorsLightGreen": "LIGHT GREEN",
"colorsGreen": "GREEN",
"homeHeaderGallery": "Gallery",
"homeHeaderCategories": "Categories",
"shrineDescription": "A fashionable retail app",
"craneDescription": "A personalised travel app",
"homeCategoryReference": "STYLES AND OTHER",
"demoInvalidURL": "Couldn't display URL:",
"demoOptionsTooltip": "Options",
"demoInfoTooltip": "Info",
"demoCodeTooltip": "Demo code",
"demoDocumentationTooltip": "API Documentation",
"demoFullscreenTooltip": "Full screen",
"settingsTextScaling": "Text scaling",
"settingsTextDirection": "Text direction",
"settingsLocale": "Locale",
"settingsPlatformMechanics": "Platform mechanics",
"settingsDarkTheme": "Dark",
"settingsSlowMotion": "Slow motion",
"settingsAbout": "About Flutter Gallery",
"settingsFeedback": "Send feedback",
"settingsAttribution": "Designed by TOASTER in London",
"demoButtonTitle": "Buttons",
"demoButtonSubtitle": "Text, elevated, outlined and more",
"demoFlatButtonTitle": "Flat Button",
"demoRaisedButtonDescription": "Raised buttons add dimension to mostly flat layouts. They emphasise functions on busy or wide spaces.",
"demoRaisedButtonTitle": "Raised Button",
"demoOutlineButtonTitle": "Outline Button",
"demoOutlineButtonDescription": "Outline buttons become opaque and elevate when pressed. They are often paired with raised buttons to indicate an alternative, secondary action.",
"demoToggleButtonTitle": "Toggle Buttons",
"colorsTeal": "TEAL",
"demoFloatingButtonTitle": "Floating Action Button",
"demoFloatingButtonDescription": "A floating action button is a circular icon button that hovers over content to promote a primary action in the application.",
"demoDialogTitle": "Dialogues",
"demoDialogSubtitle": "Simple, alert and full-screen",
"demoAlertDialogTitle": "Alert",
"demoAlertDialogDescription": "An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title and an optional list of actions.",
"demoAlertTitleDialogTitle": "Alert With Title",
"demoSimpleDialogTitle": "Simple",
"demoSimpleDialogDescription": "A simple dialogue offers the user a choice between several options. A simple dialogue has an optional title that is displayed above the choices.",
"demoFullscreenDialogTitle": "Full screen",
"demoCupertinoButtonsTitle": "Buttons",
"demoCupertinoButtonsSubtitle": "iOS-style buttons",
"demoCupertinoButtonsDescription": "An iOS-style button. It takes in text and/or an icon that fades out and in on touch. May optionally have a background.",
"demoCupertinoAlertsTitle": "Alerts",
"demoCupertinoAlertsSubtitle": "iOS-style alert dialogues",
"demoCupertinoAlertTitle": "Alert",
"demoCupertinoAlertDescription": "An alert dialogue informs the user about situations that require acknowledgement. An alert dialogue has an optional title, optional content and an optional list of actions. The title is displayed above the content and the actions are displayed below the content.",
"demoCupertinoAlertWithTitleTitle": "Alert with title",
"demoCupertinoAlertButtonsTitle": "Alert With Buttons",
"demoCupertinoAlertButtonsOnlyTitle": "Alert Buttons Only",
"demoCupertinoActionSheetTitle": "Action Sheet",
"demoCupertinoActionSheetDescription": "An action sheet is a specific style of alert that presents the user with a set of two or more choices related to the current context. An action sheet can have a title, an additional message and a list of actions.",
"demoColorsTitle": "Colours",
"demoColorsSubtitle": "All of the predefined colours",
"demoColorsDescription": "Colour and colour swatch constants which represent Material Design's colour palette.",
"buttonTextEnabled": "ENABLED",
"buttonTextDisabled": "DISABLED",
"buttonTextCreate": "Create",
"dialogSelectedOption": "You selected: '{value}'",
"dialogDiscardTitle": "Discard draft?",
"dialogLocationTitle": "Use Google's location service?",
"dialogLocationDescription": "Let Google help apps determine location. This means sending anonymous location data to Google, even when no apps are running.",
"dialogCancel": "CANCEL",
"dialogDiscard": "DISCARD",
"dialogDisagree": "DISAGREE",
"dialogAgree": "AGREE",
"dialogSetBackup": "Set backup account",
"colorsBlueGrey": "BLUE GREY",
"dialogShow": "SHOW DIALOGUE",
"dialogFullscreenTitle": "Full-Screen Dialogue",
"dialogFullscreenSave": "SAVE",
"dialogFullscreenDescription": "A full-screen dialogue demo",
"cupertinoButtonEnabled": "Enabled",
"cupertinoButtonDisabled": "Disabled",
"cupertinoButtonWithBackground": "With background",
"cupertinoAlertCancel": "Cancel",
"cupertinoAlertDiscard": "Discard",
"cupertinoAlertLocationTitle": "Allow 'Maps' to access your location while you are using the app?",
"cupertinoAlertLocationDescription": "Your current location will be displayed on the map and used for directions, nearby search results and estimated travel times.",
"cupertinoAlertAllow": "Allow",
"cupertinoAlertDontAllow": "Don't allow",
"cupertinoAlertFavoriteDessert": "Select Favourite Dessert",
"cupertinoAlertDessertDescription": "Please select your favourite type of dessert from the list below. Your selection will be used to customise the suggested list of eateries in your area.",
"cupertinoAlertCheesecake": "Cheesecake",
"cupertinoAlertTiramisu": "Tiramisu",
"cupertinoAlertApplePie": "Apple Pie",
"cupertinoAlertChocolateBrownie": "Chocolate brownie",
"cupertinoShowAlert": "Show alert",
"colorsRed": "RED",
"colorsPink": "PINK",
"colorsPurple": "PURPLE",
"colorsDeepPurple": "DEEP PURPLE",
"colorsIndigo": "INDIGO",
"colorsBlue": "BLUE",
"colorsLightBlue": "LIGHT BLUE",
"colorsCyan": "CYAN",
"dialogAddAccount": "Add account",
"Gallery": "Gallery",
"Categories": "Categories",
"SHRINE": "SHRINE",
"Basic shopping app": "Basic shopping app",
"RALLY": "RALLY",
"CRANE": "CRANE",
"Travel app": "Travel app",
"MATERIAL": "MATERIAL",
"CUPERTINO": "CUPERTINO",
"REFERENCE STYLES & MEDIA": "REFERENCE STYLES & MEDIA"
}

View File

@ -0,0 +1,44 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:adaptive_breakpoints/adaptive_breakpoints.dart';
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/material.dart';
/// The maximum width taken up by each item on the home screen.
const double maxHomeItemWidth = 1400.0;
/// Returns a boolean value whether the window is considered medium or large size.
///
/// When running on a desktop device that is also foldable, the display is not
/// considered desktop. Widgets using this method might consider the display is
/// large enough for certain layouts, which is not the case on foldable devices,
/// where only part of the display is available to said widgets.
///
/// Used to build adaptive and responsive layouts.
bool isDisplayDesktop(BuildContext context) =>
!isDisplayFoldable(context) &&
getWindowType(context) >= AdaptiveWindowType.medium;
/// Returns boolean value whether the window is considered medium size.
///
/// Used to build adaptive and responsive layouts.
bool isDisplaySmallDesktop(BuildContext context) {
return getWindowType(context) == AdaptiveWindowType.medium;
}
/// Returns a boolean value whether the display has a hinge that splits the
/// screen into two, left and right sub-screens. Horizontal splits (top and
/// bottom sub-screens) are ignored for this application.
bool isDisplayFoldable(BuildContext context) {
final DisplayFeature? hinge = MediaQuery.of(context).hinge;
if (hinge == null) {
return false;
} else {
// Vertical
return hinge.bounds.size.aspectRatio < 1;
}
}

View File

@ -0,0 +1,98 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
/// [HighlightFocus] is a helper widget for giving a child focus
/// allowing tab-navigation.
/// Wrap your widget as [child] of a [HighlightFocus] widget.
class HighlightFocus extends StatefulWidget {
const HighlightFocus({
super.key,
required this.onPressed,
required this.child,
this.highlightColor,
this.borderColor,
this.hasFocus = true,
this.debugLabel,
});
/// [onPressed] is called when you press space, enter, or numpad-enter
/// when the widget is focused.
final VoidCallback onPressed;
/// [child] is your widget.
final Widget child;
/// [highlightColor] is the color filled in the border when the widget
/// is focused.
/// Use [Colors.transparent] if you do not want one.
/// Use an opacity less than 1 to make the underlying widget visible.
final Color? highlightColor;
/// [borderColor] is the color of the border when the widget is focused.
final Color? borderColor;
/// [hasFocus] is true when focusing on the widget is allowed.
/// Set to false if you want the child to skip focus.
final bool hasFocus;
final String? debugLabel;
@override
State<HighlightFocus> createState() => _HighlightFocusState();
}
class _HighlightFocusState extends State<HighlightFocus> {
late bool isFocused;
@override
void initState() {
isFocused = false;
super.initState();
}
@override
Widget build(BuildContext context) {
final Color highlightColor = widget.highlightColor ??
Theme.of(context).colorScheme.primary.withOpacity(0.5);
final Color borderColor =
widget.borderColor ?? Theme.of(context).colorScheme.onPrimary;
final BoxDecoration highlightedDecoration = BoxDecoration(
color: highlightColor,
border: Border.all(
color: borderColor,
width: 2,
strokeAlign: BorderSide.strokeAlignOutside,
),
);
return Focus(
canRequestFocus: widget.hasFocus,
debugLabel: widget.debugLabel,
onFocusChange: (bool newValue) {
setState(() {
isFocused = newValue;
});
},
onKeyEvent: (FocusNode node, KeyEvent event) {
if ((event is KeyDownEvent || event is KeyRepeatEvent) &&
(event.logicalKey == LogicalKeyboardKey.space ||
event.logicalKey == LogicalKeyboardKey.enter ||
event.logicalKey == LogicalKeyboardKey.numpadEnter)) {
widget.onPressed();
return KeyEventResult.handled;
} else {
return KeyEventResult.ignored;
}
},
child: Container(
foregroundDecoration: isFocused ? highlightedDecoration : null,
child: widget.child,
),
);
}
}

View File

@ -0,0 +1,75 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
/// An image that shows a [placeholder] widget while the target [image] is
/// loading, then fades in the new image when it loads.
///
/// This is similar to [FadeInImage] but the difference is that it allows you
/// to specify a widget as a [placeholder], instead of just an [ImageProvider].
/// It also lets you override the [child] argument, in case you want to wrap
/// the image with another widget, for example an [Ink.image].
class FadeInImagePlaceholder extends StatelessWidget {
const FadeInImagePlaceholder({
super.key,
required this.image,
required this.placeholder,
this.child,
this.duration = const Duration(milliseconds: 500),
this.excludeFromSemantics = false,
this.width,
this.height,
this.fit,
});
/// The target image that we are loading into memory.
final ImageProvider image;
/// Widget displayed while the target [image] is loading.
final Widget placeholder;
/// What widget you want to display instead of [placeholder] after [image] is
/// loaded.
///
/// Defaults to display the [image].
final Widget? child;
/// The duration for how long the fade out of the placeholder and
/// fade in of [child] should take.
final Duration duration;
/// See [Image.excludeFromSemantics].
final bool excludeFromSemantics;
/// See [Image.width].
final double? width;
/// See [Image.height].
final double? height;
/// See [Image.fit].
final BoxFit? fit;
@override
Widget build(BuildContext context) {
return Image(
image: image,
excludeFromSemantics: excludeFromSemantics,
width: width,
height: height,
fit: fit,
frameBuilder: (BuildContext context, Widget child, int? frame, bool wasSynchronouslyLoaded) {
if (wasSynchronouslyLoaded) {
return this.child ?? child;
} else {
return AnimatedSwitcher(
duration: duration,
child: frame != null ? this.child ?? child : placeholder,
);
}
},
);
}
}

View File

@ -0,0 +1,10 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/foundation.dart';
/// Using letter spacing in Flutter for Web can cause a performance drop,
/// see https://github.com/flutter/flutter/issues/51234.
double letterSpacingOrNone(double letterSpacing) =>
kIsWeb ? 0.0 : letterSpacing;

View File

@ -0,0 +1,42 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
import '../data/gallery_options.dart';
double _textScaleFactor(BuildContext context) {
return GalleryOptions.of(context).textScaleFactor(context);
}
// When text is larger, this factor becomes larger, but at half the rate.
//
// | Text scaling | Text scale factor | reducedTextScale(context) |
// |--------------|-------------------|---------------------------|
// | Small | 0.8 | 1.0 |
// | Normal | 1.0 | 1.0 |
// | Large | 2.0 | 1.5 |
// | Huge | 3.0 | 2.0 |
double reducedTextScale(BuildContext context) {
final double textScaleFactor = _textScaleFactor(context);
return textScaleFactor >= 1 ? (1 + textScaleFactor) / 2 : 1;
}
// When text is larger, this factor becomes larger at the same rate.
// But when text is smaller, this factor stays at 1.
//
// | Text scaling | Text scale factor | cappedTextScale(context) |
// |--------------|-------------------|---------------------------|
// | Small | 0.8 | 1.0 |
// | Normal | 1.0 | 1.0 |
// | Large | 2.0 | 2.0 |
// | Huge | 3.0 | 3.0 |
double cappedTextScale(BuildContext context) {
final double textScaleFactor = _textScaleFactor(context);
return max(textScaleFactor, 1);
}

View File

@ -0,0 +1,102 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart' show timeDilation;
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:google_fonts/google_fonts.dart';
import 'constants.dart';
import 'data/gallery_options.dart';
import 'gallery_localizations.dart';
import 'layout/adaptive.dart';
import 'pages/backdrop.dart';
import 'pages/splash.dart';
import 'routes.dart';
import 'themes/gallery_theme_data.dart';
export 'package:gallery/data/demos.dart' show pumpDeferredLibraries;
void main() async {
GoogleFonts.config.allowRuntimeFetching = false;
runApp(const GalleryApp());
}
class GalleryApp extends StatelessWidget {
const GalleryApp({
super.key,
this.initialRoute,
this.isTestMode = false,
});
final String? initialRoute;
final bool isTestMode;
@override
Widget build(BuildContext context) {
return ModelBinding(
initialModel: GalleryOptions(
themeMode: ThemeMode.system,
textScaleFactor: systemTextScaleFactorOption,
customTextDirection: CustomTextDirection.localeBased,
locale: null,
timeDilation: timeDilation,
platform: defaultTargetPlatform,
isTestMode: isTestMode,
),
child: Builder(
builder: (BuildContext context) {
final GalleryOptions options = GalleryOptions.of(context);
final bool hasHinge = MediaQuery.of(context).hinge?.bounds != null;
return MaterialApp(
restorationScopeId: 'rootGallery',
title: 'Flutter Gallery',
debugShowCheckedModeBanner: false,
themeMode: options.themeMode,
theme: GalleryThemeData.lightThemeData.copyWith(
platform: options.platform,
),
darkTheme: GalleryThemeData.darkThemeData.copyWith(
platform: options.platform,
),
localizationsDelegates: const <LocalizationsDelegate<Object?>>[
...GalleryLocalizations.localizationsDelegates,
LocaleNamesLocalizationsDelegate()
],
initialRoute: initialRoute,
supportedLocales: GalleryLocalizations.supportedLocales,
locale: options.locale,
localeListResolutionCallback: (List<Locale>? locales, Iterable<Locale> supportedLocales) {
deviceLocale = locales?.first;
return basicLocaleListResolution(locales, supportedLocales);
},
onGenerateRoute: (RouteSettings settings) =>
RouteConfiguration.onGenerateRoute(settings, hasHinge),
);
},
),
);
}
}
// ignore: unreachable_from_main
class RootPage extends StatelessWidget {
// ignore: unreachable_from_main
const RootPage({
super.key,
});
@override
Widget build(BuildContext context) {
return ApplyTextOptions(
child: SplashPage(
child: Backdrop(
isDesktop: isDisplayDesktop(context),
),
),
);
}
}

View File

@ -0,0 +1,130 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import '../gallery_localizations.dart';
void showAboutDialog({
required BuildContext context,
}) {
showDialog<void>(
context: context,
builder: (BuildContext context) {
return _AboutDialog();
},
);
}
Future<String> getVersionNumber() async {
return '2.10.2+021002';
}
class _AboutDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
final TextStyle bodyTextStyle =
textTheme.bodyLarge!.apply(color: colorScheme.onPrimary);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
const String name = 'Flutter Gallery'; // Don't need to localize.
const String legalese = '© 2021 The Flutter team'; // Don't need to localize.
final String repoText = localizations.githubRepo(name);
final String seeSource = localizations.aboutDialogDescription(repoText);
final int repoLinkIndex = seeSource.indexOf(repoText);
final int repoLinkIndexEnd = repoLinkIndex + repoText.length;
final String seeSourceFirst = seeSource.substring(0, repoLinkIndex);
final String seeSourceSecond = seeSource.substring(repoLinkIndexEnd);
return AlertDialog(
backgroundColor: colorScheme.background,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
content: Container(
constraints: const BoxConstraints(maxWidth: 400),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
FutureBuilder<String>(
future: getVersionNumber(),
builder: (BuildContext context, AsyncSnapshot<String> snapshot) => SelectableText(
snapshot.hasData ? '$name ${snapshot.data}' : name,
style: textTheme.headlineMedium!.apply(
color: colorScheme.onPrimary,
),
),
),
const SizedBox(height: 24),
SelectableText.rich(
TextSpan(
children: <InlineSpan>[
TextSpan(
style: bodyTextStyle,
text: seeSourceFirst,
),
TextSpan(
style: bodyTextStyle.copyWith(
color: colorScheme.primary,
),
text: repoText,
recognizer: TapGestureRecognizer()
..onTap = () async {
final Uri url =
Uri.parse('https://github.com/flutter/gallery/');
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
},
),
TextSpan(
style: bodyTextStyle,
text: seeSourceSecond,
),
],
),
),
const SizedBox(height: 18),
SelectableText(
legalese,
style: bodyTextStyle,
),
],
),
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(
builder: (BuildContext context) => Theme(
data: Theme.of(context).copyWith(
textTheme: Typography.material2018(
platform: Theme.of(context).platform,
).black,
cardColor: Colors.white,
),
child: const LicensePage(
applicationName: name,
applicationLegalese: legalese,
),
),
));
},
child: Text(
MaterialLocalizations.of(context).viewLicensesButtonLabel,
),
),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
),
],
);
}
}

View File

@ -0,0 +1,304 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import '../constants.dart';
import '../data/gallery_options.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import 'home.dart';
import 'settings.dart';
import 'settings_icon/icon.dart' as settings_icon;
const double _settingsButtonWidth = 64;
const double _settingsButtonHeightDesktop = 56;
const double _settingsButtonHeightMobile = 40;
class Backdrop extends StatefulWidget {
const Backdrop({
super.key,
required this.isDesktop,
this.settingsPage,
this.homePage,
});
final bool isDesktop;
final Widget? settingsPage;
final Widget? homePage;
@override
State<Backdrop> createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop> with TickerProviderStateMixin {
late AnimationController _settingsPanelController;
late AnimationController _iconController;
late FocusNode _settingsPageFocusNode;
late ValueNotifier<bool> _isSettingsOpenNotifier;
late Widget _settingsPage;
late Widget _homePage;
@override
void initState() {
super.initState();
_settingsPanelController = AnimationController(
vsync: this,
duration: widget.isDesktop
? settingsPanelMobileAnimationDuration
: settingsPanelDesktopAnimationDuration);
_iconController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_settingsPageFocusNode = FocusNode();
_isSettingsOpenNotifier = ValueNotifier<bool>(false);
_settingsPage = widget.settingsPage ??
SettingsPage(
animationController: _settingsPanelController,
);
_homePage = widget.homePage ?? const HomePage();
}
@override
void dispose() {
_settingsPanelController.dispose();
_iconController.dispose();
_settingsPageFocusNode.dispose();
_isSettingsOpenNotifier.dispose();
super.dispose();
}
void _toggleSettings() {
// Animate the settings panel to open or close.
if (_isSettingsOpenNotifier.value) {
_settingsPanelController.reverse();
_iconController.reverse();
} else {
_settingsPanelController.forward();
_iconController.forward();
}
_isSettingsOpenNotifier.value = !_isSettingsOpenNotifier.value;
}
Animation<RelativeRect> _slideDownSettingsPageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0),
end: RelativeRect.fill,
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Animation<RelativeRect> _slideDownHomePageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: RelativeRect.fill,
end: RelativeRect.fromLTRB(
0,
constraints.biggest.height - galleryHeaderHeight,
0,
-galleryHeaderHeight,
),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final bool isDesktop = isDisplayDesktop(context);
final Widget settingsPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (BuildContext context, bool isSettingsOpen, Widget? child) {
return ExcludeSemantics(
excluding: !isSettingsOpen,
child: isSettingsOpen
? KeyboardListener(
includeSemantics: false,
focusNode: _settingsPageFocusNode,
onKeyEvent: (KeyEvent event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
_toggleSettings();
}
},
child: FocusScope(child: _settingsPage),
)
: ExcludeFocus(child: _settingsPage),
);
},
);
final Widget homePage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (BuildContext context, bool isSettingsOpen, Widget? child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _homePage),
);
},
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: GalleryOptions.of(context).resolvedSystemUiOverlayStyle(),
child: Stack(
children: <Widget>[
if (!isDesktop) ...<Widget>[
// Slides the settings page up and down from the top of the
// screen.
PositionedTransition(
rect: _slideDownSettingsPageAnimation(constraints),
child: settingsPage,
),
// Slides the home page up and down below the bottom of the
// screen.
PositionedTransition(
rect: _slideDownHomePageAnimation(constraints),
child: homePage,
),
],
if (isDesktop) ...<Widget>[
Semantics(sortKey: const OrdinalSortKey(2), child: homePage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (BuildContext context, bool isSettingsOpen, Widget? child) {
if (isSettingsOpen) {
return ExcludeSemantics(
child: Listener(
onPointerDown: (_) => _toggleSettings(),
child: const ModalBarrier(dismissible: false),
),
);
} else {
return Container();
}
},
),
Semantics(
sortKey: const OrdinalSortKey(3),
child: ScaleTransition(
alignment: Directionality.of(context) == TextDirection.ltr
? Alignment.topRight
: Alignment.topLeft,
scale: CurvedAnimation(
parent: _settingsPanelController,
curve: Curves.fastOutSlowIn,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryContainer,
child: Container(
constraints: const BoxConstraints(
maxHeight: 560,
maxWidth: desktopSettingsWidth,
minWidth: desktopSettingsWidth,
),
child: settingsPage,
),
),
),
),
),
],
_SettingsIcon(
animationController: _iconController,
toggleSettings: _toggleSettings,
isSettingsOpenNotifier: _isSettingsOpenNotifier,
),
],
),
);
}
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: _buildStack,
);
}
}
class _SettingsIcon extends AnimatedWidget {
const _SettingsIcon({
required this.animationController,
required this.toggleSettings,
required this.isSettingsOpenNotifier,
}) : super(listenable: animationController);
final AnimationController animationController;
final VoidCallback toggleSettings;
final ValueNotifier<bool> isSettingsOpenNotifier;
String _settingsSemanticLabel(bool isOpen, BuildContext context) {
return isOpen
? GalleryLocalizations.of(context)!.settingsButtonCloseLabel
: GalleryLocalizations.of(context)!.settingsButtonLabel;
}
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final double safeAreaTopPadding = MediaQuery.of(context).padding.top;
return Align(
alignment: AlignmentDirectional.topEnd,
child: Semantics(
sortKey: const OrdinalSortKey(1),
button: true,
enabled: true,
label: _settingsSemanticLabel(isSettingsOpenNotifier.value, context),
child: SizedBox(
width: _settingsButtonWidth,
height: isDesktop
? _settingsButtonHeightDesktop
: _settingsButtonHeightMobile + safeAreaTopPadding,
child: Material(
borderRadius: const BorderRadiusDirectional.only(
bottomStart: Radius.circular(10),
),
color:
isSettingsOpenNotifier.value & !animationController.isAnimating
? Colors.transparent
: Theme.of(context).colorScheme.secondaryContainer,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () {
toggleSettings();
SemanticsService.announce(
_settingsSemanticLabel(isSettingsOpenNotifier.value, context),
GalleryOptions.of(context).resolvedTextDirection()!,
);
},
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 3, end: 18),
child: settings_icon.SettingsIcon(animationController.value),
),
),
),
),
),
);
}
}

View File

@ -0,0 +1,346 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../constants.dart';
import '../data/demos.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import 'demo.dart';
typedef CategoryHeaderTapCallback = void Function(bool shouldOpenList);
class CategoryListItem extends StatefulWidget {
const CategoryListItem({
super.key,
this.restorationId,
required this.category,
required this.imageString,
this.demos = const <GalleryDemo>[],
this.initiallyExpanded = false,
this.onTap,
});
final GalleryDemoCategory category;
final String? restorationId;
final String imageString;
final List<GalleryDemo> demos;
final bool initiallyExpanded;
final CategoryHeaderTapCallback? onTap;
@override
State<CategoryListItem> createState() => _CategoryListItemState();
}
class _CategoryListItemState extends State<CategoryListItem>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static const Duration _expandDuration = Duration(milliseconds: 200);
late AnimationController _controller;
late Animation<double> _childrenHeightFactor;
late Animation<double> _headerChevronOpacity;
late Animation<double> _headerHeight;
late Animation<EdgeInsetsGeometry> _headerMargin;
late Animation<EdgeInsetsGeometry> _headerImagePadding;
late Animation<EdgeInsetsGeometry> _childrenPadding;
late Animation<BorderRadius?> _headerBorderRadius;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: _expandDuration, vsync: this);
_controller.addStatusListener((AnimationStatus status) {
setState(() {});
});
_childrenHeightFactor = _controller.drive(_easeInTween);
_headerChevronOpacity = _controller.drive(_easeInTween);
_headerHeight = Tween<double>(
begin: 80,
end: 96,
).animate(_controller);
_headerMargin = EdgeInsetsGeometryTween(
begin: const EdgeInsets.fromLTRB(32, 8, 32, 8),
end: EdgeInsets.zero,
).animate(_controller);
_headerImagePadding = EdgeInsetsGeometryTween(
begin: const EdgeInsets.all(8),
end: const EdgeInsetsDirectional.fromSTEB(16, 8, 8, 8),
).animate(_controller);
_childrenPadding = EdgeInsetsGeometryTween(
begin: const EdgeInsets.symmetric(horizontal: 32),
end: EdgeInsets.zero,
).animate(_controller);
_headerBorderRadius = BorderRadiusTween(
begin: BorderRadius.circular(10),
end: BorderRadius.zero,
).animate(_controller);
if (widget.initiallyExpanded) {
_controller.value = 1.0;
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool _shouldOpenList() {
switch (_controller.status) {
case AnimationStatus.completed:
case AnimationStatus.forward:
case AnimationStatus.reverse:
return false;
case AnimationStatus.dismissed:
return true;
}
}
void _handleTap() {
if (_shouldOpenList()) {
_controller.forward();
if (widget.onTap != null) {
widget.onTap!(true);
}
} else {
_controller.reverse();
if (widget.onTap != null) {
widget.onTap!(false);
}
}
}
Widget _buildHeaderWithChildren(BuildContext context, Widget? child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_CategoryHeader(
margin: _headerMargin.value,
imagePadding: _headerImagePadding.value,
borderRadius: _headerBorderRadius.value!,
height: _headerHeight.value,
chevronOpacity: _headerChevronOpacity.value,
imageString: widget.imageString,
category: widget.category,
onTap: _handleTap,
),
Padding(
padding: _childrenPadding.value,
child: ClipRect(
child: Align(
heightFactor: _childrenHeightFactor.value,
child: child,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller.view,
builder: _buildHeaderWithChildren,
child: _shouldOpenList()
? null
: _ExpandedCategoryDemos(
category: widget.category,
demos: widget.demos,
),
);
}
}
class _CategoryHeader extends StatelessWidget {
const _CategoryHeader({
this.margin,
required this.imagePadding,
required this.borderRadius,
this.height,
required this.chevronOpacity,
required this.imageString,
required this.category,
this.onTap,
});
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry imagePadding;
final double? height;
final BorderRadiusGeometry borderRadius;
final String imageString;
final GalleryDemoCategory category;
final double chevronOpacity;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Container(
margin: margin,
child: Material(
shape: RoundedRectangleBorder(borderRadius: borderRadius),
color: colorScheme.onBackground,
clipBehavior: Clip.antiAlias,
child: SizedBox(
width: MediaQuery.of(context).size.width,
child: InkWell(
// Makes integration tests possible.
key: ValueKey<String>('${category.name}CategoryHeader'),
onTap: onTap,
child: Row(
children: <Widget>[
Expanded(
child: Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
children: <Widget>[
Padding(
padding: imagePadding,
child: FadeInImage(
image: AssetImage(
imageString,
package: 'flutter_gallery_assets',
),
placeholder: MemoryImage(kTransparentImage),
fadeInDuration: entranceAnimationDuration,
width: 64,
height: 64,
excludeFromSemantics: true,
),
),
Padding(
padding: const EdgeInsetsDirectional.only(start: 8),
child: Text(
category.displayTitle(
GalleryLocalizations.of(context)!,
)!,
style:
Theme.of(context).textTheme.headlineSmall!.apply(
color: colorScheme.onSurface,
),
),
),
],
),
),
Opacity(
opacity: chevronOpacity,
child: chevronOpacity != 0
? Padding(
padding: const EdgeInsetsDirectional.only(
start: 8,
end: 32,
),
child: Icon(
Icons.keyboard_arrow_up,
color: colorScheme.onSurface,
),
)
: null,
),
],
),
),
),
),
);
}
}
class _ExpandedCategoryDemos extends StatelessWidget {
const _ExpandedCategoryDemos({
required this.category,
required this.demos,
});
final GalleryDemoCategory category;
final List<GalleryDemo> demos;
@override
Widget build(BuildContext context) {
return Column(
// Makes integration tests possible.
key: ValueKey<String>('${category.name}DemoList'),
children: <Widget>[
for (final GalleryDemo demo in demos)
CategoryDemoItem(
demo: demo,
),
const SizedBox(height: 12), // Extra space below.
],
);
}
}
class CategoryDemoItem extends StatelessWidget {
const CategoryDemoItem({super.key, required this.demo});
final GalleryDemo demo;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Material(
// Makes integration tests possible.
key: ValueKey<String>(demo.describe),
color: Theme.of(context).colorScheme.surface,
child: MergeSemantics(
child: InkWell(
onTap: () {
Navigator.of(context).restorablePushNamed(
'${DemoPage.baseRoute}/${demo.slug}',
);
},
child: Padding(
padding: EdgeInsetsDirectional.only(
start: 32,
top: 20,
end: isDisplayDesktop(context) ? 16 : 8,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Icon(
demo.icon,
color: colorScheme.primary,
),
const SizedBox(width: 40),
Flexible(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
demo.title,
style: textTheme.titleMedium!
.apply(color: colorScheme.onSurface),
),
Text(
demo.subtitle,
style: textTheme.labelSmall!.apply(
color: colorScheme.onSurface.withOpacity(0.5),
),
),
const SizedBox(height: 20),
Divider(
thickness: 1,
height: 1,
color: Theme.of(context).colorScheme.background,
),
],
),
),
],
),
),
),
),
);
}
}

View File

@ -0,0 +1,819 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:url_launcher/url_launcher_string.dart';
import '../codeviewer/code_displayer.dart';
import '../codeviewer/code_style.dart';
import '../constants.dart';
import '../data/demos.dart';
import '../data/gallery_options.dart';
import '../feature_discovery/feature_discovery.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import '../themes/gallery_theme_data.dart';
import '../themes/material_demo_theme_data.dart';
import 'splash.dart';
enum _DemoState {
normal,
options,
info,
code,
fullscreen,
}
class DemoPage extends StatefulWidget {
const DemoPage({
super.key,
required this.slug,
});
static const String baseRoute = '/demo';
final String? slug;
@override
State<DemoPage> createState() => _DemoPageState();
}
class _DemoPageState extends State<DemoPage> {
late Map<String?, GalleryDemo> slugToDemoMap;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// To make sure that we do not rebuild the map for every update to the demo
// page, we save it in a variable. The cost of running `slugToDemo` is
// still only close to constant, as it's just iterating over all of the
// demos.
slugToDemoMap = Demos.asSlugToDemoMap(context);
}
@override
Widget build(BuildContext context) {
if (widget.slug == null || !slugToDemoMap.containsKey(widget.slug)) {
// Return to root if invalid slug.
Navigator.of(context).pop();
}
return ScaffoldMessenger(
child: GalleryDemoPage(
restorationId: widget.slug!,
demo: slugToDemoMap[widget.slug]!,
));
}
}
class GalleryDemoPage extends StatefulWidget {
const GalleryDemoPage({
super.key,
required this.restorationId,
required this.demo,
});
final String restorationId;
final GalleryDemo demo;
@override
State<GalleryDemoPage> createState() => _GalleryDemoPageState();
}
class _GalleryDemoPageState extends State<GalleryDemoPage>
with RestorationMixin, TickerProviderStateMixin {
final RestorableInt _demoStateIndex = RestorableInt(_DemoState.normal.index);
final RestorableInt _configIndex = RestorableInt(0);
bool? _isDesktop;
late AnimationController _codeBackgroundColorController;
@override
String get restorationId => widget.restorationId;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_demoStateIndex, 'demo_state');
registerForRestoration(_configIndex, 'configuration_index');
}
GalleryDemoConfiguration get _currentConfig {
return widget.demo.configurations[_configIndex.value];
}
bool get _hasOptions => widget.demo.configurations.length > 1;
@override
void initState() {
super.initState();
_codeBackgroundColorController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
}
@override
void dispose() {
_demoStateIndex.dispose();
_configIndex.dispose();
_codeBackgroundColorController.dispose();
super.dispose();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_isDesktop ??= isDisplayDesktop(context);
}
/// Sets state and updates the background color for code.
void setStateAndUpdate(VoidCallback callback) {
setState(() {
callback();
if (_demoStateIndex.value == _DemoState.code.index) {
_codeBackgroundColorController.forward();
} else {
_codeBackgroundColorController.reverse();
}
});
}
void _handleTap(_DemoState newState) {
final int newStateIndex = newState.index;
// Do not allow normal state for desktop.
if (_demoStateIndex.value == newStateIndex && isDisplayDesktop(context)) {
if (_demoStateIndex.value == _DemoState.fullscreen.index) {
setStateAndUpdate(() {
_demoStateIndex.value =
_hasOptions ? _DemoState.options.index : _DemoState.info.index;
});
}
return;
}
setStateAndUpdate(() {
_demoStateIndex.value = _demoStateIndex.value == newStateIndex
? _DemoState.normal.index
: newStateIndex;
});
}
Future<void> _showDocumentation(BuildContext context) async {
final String url = _currentConfig.documentationUrl;
if (await canLaunchUrlString(url)) {
await launchUrlString(url);
} else if (context.mounted) {
await showDialog<void>(
context: context,
builder: (BuildContext context) {
return SimpleDialog(
title: Text(GalleryLocalizations.of(context)!.demoInvalidURL),
children: <Widget>[
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(url),
),
],
);
},
);
}
}
void _resolveState(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final bool isFoldable = isDisplayFoldable(context);
if (_DemoState.values[_demoStateIndex.value] == _DemoState.fullscreen &&
!isDesktop) {
// Do not allow fullscreen state for mobile.
_demoStateIndex.value = _DemoState.normal.index;
} else if (_DemoState.values[_demoStateIndex.value] == _DemoState.normal &&
(isDesktop || isFoldable)) {
// Do not allow normal state for desktop.
_demoStateIndex.value =
_hasOptions ? _DemoState.options.index : _DemoState.info.index;
} else if (isDesktop != _isDesktop) {
_isDesktop = isDesktop;
// When going from desktop to mobile, return to normal state.
if (!isDesktop) {
_demoStateIndex.value = _DemoState.normal.index;
}
}
}
@override
Widget build(BuildContext context) {
final bool isFoldable = isDisplayFoldable(context);
final bool isDesktop = isDisplayDesktop(context);
_resolveState(context);
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final Color iconColor = colorScheme.onSurface;
final Color selectedIconColor = colorScheme.primary;
final double appBarPadding = isDesktop ? 20.0 : 0.0;
final _DemoState currentDemoState = _DemoState.values[_demoStateIndex.value];
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final GalleryOptions options = GalleryOptions.of(context);
final AppBar appBar = AppBar(
systemOverlayStyle: options.resolvedSystemUiOverlayStyle(),
backgroundColor: Colors.transparent,
leading: Padding(
padding: EdgeInsetsDirectional.only(start: appBarPadding),
child: IconButton(
key: const ValueKey<String>('Back'),
icon: const BackButtonIcon(),
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
onPressed: () {
Navigator.maybePop(context);
},
),
),
actions: <Widget>[
if (_hasOptions)
IconButton(
icon: FeatureDiscovery(
title: localizations.demoOptionsFeatureTitle,
description: localizations.demoOptionsFeatureDescription,
showOverlay: !isDisplayDesktop(context) && !options.isTestMode,
color: colorScheme.primary,
onTap: () => _handleTap(_DemoState.options),
child: Icon(
Icons.tune,
color: currentDemoState == _DemoState.options
? selectedIconColor
: iconColor,
),
),
tooltip: localizations.demoOptionsTooltip,
onPressed: () => _handleTap(_DemoState.options),
),
IconButton(
icon: const Icon(Icons.info),
tooltip: localizations.demoInfoTooltip,
color: currentDemoState == _DemoState.info
? selectedIconColor
: iconColor,
onPressed: () => _handleTap(_DemoState.info),
),
IconButton(
icon: const Icon(Icons.code),
tooltip: localizations.demoCodeTooltip,
color: currentDemoState == _DemoState.code
? selectedIconColor
: iconColor,
onPressed: () => _handleTap(_DemoState.code),
),
IconButton(
icon: const Icon(Icons.library_books),
tooltip: localizations.demoDocumentationTooltip,
color: iconColor,
onPressed: () => _showDocumentation(context),
),
if (isDesktop)
IconButton(
icon: const Icon(Icons.fullscreen),
tooltip: localizations.demoFullscreenTooltip,
color: currentDemoState == _DemoState.fullscreen
? selectedIconColor
: iconColor,
onPressed: () => _handleTap(_DemoState.fullscreen),
),
SizedBox(width: appBarPadding),
],
);
final MediaQueryData mediaQuery = MediaQuery.of(context);
final double bottomSafeArea = mediaQuery.padding.bottom;
final double contentHeight = mediaQuery.size.height -
mediaQuery.padding.top -
mediaQuery.padding.bottom -
appBar.preferredSize.height;
final double maxSectionHeight = isDesktop ? contentHeight : contentHeight - 64;
final double horizontalPadding = isDesktop ? mediaQuery.size.width * 0.12 : 0.0;
const double maxSectionWidth = 420.0;
Widget section;
switch (currentDemoState) {
case _DemoState.options:
section = _DemoSectionOptions(
maxHeight: maxSectionHeight,
maxWidth: maxSectionWidth,
configurations: widget.demo.configurations,
configIndex: _configIndex.value,
onConfigChanged: (int index) {
setStateAndUpdate(() {
_configIndex.value = index;
if (!isDesktop) {
_demoStateIndex.value = _DemoState.normal.index;
}
});
},
);
case _DemoState.info:
section = _DemoSectionInfo(
maxHeight: maxSectionHeight,
maxWidth: maxSectionWidth,
title: _currentConfig.title,
description: _currentConfig.description,
);
case _DemoState.code:
final TextStyle codeTheme = GoogleFonts.robotoMono(
fontSize: 12 * options.textScaleFactor(context),
);
section = CodeStyle(
baseStyle: codeTheme.copyWith(color: const Color(0xFFFAFBFB)),
numberStyle: codeTheme.copyWith(color: const Color(0xFFBD93F9)),
commentStyle: codeTheme.copyWith(color: const Color(0xFF808080)),
keywordStyle: codeTheme.copyWith(color: const Color(0xFF1CDEC9)),
stringStyle: codeTheme.copyWith(color: const Color(0xFFFFA65C)),
punctuationStyle: codeTheme.copyWith(color: const Color(0xFF8BE9FD)),
classStyle: codeTheme.copyWith(color: const Color(0xFFD65BAD)),
constantStyle: codeTheme.copyWith(color: const Color(0xFFFF8383)),
child: _DemoSectionCode(
maxHeight: maxSectionHeight,
codeWidget: CodeDisplayPage(
_currentConfig.code,
),
),
);
case _DemoState.normal:
case _DemoState.fullscreen:
section = Container();
}
Widget body;
Widget demoContent = ScaffoldMessenger(
child: DemoWrapper(
height: contentHeight,
buildRoute: _currentConfig.buildRoute,
),
);
if (isDesktop) {
final bool isFullScreen = currentDemoState == _DemoState.fullscreen;
final Widget sectionAndDemo = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (!isFullScreen) Expanded(child: section),
SizedBox(width: !isFullScreen ? 48.0 : 0),
Expanded(child: demoContent),
],
);
body = SafeArea(
child: Padding(
padding: const EdgeInsets.only(top: 56),
child: sectionAndDemo,
),
);
} else if (isFoldable) {
body = Padding(
padding: const EdgeInsets.only(top: 12.0),
child: TwoPane(
startPane: demoContent,
endPane: section,
),
);
} else {
section = AnimatedSize(
duration: const Duration(milliseconds: 200),
alignment: Alignment.topCenter,
curve: Curves.easeIn,
child: section,
);
final bool isDemoNormal = currentDemoState == _DemoState.normal;
// Add a tap gesture to collapse the currently opened section.
demoContent = Semantics(
label:
'${GalleryLocalizations.of(context)!.demo}, ${widget.demo.title}',
child: MouseRegion(
cursor: isDemoNormal ? MouseCursor.defer : SystemMouseCursors.click,
child: GestureDetector(
onTap: isDemoNormal
? null
: () {
setStateAndUpdate(() {
_demoStateIndex.value = _DemoState.normal.index;
});
},
child: Semantics(
excludeSemantics: !isDemoNormal,
child: demoContent,
),
),
),
);
body = SafeArea(
bottom: false,
child: ListView(
// Use a non-scrollable ListView to enable animation of shifting the
// demo offscreen.
physics: const NeverScrollableScrollPhysics(),
children: <Widget>[
section,
demoContent,
// Fake the safe area to ensure the animation looks correct.
SizedBox(height: bottomSafeArea),
],
),
);
}
Widget page;
if (isDesktop || isFoldable) {
page = AnimatedBuilder(
animation: _codeBackgroundColorController,
builder: (BuildContext context, Widget? child) {
Brightness themeBrightness;
switch (GalleryOptions.of(context).themeMode) {
case ThemeMode.system:
themeBrightness = MediaQuery.of(context).platformBrightness;
case ThemeMode.light:
themeBrightness = Brightness.light;
case ThemeMode.dark:
themeBrightness = Brightness.dark;
}
Widget contents = Container(
padding: EdgeInsets.symmetric(horizontal: horizontalPadding),
child: ApplyTextOptions(
child: Scaffold(
appBar: appBar,
body: body,
backgroundColor: Colors.transparent,
),
),
);
if (themeBrightness == Brightness.light) {
// If it is currently in light mode, add a
// dark background for code.
final Widget codeBackground = SafeArea(
child: Container(
padding: const EdgeInsets.only(top: 56),
child: Container(
color: ColorTween(
begin: Colors.transparent,
end: GalleryThemeData.darkThemeData.canvasColor,
).animate(_codeBackgroundColorController).value,
),
),
);
contents = Stack(
children: <Widget>[
codeBackground,
contents,
],
);
}
return ColoredBox(
color: colorScheme.background,
child: contents,
);
});
} else {
page = ColoredBox(
color: colorScheme.background,
child: ApplyTextOptions(
child: Scaffold(
appBar: appBar,
body: body,
resizeToAvoidBottomInset: false,
),
),
);
}
// Add the splash page functionality for desktop.
if (isDesktop) {
page = MediaQuery.removePadding(
removeTop: true,
context: context,
child: SplashPage(
child: page,
),
);
}
return FeatureDiscoveryController(page);
}
}
class _DemoSectionOptions extends StatelessWidget {
const _DemoSectionOptions({
required this.maxHeight,
required this.maxWidth,
required this.configurations,
required this.configIndex,
required this.onConfigChanged,
});
final double maxHeight;
final double maxWidth;
final List<GalleryDemoConfiguration> configurations;
final int configIndex;
final ValueChanged<int> onConfigChanged;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Align(
alignment: AlignmentDirectional.topStart,
child: Container(
constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: const EdgeInsetsDirectional.only(
start: 24,
top: 12,
end: 24,
),
child: Text(
GalleryLocalizations.of(context)!.demoOptionsTooltip,
style: textTheme.headlineMedium!.apply(
color: colorScheme.onSurface,
fontSizeDelta:
isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0,
),
),
),
Divider(
thickness: 1,
height: 16,
color: colorScheme.onSurface,
),
Flexible(
child: ListView(
shrinkWrap: true,
children: <Widget>[
for (final GalleryDemoConfiguration configuration in configurations)
_DemoSectionOptionsItem(
title: configuration.title,
isSelected: configuration == configurations[configIndex],
onTap: () {
onConfigChanged(configurations.indexOf(configuration));
},
),
],
),
),
const SizedBox(height: 12),
],
),
),
);
}
}
class _DemoSectionOptionsItem extends StatelessWidget {
const _DemoSectionOptionsItem({
required this.title,
required this.isSelected,
this.onTap,
});
final String title;
final bool isSelected;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Material(
color: isSelected ? colorScheme.surface : null,
child: InkWell(
onTap: onTap,
child: Container(
constraints: const BoxConstraints(minWidth: double.infinity),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
child: Text(
title,
style: Theme.of(context).textTheme.bodyMedium!.apply(
color:
isSelected ? colorScheme.primary : colorScheme.onSurface,
),
),
),
),
);
}
}
class _DemoSectionInfo extends StatelessWidget {
const _DemoSectionInfo({
required this.maxHeight,
required this.maxWidth,
required this.title,
required this.description,
});
final double maxHeight;
final double maxWidth;
final String title;
final String description;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colorScheme = Theme.of(context).colorScheme;
return Align(
alignment: AlignmentDirectional.topStart,
child: Container(
padding: const EdgeInsetsDirectional.only(
start: 24,
top: 12,
end: 24,
bottom: 32,
),
constraints: BoxConstraints(maxHeight: maxHeight, maxWidth: maxWidth),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
SelectableText(
title,
style: textTheme.headlineMedium!.apply(
color: colorScheme.onSurface,
fontSizeDelta:
isDisplayDesktop(context) ? desktopDisplay1FontDelta : 0,
),
),
const SizedBox(height: 12),
SelectableText(
description,
style: textTheme.bodyMedium!.apply(
color: colorScheme.onSurface,
),
),
],
),
),
),
);
}
}
class DemoWrapper extends StatelessWidget {
const DemoWrapper({
super.key,
required this.height,
required this.buildRoute,
});
final double height;
final WidgetBuilder buildRoute;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 16),
height: height,
child: ClipRRect(
clipBehavior: Clip.antiAliasWithSaveLayer,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(10.0),
bottom: Radius.circular(2.0),
),
child: Theme(
data: MaterialDemoThemeData.themeData.copyWith(
platform: GalleryOptions.of(context).platform,
),
child: CupertinoTheme(
data: const CupertinoThemeData()
.copyWith(brightness: Brightness.light),
child: ApplyTextOptions(
child: Builder(builder: buildRoute),
),
),
),
),
);
}
}
class _DemoSectionCode extends StatelessWidget {
const _DemoSectionCode({
this.maxHeight,
this.codeWidget,
});
final double? maxHeight;
final Widget? codeWidget;
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
return Theme(
data: GalleryThemeData.darkThemeData,
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Container(
color: isDesktop ? null : GalleryThemeData.darkThemeData.canvasColor,
padding: const EdgeInsets.symmetric(horizontal: 16),
height: maxHeight,
child: codeWidget,
),
),
);
}
}
class CodeDisplayPage extends StatelessWidget {
const CodeDisplayPage(this.code, {super.key});
final CodeDisplayer code;
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final TextSpan richTextCode = code(context);
final String plainTextCode = richTextCode.toPlainText();
void showSnackBarOnCopySuccess(dynamic result) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
GalleryLocalizations.of(context)!
.demoCodeViewerCopiedToClipboardMessage,
),
),
);
}
void showSnackBarOnCopyFailure(Object exception) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
GalleryLocalizations.of(context)!
.demoCodeViewerFailedToCopyToClipboardMessage(exception),
),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: isDesktop
? const EdgeInsets.only(bottom: 8)
: const EdgeInsets.symmetric(vertical: 8),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white.withOpacity(0.15),
padding: const EdgeInsets.symmetric(horizontal: 8),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(4)),
),
),
onPressed: () async {
await Clipboard.setData(ClipboardData(text: plainTextCode))
.then(showSnackBarOnCopySuccess)
.catchError(showSnackBarOnCopyFailure);
},
child: Text(
GalleryLocalizations.of(context)!.demoCodeViewerCopyAll,
style: Theme.of(context).textTheme.labelLarge!.copyWith(
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
),
Expanded(
child: SingleChildScrollView(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SelectableText.rich(
richTextCode,
textDirection: TextDirection.ltr,
),
),
),
),
],
);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,472 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localized_locales/flutter_localized_locales.dart';
import 'package:url_launcher/url_launcher.dart';
import '../constants.dart';
import '../data/gallery_options.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import 'about.dart' as about;
import 'home.dart';
import 'settings_list_item.dart';
enum _ExpandableSetting {
textScale,
textDirection,
locale,
platform,
theme,
}
class SettingsPage extends StatefulWidget {
const SettingsPage({
super.key,
required this.animationController,
});
final AnimationController animationController;
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
_ExpandableSetting? _expandedSettingId;
late Animation<double> _staggerSettingsItemsAnimation;
void onTapSetting(_ExpandableSetting settingId) {
setState(() {
if (_expandedSettingId == settingId) {
_expandedSettingId = null;
} else {
_expandedSettingId = settingId;
}
});
}
void _closeSettingId(AnimationStatus status) {
if (status == AnimationStatus.dismissed) {
setState(() {
_expandedSettingId = null;
});
}
}
@override
void initState() {
super.initState();
// When closing settings, also shrink expanded setting.
widget.animationController.addStatusListener(_closeSettingId);
_staggerSettingsItemsAnimation = CurvedAnimation(
parent: widget.animationController,
curve: const Interval(
0.4,
1.0,
curve: Curves.ease,
),
);
}
@override
void dispose() {
super.dispose();
widget.animationController.removeStatusListener(_closeSettingId);
}
/// Given a [Locale], returns a [DisplayOption] with its native name for a
/// title and its name in the currently selected locale for a subtitle. If the
/// native name can't be determined, it is omitted. If the locale can't be
/// determined, the locale code is used.
DisplayOption _getLocaleDisplayOption(BuildContext context, Locale? locale) {
final String localeCode = locale.toString();
final String? localeName = LocaleNames.of(context)!.nameOf(localeCode);
if (localeName != null) {
final String? localeNativeName =
LocaleNamesLocalizationsDelegate.nativeLocaleNames[localeCode];
return localeNativeName != null
? DisplayOption(localeNativeName, subtitle: localeName)
: DisplayOption(localeName);
} else {
// gsw, fil, and es_419 aren't in flutter_localized_countries' dataset
// so we handle them separately
switch (localeCode) {
case 'gsw':
return DisplayOption('Schwiizertüütsch', subtitle: 'Swiss German');
case 'fil':
return DisplayOption('Filipino', subtitle: 'Filipino');
case 'es_419':
return DisplayOption(
'español (Latinoamérica)',
subtitle: 'Spanish (Latin America)',
);
}
}
return DisplayOption(localeCode);
}
/// Create a sorted by native name map of supported locales to their
/// intended display string, with a system option as the first element.
LinkedHashMap<Locale, DisplayOption> _getLocaleOptions() {
final LinkedHashMap<Locale, DisplayOption> localeOptions = LinkedHashMap<Locale, DisplayOption>.of(<Locale, DisplayOption>{
systemLocaleOption: DisplayOption(
GalleryLocalizations.of(context)!.settingsSystemDefault +
(deviceLocale != null
? ' - ${_getLocaleDisplayOption(context, deviceLocale).title}'
: ''),
),
});
final List<Locale> supportedLocales =
List<Locale>.from(GalleryLocalizations.supportedLocales);
supportedLocales.removeWhere((Locale locale) => locale == deviceLocale);
final List<MapEntry<Locale, DisplayOption>> displayLocales = Map<Locale, DisplayOption>.fromIterable(
supportedLocales,
value: (dynamic locale) =>
_getLocaleDisplayOption(context, locale as Locale?),
).entries.toList()
..sort((MapEntry<Locale, DisplayOption> l1, MapEntry<Locale, DisplayOption> l2) => compareAsciiUpperCase(l1.value.title, l2.value.title));
localeOptions.addAll(LinkedHashMap<Locale, DisplayOption>.fromEntries(displayLocales));
return localeOptions;
}
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final GalleryOptions options = GalleryOptions.of(context);
final bool isDesktop = isDisplayDesktop(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final List<Widget> settingsListItems = <Widget>[
SettingsListItem<double?>(
title: localizations.settingsTextScaling,
selectedOption: options.textScaleFactor(
context,
useSentinel: true,
),
optionsMap: LinkedHashMap<double?, DisplayOption>.of(<double?, DisplayOption>{
systemTextScaleFactorOption: DisplayOption(
localizations.settingsSystemDefault,
),
0.8: DisplayOption(
localizations.settingsTextScalingSmall,
),
1.0: DisplayOption(
localizations.settingsTextScalingNormal,
),
2.0: DisplayOption(
localizations.settingsTextScalingLarge,
),
3.0: DisplayOption(
localizations.settingsTextScalingHuge,
),
}),
onOptionChanged: (double? newTextScale) => GalleryOptions.update(
context,
options.copyWith(textScaleFactor: newTextScale),
),
onTapSetting: () => onTapSetting(_ExpandableSetting.textScale),
isExpanded: _expandedSettingId == _ExpandableSetting.textScale,
),
SettingsListItem<CustomTextDirection?>(
title: localizations.settingsTextDirection,
selectedOption: options.customTextDirection,
optionsMap: LinkedHashMap<CustomTextDirection?, DisplayOption>.of(<CustomTextDirection?, DisplayOption>{
CustomTextDirection.localeBased: DisplayOption(
localizations.settingsTextDirectionLocaleBased,
),
CustomTextDirection.ltr: DisplayOption(
localizations.settingsTextDirectionLTR,
),
CustomTextDirection.rtl: DisplayOption(
localizations.settingsTextDirectionRTL,
),
}),
onOptionChanged: (CustomTextDirection? newTextDirection) => GalleryOptions.update(
context,
options.copyWith(customTextDirection: newTextDirection),
),
onTapSetting: () => onTapSetting(_ExpandableSetting.textDirection),
isExpanded: _expandedSettingId == _ExpandableSetting.textDirection,
),
SettingsListItem<Locale?>(
title: localizations.settingsLocale,
selectedOption: options.locale == deviceLocale
? systemLocaleOption
: options.locale,
optionsMap: _getLocaleOptions(),
onOptionChanged: (Locale? newLocale) {
if (newLocale == systemLocaleOption) {
newLocale = deviceLocale;
}
GalleryOptions.update(
context,
options.copyWith(locale: newLocale),
);
},
onTapSetting: () => onTapSetting(_ExpandableSetting.locale),
isExpanded: _expandedSettingId == _ExpandableSetting.locale,
),
SettingsListItem<TargetPlatform?>(
title: localizations.settingsPlatformMechanics,
selectedOption: options.platform,
optionsMap: LinkedHashMap<TargetPlatform?, DisplayOption>.of(<TargetPlatform?, DisplayOption>{
TargetPlatform.android: DisplayOption('Android'),
TargetPlatform.iOS: DisplayOption('iOS'),
TargetPlatform.macOS: DisplayOption('macOS'),
TargetPlatform.linux: DisplayOption('Linux'),
TargetPlatform.windows: DisplayOption('Windows'),
}),
onOptionChanged: (TargetPlatform? newPlatform) => GalleryOptions.update(
context,
options.copyWith(platform: newPlatform),
),
onTapSetting: () => onTapSetting(_ExpandableSetting.platform),
isExpanded: _expandedSettingId == _ExpandableSetting.platform,
),
SettingsListItem<ThemeMode?>(
title: localizations.settingsTheme,
selectedOption: options.themeMode,
optionsMap: LinkedHashMap<ThemeMode?, DisplayOption>.of(<ThemeMode?, DisplayOption>{
ThemeMode.system: DisplayOption(
localizations.settingsSystemDefault,
),
ThemeMode.dark: DisplayOption(
localizations.settingsDarkTheme,
),
ThemeMode.light: DisplayOption(
localizations.settingsLightTheme,
),
}),
onOptionChanged: (ThemeMode? newThemeMode) => GalleryOptions.update(
context,
options.copyWith(themeMode: newThemeMode),
),
onTapSetting: () => onTapSetting(_ExpandableSetting.theme),
isExpanded: _expandedSettingId == _ExpandableSetting.theme,
),
ToggleSetting(
text: GalleryLocalizations.of(context)!.settingsSlowMotion,
value: options.timeDilation != 1.0,
onChanged: (bool isOn) => GalleryOptions.update(
context,
options.copyWith(timeDilation: isOn ? 5.0 : 1.0),
),
),
];
return Material(
color: colorScheme.secondaryContainer,
child: Padding(
padding: isDesktop
? EdgeInsets.zero
: const EdgeInsets.only(
bottom: galleryHeaderHeight,
),
// Remove ListView top padding as it is already accounted for.
child: MediaQuery.removePadding(
removeTop: isDesktop,
context: context,
child: ListView(
children: <Widget>[
if (isDesktop)
const SizedBox(height: firstHeaderDesktopTopPadding),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: ExcludeSemantics(
child: Header(
color: Theme.of(context).colorScheme.onSurface,
text: localizations.settingsTitle,
),
),
),
if (isDesktop)
...settingsListItems
else ...<Widget>[
_AnimateSettingsListItems(
animation: _staggerSettingsItemsAnimation,
children: settingsListItems,
),
const SizedBox(height: 16),
Divider(thickness: 2, height: 0, color: colorScheme.outline),
const SizedBox(height: 12),
const SettingsAbout(),
const SettingsFeedback(),
const SizedBox(height: 12),
Divider(thickness: 2, height: 0, color: colorScheme.outline),
const SettingsAttribution(),
],
],
),
),
),
);
}
}
class SettingsAbout extends StatelessWidget {
const SettingsAbout({super.key});
@override
Widget build(BuildContext context) {
return _SettingsLink(
title: GalleryLocalizations.of(context)!.settingsAbout,
icon: Icons.info_outline,
onTap: () {
about.showAboutDialog(context: context);
},
);
}
}
class SettingsFeedback extends StatelessWidget {
const SettingsFeedback({super.key});
@override
Widget build(BuildContext context) {
return _SettingsLink(
title: GalleryLocalizations.of(context)!.settingsFeedback,
icon: Icons.feedback,
onTap: () async {
final Uri url =
Uri.parse('https://github.com/flutter/gallery/issues/new/choose/');
if (await canLaunchUrl(url)) {
await launchUrl(url);
}
},
);
}
}
class SettingsAttribution extends StatelessWidget {
const SettingsAttribution({super.key});
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final double verticalPadding = isDesktop ? 0.0 : 28.0;
return MergeSemantics(
child: Padding(
padding: EdgeInsetsDirectional.only(
start: isDesktop ? 24 : 32,
end: isDesktop ? 0 : 32,
top: verticalPadding,
bottom: verticalPadding,
),
child: SelectableText(
GalleryLocalizations.of(context)!.settingsAttribution,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontSize: 12,
color: Theme.of(context).colorScheme.onSecondary,
),
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
),
),
);
}
}
class _SettingsLink extends StatelessWidget {
const _SettingsLink({
required this.title,
this.icon,
this.onTap,
});
final String title;
final IconData? icon;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final TextTheme textTheme = Theme.of(context).textTheme;
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final bool isDesktop = isDisplayDesktop(context);
return InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.symmetric(
horizontal: isDesktop ? 24 : 32,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
icon,
color: colorScheme.onSecondary.withOpacity(0.5),
size: 24,
),
Flexible(
child: Padding(
padding: const EdgeInsetsDirectional.only(
start: 16,
top: 12,
bottom: 12,
),
child: Text(
title,
style: textTheme.titleSmall!.apply(
color: colorScheme.onSecondary,
),
textAlign: isDesktop ? TextAlign.end : TextAlign.start,
),
),
),
],
),
),
);
}
}
/// Animate the settings list items to stagger in from above.
class _AnimateSettingsListItems extends StatelessWidget {
const _AnimateSettingsListItems({
required this.animation,
required this.children,
});
final Animation<double> animation;
final List<Widget> children;
@override
Widget build(BuildContext context) {
const double dividingPadding = 4.0;
final Tween<double> dividerTween = Tween<double>(
begin: 0,
end: dividingPadding,
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: Column(
children: <Widget>[
for (final Widget child in children)
AnimatedBuilder(
animation: animation,
builder: (BuildContext context, Widget? child) {
return Padding(
padding: EdgeInsets.only(
top: dividerTween.animate(animation).value,
),
child: child,
);
},
child: child,
),
],
),
);
}
}

View File

@ -0,0 +1,191 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
import 'metrics.dart';
class SettingsIcon extends StatelessWidget {
const SettingsIcon(this.time, {super.key});
final double time;
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _SettingsIconPainter(time: time, context: context),
);
}
}
class _SettingsIconPainter extends CustomPainter {
_SettingsIconPainter({required this.time, required this.context});
final double time;
final BuildContext context;
late Offset _center;
late double _scaling;
late Canvas _canvas;
/// Computes [_center] and [_scaling], parameters used to convert offsets
/// and lengths in relative units into logical pixels.
///
/// The icon is aligned to the bottom-start corner.
void _computeCenterAndScaling(Size size) {
_scaling = min(size.width / unitWidth, size.height / unitHeight);
_center = Directionality.of(context) == TextDirection.ltr
? Offset(
unitWidth * _scaling / 2, size.height - unitHeight * _scaling / 2)
: Offset(size.width - unitWidth * _scaling / 2,
size.height - unitHeight * _scaling / 2);
}
/// Transforms an offset in relative units into an offset in logical pixels.
Offset _transform(Offset offset) {
return _center + offset * _scaling;
}
/// Transforms a length in relative units into a dimension in logical pixels.
double _size(double length) {
return length * _scaling;
}
/// A rectangle with a fixed location, used to locate gradients.
Rect get _fixedRect {
final Offset topLeft = Offset(-_size(stickLength / 2), -_size(stickWidth / 2));
final Offset bottomRight = Offset(_size(stickLength / 2), _size(stickWidth / 2));
return Rect.fromPoints(topLeft, bottomRight);
}
/// Black or white paint, depending on brightness.
Paint get _monoPaint {
final Color monoColor =
Theme.of(context).colorScheme.brightness == Brightness.light
? Colors.black
: Colors.white;
return Paint()..color = monoColor;
}
/// Pink paint with horizontal gradient.
Paint get _pinkPaint {
const LinearGradient shader = LinearGradient(colors: <Color>[pinkLeft, pinkRight]);
final Rect shaderRect = _fixedRect.translate(
_size(-(stickLength - colorLength(time)) / 2),
0,
);
return Paint()..shader = shader.createShader(shaderRect);
}
/// Teal paint with horizontal gradient.
Paint get _tealPaint {
const LinearGradient shader = LinearGradient(colors: <Color>[tealLeft, tealRight]);
final Rect shaderRect = _fixedRect.translate(
_size((stickLength - colorLength(time)) / 2),
0,
);
return Paint()..shader = shader.createShader(shaderRect);
}
/// Paints a stadium-shaped stick.
void _paintStick({
required Offset center,
required double length,
required double width,
double angle = 0,
required Paint paint,
}) {
// Convert to pixels.
center = _transform(center);
length = _size(length);
width = _size(width);
// Paint.
width = min(width, length);
final double stretch = length / 2;
final double radius = width / 2;
_canvas.save();
_canvas.translate(center.dx, center.dy);
_canvas.rotate(angle);
final Rect leftOval = Rect.fromCircle(
center: Offset(-stretch + radius, 0),
radius: radius,
);
final Rect rightOval = Rect.fromCircle(
center: Offset(stretch - radius, 0),
radius: radius,
);
_canvas.drawPath(
Path()
..arcTo(leftOval, pi / 2, pi, false)
..arcTo(rightOval, -pi / 2, pi, false),
paint,
);
_canvas.restore();
}
@override
void paint(Canvas canvas, Size size) {
_computeCenterAndScaling(size);
_canvas = canvas;
if (isTransitionPhase(time)) {
_paintStick(
center: upperColorOffset(time),
length: colorLength(time),
width: stickWidth,
paint: _pinkPaint,
);
_paintStick(
center: lowerColorOffset(time),
length: colorLength(time),
width: stickWidth,
paint: _tealPaint,
);
_paintStick(
center: upperMonoOffset(time),
length: monoLength(time),
width: knobDiameter,
paint: _monoPaint,
);
_paintStick(
center: lowerMonoOffset(time),
length: monoLength(time),
width: knobDiameter,
paint: _monoPaint,
);
} else {
_paintStick(
center: upperKnobCenter,
length: stickLength,
width: knobDiameter,
angle: -knobRotation(time),
paint: _monoPaint,
);
_paintStick(
center: knobCenter(time),
length: stickLength,
width: knobDiameter,
angle: knobRotation(time),
paint: _monoPaint,
);
}
}
@override
bool shouldRepaint(CustomPainter oldDelegate) =>
oldDelegate is! _SettingsIconPainter || oldDelegate.time != time;
}

View File

@ -0,0 +1,113 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:flutter/material.dart';
// Color gradients.
const Color pinkLeft = Color(0xFFFF5983);
const Color pinkRight = Color(0xFFFF8383);
const Color tealLeft = Color(0xFF1CDDC8);
const Color tealRight = Color(0xFF00A5B3);
// Dimensions.
const int unitHeight = 1;
const int unitWidth = 1;
const double stickLength = 5 / 9;
const double stickWidth = 5 / 36;
const double stickRadius = stickWidth / 2;
const double knobDiameter = 5 / 54;
const double knobRadius = knobDiameter / 2;
const double stickGap = 5 / 54;
// Locations.
const double knobDistanceFromCenter = stickGap / 2 + stickWidth / 2;
const Offset lowerKnobCenter = Offset(0, knobDistanceFromCenter);
const Offset upperKnobCenter = Offset(0, -knobDistanceFromCenter);
const double knobDeviation = stickLength / 2 - stickRadius;
// Key moments in animation.
const double _colorKnobContractionBegins = 1 / 23;
const double _monoKnobExpansionEnds = 11 / 23;
const double _colorKnobContractionEnds = 14 / 23;
// Stages.
bool isTransitionPhase(double time) => time < _colorKnobContractionEnds;
// Curve easing.
const Cubic _curve = Curves.easeInOutCubic;
double _progress(
double time, {
required double begin,
required double end,
}) =>
_curve.transform(((time - begin) / (end - begin)).clamp(0, 1).toDouble());
double _monoKnobProgress(double time) => _progress(
time,
begin: 0,
end: _monoKnobExpansionEnds,
);
double _colorKnobProgress(double time) => _progress(
time,
begin: _colorKnobContractionBegins,
end: _colorKnobContractionEnds,
);
double _rotationProgress(double time) => _progress(
time,
begin: _colorKnobContractionEnds,
end: 1,
);
// Changing lengths: mono.
double monoLength(double time) =>
_monoKnobProgress(time) * (stickLength - knobDiameter) + knobDiameter;
double _monoLengthLeft(double time) =>
min(monoLength(time) - knobRadius, stickRadius);
double _monoLengthRight(double time) =>
monoLength(time) - _monoLengthLeft(time);
double _monoHorizontalOffset(double time) =>
(_monoLengthRight(time) - _monoLengthLeft(time)) / 2 - knobDeviation;
Offset upperMonoOffset(double time) =>
upperKnobCenter + Offset(_monoHorizontalOffset(time), 0);
Offset lowerMonoOffset(double time) =>
lowerKnobCenter + Offset(-_monoHorizontalOffset(time), 0);
// Changing lengths: color.
double colorLength(double time) => (1 - _colorKnobProgress(time)) * stickLength;
Offset upperColorOffset(double time) =>
upperKnobCenter + Offset(stickLength / 2 - colorLength(time) / 2, 0);
Offset lowerColorOffset(double time) =>
lowerKnobCenter + Offset(-stickLength / 2 + colorLength(time) / 2, 0);
// Moving objects.
double knobRotation(double time) => _rotationProgress(time) * pi / 4;
Offset knobCenter(double time) {
final double progress = _rotationProgress(time);
if (progress == 0) {
return lowerKnobCenter;
} else if (progress == 1) {
return upperKnobCenter;
} else {
// Calculates the current location.
final Offset center = Offset(knobDistanceFromCenter / tan(pi / 8), 0);
final double radius = (lowerKnobCenter - center).distance;
final double angle = pi + (progress - 1 / 2) * pi / 4;
return center + Offset.fromDirection(angle, radius);
}
}

View File

@ -0,0 +1,340 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'package:flutter/material.dart';
// Common constants between SlowMotionSetting and SettingsListItem.
final BorderRadius settingItemBorderRadius = BorderRadius.circular(10);
const EdgeInsetsDirectional settingItemHeaderMargin = EdgeInsetsDirectional.fromSTEB(32, 0, 32, 8);
class DisplayOption {
DisplayOption(this.title, {this.subtitle});
final String title;
final String? subtitle;
}
class ToggleSetting extends StatelessWidget {
const ToggleSetting({
super.key,
required this.text,
required this.value,
required this.onChanged,
});
final String text;
final bool value;
final void Function(bool) onChanged;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
return Semantics(
container: true,
child: Container(
margin: settingItemHeaderMargin,
child: Material(
shape: RoundedRectangleBorder(borderRadius: settingItemBorderRadius),
color: colorScheme.secondary,
clipBehavior: Clip.antiAlias,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
SelectableText(
text,
style: textTheme.titleMedium!.apply(
color: colorScheme.onSurface,
),
),
],
),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(end: 8),
child: Switch(
activeColor: colorScheme.primary,
value: value,
onChanged: onChanged,
),
),
],
),
),
),
);
}
}
class SettingsListItem<T> extends StatefulWidget {
const SettingsListItem({
super.key,
required this.optionsMap,
required this.title,
required this.selectedOption,
required this.onOptionChanged,
required this.onTapSetting,
required this.isExpanded,
});
final LinkedHashMap<T, DisplayOption> optionsMap;
final String title;
final T selectedOption;
final ValueChanged<T> onOptionChanged;
final void Function() onTapSetting;
final bool isExpanded;
@override
State<SettingsListItem<T?>> createState() => _SettingsListItemState<T?>();
}
class _SettingsListItemState<T> extends State<SettingsListItem<T?>>
with SingleTickerProviderStateMixin {
static final Animatable<double> _easeInTween =
CurveTween(curve: Curves.easeIn);
static const Duration _expandDuration = Duration(milliseconds: 150);
late AnimationController _controller;
late Animation<double> _childrenHeightFactor;
late Animation<double> _headerChevronRotation;
late Animation<double> _headerSubtitleHeight;
late Animation<EdgeInsetsGeometry> _headerMargin;
late Animation<EdgeInsetsGeometry> _headerPadding;
late Animation<EdgeInsetsGeometry> _childrenPadding;
late Animation<BorderRadius?> _headerBorderRadius;
// For ease of use. Correspond to the keys and values of `widget.optionsMap`.
late Iterable<T?> _options;
late Iterable<DisplayOption> _displayOptions;
@override
void initState() {
super.initState();
_controller = AnimationController(duration: _expandDuration, vsync: this);
_childrenHeightFactor = _controller.drive(_easeInTween);
_headerChevronRotation =
Tween<double>(begin: 0, end: 0.5).animate(_controller);
_headerMargin = EdgeInsetsGeometryTween(
begin: settingItemHeaderMargin,
end: EdgeInsets.zero,
).animate(_controller);
_headerPadding = EdgeInsetsGeometryTween(
begin: const EdgeInsetsDirectional.fromSTEB(16, 10, 0, 10),
end: const EdgeInsetsDirectional.fromSTEB(32, 18, 32, 20),
).animate(_controller);
_headerSubtitleHeight =
_controller.drive(Tween<double>(begin: 1.0, end: 0.0));
_childrenPadding = EdgeInsetsGeometryTween(
begin: const EdgeInsets.symmetric(horizontal: 32),
end: EdgeInsets.zero,
).animate(_controller);
_headerBorderRadius = BorderRadiusTween(
begin: settingItemBorderRadius,
end: BorderRadius.zero,
).animate(_controller);
if (widget.isExpanded) {
_controller.value = 1.0;
}
_options = widget.optionsMap.keys;
_displayOptions = widget.optionsMap.values;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _handleExpansion() {
if (widget.isExpanded) {
_controller.forward();
} else {
_controller.reverse().then<void>((void value) {
if (!mounted) {
return;
}
});
}
}
Widget _buildHeaderWithChildren(BuildContext context, Widget? child) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
_CategoryHeader(
margin: _headerMargin.value,
padding: _headerPadding.value,
borderRadius: _headerBorderRadius.value!,
subtitleHeight: _headerSubtitleHeight,
chevronRotation: _headerChevronRotation,
title: widget.title,
subtitle: widget.optionsMap[widget.selectedOption]?.title ?? '',
onTap: () => widget.onTapSetting(),
),
Padding(
padding: _childrenPadding.value,
child: ClipRect(
child: Align(
heightFactor: _childrenHeightFactor.value,
child: child,
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
_handleExpansion();
final ThemeData theme = Theme.of(context);
return AnimatedBuilder(
animation: _controller.view,
builder: _buildHeaderWithChildren,
child: Container(
constraints: const BoxConstraints(maxHeight: 384),
margin: const EdgeInsetsDirectional.only(start: 24, bottom: 40),
decoration: BoxDecoration(
border: BorderDirectional(
start: BorderSide(
width: 2,
color: theme.colorScheme.background,
),
),
),
child: ListView.builder(
shrinkWrap: true,
itemCount: widget.isExpanded ? _options.length : 0,
itemBuilder: (BuildContext context, int index) {
final DisplayOption displayOption = _displayOptions.elementAt(index);
return RadioListTile<T?>(
value: _options.elementAt(index),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
displayOption.title,
style: theme.textTheme.bodyLarge!.copyWith(
color: Theme.of(context).colorScheme.onPrimary,
),
),
if (displayOption.subtitle != null)
Text(
displayOption.subtitle!,
style: theme.textTheme.bodyLarge!.copyWith(
fontSize: 12,
color: Theme.of(context)
.colorScheme
.onPrimary
.withOpacity(0.8),
),
),
],
),
groupValue: widget.selectedOption,
onChanged: (T? newOption) => widget.onOptionChanged(newOption),
activeColor: Theme.of(context).colorScheme.primary,
dense: true,
);
},
),
),
);
}
}
class _CategoryHeader extends StatelessWidget {
const _CategoryHeader({
this.margin,
required this.padding,
required this.borderRadius,
required this.subtitleHeight,
required this.chevronRotation,
required this.title,
required this.subtitle,
this.onTap,
});
final EdgeInsetsGeometry? margin;
final EdgeInsetsGeometry padding;
final BorderRadiusGeometry borderRadius;
final String title;
final String subtitle;
final Animation<double> subtitleHeight;
final Animation<double> chevronRotation;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final ColorScheme colorScheme = Theme.of(context).colorScheme;
final TextTheme textTheme = Theme.of(context).textTheme;
return Container(
margin: margin,
child: Material(
shape: RoundedRectangleBorder(borderRadius: borderRadius),
color: colorScheme.secondary,
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: onTap,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
title,
style: textTheme.titleMedium!.apply(
color: colorScheme.onSurface,
),
),
SizeTransition(
sizeFactor: subtitleHeight,
child: Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.labelSmall!.apply(
color: colorScheme.primary,
),
),
)
],
),
),
),
Padding(
padding: const EdgeInsetsDirectional.only(
start: 8,
end: 24,
),
child: RotationTransition(
turns: chevronRotation,
child: const Icon(Icons.arrow_drop_down),
),
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,276 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math';
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/material.dart';
import '../constants.dart';
import '../gallery_localizations.dart';
import '../layout/adaptive.dart';
import 'home.dart';
const double homePeekDesktop = 210.0;
const double homePeekMobile = 60.0;
class SplashPageAnimation extends InheritedWidget {
const SplashPageAnimation({
super.key,
required this.isFinished,
required super.child,
});
final bool isFinished;
static SplashPageAnimation? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType();
}
@override
bool updateShouldNotify(SplashPageAnimation oldWidget) => true;
}
class SplashPage extends StatefulWidget {
const SplashPage({
super.key,
required this.child,
});
final Widget child;
@override
State<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends State<SplashPage>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late int _effect;
final Random _random = Random();
// A map of the effect index to its duration. This duration is used to
// determine how long to display the splash animation at launch.
//
// If a new effect is added, this map should be updated.
final Map<int, int> _effectDurations = <int, int>{
1: 5,
2: 4,
3: 4,
4: 5,
5: 5,
6: 4,
7: 4,
8: 4,
9: 3,
10: 6,
};
bool get _isSplashVisible {
return _controller.status == AnimationStatus.completed ||
_controller.status == AnimationStatus.forward;
}
@override
void initState() {
super.initState();
// If the number of included effects changes, this number should be changed.
_effect = _random.nextInt(_effectDurations.length) + 1;
_controller =
AnimationController(duration: splashPageAnimationDuration, vsync: this)
..addListener(() {
setState(() {});
});
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<RelativeRect> _getPanelAnimation(
BuildContext context,
BoxConstraints constraints,
) {
final double height = constraints.biggest.height -
(isDisplayDesktop(context) ? homePeekDesktop : homePeekMobile);
return RelativeRectTween(
begin: RelativeRect.fill,
end: RelativeRect.fromLTRB(0, height, 0, 0),
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
}
@override
Widget build(BuildContext context) {
return NotificationListener<ToggleSplashNotification>(
onNotification: (_) {
_controller.forward();
return true;
},
child: SplashPageAnimation(
isFinished: _controller.status == AnimationStatus.dismissed,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> animation = _getPanelAnimation(context, constraints);
Widget frontLayer = widget.child;
if (_isSplashVisible) {
frontLayer = MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_controller.reverse();
},
onVerticalDragEnd: (DragEndDetails details) {
if (details.velocity.pixelsPerSecond.dy < -200) {
_controller.reverse();
}
},
child: IgnorePointer(child: frontLayer),
),
);
}
if (isDisplayDesktop(context)) {
frontLayer = Padding(
padding: const EdgeInsets.only(top: 136),
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(40),
),
child: frontLayer,
),
);
}
if (isDisplayFoldable(context)) {
return TwoPane(
startPane: frontLayer,
endPane: GestureDetector(
onTap: () {
if (_isSplashVisible) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: _SplashBackLayer(
isSplashCollapsed: !_isSplashVisible, effect: _effect),
),
);
} else {
return Stack(
children: <Widget>[
_SplashBackLayer(
isSplashCollapsed: !_isSplashVisible,
effect: _effect,
onTap: () {
_controller.forward();
},
),
PositionedTransition(
rect: animation,
child: frontLayer,
),
],
);
}
},
),
),
);
}
}
class _SplashBackLayer extends StatelessWidget {
const _SplashBackLayer({
required this.isSplashCollapsed,
required this.effect,
this.onTap,
});
final bool isSplashCollapsed;
final int effect;
final GestureTapCallback? onTap;
@override
Widget build(BuildContext context) {
final String effectAsset = 'splash_effects/splash_effect_$effect.gif';
final Image flutterLogo = Image.asset(
'assets/logo/flutter_logo.png',
package: 'flutter_gallery_assets',
);
Widget? child;
if (isSplashCollapsed) {
if (isDisplayDesktop(context)) {
child = Padding(
padding: const EdgeInsets.only(top: 50),
child: Align(
alignment: Alignment.topCenter,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: onTap,
child: flutterLogo,
),
),
),
);
}
if (isDisplayFoldable(context)) {
child = ColoredBox(
color: Theme.of(context).colorScheme.background,
child: Stack(
children: <Widget>[
Center(
child: flutterLogo,
),
Padding(
padding: const EdgeInsets.only(top: 100.0),
child: Center(
child: Text(
GalleryLocalizations.of(context)!.splashSelectDemo,
),
),
)
],
),
);
}
} else {
child = Stack(
children: <Widget>[
Center(
child: Image.asset(
effectAsset,
package: 'flutter_gallery_assets',
),
),
Center(child: flutterLogo),
],
);
}
return ExcludeSemantics(
child: Material(
// This is the background color of the gifs.
color: const Color(0xFF030303),
child: Padding(
padding: EdgeInsets.only(
bottom: isDisplayDesktop(context)
? homePeekDesktop
: isDisplayFoldable(context)
? 0
: homePeekMobile,
),
child: child,
),
),
);
}
}

View File

@ -0,0 +1,194 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:dual_screen/dual_screen.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'deferred_widget.dart';
import 'main.dart';
import 'pages/demo.dart';
import 'pages/home.dart';
import 'studies/crane/app.dart' deferred as crane;
import 'studies/crane/routes.dart' as crane_routes;
import 'studies/fortnightly/app.dart' deferred as fortnightly;
import 'studies/fortnightly/routes.dart' as fortnightly_routes;
import 'studies/rally/app.dart' deferred as rally;
import 'studies/rally/routes.dart' as rally_routes;
import 'studies/reply/app.dart' as reply;
import 'studies/reply/routes.dart' as reply_routes;
import 'studies/shrine/app.dart' deferred as shrine;
import 'studies/shrine/routes.dart' as shrine_routes;
import 'studies/starter/app.dart' as starter_app;
import 'studies/starter/routes.dart' as starter_app_routes;
typedef PathWidgetBuilder = Widget Function(BuildContext, String?);
class Path {
const Path(this.pattern, this.builder, {this.openInSecondScreen = false});
/// A RegEx string for route matching.
final String pattern;
/// The builder for the associated pattern route. The first argument is the
/// [BuildContext] and the second argument a RegEx match if that is included
/// in the pattern.
///
/// ```dart
/// Path(
/// 'r'^/demo/([\w-]+)$',
/// (context, matches) => Page(argument: match),
/// )
/// ```
final PathWidgetBuilder builder;
/// If the route should open on the second screen on foldables.
final bool openInSecondScreen;
}
class RouteConfiguration {
/// List of [Path] to for route matching. When a named route is pushed with
/// [Navigator.pushNamed], the route name is matched with the [Path.pattern]
/// in the list below. As soon as there is a match, the associated builder
/// will be returned. This means that the paths higher up in the list will
/// take priority.
static List<Path> paths = <Path>[
Path(
r'^' + DemoPage.baseRoute + r'/([\w-]+)$',
(BuildContext context, String? match) => DemoPage(slug: match),
),
Path(
r'^' + rally_routes.homeRoute,
(BuildContext context, String? match) => StudyWrapper(
study: DeferredWidget(rally.loadLibrary,
() => rally.RallyApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + shrine_routes.homeRoute,
(BuildContext context, String? match) => StudyWrapper(
study: DeferredWidget(shrine.loadLibrary,
() => shrine.ShrineApp()), // ignore: prefer_const_constructors
),
openInSecondScreen: true,
),
Path(
r'^' + crane_routes.defaultRoute,
(BuildContext context, String? match) => StudyWrapper(
study: DeferredWidget(crane.loadLibrary,
() => crane.CraneApp(), // ignore: prefer_const_constructors
placeholder: const DeferredLoadingPlaceholder(name: 'Crane')),
),
openInSecondScreen: true,
),
Path(
r'^' + fortnightly_routes.defaultRoute,
(BuildContext context, String? match) => StudyWrapper(
study: DeferredWidget(
fortnightly.loadLibrary,
// ignore: prefer_const_constructors
() => fortnightly.FortnightlyApp()),
),
openInSecondScreen: true,
),
Path(
r'^' + reply_routes.homeRoute,
// ignore: prefer_const_constructors
(BuildContext context, String? match) =>
const StudyWrapper(study: reply.ReplyApp(), hasBottomNavBar: true),
openInSecondScreen: true,
),
Path(
r'^' + starter_app_routes.defaultRoute,
(BuildContext context, String? match) => const StudyWrapper(
study: starter_app.StarterApp(),
),
openInSecondScreen: true,
),
Path(
r'^/',
(BuildContext context, String? match) => const RootPage(),
),
];
/// The route generator callback used when the app is navigated to a named
/// route. Set it on the [MaterialApp.onGenerateRoute] or
/// [WidgetsApp.onGenerateRoute] to make use of the [paths] for route
/// matching.
static Route<dynamic>? onGenerateRoute(
RouteSettings settings,
bool hasHinge,
) {
for (final Path path in paths) {
final RegExp regExpPattern = RegExp(path.pattern);
if (regExpPattern.hasMatch(settings.name!)) {
final RegExpMatch firstMatch = regExpPattern.firstMatch(settings.name!)!;
final String? match = (firstMatch.groupCount == 1) ? firstMatch.group(1) : null;
if (kIsWeb) {
return NoAnimationMaterialPageRoute<void>(
builder: (BuildContext context) => path.builder(context, match),
settings: settings,
);
}
if (path.openInSecondScreen && hasHinge) {
return TwoPanePageRoute<void>(
builder: (BuildContext context) => path.builder(context, match),
settings: settings,
);
} else {
return MaterialPageRoute<void>(
builder: (BuildContext context) => path.builder(context, match),
settings: settings,
);
}
}
}
// If no match was found, we let [WidgetsApp.onUnknownRoute] handle it.
return null;
}
}
class NoAnimationMaterialPageRoute<T> extends MaterialPageRoute<T> {
NoAnimationMaterialPageRoute({
required super.builder,
super.settings,
});
@override
Widget buildTransitions(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child,
) {
return child;
}
}
class TwoPanePageRoute<T> extends OverlayRoute<T> {
TwoPanePageRoute({
required this.builder,
super.settings,
});
final WidgetBuilder builder;
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield OverlayEntry(builder: (BuildContext context) {
final Rect? hinge = MediaQuery.of(context).hinge?.bounds;
if (hinge == null) {
return builder.call(context);
} else {
return Positioned(
top: 0,
left: hinge.right,
right: 0,
bottom: 0,
child: builder.call(context));
}
});
}
}

View File

@ -0,0 +1,60 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../data/gallery_options.dart';
import '../../gallery_localizations.dart';
import 'backdrop.dart';
import 'backlayer.dart';
import 'eat_form.dart';
import 'fly_form.dart';
import 'routes.dart' as routes;
import 'sleep_form.dart';
import 'theme.dart';
class CraneApp extends StatelessWidget {
const CraneApp({super.key});
static const String defaultRoute = routes.defaultRoute;
@override
Widget build(BuildContext context) {
return MaterialApp(
restorationScopeId: 'crane_app',
title: 'Crane',
debugShowCheckedModeBanner: false,
localizationsDelegates: GalleryLocalizations.localizationsDelegates,
supportedLocales: GalleryLocalizations.supportedLocales,
locale: GalleryOptions.of(context).locale,
initialRoute: CraneApp.defaultRoute,
routes: <String, WidgetBuilder>{
CraneApp.defaultRoute: (BuildContext context) => const _Home(),
},
theme: craneTheme.copyWith(
platform: GalleryOptions.of(context).platform,
),
);
}
}
class _Home extends StatelessWidget {
const _Home();
@override
Widget build(BuildContext context) {
return const ApplyTextOptions(
child: Backdrop(
frontLayer: SizedBox(),
backLayerItems: <BackLayerItem>[
FlyForm(),
SleepForm(),
EatForm(),
],
frontTitle: Text('CRANE'),
backTitle: Text('MENU'),
),
);
}
}

View File

@ -0,0 +1,390 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
import '../../data/gallery_options.dart';
import '../../gallery_localizations.dart';
import '../../layout/adaptive.dart';
import '../../layout/image_placeholder.dart';
import 'backlayer.dart';
import 'border_tab_indicator.dart';
import 'colors.dart';
import 'header_form.dart';
import 'item_cards.dart';
import 'model/data.dart';
import 'model/destination.dart';
class _FrontLayer extends StatefulWidget {
const _FrontLayer({
required this.title,
required this.index,
required this.mobileTopOffset,
required this.restorationId,
});
final String title;
final int index;
final double mobileTopOffset;
final String restorationId;
@override
_FrontLayerState createState() => _FrontLayerState();
}
class _FrontLayerState extends State<_FrontLayer> {
List<Destination>? destinations;
static const double frontLayerBorderRadius = 16.0;
static const EdgeInsets bottomPadding = EdgeInsets.only(bottom: 120);
@override
void didChangeDependencies() {
super.didChangeDependencies();
// We use didChangeDependencies because the initialization involves an
// InheritedWidget (for localization). However, we don't need to get
// destinations again when, say, resizing the window.
if (destinations == null) {
if (widget.index == 0) {
destinations = getFlyDestinations(context);
}
if (widget.index == 1) {
destinations = getSleepDestinations(context);
}
if (widget.index == 2) {
destinations = getEatDestinations(context);
}
}
}
Widget _header() {
return Align(
alignment: AlignmentDirectional.centerStart,
child: Padding(
padding: const EdgeInsets.only(
top: 20,
bottom: 22,
),
child: SelectableText(
widget.title,
style: Theme.of(context).textTheme.titleSmall,
),
),
);
}
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final bool isSmallDesktop = isDisplaySmallDesktop(context);
final int crossAxisCount = isDesktop ? 4 : 1;
return FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Padding(
padding: isDesktop
? EdgeInsets.zero
: EdgeInsets.only(top: widget.mobileTopOffset),
child: PhysicalShape(
elevation: 16,
color: cranePrimaryWhite,
clipper: const ShapeBorderClipper(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(frontLayerBorderRadius),
topRight: Radius.circular(frontLayerBorderRadius),
),
),
),
child: Padding(
padding: isDesktop
? EdgeInsets.symmetric(
horizontal:
isSmallDesktop ? appPaddingSmall : appPaddingLarge)
.add(bottomPadding)
: const EdgeInsets.symmetric(horizontal: 20).add(bottomPadding),
child: Column(
children: <Widget>[
_header(),
Expanded(
child: MasonryGridView.count(
key: ValueKey<String>('CraneListView-${widget.index}'),
restorationId: widget.restorationId,
crossAxisCount: crossAxisCount,
crossAxisSpacing: 16.0,
itemBuilder: (BuildContext context, int index) =>
DestinationCard(destination: destinations![index]),
itemCount: destinations!.length,
),
),
],
),
),
),
),
);
}
}
/// Builds a Backdrop.
///
/// A Backdrop widget has two layers, front and back. The front layer is shown
/// by default, and slides down to show the back layer, from which a user
/// can make a selection. The user can also configure the titles for when the
/// front or back layer is showing.
class Backdrop extends StatefulWidget {
const Backdrop({
super.key,
required this.frontLayer,
required this.backLayerItems,
required this.frontTitle,
required this.backTitle,
});
final Widget frontLayer;
final List<BackLayerItem> backLayerItems;
final Widget frontTitle;
final Widget backTitle;
@override
State<Backdrop> createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop>
with TickerProviderStateMixin, RestorationMixin {
final RestorableInt tabIndex = RestorableInt(0);
late TabController _tabController;
late Animation<Offset> _flyLayerHorizontalOffset;
late Animation<Offset> _sleepLayerHorizontalOffset;
late Animation<Offset> _eatLayerHorizontalOffset;
// How much the 'sleep' front layer is vertically offset relative to other
// front layers, in pixels, with the mobile layout.
static const double _sleepLayerTopOffset = 60.0;
@override
String get restorationId => 'tab_non_scrollable_demo';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(tabIndex, 'tab_index');
_tabController.index = tabIndex.value;
}
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(() {
// When the tab controller's value is updated, make sure to update the
// tab index value, which is state restorable.
setState(() {
tabIndex.value = _tabController.index;
});
});
// Offsets to create a horizontal gap between front layers.
final Animation<double> tabControllerAnimation = _tabController.animation!;
_flyLayerHorizontalOffset = tabControllerAnimation.drive(
Tween<Offset>(begin: Offset.zero, end: const Offset(-0.05, 0)));
_sleepLayerHorizontalOffset = tabControllerAnimation.drive(
Tween<Offset>(begin: const Offset(0.05, 0), end: Offset.zero));
_eatLayerHorizontalOffset = tabControllerAnimation.drive(Tween<Offset>(
begin: const Offset(0.10, 0), end: const Offset(0.05, 0)));
}
@override
void dispose() {
_tabController.dispose();
tabIndex.dispose();
super.dispose();
}
void _handleTabs(int tabIndex) {
_tabController.animateTo(tabIndex,
duration: const Duration(milliseconds: 300));
}
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return Material(
color: cranePurple800,
child: Padding(
padding: const EdgeInsets.only(top: 12),
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: Scaffold(
backgroundColor: cranePurple800,
appBar: AppBar(
automaticallyImplyLeading: false,
systemOverlayStyle: SystemUiOverlayStyle.light,
elevation: 0,
titleSpacing: 0,
flexibleSpace: CraneAppBar(
tabController: _tabController,
tabHandler: _handleTabs,
),
),
body: Stack(
children: <Widget>[
BackLayer(
tabController: _tabController,
backLayerItems: widget.backLayerItems,
),
Container(
margin: EdgeInsets.only(
top: isDesktop
? (isDisplaySmallDesktop(context)
? textFieldHeight * 3
: textFieldHeight * 2) +
20 * textScaleFactor / 2
: 175 + 140 * textScaleFactor / 2,
),
// To display the middle front layer higher than the others,
// we allow the TabBarView to overflow by an offset
// (doubled because it technically overflows top & bottom).
// The other front layers are top padded by this offset.
child: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return OverflowBox(
maxHeight:
constraints.maxHeight + _sleepLayerTopOffset * 2,
child: TabBarView(
physics: isDesktop
? const NeverScrollableScrollPhysics()
: null, // use default TabBarView physics
controller: _tabController,
children: <Widget>[
SlideTransition(
position: _flyLayerHorizontalOffset,
child: _FrontLayer(
title: localizations.craneFlySubhead,
index: 0,
mobileTopOffset: _sleepLayerTopOffset,
restorationId: 'fly-subhead',
),
),
SlideTransition(
position: _sleepLayerHorizontalOffset,
child: _FrontLayer(
title: localizations.craneSleepSubhead,
index: 1,
mobileTopOffset: 0,
restorationId: 'sleep-subhead',
),
),
SlideTransition(
position: _eatLayerHorizontalOffset,
child: _FrontLayer(
title: localizations.craneEatSubhead,
index: 2,
mobileTopOffset: _sleepLayerTopOffset,
restorationId: 'eat-subhead',
),
),
],
),
);
}),
),
],
),
),
),
),
);
}
}
class CraneAppBar extends StatefulWidget {
const CraneAppBar({
super.key,
this.tabHandler,
required this.tabController,
});
final void Function(int)? tabHandler;
final TabController tabController;
@override
State<CraneAppBar> createState() => _CraneAppBarState();
}
class _CraneAppBarState extends State<CraneAppBar> {
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final bool isSmallDesktop = isDisplaySmallDesktop(context);
final double textScaleFactor = GalleryOptions.of(context).textScaleFactor(context);
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return SafeArea(
child: Padding(
padding: EdgeInsets.symmetric(
horizontal:
isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: <Widget>[
const ExcludeSemantics(
child: FadeInImagePlaceholder(
image: AssetImage(
'crane/logo/logo.png',
package: 'flutter_gallery_assets',
),
placeholder: SizedBox(
width: 40,
height: 60,
),
width: 40,
height: 60,
),
),
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 24),
child: Theme(
data: Theme.of(context).copyWith(
splashColor: Colors.transparent,
),
child: TabBar(
indicator: BorderTabIndicator(
indicatorHeight: isDesktop ? 28 : 32,
textScaleFactor: textScaleFactor,
),
controller: widget.tabController,
labelPadding: const EdgeInsets.symmetric(horizontal: 32),
isScrollable: true,
// left-align tabs on desktop
labelStyle: Theme.of(context).textTheme.labelLarge,
labelColor: cranePrimaryWhite,
physics: const BouncingScrollPhysics(),
unselectedLabelColor: cranePrimaryWhite.withOpacity(.6),
onTap: (int index) => widget.tabController.animateTo(
index,
duration: const Duration(milliseconds: 300),
),
tabs: <Widget>[
Tab(text: localizations.craneFly),
Tab(text: localizations.craneSleep),
Tab(text: localizations.craneEat),
],
),
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,48 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
abstract class BackLayerItem extends StatefulWidget {
const BackLayerItem({super.key, required this.index});
final int index;
}
class BackLayer extends StatefulWidget {
const BackLayer({
super.key,
required this.backLayerItems,
required this.tabController,
});
final List<BackLayerItem> backLayerItems;
final TabController tabController;
@override
State<BackLayer> createState() => _BackLayerState();
}
class _BackLayerState extends State<BackLayer> {
@override
void initState() {
super.initState();
widget.tabController.addListener(() => setState(() {}));
}
@override
Widget build(BuildContext context) {
final int tabIndex = widget.tabController.index;
return IndexedStack(
index: tabIndex,
children: <Widget>[
for (final BackLayerItem backLayerItem in widget.backLayerItems)
ExcludeFocus(
excluding: backLayerItem.index != tabIndex,
child: backLayerItem,
)
],
);
}
}

View File

@ -0,0 +1,51 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
class BorderTabIndicator extends Decoration {
const BorderTabIndicator({
required this.indicatorHeight,
required this.textScaleFactor,
}) : super();
final double indicatorHeight;
final double textScaleFactor;
@override
BorderPainter createBoxPainter([VoidCallback? onChanged]) {
return BorderPainter(this, indicatorHeight, textScaleFactor, onChanged);
}
}
class BorderPainter extends BoxPainter {
BorderPainter(
this.decoration,
this.indicatorHeight,
this.textScaleFactor,
VoidCallback? onChanged,
) : assert(indicatorHeight >= 0),
super(onChanged);
final BorderTabIndicator decoration;
final double indicatorHeight;
final double textScaleFactor;
@override
void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) {
assert(configuration.size != null);
final double horizontalInset = 16 - 4 * textScaleFactor;
final Rect rect = Offset(offset.dx + horizontalInset,
(configuration.size!.height / 2) - indicatorHeight / 2 - 1) &
Size(configuration.size!.width - 2 * horizontalInset, indicatorHeight);
final Paint paint = Paint();
paint.color = Colors.white;
paint.style = PaintingStyle.stroke;
paint.strokeWidth = 2;
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(56)),
paint,
);
}
}

View File

@ -0,0 +1,20 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
const Color cranePurple700 = Color(0xFF720D5D);
const Color cranePurple800 = Color(0xFF5D1049);
const Color cranePurple900 = Color(0xFF4E0D3A);
const Color craneRed700 = Color(0xFFE30425);
const Color craneWhite60 = Color(0x99FFFFFF);
const Color cranePrimaryWhite = Color(0xFFFFFFFF);
const Color craneErrorOrange = Color(0xFFFF9100);
const Color craneAlpha = Color(0x00FFFFFF);
const Color craneGrey = Color(0xFF747474);
const Color craneBlack = Color(0xFF1E252D);

View File

@ -0,0 +1,76 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'backlayer.dart';
import 'header_form.dart';
class EatForm extends BackLayerItem {
const EatForm({super.key}) : super(index: 2);
@override
State<EatForm> createState() => _EatFormState();
}
class _EatFormState extends State<EatForm> with RestorationMixin {
final RestorableTextEditingController dinerController = RestorableTextEditingController();
final RestorableTextEditingController dateController = RestorableTextEditingController();
final RestorableTextEditingController timeController = RestorableTextEditingController();
final RestorableTextEditingController locationController = RestorableTextEditingController();
@override
String get restorationId => 'eat_form';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(dinerController, 'diner_controller');
registerForRestoration(dateController, 'date_controller');
registerForRestoration(timeController, 'time_controller');
registerForRestoration(locationController, 'location_controller');
}
@override
void dispose() {
dinerController.dispose();
dateController.dispose();
timeController.dispose();
locationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return HeaderForm(
fields: <HeaderFormField>[
HeaderFormField(
index: 0,
iconData: Icons.person,
title: localizations.craneFormDiners,
textController: dinerController.value,
),
HeaderFormField(
index: 1,
iconData: Icons.date_range,
title: localizations.craneFormDate,
textController: dateController.value,
),
HeaderFormField(
index: 2,
iconData: Icons.access_time,
title: localizations.craneFormTime,
textController: timeController.value,
),
HeaderFormField(
index: 3,
iconData: Icons.restaurant_menu,
title: localizations.craneFormLocation,
textController: locationController.value,
),
],
);
}
}

View File

@ -0,0 +1,76 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'backlayer.dart';
import 'header_form.dart';
class FlyForm extends BackLayerItem {
const FlyForm({super.key}) : super(index: 0);
@override
State<FlyForm> createState() => _FlyFormState();
}
class _FlyFormState extends State<FlyForm> with RestorationMixin {
final RestorableTextEditingController travelerController = RestorableTextEditingController();
final RestorableTextEditingController countryDestinationController = RestorableTextEditingController();
final RestorableTextEditingController destinationController = RestorableTextEditingController();
final RestorableTextEditingController dateController = RestorableTextEditingController();
@override
String get restorationId => 'fly_form';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(travelerController, 'diner_controller');
registerForRestoration(countryDestinationController, 'date_controller');
registerForRestoration(destinationController, 'time_controller');
registerForRestoration(dateController, 'location_controller');
}
@override
void dispose() {
travelerController.dispose();
countryDestinationController.dispose();
destinationController.dispose();
dateController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return HeaderForm(
fields: <HeaderFormField>[
HeaderFormField(
index: 0,
iconData: Icons.person,
title: localizations.craneFormTravelers,
textController: travelerController.value,
),
HeaderFormField(
index: 1,
iconData: Icons.place,
title: localizations.craneFormOrigin,
textController: countryDestinationController.value,
),
HeaderFormField(
index: 2,
iconData: Icons.airplanemode_active,
title: localizations.craneFormDestination,
textController: destinationController.value,
),
HeaderFormField(
index: 3,
iconData: Icons.date_range,
title: localizations.craneFormDates,
textController: dateController.value,
),
],
);
}
}

View File

@ -0,0 +1,110 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../layout/adaptive.dart';
import 'colors.dart';
const double textFieldHeight = 60.0;
const double appPaddingLarge = 120.0;
const double appPaddingSmall = 24.0;
class HeaderFormField {
const HeaderFormField({
required this.index,
required this.iconData,
required this.title,
required this.textController,
});
final int index;
final IconData iconData;
final String title;
final TextEditingController textController;
}
class HeaderForm extends StatelessWidget {
const HeaderForm({super.key, required this.fields});
final List<HeaderFormField> fields;
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final bool isSmallDesktop = isDisplaySmallDesktop(context);
return Padding(
padding: EdgeInsets.symmetric(
horizontal:
isDesktop && !isSmallDesktop ? appPaddingLarge : appPaddingSmall,
),
child: isDesktop
? LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
int crossAxisCount = isSmallDesktop ? 2 : 4;
if (fields.length < crossAxisCount) {
crossAxisCount = fields.length;
}
final double itemWidth = constraints.maxWidth / crossAxisCount;
return GridView.count(
crossAxisCount: crossAxisCount,
childAspectRatio: itemWidth / textFieldHeight,
physics: const NeverScrollableScrollPhysics(),
children: <Widget>[
for (final HeaderFormField field in fields)
if ((field.index + 1) % crossAxisCount == 0)
_HeaderTextField(field: field)
else
Padding(
padding: const EdgeInsetsDirectional.only(end: 16),
child: _HeaderTextField(field: field),
),
],
);
})
: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
for (final HeaderFormField field in fields)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: _HeaderTextField(field: field),
)
],
),
);
}
}
class _HeaderTextField extends StatelessWidget {
const _HeaderTextField({required this.field});
final HeaderFormField field;
@override
Widget build(BuildContext context) {
return TextField(
controller: field.textController,
cursorColor: Theme.of(context).colorScheme.secondary,
style:
Theme.of(context).textTheme.bodyLarge!.copyWith(color: Colors.white),
onTap: () {},
decoration: InputDecoration(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.all(16),
fillColor: cranePurple700,
filled: true,
hintText: field.title,
floatingLabelBehavior: FloatingLabelBehavior.never,
prefixIcon: Icon(
field.iconData,
size: 24,
color: Theme.of(context).iconTheme.color,
),
),
);
}
}

View File

@ -0,0 +1,122 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../layout/adaptive.dart';
import '../../layout/highlight_focus.dart';
import '../../layout/image_placeholder.dart';
import 'model/destination.dart';
// Width and height for thumbnail images.
const double mobileThumbnailSize = 60.0;
class DestinationCard extends StatelessWidget {
const DestinationCard({
super.key,
required this.destination,
});
final Destination destination;
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
final TextTheme textTheme = Theme.of(context).textTheme;
final Widget card = isDesktop
? Padding(
padding: const EdgeInsets.only(bottom: 40),
child: Semantics(
container: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: _DestinationImage(destination: destination),
),
Padding(
padding: const EdgeInsets.only(top: 20, bottom: 10),
child: SelectableText(
destination.destination,
style: textTheme.titleMedium,
),
),
SelectableText(
destination.subtitle(context),
semanticsLabel: destination.subtitleSemantics(context),
style: textTheme.titleSmall,
),
],
),
),
)
: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
contentPadding: const EdgeInsetsDirectional.only(end: 8),
leading: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: SizedBox(
width: mobileThumbnailSize,
height: mobileThumbnailSize,
child: _DestinationImage(destination: destination),
),
),
title: SelectableText(destination.destination,
style: textTheme.titleMedium),
subtitle: SelectableText(
destination.subtitle(context),
semanticsLabel: destination.subtitleSemantics(context),
style: textTheme.titleSmall,
),
),
const Divider(thickness: 1),
],
);
return HighlightFocus(
debugLabel: 'DestinationCard: ${destination.destination}',
highlightColor: Colors.red.withOpacity(0.1),
onPressed: () {},
child: card,
);
}
}
class _DestinationImage extends StatelessWidget {
const _DestinationImage({
required this.destination,
});
final Destination destination;
@override
Widget build(BuildContext context) {
final bool isDesktop = isDisplayDesktop(context);
return Semantics(
label: destination.assetSemanticLabel,
child: ExcludeSemantics(
child: FadeInImagePlaceholder(
image: AssetImage(
destination.assetName,
package: 'flutter_gallery_assets',
),
fit: BoxFit.cover,
width: isDesktop ? null : mobileThumbnailSize,
height: isDesktop ? null : mobileThumbnailSize,
placeholder: LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return Container(
color: Colors.black.withOpacity(0.1),
width: constraints.maxWidth,
height: constraints.maxWidth / destination.imageAspectRatio,
);
}),
),
),
);
}
}

View File

@ -0,0 +1,296 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../../gallery_localizations.dart';
import 'destination.dart';
List<FlyDestination> getFlyDestinations(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <FlyDestination>[
FlyDestination(
id: 0,
destination: localizations.craneFly0,
stops: 1,
duration: const Duration(hours: 6, minutes: 15),
assetSemanticLabel: localizations.craneFly0SemanticLabel,
),
FlyDestination(
id: 1,
destination: localizations.craneFly1,
stops: 0,
duration: const Duration(hours: 13, minutes: 30),
assetSemanticLabel: localizations.craneFly1SemanticLabel,
imageAspectRatio: 400 / 410,
),
FlyDestination(
id: 2,
destination: localizations.craneFly2,
stops: 0,
duration: const Duration(hours: 5, minutes: 16),
assetSemanticLabel: localizations.craneFly2SemanticLabel,
imageAspectRatio: 400 / 394,
),
FlyDestination(
id: 3,
destination: localizations.craneFly3,
stops: 2,
duration: const Duration(hours: 19, minutes: 40),
assetSemanticLabel: localizations.craneFly3SemanticLabel,
imageAspectRatio: 400 / 377,
),
FlyDestination(
id: 4,
destination: localizations.craneFly4,
stops: 0,
duration: const Duration(hours: 8, minutes: 24),
assetSemanticLabel: localizations.craneFly4SemanticLabel,
imageAspectRatio: 400 / 308,
),
FlyDestination(
id: 5,
destination: localizations.craneFly5,
stops: 1,
duration: const Duration(hours: 14, minutes: 12),
assetSemanticLabel: localizations.craneFly5SemanticLabel,
imageAspectRatio: 400 / 418,
),
FlyDestination(
id: 6,
destination: localizations.craneFly6,
stops: 0,
duration: const Duration(hours: 5, minutes: 24),
assetSemanticLabel: localizations.craneFly6SemanticLabel,
imageAspectRatio: 400 / 345,
),
FlyDestination(
id: 7,
destination: localizations.craneFly7,
stops: 1,
duration: const Duration(hours: 5, minutes: 43),
assetSemanticLabel: localizations.craneFly7SemanticLabel,
imageAspectRatio: 400 / 408,
),
FlyDestination(
id: 8,
destination: localizations.craneFly8,
stops: 0,
duration: const Duration(hours: 8, minutes: 25),
assetSemanticLabel: localizations.craneFly8SemanticLabel,
imageAspectRatio: 400 / 399,
),
FlyDestination(
id: 9,
destination: localizations.craneFly9,
stops: 1,
duration: const Duration(hours: 15, minutes: 52),
assetSemanticLabel: localizations.craneFly9SemanticLabel,
imageAspectRatio: 400 / 379,
),
FlyDestination(
id: 10,
destination: localizations.craneFly10,
stops: 0,
duration: const Duration(hours: 5, minutes: 57),
assetSemanticLabel: localizations.craneFly10SemanticLabel,
imageAspectRatio: 400 / 307,
),
FlyDestination(
id: 11,
destination: localizations.craneFly11,
stops: 1,
duration: const Duration(hours: 13, minutes: 24),
assetSemanticLabel: localizations.craneFly11SemanticLabel,
imageAspectRatio: 400 / 369,
),
FlyDestination(
id: 12,
destination: localizations.craneFly12,
stops: 2,
duration: const Duration(hours: 10, minutes: 20),
assetSemanticLabel: localizations.craneFly12SemanticLabel,
imageAspectRatio: 400 / 394,
),
FlyDestination(
id: 13,
destination: localizations.craneFly13,
stops: 0,
duration: const Duration(hours: 7, minutes: 15),
assetSemanticLabel: localizations.craneFly13SemanticLabel,
imageAspectRatio: 400 / 433,
),
];
}
List<SleepDestination> getSleepDestinations(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <SleepDestination>[
SleepDestination(
id: 0,
destination: localizations.craneSleep0,
total: 2241,
assetSemanticLabel: localizations.craneSleep0SemanticLabel,
imageAspectRatio: 400 / 308,
),
SleepDestination(
id: 1,
destination: localizations.craneSleep1,
total: 876,
assetSemanticLabel: localizations.craneSleep1SemanticLabel,
),
SleepDestination(
id: 2,
destination: localizations.craneSleep2,
total: 1286,
assetSemanticLabel: localizations.craneSleep2SemanticLabel,
imageAspectRatio: 400 / 377,
),
SleepDestination(
id: 3,
destination: localizations.craneSleep3,
total: 496,
assetSemanticLabel: localizations.craneSleep3SemanticLabel,
imageAspectRatio: 400 / 379,
),
SleepDestination(
id: 4,
destination: localizations.craneSleep4,
total: 390,
assetSemanticLabel: localizations.craneSleep4SemanticLabel,
imageAspectRatio: 400 / 418,
),
SleepDestination(
id: 5,
destination: localizations.craneSleep5,
total: 876,
assetSemanticLabel: localizations.craneSleep5SemanticLabel,
imageAspectRatio: 400 / 410,
),
SleepDestination(
id: 6,
destination: localizations.craneSleep6,
total: 989,
assetSemanticLabel: localizations.craneSleep6SemanticLabel,
imageAspectRatio: 400 / 394,
),
SleepDestination(
id: 7,
destination: localizations.craneSleep7,
total: 306,
assetSemanticLabel: localizations.craneSleep7SemanticLabel,
imageAspectRatio: 400 / 266,
),
SleepDestination(
id: 8,
destination: localizations.craneSleep8,
total: 385,
assetSemanticLabel: localizations.craneSleep8SemanticLabel,
imageAspectRatio: 400 / 376,
),
SleepDestination(
id: 9,
destination: localizations.craneSleep9,
total: 989,
assetSemanticLabel: localizations.craneSleep9SemanticLabel,
imageAspectRatio: 400 / 369,
),
SleepDestination(
id: 10,
destination: localizations.craneSleep10,
total: 1380,
assetSemanticLabel: localizations.craneSleep10SemanticLabel,
imageAspectRatio: 400 / 307,
),
SleepDestination(
id: 11,
destination: localizations.craneSleep11,
total: 1109,
assetSemanticLabel: localizations.craneSleep11SemanticLabel,
imageAspectRatio: 400 / 456,
),
];
}
List<EatDestination> getEatDestinations(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return <EatDestination>[
EatDestination(
id: 0,
destination: localizations.craneEat0,
total: 354,
assetSemanticLabel: localizations.craneEat0SemanticLabel,
imageAspectRatio: 400 / 444,
),
EatDestination(
id: 1,
destination: localizations.craneEat1,
total: 623,
assetSemanticLabel: localizations.craneEat1SemanticLabel,
imageAspectRatio: 400 / 340,
),
EatDestination(
id: 2,
destination: localizations.craneEat2,
total: 124,
assetSemanticLabel: localizations.craneEat2SemanticLabel,
imageAspectRatio: 400 / 406,
),
EatDestination(
id: 3,
destination: localizations.craneEat3,
total: 495,
assetSemanticLabel: localizations.craneEat3SemanticLabel,
imageAspectRatio: 400 / 323,
),
EatDestination(
id: 4,
destination: localizations.craneEat4,
total: 683,
assetSemanticLabel: localizations.craneEat4SemanticLabel,
imageAspectRatio: 400 / 404,
),
EatDestination(
id: 5,
destination: localizations.craneEat5,
total: 786,
assetSemanticLabel: localizations.craneEat5SemanticLabel,
imageAspectRatio: 400 / 407,
),
EatDestination(
id: 6,
destination: localizations.craneEat6,
total: 323,
assetSemanticLabel: localizations.craneEat6SemanticLabel,
imageAspectRatio: 400 / 431,
),
EatDestination(
id: 7,
destination: localizations.craneEat7,
total: 285,
assetSemanticLabel: localizations.craneEat7SemanticLabel,
imageAspectRatio: 400 / 422,
),
EatDestination(
id: 8,
destination: localizations.craneEat8,
total: 323,
assetSemanticLabel: localizations.craneEat8SemanticLabel,
imageAspectRatio: 400 / 300,
),
EatDestination(
id: 9,
destination: localizations.craneEat9,
total: 1406,
assetSemanticLabel: localizations.craneEat9SemanticLabel,
imageAspectRatio: 400 / 451,
),
EatDestination(
id: 10,
destination: localizations.craneEat10,
total: 849,
assetSemanticLabel: localizations.craneEat10SemanticLabel,
imageAspectRatio: 400 / 266,
),
];
}

View File

@ -0,0 +1,118 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../../data/gallery_options.dart';
import '../../../gallery_localizations.dart';
import 'formatters.dart';
abstract class Destination {
const Destination({
required this.id,
required this.destination,
required this.assetSemanticLabel,
required this.imageAspectRatio,
});
final int id;
final String destination;
final String assetSemanticLabel;
final double imageAspectRatio;
String get assetName;
String subtitle(BuildContext context);
String subtitleSemantics(BuildContext context) => subtitle(context);
@override
String toString() => '$destination (id=$id)';
}
class FlyDestination extends Destination {
const FlyDestination({
required super.id,
required super.destination,
required super.assetSemanticLabel,
required this.stops,
super.imageAspectRatio = 1,
this.duration,
});
final int stops;
final Duration? duration;
@override
String get assetName => 'crane/destinations/fly_$id.jpg';
@override
String subtitle(BuildContext context) {
final String stopsText = GalleryLocalizations.of(context)!.craneFlyStops(stops);
if (duration == null) {
return stopsText;
} else {
final TextDirection? textDirection = GalleryOptions.of(context).resolvedTextDirection();
final String durationText =
formattedDuration(context, duration!, abbreviated: true);
return textDirection == TextDirection.ltr
? '$stopsText · $durationText'
: '$durationText · $stopsText';
}
}
@override
String subtitleSemantics(BuildContext context) {
final String stopsText = GalleryLocalizations.of(context)!.craneFlyStops(stops);
if (duration == null) {
return stopsText;
} else {
final String durationText =
formattedDuration(context, duration!, abbreviated: false);
return '$stopsText, $durationText';
}
}
}
class SleepDestination extends Destination {
const SleepDestination({
required super.id,
required super.destination,
required super.assetSemanticLabel,
required this.total,
super.imageAspectRatio = 1,
});
final int total;
@override
String get assetName => 'crane/destinations/sleep_$id.jpg';
@override
String subtitle(BuildContext context) {
return GalleryLocalizations.of(context)!.craneSleepProperties(total);
}
}
class EatDestination extends Destination {
const EatDestination({
required super.id,
required super.destination,
required super.assetSemanticLabel,
required this.total,
super.imageAspectRatio = 1,
});
final int total;
@override
String get assetName => 'crane/destinations/eat_$id.jpg';
@override
String subtitle(BuildContext context) {
return GalleryLocalizations.of(context)!.craneEatRestaurants(total);
}
}

View File

@ -0,0 +1,16 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../../gallery_localizations.dart';
// Duration of time (e.g. 16h 12m)
String formattedDuration(BuildContext context, Duration duration,
{bool? abbreviated}) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
final String hoursShortForm = localizations.craneHours(duration.inHours);
final String minutesShortForm = localizations.craneMinutes(duration.inMinutes % 60);
return localizations.craneFlightDuration(hoursShortForm, minutesShortForm);
}

View File

@ -0,0 +1,5 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const String defaultRoute = '/crane';

View File

@ -0,0 +1,68 @@
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import '../../gallery_localizations.dart';
import 'backlayer.dart';
import 'header_form.dart';
class SleepForm extends BackLayerItem {
const SleepForm({super.key}) : super(index: 1);
@override
State<SleepForm> createState() => _SleepFormState();
}
class _SleepFormState extends State<SleepForm> with RestorationMixin {
final RestorableTextEditingController travelerController = RestorableTextEditingController();
final RestorableTextEditingController dateController = RestorableTextEditingController();
final RestorableTextEditingController locationController = RestorableTextEditingController();
@override
String get restorationId => 'sleep_form';
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(travelerController, 'diner_controller');
registerForRestoration(dateController, 'date_controller');
registerForRestoration(locationController, 'time_controller');
}
@override
void dispose() {
travelerController.dispose();
dateController.dispose();
locationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final GalleryLocalizations localizations = GalleryLocalizations.of(context)!;
return HeaderForm(
fields: <HeaderFormField>[
HeaderFormField(
index: 0,
iconData: Icons.person,
title: localizations.craneFormTravelers,
textController: travelerController.value,
),
HeaderFormField(
index: 1,
iconData: Icons.date_range,
title: localizations.craneFormDates,
textController: dateController.value,
),
HeaderFormField(
index: 2,
iconData: Icons.hotel,
title: localizations.craneFormLocation,
textController: locationController.value,
),
],
);
}
}

Some files were not shown because too many files have changed in this diff Show More