| // 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:flare_dart/math/mat2d.dart'; |
| import 'package:flare_flutter/flare.dart'; |
| import 'package:flare_flutter/flare_actor.dart'; |
| import 'package:flare_flutter/flare_controller.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/services.dart'; |
| 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'; |
| |
| class Backdrop extends StatefulWidget { |
| final Widget frontLayer; |
| final Widget backLayer; |
| |
| Backdrop({ |
| Key key, |
| @required this.frontLayer, |
| @required this.backLayer, |
| }) : assert(frontLayer != null), |
| assert(backLayer != null), |
| super(key: key); |
| |
| @override |
| _BackdropState createState() => _BackdropState(); |
| } |
| |
| class _BackdropState extends State<Backdrop> |
| with SingleTickerProviderStateMixin, FlareController { |
| AnimationController _controller; |
| Animation<double> _animationReversed; |
| FlareAnimationLayer _animationLayer; |
| FlutterActorArtboard _artboard; |
| |
| double settingsButtonWidth = 64; |
| double settingsButtonHeightDesktop = 56; |
| double settingsButtonHeightMobile = 40; |
| |
| bool _isSettingsOpen; |
| FocusNode frontLayerFocusNode; |
| FocusNode backLayerFocusNode; |
| |
| @override |
| void initState() { |
| 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(); |
| } |
| |
| @override |
| void initialize(FlutterActorArtboard artboard) { |
| _artboard = artboard; |
| initAnimationLayer(); |
| } |
| |
| @override |
| void setViewTransform(Mat2D viewTransform) { |
| // This is a necessary override for the [FlareController] mixin. |
| } |
| |
| @override |
| bool advance(FlutterActorArtboard artboard, double elapsed) { |
| if (_animationLayer != null) { |
| FlareAnimationLayer layer = _animationLayer; |
| layer.time = _animationReversed.value * layer.duration; |
| layer.animation.apply(layer.time, _artboard, 1); |
| if (layer.isDone || layer.time == 0) { |
| _animationLayer = null; |
| } |
| } |
| return _animationLayer != null; |
| } |
| |
| void initAnimationLayer() { |
| if (_artboard != null) { |
| final animationName = "Animations"; |
| ActorAnimation animation = _artboard.getAnimation(animationName); |
| _animationLayer = FlareAnimationLayer() |
| ..name = animationName |
| ..animation = animation; |
| } |
| } |
| |
| void toggleSettings() { |
| _controller.fling(velocity: _isSettingsOpen ? 1 : -1); |
| initAnimationLayer(); |
| isActive.value = true; |
| _isSettingsOpen = !_isSettingsOpen; |
| } |
| |
| Animation<RelativeRect> _getPanelAnimation(BoxConstraints constraints) { |
| final double height = constraints.biggest.height; |
| 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)); |
| } |
| |
| Widget _galleryHeader() { |
| return ExcludeSemantics( |
| excluding: _isSettingsOpen, |
| child: Semantics( |
| sortKey: OrdinalSortKey( |
| GalleryOptions.of(context).textDirection() == TextDirection.ltr |
| ? 1.0 |
| : 2.0, |
| name: 'header', |
| ), |
| label: GalleryLocalizations.of(context).homeHeaderGallery, |
| child: Container(), |
| ), |
| ); |
| } |
| |
| Widget _buildStack(BuildContext context, BoxConstraints constraints) { |
| final Animation<RelativeRect> animation = _getPanelAnimation(constraints); |
| final isDesktop = isDisplayDesktop(context); |
| final safeAreaTopPadding = MediaQuery.of(context).padding.top; |
| |
| final Widget frontLayer = ExcludeSemantics( |
| child: DefaultFocusTraversal( |
| policy: WidgetOrderFocusTraversalPolicy(), |
| child: Focus( |
| skipTraversal: !_isSettingsOpen, |
| child: InheritedBackdrop( |
| toggleSettings: toggleSettings, |
| child: widget.frontLayer, |
| settingsButtonWidth: settingsButtonWidth, |
| desktopSettingsButtonHeight: settingsButtonHeightDesktop, |
| mobileSettingsButtonHeight: settingsButtonHeightMobile, |
| ), |
| ), |
| ), |
| excluding: !_isSettingsOpen, |
| ); |
| final Widget backLayer = ExcludeSemantics( |
| child: widget.backLayer, |
| excluding: _isSettingsOpen, |
| ); |
| |
| return DefaultFocusTraversal( |
| child: InheritedBackdropFocusNodes( |
| frontLayerFocusNode: frontLayerFocusNode, |
| backLayerFocusNode: backLayerFocusNode, |
| child: Container( |
| child: Stack( |
| children: [ |
| if (!isDesktop) ...[ |
| _galleryHeader(), |
| frontLayer, |
| PositionedTransition( |
| rect: animation, |
| child: backLayer, |
| ), |
| ], |
| if (isDesktop) ...[ |
| _galleryHeader(), |
| backLayer, |
| if (_isSettingsOpen) ...[ |
| ExcludeSemantics( |
| child: ModalBarrier( |
| dismissible: true, |
| ), |
| ), |
| Semantics( |
| label: GalleryLocalizations.of(context) |
| .settingsButtonCloseLabel, |
| child: GestureDetector( |
| onTap: toggleSettings, |
| ), |
| ) |
| ], |
| ScaleTransition( |
| alignment: Directionality.of(context) == TextDirection.ltr |
| ? Alignment.topRight |
| : Alignment.topLeft, |
| scale: CurvedAnimation( |
| parent: _animationReversed, |
| 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: frontLayer, |
| ), |
| ), |
| ), |
| ), |
| ], |
| Align( |
| alignment: AlignmentDirectional.topEnd, |
| child: Semantics( |
| button: true, |
| label: _isSettingsOpen |
| ? GalleryLocalizations.of(context) |
| .settingsButtonCloseLabel |
| : GalleryLocalizations.of(context).settingsButtonLabel, |
| child: SizedBox( |
| width: settingsButtonWidth, |
| height: isDesktop |
| ? settingsButtonHeightDesktop |
| : settingsButtonHeightMobile + safeAreaTopPadding, |
| child: Material( |
| borderRadius: BorderRadiusDirectional.only( |
| bottomStart: Radius.circular(10), |
| ), |
| color: _isSettingsOpen & !_controller.isAnimating |
| ? Colors.transparent |
| : Theme.of(context).colorScheme.secondaryVariant, |
| child: InkWell( |
| onTap: toggleSettings, |
| child: Padding( |
| padding: const EdgeInsetsDirectional.only( |
| start: 3, end: 18), |
| child: Focus( |
| onFocusChange: (hasFocus) { |
| if (!hasFocus) { |
| FocusScope.of(context).requestFocus( |
| (_isSettingsOpen) |
| ? frontLayerFocusNode |
| : backLayerFocusNode); |
| } |
| }, |
| child: FlareActor( |
| Theme.of(context).colorScheme.brightness == |
| Brightness.light |
| ? 'assets/icons/settings/settings_light.flr' |
| : 'assets/icons/settings/settings_dark.flr', |
| alignment: Directionality.of(context) == |
| TextDirection.ltr |
| ? Alignment.bottomLeft |
| : Alignment.bottomRight, |
| fit: BoxFit.contain, |
| controller: this, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ], |
| ), |
| ), |
| ), |
| ); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return LayoutBuilder( |
| builder: _buildStack, |
| ); |
| } |
| } |
| |
| 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, |
| @required this.frontLayerFocusNode, |
| @required this.backLayerFocusNode, |
| }) : assert(child != null), |
| super(child: child); |
| |
| final FocusNode frontLayerFocusNode; |
| final FocusNode backLayerFocusNode; |
| |
| static InheritedBackdropFocusNodes of(BuildContext context) => |
| context.dependOnInheritedWidgetOfExactType(); |
| |
| @override |
| bool updateShouldNotify(InheritedWidget oldWidget) => true; |
| } |