blob: dcc51cf32b27424ca49138d8a175e2fe1a46c138 [file] [log] [blame]
// Copyright 2019 The Flutter team. 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 'package:flutter_gen/gen_l10n/gallery_localizations.dart';
import 'package:gallery/constants.dart';
import 'package:gallery/data/gallery_options.dart';
import 'package:gallery/layout/adaptive.dart';
import 'package:gallery/pages/home.dart';
import 'package:gallery/pages/settings.dart';
import 'package:gallery/pages/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({
Key key,
this.settingsPage,
this.homePage,
}) : super(key: key);
final Widget settingsPage;
final Widget homePage;
@override
_BackdropState createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop> with TickerProviderStateMixin {
AnimationController _settingsPanelController;
AnimationController _iconController;
FocusNode _settingsPageFocusNode;
ValueNotifier<bool> _isSettingsOpenNotifier;
Widget _settingsPage;
Widget _homePage;
@override
void initState() {
super.initState();
_settingsPanelController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 200),
);
_iconController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 500),
);
_settingsPageFocusNode = FocusNode();
_isSettingsOpenNotifier = ValueNotifier(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: const RelativeRect.fromLTRB(0, 0, 0, 0),
).animate(
CurvedAnimation(
parent: _settingsPanelController,
curve: const Interval(
0.0,
0.4,
curve: Curves.ease,
),
),
);
}
Animation<RelativeRect> _slideDownHomePageAnimation(
BoxConstraints constraints) {
return RelativeRectTween(
begin: const RelativeRect.fromLTRB(0, 0, 0, 0),
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 isDesktop = isDisplayDesktop(context);
final Widget settingsPage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: !isSettingsOpen,
child: isSettingsOpen
? RawKeyboardListener(
includeSemantics: false,
focusNode: _settingsPageFocusNode,
onKey: (event) {
if (event.logicalKey == LogicalKeyboardKey.escape) {
_toggleSettings();
}
},
child: FocusScope(child: _settingsPage),
)
: ExcludeFocus(child: _settingsPage),
);
},
);
final Widget homePage = ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, child) {
return ExcludeSemantics(
excluding: isSettingsOpen,
child: FocusTraversalGroup(child: _homePage),
);
},
);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: GalleryOptions.of(context).resolvedSystemUiOverlayStyle(),
child: Stack(
children: [
if (!isDesktop) ...[
// 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) ...[
Semantics(sortKey: const OrdinalSortKey(2), child: homePage),
ValueListenableBuilder<bool>(
valueListenable: _isSettingsOpenNotifier,
builder: (context, isSettingsOpen, 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.easeIn,
reverseCurve: Curves.easeOut,
),
child: Align(
alignment: AlignmentDirectional.topEnd,
child: Material(
elevation: 7,
clipBehavior: Clip.antiAlias,
borderRadius: BorderRadius.circular(40),
color: Theme.of(context).colorScheme.secondaryVariant,
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(
{this.animationController,
this.toggleSettings,
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 isDesktop = isDisplayDesktop(context);
final 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.secondaryVariant,
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),
),
),
),
),
),
);
}
}