[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
pull/257/head
Per Classon 5 years ago committed by GitHub
parent be484af4d2
commit 264d697c61
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -12,8 +12,6 @@ import 'package:gallery/constants.dart';
import 'package:gallery/data/gallery_options.dart'; import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/l10n/gallery_localizations.dart'; import 'package:gallery/l10n/gallery_localizations.dart';
import 'package:gallery/pages/backdrop.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/pages/splash.dart';
import 'package:gallery/themes/gallery_theme_data.dart'; import 'package:gallery/themes/gallery_theme_data.dart';
@ -70,10 +68,7 @@ class GalleryApp extends StatelessWidget {
}, },
home: ApplyTextOptions( home: ApplyTextOptions(
child: SplashPage( child: SplashPage(
child: Backdrop( child: AnimatedBackdrop(),
frontLayer: SettingsPage(),
backLayer: HomePage(),
),
), ),
), ),
); );

@ -13,17 +13,93 @@ import 'package:gallery/constants.dart';
import 'package:gallery/data/gallery_options.dart'; import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/l10n/gallery_localizations.dart'; import 'package:gallery/l10n/gallery_localizations.dart';
import 'package:gallery/layout/adaptive.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 { class Backdrop extends StatefulWidget {
final Widget frontLayer; final Widget frontLayer;
final Widget backLayer; final Widget backLayer;
final AnimationController controller;
final Animation<double> openSettingsAnimation;
final ValueNotifier<bool> isSettingsOpenNotifier;
Backdrop({ Backdrop({
Key key, Key key,
@required this.frontLayer, @required this.frontLayer,
@required this.backLayer, @required this.backLayer,
@required this.controller,
@required this.openSettingsAnimation,
@required this.isSettingsOpenNotifier,
}) : assert(frontLayer != null), }) : assert(frontLayer != null),
assert(backLayer != null), assert(backLayer != null),
assert(controller != null),
assert(isSettingsOpenNotifier != null),
assert(openSettingsAnimation != null),
super(key: key); super(key: key);
@override @override
@ -32,8 +108,6 @@ class Backdrop extends StatefulWidget {
class _BackdropState extends State<Backdrop> class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin, FlareController { with SingleTickerProviderStateMixin, FlareController {
AnimationController _controller;
Animation<double> _animationReversed;
FlareAnimationLayer _animationLayer; FlareAnimationLayer _animationLayer;
FlutterActorArtboard _artboard; FlutterActorArtboard _artboard;
@ -41,7 +115,6 @@ class _BackdropState extends State<Backdrop>
double settingsButtonHeightDesktop = 56; double settingsButtonHeightDesktop = 56;
double settingsButtonHeightMobile = 40; double settingsButtonHeightMobile = 40;
bool _isSettingsOpen;
FocusNode frontLayerFocusNode; FocusNode frontLayerFocusNode;
FocusNode backLayerFocusNode; FocusNode backLayerFocusNode;
@ -50,20 +123,10 @@ class _BackdropState extends State<Backdrop>
super.initState(); super.initState();
frontLayerFocusNode = FocusNode(); frontLayerFocusNode = FocusNode();
backLayerFocusNode = 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 @override
void dispose() { void dispose() {
_controller.dispose();
frontLayerFocusNode.dispose(); frontLayerFocusNode.dispose();
backLayerFocusNode.dispose(); backLayerFocusNode.dispose();
super.dispose(); super.dispose();
@ -84,7 +147,7 @@ class _BackdropState extends State<Backdrop>
bool advance(FlutterActorArtboard artboard, double elapsed) { bool advance(FlutterActorArtboard artboard, double elapsed) {
if (_animationLayer != null) { if (_animationLayer != null) {
FlareAnimationLayer layer = _animationLayer; FlareAnimationLayer layer = _animationLayer;
layer.time = _animationReversed.value * layer.duration; layer.time = widget.controller.value * layer.duration;
layer.animation.apply(layer.time, _artboard, 1); layer.animation.apply(layer.time, _artboard, 1);
if (layer.isDone || layer.time == 0) { if (layer.isDone || layer.time == 0) {
_animationLayer = null; _animationLayer = null;
@ -104,10 +167,16 @@ class _BackdropState extends State<Backdrop>
} }
void toggleSettings() { 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(); initAnimationLayer();
isActive.value = true; isActive.value = true;
_isSettingsOpen = !_isSettingsOpen;
} }
Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) { Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) {
@ -115,14 +184,17 @@ class _BackdropState extends State<Backdrop>
final double top = height - galleryHeaderHeight; final double top = height - galleryHeaderHeight;
final double bottom = -galleryHeaderHeight; final double bottom = -galleryHeaderHeight;
return RelativeRectTween( return RelativeRectTween(
begin: RelativeRect.fromLTRB(0, top, 0, bottom), begin: RelativeRect.fromLTRB(0, 0, 0, 0),
end: RelativeRect.fromLTRB(0, 0, 0, 0), end: RelativeRect.fromLTRB(0, top, 0, bottom),
).animate(CurvedAnimation(parent: _controller, curve: Curves.linear)); ).animate(CurvedAnimation(
parent: widget.openSettingsAnimation,
curve: Curves.linear,
));
} }
Widget _galleryHeader() { Widget _galleryHeader() {
return ExcludeSemantics( return ExcludeSemantics(
excluding: _isSettingsOpen, excluding: widget.isSettingsOpenNotifier.value,
child: Semantics( child: Semantics(
sortKey: OrdinalSortKey( sortKey: OrdinalSortKey(
GalleryOptions.of(context).textDirection() == TextDirection.ltr GalleryOptions.of(context).textDirection() == TextDirection.ltr
@ -137,7 +209,6 @@ class _BackdropState extends State<Backdrop>
} }
Widget _buildStack(BuildContext context, BoxConstraints constraints) { Widget _buildStack(BuildContext context, BoxConstraints constraints) {
final Animation<RelativeRect> animation = _getPanelAnimation(constraints);
final isDesktop = isDisplayDesktop(context); final isDesktop = isDisplayDesktop(context);
final safeAreaTopPadding = MediaQuery.of(context).padding.top; final safeAreaTopPadding = MediaQuery.of(context).padding.top;
@ -145,21 +216,15 @@ class _BackdropState extends State<Backdrop>
child: DefaultFocusTraversal( child: DefaultFocusTraversal(
policy: WidgetOrderFocusTraversalPolicy(), policy: WidgetOrderFocusTraversalPolicy(),
child: Focus( child: Focus(
skipTraversal: !_isSettingsOpen, skipTraversal: !widget.isSettingsOpenNotifier.value,
child: InheritedBackdrop( child: widget.frontLayer,
toggleSettings: toggleSettings,
child: widget.frontLayer,
settingsButtonWidth: settingsButtonWidth,
desktopSettingsButtonHeight: settingsButtonHeightDesktop,
mobileSettingsButtonHeight: settingsButtonHeightMobile,
),
), ),
), ),
excluding: !_isSettingsOpen, excluding: !widget.isSettingsOpenNotifier.value,
); );
final Widget backLayer = ExcludeSemantics( final Widget backLayer = ExcludeSemantics(
child: widget.backLayer, child: widget.backLayer,
excluding: _isSettingsOpen, excluding: widget.isSettingsOpenNotifier.value,
); );
return DefaultFocusTraversal( return DefaultFocusTraversal(
@ -173,14 +238,14 @@ class _BackdropState extends State<Backdrop>
_galleryHeader(), _galleryHeader(),
frontLayer, frontLayer,
PositionedTransition( PositionedTransition(
rect: animation, rect: _getPanelAnimation(constraints),
child: backLayer, child: backLayer,
), ),
], ],
if (isDesktop) ...[ if (isDesktop) ...[
_galleryHeader(), _galleryHeader(),
backLayer, backLayer,
if (_isSettingsOpen) ...[ if (widget.isSettingsOpenNotifier.value) ...[
ExcludeSemantics( ExcludeSemantics(
child: ModalBarrier( child: ModalBarrier(
dismissible: true, dismissible: true,
@ -199,7 +264,9 @@ class _BackdropState extends State<Backdrop>
? Alignment.topRight ? Alignment.topRight
: Alignment.topLeft, : Alignment.topLeft,
scale: CurvedAnimation( scale: CurvedAnimation(
parent: _animationReversed, parent: isDesktop
? widget.controller
: widget.openSettingsAnimation,
curve: Curves.easeIn, curve: Curves.easeIn,
reverseCurve: Curves.easeOut, reverseCurve: Curves.easeOut,
), ),
@ -226,7 +293,7 @@ class _BackdropState extends State<Backdrop>
alignment: AlignmentDirectional.topEnd, alignment: AlignmentDirectional.topEnd,
child: Semantics( child: Semantics(
button: true, button: true,
label: _isSettingsOpen label: widget.isSettingsOpenNotifier.value
? GalleryLocalizations.of(context) ? GalleryLocalizations.of(context)
.settingsButtonCloseLabel .settingsButtonCloseLabel
: GalleryLocalizations.of(context).settingsButtonLabel, : GalleryLocalizations.of(context).settingsButtonLabel,
@ -239,7 +306,8 @@ class _BackdropState extends State<Backdrop>
borderRadius: BorderRadiusDirectional.only( borderRadius: BorderRadiusDirectional.only(
bottomStart: Radius.circular(10), bottomStart: Radius.circular(10),
), ),
color: _isSettingsOpen & !_controller.isAnimating color: widget.isSettingsOpenNotifier.value &
!widget.controller.isAnimating
? Colors.transparent ? Colors.transparent
: Theme.of(context).colorScheme.secondaryVariant, : Theme.of(context).colorScheme.secondaryVariant,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
@ -252,7 +320,7 @@ class _BackdropState extends State<Backdrop>
onFocusChange: (hasFocus) { onFocusChange: (hasFocus) {
if (!hasFocus) { if (!hasFocus) {
FocusScope.of(context).requestFocus( FocusScope.of(context).requestFocus(
(_isSettingsOpen) (widget.isSettingsOpenNotifier.value)
? frontLayerFocusNode ? frontLayerFocusNode
: backLayerFocusNode); : 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 { class InheritedBackdropFocusNodes extends InheritedWidget {
InheritedBackdropFocusNodes({ InheritedBackdropFocusNodes({
@required Widget child, @required Widget child,

@ -4,7 +4,7 @@
import 'dart:collection'; import 'dart:collection';
import "package:collection/collection.dart"; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localized_countries/flutter_localized_countries.dart'; import 'package:flutter_localized_countries/flutter_localized_countries.dart';
import 'package:gallery/constants.dart'; import 'package:gallery/constants.dart';
@ -26,6 +26,17 @@ enum _ExpandableSetting {
} }
class SettingsPage extends StatefulWidget { 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 @override
_SettingsPageState createState() => _SettingsPageState(); _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 /// 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 options = GalleryOptions.of(context);
final isDesktop = isDisplayDesktop(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( return Material(
color: colorScheme.secondaryVariant, color: colorScheme.secondaryVariant,
child: Padding( child: _AnimatedSettingsPage(
padding: isDesktop animation: widget.openSettingsAnimation,
? EdgeInsets.zero child: Padding(
: EdgeInsets.only(bottom: galleryHeaderHeight), padding: isDesktop
// Remove ListView top padding as it is already accounted for. ? EdgeInsets.zero
child: MediaQuery.removePadding( : EdgeInsets.only(
removeTop: isDesktop, bottom: galleryHeaderHeight,
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,
),
),
), ),
), // Remove ListView top padding as it is already accounted for.
SettingsListItem<double>( child: MediaQuery.removePadding(
title: GalleryLocalizations.of(context).settingsTextScaling, removeTop: isDesktop,
selectedOption: options.textScaleFactor( context: context,
context, child: ListView(
useSentinel: true, children: [
), if (isDesktop) SizedBox(height: firstHeaderDesktopTopPadding),
options: LinkedHashMap.of({ Focus(
systemTextScaleFactorOption: DisplayOption( focusNode: InheritedBackdropFocusNodes.of(context)
GalleryLocalizations.of(context).settingsSystemDefault, .frontLayerFocusNode,
), child: Padding(
0.8: DisplayOption( padding: EdgeInsets.symmetric(horizontal: 32),
GalleryLocalizations.of(context).settingsTextScalingSmall, child: ExcludeSemantics(
), child: Header(
1.0: DisplayOption( color: Theme.of(context).colorScheme.onSurface,
GalleryLocalizations.of(context).settingsTextScalingNormal, text: GalleryLocalizations.of(context).settingsTitle,
), ),
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: () => if (isDesktop)
onTapSetting(_ExpandableSetting.textDirection), ...settingsListItems
isExpanded: else ...[
expandedSettingId == _ExpandableSetting.textDirection, _AnimateSettingsListItems(
), animation: widget.staggerSettingsItemsAnimation,
SettingsListItem<Locale>( children: settingsListItems,
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( SizedBox(height: 16),
GalleryLocalizations.of(context).settingsPlatformIOS, Divider(
), thickness: 2, height: 0, color: colorScheme.background),
}), SizedBox(height: 12),
onOptionChanged: (newPlatform) => GalleryOptions.update( SettingsAbout(),
context, SettingsFeedback(),
options.copyWith(platform: newPlatform), SizedBox(height: 12),
), Divider(
onTapSetting: () => onTapSetting(_ExpandableSetting.platform), thickness: 2, height: 0, color: colorScheme.background),
isExpanded: expandedSettingId == _ExpandableSetting.platform, SettingsAttribution(),
), ],
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(),
], ],
], ),
), ),
), ),
), ),
@ -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() { void main() {
testWidgets('Home page hides settings semantics when closed', (tester) async { 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( await tester.pumpWidget(
MaterialApp( MaterialApp(
localizationsDelegates: [GalleryLocalizations.delegate], localizationsDelegates: [GalleryLocalizations.delegate],
@ -20,6 +25,9 @@ void main() {
child: Backdrop( child: Backdrop(
frontLayer: Text('Front'), frontLayer: Text('Front'),
backLayer: Text('Back'), 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. // bottom by utilizing an invisible widget within the Settings Page.
expect(tester.getSemantics(find.text('Back')).owner, null); expect(tester.getSemantics(find.text('Back')).owner, null);
expect(tester.getSemantics(find.text('Front')).label, 'Front'); expect(tester.getSemantics(find.text('Front')).label, 'Front');
animationController.dispose();
}); });
} }

Loading…
Cancel
Save