blob: 8c606b4b61411dab71edfec1a561d6bff57ae1a0 [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 'dart:math';
import 'package:flutter/material.dart';
import 'package:gallery/studies/shrine/page_status.dart';
import 'package:meta/meta.dart';
import 'package:gallery/l10n/gallery_localizations.dart';
import 'package:gallery/studies/shrine/category_menu_page.dart';
const Cubic _accelerateCurve = Cubic(0.548, 0, 0.757, 0.464);
const Cubic _decelerateCurve = Cubic(0.23, 0.94, 0.41, 1);
const _peakVelocityTime = 0.248210;
const _peakVelocityProgress = 0.379146;
class _FrontLayer extends StatelessWidget {
const _FrontLayer({
Key key,
this.onTap,
this.child,
}) : super(key: key);
final VoidCallback onTap;
final Widget child;
@override
Widget build(BuildContext context) {
// An area at the top of the product page.
// When the menu page is shown, tapping this area will close the menu
// page and reveal the product page.
final Widget pageTopArea = Container(
height: 40,
alignment: AlignmentDirectional.centerStart,
);
return Material(
elevation: 16,
shape: const BeveledRectangleBorder(
borderRadius:
BorderRadiusDirectional.only(topStart: Radius.circular(46)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
onTap != null
? GestureDetector(
behavior: HitTestBehavior.opaque,
excludeFromSemantics:
true, // Because there is already a "Close Menu" button on screen.
onTap: onTap,
child: pageTopArea,
)
: pageTopArea,
Expanded(
child: child,
),
],
),
);
}
}
class _BackdropTitle extends AnimatedWidget {
const _BackdropTitle({
Key key,
this.listenable,
this.onPress,
@required this.frontTitle,
@required this.backTitle,
}) : assert(frontTitle != null),
assert(backTitle != null),
super(key: key, listenable: listenable);
final Animation<double> listenable;
final void Function() onPress;
final Widget frontTitle;
final Widget backTitle;
@override
Widget build(BuildContext context) {
final Animation<double> animation = CurvedAnimation(
parent: listenable,
curve: const Interval(0, 0.78),
);
final double textDirectionScalar =
Directionality.of(context) == TextDirection.ltr ? 1 : -1;
final slantedMenuIcon =
ImageIcon(AssetImage('packages/shrine_images/slanted_menu.png'));
final directionalSlantedMenuIcon =
Directionality.of(context) == TextDirection.ltr
? slantedMenuIcon
: Transform(
alignment: Alignment.center,
transform: Matrix4.rotationY(pi),
child: slantedMenuIcon,
);
final String menuButtonTooltip = animation.isCompleted
? GalleryLocalizations.of(context).shrineTooltipOpenMenu
: animation.isDismissed
? GalleryLocalizations.of(context).shrineTooltipCloseMenu
: null;
return DefaultTextStyle(
style: Theme.of(context).primaryTextTheme.title,
softWrap: false,
overflow: TextOverflow.ellipsis,
child: Row(children: [
// branded icon
SizedBox(
width: 72,
child: Semantics(
container: true,
child: IconButton(
padding: const EdgeInsetsDirectional.only(end: 8),
onPressed: onPress,
tooltip: menuButtonTooltip,
icon: Stack(children: [
Opacity(
opacity: animation.value,
child: directionalSlantedMenuIcon,
),
FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(1 * textDirectionScalar, 0),
).evaluate(animation),
child: const ImageIcon(
AssetImage('packages/shrine_images/diamond.png'),
),
),
]),
),
),
),
// Here, we do a custom cross fade between backTitle and frontTitle.
// This makes a smooth animation between the two texts.
Semantics(
container: true,
child: Stack(
children: [
Opacity(
opacity: CurvedAnimation(
parent: ReverseAnimation(animation),
curve: const Interval(0.5, 1),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset.zero,
end: Offset(0.5 * textDirectionScalar, 0),
).evaluate(animation),
child: backTitle,
),
),
Opacity(
opacity: CurvedAnimation(
parent: animation,
curve: const Interval(0.5, 1),
).value,
child: FractionalTranslation(
translation: Tween<Offset>(
begin: Offset(-0.25 * textDirectionScalar, 0),
end: Offset.zero,
).evaluate(animation),
child: frontTitle,
),
),
],
),
),
]),
);
}
}
/// Builds a Backdrop.
///
/// A Backdrop widget has two layers, front and back. The front layer is shown
/// by default, and slides down to show the back layer, from which a user
/// can make a selection. The user can also configure the titles for when the
/// front or back layer is showing.
class Backdrop extends StatefulWidget {
const Backdrop({
@required this.frontLayer,
@required this.backLayer,
@required this.frontTitle,
@required this.backTitle,
@required this.controller,
}) : assert(frontLayer != null),
assert(backLayer != null),
assert(frontTitle != null),
assert(backTitle != null),
assert(controller != null);
final Widget frontLayer;
final Widget backLayer;
final Widget frontTitle;
final Widget backTitle;
final AnimationController controller;
@override
_BackdropState createState() => _BackdropState();
}
class _BackdropState extends State<Backdrop>
with SingleTickerProviderStateMixin {
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop');
AnimationController _controller;
Animation<RelativeRect> _layerAnimation;
@override
void initState() {
super.initState();
_controller = widget.controller;
}
bool get _frontLayerVisible {
final AnimationStatus status = _controller.status;
return status == AnimationStatus.completed ||
status == AnimationStatus.forward;
}
void _toggleBackdropLayerVisibility() {
// Call setState here to update layerAnimation if that's necessary
setState(() {
_frontLayerVisible ? _controller.reverse() : _controller.forward();
});
}
// _layerAnimation animates the front layer between open and close.
// _getLayerAnimation adjusts the values in the TweenSequence so the
// curve and timing are correct in both directions.
Animation<RelativeRect> _getLayerAnimation(Size layerSize, double layerTop) {
Curve firstCurve; // Curve for first TweenSequenceItem
Curve secondCurve; // Curve for second TweenSequenceItem
double firstWeight; // Weight of first TweenSequenceItem
double secondWeight; // Weight of second TweenSequenceItem
Animation<double> animation; // Animation on which TweenSequence runs
if (_frontLayerVisible) {
firstCurve = _accelerateCurve;
secondCurve = _decelerateCurve;
firstWeight = _peakVelocityTime;
secondWeight = 1 - _peakVelocityTime;
animation = CurvedAnimation(
parent: _controller.view,
curve: const Interval(0, 0.78),
);
} else {
// These values are only used when the controller runs from t=1.0 to t=0.0
firstCurve = _decelerateCurve.flipped;
secondCurve = _accelerateCurve.flipped;
firstWeight = 1 - _peakVelocityTime;
secondWeight = _peakVelocityTime;
animation = _controller.view;
}
return TweenSequence<RelativeRect>(
[
TweenSequenceItem<RelativeRect>(
tween: RelativeRectTween(
begin: RelativeRect.fromLTRB(
0,
layerTop,
0,
layerTop - layerSize.height,
),
end: RelativeRect.fromLTRB(
0,
layerTop * _peakVelocityProgress,
0,
(layerTop - layerSize.height) * _peakVelocityProgress,
),
).chain(CurveTween(curve: firstCurve)),
weight: firstWeight,
),
TweenSequenceItem<RelativeRect>(
tween: RelativeRectTween(
begin: RelativeRect.fromLTRB(
0,
layerTop * _peakVelocityProgress,
0,
(layerTop - layerSize.height) * _peakVelocityProgress,
),
end: RelativeRect.fill,
).chain(CurveTween(curve: secondCurve)),
weight: secondWeight,
),
],
).animate(animation);
}
Widget _buildStack(BuildContext context, BoxConstraints constraints) {
const double layerTitleHeight = 48;
final Size layerSize = constraints.biggest;
final double layerTop = layerSize.height - layerTitleHeight;
_layerAnimation = _getLayerAnimation(layerSize, layerTop);
return Stack(
key: _backdropKey,
children: [
ExcludeSemantics(
excluding: _frontLayerVisible,
child: widget.backLayer,
),
PositionedTransition(
rect: _layerAnimation,
child: ExcludeSemantics(
excluding: !_frontLayerVisible,
child: AnimatedBuilder(
animation: PageStatus.of(context).cartController,
builder: (context, child) => AnimatedBuilder(
animation: PageStatus.of(context).menuController,
builder: (context, child) => _FrontLayer(
onTap: menuPageIsVisible(context)
? _toggleBackdropLayerVisibility
: null,
child: widget.frontLayer,
),
),
),
),
),
],
);
}
@override
Widget build(BuildContext context) {
final AppBar appBar = AppBar(
brightness: Brightness.light,
elevation: 0,
titleSpacing: 0,
title: _BackdropTitle(
listenable: _controller.view,
onPress: _toggleBackdropLayerVisibility,
frontTitle: widget.frontTitle,
backTitle: widget.backTitle,
),
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: GalleryLocalizations.of(context).shrineTooltipSearch,
onPressed: () {},
),
IconButton(
icon: const Icon(Icons.tune),
tooltip: GalleryLocalizations.of(context).shrineTooltipSettings,
onPressed: () {},
),
],
);
return AnimatedBuilder(
animation: PageStatus.of(context).cartController,
builder: (context, child) => ExcludeSemantics(
excluding: cartPageIsVisible(context),
child: Scaffold(
appBar: appBar,
body: LayoutBuilder(
builder: _buildStack,
),
),
),
);
}
}
class DesktopBackdrop extends StatelessWidget {
const DesktopBackdrop({
@required this.frontLayer,
@required this.backLayer,
});
final Widget frontLayer;
final Widget backLayer;
@override
Widget build(BuildContext context) {
return Stack(
children: [
backLayer,
Padding(
padding: EdgeInsetsDirectional.only(
start: desktopCategoryMenuPageWidth(context: context),
),
child: Material(
elevation: 16,
color: Colors.white,
child: frontLayer,
),
)
],
);
}
}