Created
June 22, 2021 10:51
-
-
Save matiszz/e9b09291e22779658018f25848e45852 to your computer and use it in GitHub Desktop.
Trial workaround for Lesson 07 of the Flutter Udacity course
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' as math; | |
import 'package:flutter/material.dart'; | |
import 'package:meta/meta.dart'; | |
import 'category.dart'; | |
const double _kFlingVelocity = 2.0; | |
class _BackdropPanel extends StatelessWidget { | |
const _BackdropPanel({ | |
Key? key, | |
required this.onTap, | |
required this.onVerticalDragUpdate, | |
required this.onVerticalDragEnd, | |
required this.title, | |
required this.child, | |
}) : super(key: key); | |
final VoidCallback onTap; | |
final GestureDragUpdateCallback onVerticalDragUpdate; | |
final GestureDragEndCallback onVerticalDragEnd; | |
final Widget title; | |
final Widget child; | |
@override | |
Widget build(BuildContext context) { | |
return Material( | |
elevation: 2.0, | |
borderRadius: BorderRadius.only( | |
topLeft: Radius.circular(16.0), | |
topRight: Radius.circular(16.0), | |
), | |
child: Column( | |
crossAxisAlignment: CrossAxisAlignment.stretch, | |
children: <Widget>[ | |
GestureDetector( | |
behavior: HitTestBehavior.opaque, | |
onVerticalDragUpdate: onVerticalDragUpdate, | |
onVerticalDragEnd: onVerticalDragEnd, | |
onTap: onTap, | |
child: Container( | |
height: 48.0, | |
padding: EdgeInsetsDirectional.only(start: 16.0), | |
alignment: AlignmentDirectional.centerStart, | |
child: title, | |
), | |
), | |
Divider( | |
height: 1.0, | |
), | |
Expanded( | |
child: child, | |
), | |
], | |
), | |
); | |
} | |
} | |
class _BackdropTitle extends AnimatedWidget { | |
final Widget frontTitle; | |
final Widget backTitle; | |
final Animation<double> animation; | |
const _BackdropTitle({ | |
Key? key, | |
required Listenable listenable, | |
required this.frontTitle, | |
required this.backTitle, | |
required this.animation, | |
}) : super(key: key, listenable: animation); | |
@override | |
Widget build(BuildContext context) { | |
return Stack( | |
children: <Widget>[ | |
Opacity( | |
opacity: CurvedAnimation( | |
parent: ReverseAnimation(animation), | |
curve: Interval(0.5, 1.0), | |
).value, | |
child: backTitle, | |
), | |
Opacity( | |
opacity: CurvedAnimation( | |
parent: animation, | |
curve: Interval(0.5, 1.0), | |
).value, | |
child: frontTitle, | |
), | |
], | |
); | |
} | |
} | |
/// Builds a Backdrop. | |
/// | |
/// A Backdrop widget has two panels, front and back. The front panel is shown | |
/// by default, and slides down to show the back panel, from which a user | |
/// can make a selection. The user can also configure the titles for when the | |
/// front or back panel is showing. | |
class Backdrop extends StatefulWidget { | |
final Category currentCategory; | |
final Widget frontPanel; | |
final Widget backPanel; | |
final Widget frontTitle; | |
final Widget backTitle; | |
const Backdrop({ | |
required this.currentCategory, | |
required this.frontPanel, | |
required this.backPanel, | |
required this.frontTitle, | |
required this.backTitle, | |
}); | |
@override | |
_BackdropState createState() => _BackdropState(); | |
} | |
class _BackdropState extends State<Backdrop> | |
with SingleTickerProviderStateMixin { | |
final GlobalKey _backdropKey = GlobalKey(debugLabel: 'Backdrop'); | |
late AnimationController _controller; | |
@override | |
void initState() { | |
super.initState(); | |
// This creates an [AnimationController] that can allows for animation for | |
// the BackdropPanel. 0.00 means that the front panel is in "tab" (hidden) | |
// mode, while 1.0 means that the front panel is open. | |
_controller = AnimationController( | |
duration: Duration(milliseconds: 300), | |
value: 1.0, | |
vsync: this, | |
); | |
} | |
@override | |
void didUpdateWidget(Backdrop old) { | |
super.didUpdateWidget(old); | |
if (widget.currentCategory != old.currentCategory) { | |
setState(() { | |
_controller.fling( | |
velocity: | |
_backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity); | |
}); | |
} else if (!_backdropPanelVisible) { | |
setState(() { | |
_controller.fling(velocity: _kFlingVelocity); | |
}); | |
} | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
super.dispose(); | |
} | |
bool get _backdropPanelVisible { | |
final AnimationStatus status = _controller.status; | |
return status == AnimationStatus.completed || | |
status == AnimationStatus.forward; | |
} | |
void _toggleBackdropPanelVisibility() { | |
FocusScope.of(context).requestFocus(FocusNode()); | |
_controller.fling( | |
velocity: _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity); | |
} | |
double get _backdropHeight { | |
return 500.0; | |
} | |
// By design: the panel can only be opened with a swipe. To close the panel | |
// the user must either tap its heading or the backdrop's menu icon. | |
void _handleDragUpdate(DragUpdateDetails details) { | |
if (_controller.isAnimating || | |
_controller.status == AnimationStatus.completed) return; | |
_controller.value -= (details.primaryDelta! / _backdropHeight)!; | |
} | |
void _handleDragEnd(DragEndDetails details) { | |
if (_controller.isAnimating || | |
_controller.status == AnimationStatus.completed) return; | |
final double flingVelocity = | |
details.velocity.pixelsPerSecond.dy / _backdropHeight; | |
if (flingVelocity < 0.0) | |
_controller.fling(velocity: math.max(_kFlingVelocity, -flingVelocity)); | |
else if (flingVelocity > 0.0) | |
_controller.fling(velocity: math.min(-_kFlingVelocity, -flingVelocity)); | |
else | |
_controller.fling( | |
velocity: | |
_controller.value < 0.5 ? -_kFlingVelocity : _kFlingVelocity); | |
} | |
Widget _buildStack(BuildContext context, BoxConstraints constraints) { | |
const double panelTitleHeight = 48.0; | |
final Size panelSize = constraints.biggest; | |
final double panelTop = panelSize.height - panelTitleHeight; | |
Animation<RelativeRect> panelAnimation = RelativeRectTween( | |
begin: RelativeRect.fromLTRB( | |
0.0, panelTop, 0.0, panelTop - panelSize.height), | |
end: RelativeRect.fromLTRB(0.0, 0.0, 0.0, 0.0), | |
).animate(_controller.view); | |
return Container( | |
key: _backdropKey, | |
color: widget.currentCategory.color, | |
child: Stack( | |
children: <Widget>[ | |
widget.backPanel, | |
PositionedTransition( | |
rect: panelAnimation, | |
child: _BackdropPanel( | |
onTap: _toggleBackdropPanelVisibility, | |
onVerticalDragUpdate: _handleDragUpdate, | |
onVerticalDragEnd: _handleDragEnd, | |
title: Text(widget.currentCategory.name), | |
child: widget.frontPanel, | |
), | |
), | |
], | |
), | |
); | |
} | |
@override | |
Widget build(BuildContext context) { | |
AnimationController controller = AnimationController( | |
duration: const Duration(milliseconds: 500), vsync: this); | |
return Scaffold( | |
appBar: AppBar( | |
backgroundColor: widget.currentCategory.color, | |
elevation: 0.0, | |
leading: IconButton( | |
onPressed: _toggleBackdropPanelVisibility, | |
icon: AnimatedIcon( | |
icon: AnimatedIcons.close_menu, | |
progress: _controller.view, | |
), | |
), | |
title: _BackdropTitle( | |
listenable: _controller.view, | |
frontTitle: widget.frontTitle, | |
backTitle: widget.backTitle, | |
animation: CurvedAnimation(parent: controller, curve: Curves.easeOut), | |
), | |
), | |
body: LayoutBuilder( | |
builder: _buildStack, | |
), | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment