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/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(
|
// Remove ListView top padding as it is already accounted for.
|
||||||
children: [
|
child: MediaQuery.removePadding(
|
||||||
SizedBox(height: firstHeaderDesktopTopPadding),
|
removeTop: isDesktop,
|
||||||
Focus(
|
context: context,
|
||||||
focusNode:
|
child: ListView(
|
||||||
InheritedBackdropFocusNodes.of(context).frontLayerFocusNode,
|
children: [
|
||||||
child: Padding(
|
if (isDesktop) SizedBox(height: firstHeaderDesktopTopPadding),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 32),
|
Focus(
|
||||||
child: ExcludeSemantics(
|
focusNode: InheritedBackdropFocusNodes.of(context)
|
||||||
child: Header(
|
.frontLayerFocusNode,
|
||||||
color: Theme.of(context).colorScheme.onSurface,
|
child: Padding(
|
||||||
text: GalleryLocalizations.of(context).settingsTitle,
|
padding: EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: ExcludeSemantics(
|
||||||
|
child: Header(
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
text: GalleryLocalizations.of(context).settingsTitle,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if (isDesktop)
|
||||||
SettingsListItem<double>(
|
...settingsListItems
|
||||||
title: GalleryLocalizations.of(context).settingsTextScaling,
|
else ...[
|
||||||
selectedOption: options.textScaleFactor(
|
_AnimateSettingsListItems(
|
||||||
context,
|
animation: widget.staggerSettingsItemsAnimation,
|
||||||
useSentinel: true,
|
children: settingsListItems,
|
||||||
),
|
|
||||||
options: LinkedHashMap.of({
|
|
||||||
systemTextScaleFactorOption: DisplayOption(
|
|
||||||
GalleryLocalizations.of(context).settingsSystemDefault,
|
|
||||||
),
|
),
|
||||||
0.8: DisplayOption(
|
SizedBox(height: 16),
|
||||||
GalleryLocalizations.of(context).settingsTextScalingSmall,
|
Divider(
|
||||||
),
|
thickness: 2, height: 0, color: colorScheme.background),
|
||||||
1.0: DisplayOption(
|
SizedBox(height: 12),
|
||||||
GalleryLocalizations.of(context).settingsTextScalingNormal,
|
SettingsAbout(),
|
||||||
),
|
SettingsFeedback(),
|
||||||
2.0: DisplayOption(
|
SizedBox(height: 12),
|
||||||
GalleryLocalizations.of(context).settingsTextScalingLarge,
|
Divider(
|
||||||
),
|
thickness: 2, height: 0, color: colorScheme.background),
|
||||||
3.0: DisplayOption(
|
SettingsAttribution(),
|
||||||
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(),
|
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -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();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user