mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[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:
parent
c530276f78
commit
2d4f5a65c4
29
dev/integration_tests/new_gallery/README.md
Normal file
29
dev/integration_tests/new_gallery/README.md
Normal 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.
|
@ -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);
|
@ -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;
|
||||
}
|
53
dev/integration_tests/new_gallery/lib/constants.dart
Normal file
53
dev/integration_tests/new_gallery/lib/constants.dart
Normal 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,
|
||||
]);
|
1323
dev/integration_tests/new_gallery/lib/data/demos.dart
Normal file
1323
dev/integration_tests/new_gallery/lib/data/demos.dart
Normal file
File diff suppressed because it is too large
Load Diff
282
dev/integration_tests/new_gallery/lib/data/gallery_options.dart
Normal file
282
dev/integration_tests/new_gallery/lib/data/gallery_options.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
174
dev/integration_tests/new_gallery/lib/data/icons.dart
Normal file
174
dev/integration_tests/new_gallery/lib/data/icons.dart
Normal 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;
|
||||
}
|
120
dev/integration_tests/new_gallery/lib/deferred_widget.dart
Normal file
120
dev/integration_tests/new_gallery/lib/deferred_widget.dart
Normal 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()),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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';
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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,
|
||||
}
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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,
|
||||
}
|
@ -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';
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
@ -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
|
@ -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.
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
4950
dev/integration_tests/new_gallery/lib/gallery_localizations.dart
Normal file
4950
dev/integration_tests/new_gallery/lib/gallery_localizations.dart
Normal file
File diff suppressed because it is too large
Load Diff
5080
dev/integration_tests/new_gallery/lib/gallery_localizations_en.dart
Normal file
5080
dev/integration_tests/new_gallery/lib/gallery_localizations_en.dart
Normal file
File diff suppressed because it is too large
Load Diff
3426
dev/integration_tests/new_gallery/lib/l10n/intl_en.arb
Normal file
3426
dev/integration_tests/new_gallery/lib/l10n/intl_en.arb
Normal file
File diff suppressed because it is too large
Load Diff
828
dev/integration_tests/new_gallery/lib/l10n/intl_en_IS.arb
Normal file
828
dev/integration_tests/new_gallery/lib/l10n/intl_en_IS.arb
Normal 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": "A–Z",
|
||||
"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"
|
||||
}
|
44
dev/integration_tests/new_gallery/lib/layout/adaptive.dart
Normal file
44
dev/integration_tests/new_gallery/lib/layout/adaptive.dart
Normal 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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
42
dev/integration_tests/new_gallery/lib/layout/text_scale.dart
Normal file
42
dev/integration_tests/new_gallery/lib/layout/text_scale.dart
Normal 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);
|
||||
}
|
102
dev/integration_tests/new_gallery/lib/main.dart
Normal file
102
dev/integration_tests/new_gallery/lib/main.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
130
dev/integration_tests/new_gallery/lib/pages/about.dart
Normal file
130
dev/integration_tests/new_gallery/lib/pages/about.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
304
dev/integration_tests/new_gallery/lib/pages/backdrop.dart
Normal file
304
dev/integration_tests/new_gallery/lib/pages/backdrop.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
819
dev/integration_tests/new_gallery/lib/pages/demo.dart
Normal file
819
dev/integration_tests/new_gallery/lib/pages/demo.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
1212
dev/integration_tests/new_gallery/lib/pages/home.dart
Normal file
1212
dev/integration_tests/new_gallery/lib/pages/home.dart
Normal file
File diff suppressed because it is too large
Load Diff
472
dev/integration_tests/new_gallery/lib/pages/settings.dart
Normal file
472
dev/integration_tests/new_gallery/lib/pages/settings.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
276
dev/integration_tests/new_gallery/lib/pages/splash.dart
Normal file
276
dev/integration_tests/new_gallery/lib/pages/splash.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
194
dev/integration_tests/new_gallery/lib/routes.dart
Normal file
194
dev/integration_tests/new_gallery/lib/routes.dart
Normal 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));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
60
dev/integration_tests/new_gallery/lib/studies/crane/app.dart
Normal file
60
dev/integration_tests/new_gallery/lib/studies/crane/app.dart
Normal 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'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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,
|
||||
),
|
||||
];
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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';
|
@ -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
Loading…
Reference in New Issue
Block a user