mirror of
https://github.com/flutter/samples.git
synced 2025-11-11 15:28:44 +00:00
[Gallery] Add animation for settings backdrop (#252)
* Lift up AnimationController to parnet widget AnimatedBackdrop so it can be shared with child widgets. Make the boolean 'isSettingsOpen' a ValueNotifier so we can listen to changes for it in SettingsPage. * When closing settings, also shrink expanded setting. * Animate the settings page to slide in from above. * Add in stagger animation for setting items. * Make sure that state is updated so the test passes * Use setState for when closing expanded setting * Move build method last, move animations initialization to initState and fix spelling mistake
This commit is contained in:
@@ -12,8 +12,6 @@ import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/pages/backdrop.dart';
|
||||
import 'package:gallery/pages/home.dart';
|
||||
import 'package:gallery/pages/settings.dart';
|
||||
import 'package:gallery/pages/splash.dart';
|
||||
import 'package:gallery/themes/gallery_theme_data.dart';
|
||||
|
||||
@@ -70,10 +68,7 @@ class GalleryApp extends StatelessWidget {
|
||||
},
|
||||
home: ApplyTextOptions(
|
||||
child: SplashPage(
|
||||
child: Backdrop(
|
||||
frontLayer: SettingsPage(),
|
||||
backLayer: HomePage(),
|
||||
),
|
||||
child: AnimatedBackdrop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -13,17 +13,93 @@ import 'package:gallery/constants.dart';
|
||||
import 'package:gallery/data/gallery_options.dart';
|
||||
import 'package:gallery/l10n/gallery_localizations.dart';
|
||||
import 'package:gallery/layout/adaptive.dart';
|
||||
import 'package:gallery/pages/home.dart';
|
||||
import 'package:gallery/pages/settings.dart';
|
||||
|
||||
class AnimatedBackdrop extends StatefulWidget {
|
||||
@override
|
||||
_AnimatedBackdropState createState() => _AnimatedBackdropState();
|
||||
}
|
||||
|
||||
class _AnimatedBackdropState extends State<AnimatedBackdrop>
|
||||
with SingleTickerProviderStateMixin {
|
||||
AnimationController backdropController;
|
||||
ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
Animation<double> openSettingsAnimation;
|
||||
Animation<double> staggerSettingsItemsAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
backdropController = AnimationController(
|
||||
duration: Duration(milliseconds: 200),
|
||||
vsync: this,
|
||||
)..addListener(() {
|
||||
setState(() {
|
||||
// The state that has changed here is the animation.
|
||||
});
|
||||
});
|
||||
isSettingsOpenNotifier = ValueNotifier(false);
|
||||
openSettingsAnimation = CurvedAnimation(
|
||||
parent: backdropController,
|
||||
curve: Interval(
|
||||
0.0,
|
||||
0.4,
|
||||
curve: Curves.ease,
|
||||
),
|
||||
);
|
||||
staggerSettingsItemsAnimation = CurvedAnimation(
|
||||
parent: backdropController,
|
||||
curve: Interval(
|
||||
0.5,
|
||||
1.0,
|
||||
curve: Curves.easeIn,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
backdropController.dispose();
|
||||
isSettingsOpenNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Backdrop(
|
||||
controller: backdropController,
|
||||
isSettingsOpenNotifier: isSettingsOpenNotifier,
|
||||
openSettingsAnimation: openSettingsAnimation,
|
||||
frontLayer: SettingsPage(
|
||||
openSettingsAnimation: openSettingsAnimation,
|
||||
staggerSettingsItemsAnimation: staggerSettingsItemsAnimation,
|
||||
isSettingsOpenNotifier: isSettingsOpenNotifier,
|
||||
),
|
||||
backLayer: HomePage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Backdrop extends StatefulWidget {
|
||||
final Widget frontLayer;
|
||||
final Widget backLayer;
|
||||
final AnimationController controller;
|
||||
final Animation<double> openSettingsAnimation;
|
||||
final ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
|
||||
Backdrop({
|
||||
Key key,
|
||||
@required this.frontLayer,
|
||||
@required this.backLayer,
|
||||
@required this.controller,
|
||||
@required this.openSettingsAnimation,
|
||||
@required this.isSettingsOpenNotifier,
|
||||
}) : assert(frontLayer != null),
|
||||
assert(backLayer != null),
|
||||
assert(controller != null),
|
||||
assert(isSettingsOpenNotifier != null),
|
||||
assert(openSettingsAnimation != null),
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
@@ -32,8 +108,6 @@ class Backdrop extends StatefulWidget {
|
||||
|
||||
class _BackdropState extends State<Backdrop>
|
||||
with SingleTickerProviderStateMixin, FlareController {
|
||||
AnimationController _controller;
|
||||
Animation<double> _animationReversed;
|
||||
FlareAnimationLayer _animationLayer;
|
||||
FlutterActorArtboard _artboard;
|
||||
|
||||
@@ -41,7 +115,6 @@ class _BackdropState extends State<Backdrop>
|
||||
double settingsButtonHeightDesktop = 56;
|
||||
double settingsButtonHeightMobile = 40;
|
||||
|
||||
bool _isSettingsOpen;
|
||||
FocusNode frontLayerFocusNode;
|
||||
FocusNode backLayerFocusNode;
|
||||
|
||||
@@ -50,20 +123,10 @@ class _BackdropState extends State<Backdrop>
|
||||
super.initState();
|
||||
frontLayerFocusNode = FocusNode();
|
||||
backLayerFocusNode = FocusNode();
|
||||
|
||||
_isSettingsOpen = false;
|
||||
_controller = AnimationController(
|
||||
duration: Duration(milliseconds: 100), value: 1, vsync: this)
|
||||
..addListener(() {
|
||||
this.setState(() {});
|
||||
});
|
||||
_animationReversed =
|
||||
Tween<double>(begin: 1.0, end: 0.0).animate(_controller);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
frontLayerFocusNode.dispose();
|
||||
backLayerFocusNode.dispose();
|
||||
super.dispose();
|
||||
@@ -84,7 +147,7 @@ class _BackdropState extends State<Backdrop>
|
||||
bool advance(FlutterActorArtboard artboard, double elapsed) {
|
||||
if (_animationLayer != null) {
|
||||
FlareAnimationLayer layer = _animationLayer;
|
||||
layer.time = _animationReversed.value * layer.duration;
|
||||
layer.time = widget.controller.value * layer.duration;
|
||||
layer.animation.apply(layer.time, _artboard, 1);
|
||||
if (layer.isDone || layer.time == 0) {
|
||||
_animationLayer = null;
|
||||
@@ -104,10 +167,16 @@ class _BackdropState extends State<Backdrop>
|
||||
}
|
||||
|
||||
void toggleSettings() {
|
||||
_controller.fling(velocity: _isSettingsOpen ? 1 : -1);
|
||||
// Animate the settings panel to open or close.
|
||||
widget.controller
|
||||
.fling(velocity: widget.isSettingsOpenNotifier.value ? -1 : 1);
|
||||
setState(() {
|
||||
widget.isSettingsOpenNotifier.value =
|
||||
!widget.isSettingsOpenNotifier.value;
|
||||
});
|
||||
// Animate the settings icon.
|
||||
initAnimationLayer();
|
||||
isActive.value = true;
|
||||
_isSettingsOpen = !_isSettingsOpen;
|
||||
}
|
||||
|
||||
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
|
||||
@@ -115,14 +184,17 @@ class _BackdropState extends State<Backdrop>
|
||||
final double top = height - galleryHeaderHeight;
|
||||
final double bottom = -galleryHeaderHeight;
|
||||
return RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(0, top, 0, bottom),
|
||||
end: RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
).animate(CurvedAnimation(parent: _controller, curve: Curves.linear));
|
||||
begin: RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
end: RelativeRect.fromLTRB(0, top, 0, bottom),
|
||||
).animate(CurvedAnimation(
|
||||
parent: widget.openSettingsAnimation,
|
||||
curve: Curves.linear,
|
||||
));
|
||||
}
|
||||
|
||||
Widget _galleryHeader() {
|
||||
return ExcludeSemantics(
|
||||
excluding: _isSettingsOpen,
|
||||
excluding: widget.isSettingsOpenNotifier.value,
|
||||
child: Semantics(
|
||||
sortKey: OrdinalSortKey(
|
||||
GalleryOptions.of(context).textDirection() == TextDirection.ltr
|
||||
@@ -137,7 +209,6 @@ class _BackdropState extends State<Backdrop>
|
||||
}
|
||||
|
||||
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
|
||||
final Animation<RelativeRect> animation = _getPanelAnimation(constraints);
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
final safeAreaTopPadding = MediaQuery.of(context).padding.top;
|
||||
|
||||
@@ -145,21 +216,15 @@ class _BackdropState extends State<Backdrop>
|
||||
child: DefaultFocusTraversal(
|
||||
policy: WidgetOrderFocusTraversalPolicy(),
|
||||
child: Focus(
|
||||
skipTraversal: !_isSettingsOpen,
|
||||
child: InheritedBackdrop(
|
||||
toggleSettings: toggleSettings,
|
||||
child: widget.frontLayer,
|
||||
settingsButtonWidth: settingsButtonWidth,
|
||||
desktopSettingsButtonHeight: settingsButtonHeightDesktop,
|
||||
mobileSettingsButtonHeight: settingsButtonHeightMobile,
|
||||
),
|
||||
skipTraversal: !widget.isSettingsOpenNotifier.value,
|
||||
child: widget.frontLayer,
|
||||
),
|
||||
),
|
||||
excluding: !_isSettingsOpen,
|
||||
excluding: !widget.isSettingsOpenNotifier.value,
|
||||
);
|
||||
final Widget backLayer = ExcludeSemantics(
|
||||
child: widget.backLayer,
|
||||
excluding: _isSettingsOpen,
|
||||
excluding: widget.isSettingsOpenNotifier.value,
|
||||
);
|
||||
|
||||
return DefaultFocusTraversal(
|
||||
@@ -173,14 +238,14 @@ class _BackdropState extends State<Backdrop>
|
||||
_galleryHeader(),
|
||||
frontLayer,
|
||||
PositionedTransition(
|
||||
rect: animation,
|
||||
rect: _getPanelAnimation(constraints),
|
||||
child: backLayer,
|
||||
),
|
||||
],
|
||||
if (isDesktop) ...[
|
||||
_galleryHeader(),
|
||||
backLayer,
|
||||
if (_isSettingsOpen) ...[
|
||||
if (widget.isSettingsOpenNotifier.value) ...[
|
||||
ExcludeSemantics(
|
||||
child: ModalBarrier(
|
||||
dismissible: true,
|
||||
@@ -199,7 +264,9 @@ class _BackdropState extends State<Backdrop>
|
||||
? Alignment.topRight
|
||||
: Alignment.topLeft,
|
||||
scale: CurvedAnimation(
|
||||
parent: _animationReversed,
|
||||
parent: isDesktop
|
||||
? widget.controller
|
||||
: widget.openSettingsAnimation,
|
||||
curve: Curves.easeIn,
|
||||
reverseCurve: Curves.easeOut,
|
||||
),
|
||||
@@ -226,7 +293,7 @@ class _BackdropState extends State<Backdrop>
|
||||
alignment: AlignmentDirectional.topEnd,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: _isSettingsOpen
|
||||
label: widget.isSettingsOpenNotifier.value
|
||||
? GalleryLocalizations.of(context)
|
||||
.settingsButtonCloseLabel
|
||||
: GalleryLocalizations.of(context).settingsButtonLabel,
|
||||
@@ -239,7 +306,8 @@ class _BackdropState extends State<Backdrop>
|
||||
borderRadius: BorderRadiusDirectional.only(
|
||||
bottomStart: Radius.circular(10),
|
||||
),
|
||||
color: _isSettingsOpen & !_controller.isAnimating
|
||||
color: widget.isSettingsOpenNotifier.value &
|
||||
!widget.controller.isAnimating
|
||||
? Colors.transparent
|
||||
: Theme.of(context).colorScheme.secondaryVariant,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
@@ -252,7 +320,7 @@ class _BackdropState extends State<Backdrop>
|
||||
onFocusChange: (hasFocus) {
|
||||
if (!hasFocus) {
|
||||
FocusScope.of(context).requestFocus(
|
||||
(_isSettingsOpen)
|
||||
(widget.isSettingsOpenNotifier.value)
|
||||
? frontLayerFocusNode
|
||||
: backLayerFocusNode);
|
||||
}
|
||||
@@ -291,30 +359,6 @@ class _BackdropState extends State<Backdrop>
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedBackdrop extends InheritedWidget {
|
||||
final void Function() toggleSettings;
|
||||
final double settingsButtonWidth;
|
||||
final double desktopSettingsButtonHeight;
|
||||
final double mobileSettingsButtonHeight;
|
||||
|
||||
InheritedBackdrop({
|
||||
this.toggleSettings,
|
||||
this.settingsButtonWidth,
|
||||
this.desktopSettingsButtonHeight,
|
||||
this.mobileSettingsButtonHeight,
|
||||
Widget child,
|
||||
}) : super(child: child);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(InheritedWidget oldWidget) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static InheritedBackdrop of(BuildContext context) {
|
||||
return context.dependOnInheritedWidgetOfExactType();
|
||||
}
|
||||
}
|
||||
|
||||
class InheritedBackdropFocusNodes extends InheritedWidget {
|
||||
InheritedBackdropFocusNodes({
|
||||
@required Widget child,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:collection';
|
||||
|
||||
import "package:collection/collection.dart";
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localized_countries/flutter_localized_countries.dart';
|
||||
import 'package:gallery/constants.dart';
|
||||
@@ -26,6 +26,17 @@ enum _ExpandableSetting {
|
||||
}
|
||||
|
||||
class SettingsPage extends StatefulWidget {
|
||||
SettingsPage({
|
||||
Key key,
|
||||
@required this.openSettingsAnimation,
|
||||
@required this.staggerSettingsItemsAnimation,
|
||||
@required this.isSettingsOpenNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
final Animation<double> openSettingsAnimation;
|
||||
final Animation<double> staggerSettingsItemsAnimation;
|
||||
final ValueNotifier<bool> isSettingsOpenNotifier;
|
||||
|
||||
@override
|
||||
_SettingsPageState createState() => _SettingsPageState();
|
||||
}
|
||||
@@ -54,6 +65,15 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// When closing settings, also shrink expanded setting.
|
||||
widget.isSettingsOpenNotifier.addListener(() {
|
||||
if (!widget.isSettingsOpenNotifier.value) {
|
||||
setState(() {
|
||||
expandedSettingId = null;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Given a [Locale], returns a [DisplayOption] with its native name for a
|
||||
@@ -119,156 +139,168 @@ class _SettingsPageState extends State<SettingsPage> {
|
||||
final options = GalleryOptions.of(context);
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
final settingsListItems = [
|
||||
SettingsListItem<double>(
|
||||
title: GalleryLocalizations.of(context).settingsTextScaling,
|
||||
selectedOption: options.textScaleFactor(
|
||||
context,
|
||||
useSentinel: true,
|
||||
),
|
||||
options: LinkedHashMap.of({
|
||||
systemTextScaleFactorOption: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
),
|
||||
0.8: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingSmall,
|
||||
),
|
||||
1.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingNormal,
|
||||
),
|
||||
2.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingLarge,
|
||||
),
|
||||
3.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingHuge,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextScale) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(textScaleFactor: newTextScale),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.textScale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.textScale,
|
||||
),
|
||||
SettingsListItem<CustomTextDirection>(
|
||||
title: GalleryLocalizations.of(context).settingsTextDirection,
|
||||
selectedOption: options.customTextDirection,
|
||||
options: LinkedHashMap.of({
|
||||
CustomTextDirection.localeBased: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionLocaleBased,
|
||||
),
|
||||
CustomTextDirection.ltr: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionLTR,
|
||||
),
|
||||
CustomTextDirection.rtl: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionRTL,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextDirection) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(customTextDirection: newTextDirection),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.textDirection),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.textDirection,
|
||||
),
|
||||
SettingsListItem<Locale>(
|
||||
title: GalleryLocalizations.of(context).settingsLocale,
|
||||
selectedOption: options.locale == deviceLocale
|
||||
? systemLocaleOption
|
||||
: options.locale,
|
||||
options: _getLocaleOptions(),
|
||||
onOptionChanged: (newLocale) {
|
||||
if (newLocale == systemLocaleOption) {
|
||||
newLocale = deviceLocale;
|
||||
}
|
||||
GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(locale: newLocale),
|
||||
);
|
||||
},
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.locale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.locale,
|
||||
),
|
||||
SettingsListItem<TargetPlatform>(
|
||||
title: GalleryLocalizations.of(context).settingsPlatformMechanics,
|
||||
selectedOption: options.platform,
|
||||
options: LinkedHashMap.of({
|
||||
TargetPlatform.android: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformAndroid,
|
||||
),
|
||||
TargetPlatform.iOS: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformIOS,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newPlatform) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(platform: newPlatform),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.platform),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.platform,
|
||||
),
|
||||
SettingsListItem<ThemeMode>(
|
||||
title: GalleryLocalizations.of(context).settingsTheme,
|
||||
selectedOption: options.themeMode,
|
||||
options: LinkedHashMap.of({
|
||||
ThemeMode.system: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
),
|
||||
ThemeMode.dark: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsDarkTheme,
|
||||
),
|
||||
ThemeMode.light: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsLightTheme,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newThemeMode) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(themeMode: newThemeMode),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.theme),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.theme,
|
||||
),
|
||||
SlowMotionSetting(),
|
||||
];
|
||||
|
||||
return Material(
|
||||
color: colorScheme.secondaryVariant,
|
||||
child: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.only(bottom: galleryHeaderHeight),
|
||||
// Remove ListView top padding as it is already accounted for.
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: isDesktop,
|
||||
context: context,
|
||||
child: ListView(
|
||||
children: [
|
||||
SizedBox(height: firstHeaderDesktopTopPadding),
|
||||
Focus(
|
||||
focusNode:
|
||||
InheritedBackdropFocusNodes.of(context).frontLayerFocusNode,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ExcludeSemantics(
|
||||
child: Header(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
text: GalleryLocalizations.of(context).settingsTitle,
|
||||
child: _AnimatedSettingsPage(
|
||||
animation: widget.openSettingsAnimation,
|
||||
child: Padding(
|
||||
padding: isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.only(
|
||||
bottom: galleryHeaderHeight,
|
||||
),
|
||||
// Remove ListView top padding as it is already accounted for.
|
||||
child: MediaQuery.removePadding(
|
||||
removeTop: isDesktop,
|
||||
context: context,
|
||||
child: ListView(
|
||||
children: [
|
||||
if (isDesktop) SizedBox(height: firstHeaderDesktopTopPadding),
|
||||
Focus(
|
||||
focusNode: InheritedBackdropFocusNodes.of(context)
|
||||
.frontLayerFocusNode,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||
child: ExcludeSemantics(
|
||||
child: Header(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
text: GalleryLocalizations.of(context).settingsTitle,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SettingsListItem<double>(
|
||||
title: GalleryLocalizations.of(context).settingsTextScaling,
|
||||
selectedOption: options.textScaleFactor(
|
||||
context,
|
||||
useSentinel: true,
|
||||
),
|
||||
options: LinkedHashMap.of({
|
||||
systemTextScaleFactorOption: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
if (isDesktop)
|
||||
...settingsListItems
|
||||
else ...[
|
||||
_AnimateSettingsListItems(
|
||||
animation: widget.staggerSettingsItemsAnimation,
|
||||
children: settingsListItems,
|
||||
),
|
||||
0.8: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingSmall,
|
||||
),
|
||||
1.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingNormal,
|
||||
),
|
||||
2.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingLarge,
|
||||
),
|
||||
3.0: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextScalingHuge,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextScale) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(textScaleFactor: newTextScale),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.textScale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.textScale,
|
||||
),
|
||||
SettingsListItem<CustomTextDirection>(
|
||||
title: GalleryLocalizations.of(context).settingsTextDirection,
|
||||
selectedOption: options.customTextDirection,
|
||||
options: LinkedHashMap.of({
|
||||
CustomTextDirection.localeBased: DisplayOption(
|
||||
GalleryLocalizations.of(context)
|
||||
.settingsTextDirectionLocaleBased,
|
||||
),
|
||||
CustomTextDirection.ltr: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionLTR,
|
||||
),
|
||||
CustomTextDirection.rtl: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsTextDirectionRTL,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newTextDirection) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(customTextDirection: newTextDirection),
|
||||
),
|
||||
onTapSetting: () =>
|
||||
onTapSetting(_ExpandableSetting.textDirection),
|
||||
isExpanded:
|
||||
expandedSettingId == _ExpandableSetting.textDirection,
|
||||
),
|
||||
SettingsListItem<Locale>(
|
||||
title: GalleryLocalizations.of(context).settingsLocale,
|
||||
selectedOption: options.locale == deviceLocale
|
||||
? systemLocaleOption
|
||||
: options.locale,
|
||||
options: _getLocaleOptions(),
|
||||
onOptionChanged: (newLocale) {
|
||||
if (newLocale == systemLocaleOption) {
|
||||
newLocale = deviceLocale;
|
||||
}
|
||||
GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(locale: newLocale),
|
||||
);
|
||||
},
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.locale),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.locale,
|
||||
),
|
||||
SettingsListItem<TargetPlatform>(
|
||||
title:
|
||||
GalleryLocalizations.of(context).settingsPlatformMechanics,
|
||||
selectedOption: options.platform,
|
||||
options: LinkedHashMap.of({
|
||||
TargetPlatform.android: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformAndroid,
|
||||
),
|
||||
TargetPlatform.iOS: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsPlatformIOS,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newPlatform) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(platform: newPlatform),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.platform),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.platform,
|
||||
),
|
||||
SettingsListItem<ThemeMode>(
|
||||
title: GalleryLocalizations.of(context).settingsTheme,
|
||||
selectedOption: options.themeMode,
|
||||
options: LinkedHashMap.of({
|
||||
ThemeMode.system: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
||||
),
|
||||
ThemeMode.dark: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsDarkTheme,
|
||||
),
|
||||
ThemeMode.light: DisplayOption(
|
||||
GalleryLocalizations.of(context).settingsLightTheme,
|
||||
),
|
||||
}),
|
||||
onOptionChanged: (newThemeMode) => GalleryOptions.update(
|
||||
context,
|
||||
options.copyWith(themeMode: newThemeMode),
|
||||
),
|
||||
onTapSetting: () => onTapSetting(_ExpandableSetting.theme),
|
||||
isExpanded: expandedSettingId == _ExpandableSetting.theme,
|
||||
),
|
||||
SlowMotionSetting(),
|
||||
if (!isDesktop) ...[
|
||||
SizedBox(height: 16),
|
||||
Divider(thickness: 2, height: 0, color: colorScheme.background),
|
||||
SizedBox(height: 12),
|
||||
SettingsAbout(),
|
||||
SettingsFeedback(),
|
||||
SizedBox(height: 12),
|
||||
Divider(thickness: 2, height: 0, color: colorScheme.background),
|
||||
SettingsAttribution(),
|
||||
SizedBox(height: 16),
|
||||
Divider(
|
||||
thickness: 2, height: 0, color: colorScheme.background),
|
||||
SizedBox(height: 12),
|
||||
SettingsAbout(),
|
||||
SettingsFeedback(),
|
||||
SizedBox(height: 12),
|
||||
Divider(
|
||||
thickness: 2, height: 0, color: colorScheme.background),
|
||||
SettingsAttribution(),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -383,3 +415,93 @@ class _SettingsLink extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the settings page to slide in from above.
|
||||
class _AnimatedSettingsPage extends StatelessWidget {
|
||||
const _AnimatedSettingsPage({
|
||||
Key key,
|
||||
@required this.animation,
|
||||
@required this.child,
|
||||
}) : super(key: key);
|
||||
|
||||
final Widget child;
|
||||
final Animation<double> animation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDesktop = isDisplayDesktop(context);
|
||||
|
||||
if (isDesktop) {
|
||||
return child;
|
||||
} else {
|
||||
return LayoutBuilder(builder: (context, constraints) {
|
||||
return Stack(
|
||||
children: [
|
||||
PositionedTransition(
|
||||
rect: RelativeRectTween(
|
||||
begin: RelativeRect.fromLTRB(0, -constraints.maxHeight, 0, 0),
|
||||
end: RelativeRect.fromLTRB(0, 0, 0, 0),
|
||||
).animate(
|
||||
CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.linear,
|
||||
),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate the settings list items to stagger in from above.
|
||||
class _AnimateSettingsListItems extends StatelessWidget {
|
||||
const _AnimateSettingsListItems({
|
||||
Key key,
|
||||
this.animation,
|
||||
this.children,
|
||||
this.topPadding,
|
||||
this.bottomPadding,
|
||||
}) : super(key: key);
|
||||
|
||||
final Animation<double> animation;
|
||||
final List<Widget> children;
|
||||
final double topPadding;
|
||||
final double bottomPadding;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final startDividingPadding = 4.0;
|
||||
final topPaddingTween = Tween<double>(
|
||||
begin: 0,
|
||||
end: children.length * startDividingPadding,
|
||||
);
|
||||
final dividerTween = Tween<double>(
|
||||
begin: startDividingPadding,
|
||||
end: 0,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: topPaddingTween.animate(animation).value),
|
||||
child: Column(
|
||||
children: [
|
||||
for (Widget child in children)
|
||||
AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(
|
||||
top: dividerTween.animate(animation).value,
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: child,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ import 'package:gallery/pages/backdrop.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Home page hides settings semantics when closed', (tester) async {
|
||||
final animationController = AnimationController(
|
||||
duration: Duration(milliseconds: 1),
|
||||
vsync: const TestVSync(),
|
||||
);
|
||||
final isSettingsOpen = ValueNotifier(false);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: [GalleryLocalizations.delegate],
|
||||
@@ -20,6 +25,9 @@ void main() {
|
||||
child: Backdrop(
|
||||
frontLayer: Text('Front'),
|
||||
backLayer: Text('Back'),
|
||||
controller: animationController,
|
||||
isSettingsOpenNotifier: isSettingsOpen,
|
||||
openSettingsAnimation: animationController,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -40,5 +48,7 @@ void main() {
|
||||
// bottom by utilizing an invisible widget within the Settings Page.
|
||||
expect(tester.getSemantics(find.text('Back')).owner, null);
|
||||
expect(tester.getSemantics(find.text('Front')).label, 'Front');
|
||||
|
||||
animationController.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user