Skip to content

Instantly share code, notes, and snippets.

@matiszz
Created June 22, 2021 10:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save matiszz/e9b09291e22779658018f25848e45852 to your computer and use it in GitHub Desktop.
Save matiszz/e9b09291e22779658018f25848e45852 to your computer and use it in GitHub Desktop.
Trial workaround for Lesson 07 of the Flutter Udacity course
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