OnDismiss Feature

This commit is contained in:
David 2020-09-20 18:42:43 +02:00
parent afbc315d87
commit 8d60fda628
1 changed files with 163 additions and 64 deletions

View File

@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
typedef Widget MiniplayerBuilder(double height, double percentage);
enum SnapPosition { MAX, MIN, DISMISS }
class Miniplayer extends StatefulWidget {
final double minHeight, maxHeight, elevation;
final MiniplayerBuilder builder;
@ -13,6 +15,7 @@ class Miniplayer extends StatefulWidget {
final Color backgroundColor;
final Duration duration;
final ValueNotifier<double> valueNotifier;
final Function onDismiss;
const Miniplayer({
Key key,
@ -24,6 +27,7 @@ class Miniplayer extends StatefulWidget {
this.backgroundColor = const Color(0x70000000),
this.valueNotifier,
this.duration = const Duration(milliseconds: 300),
this.onDismiss,
}) : super(key: key);
@override
@ -32,18 +36,37 @@ class Miniplayer extends StatefulWidget {
class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
ValueNotifier<double> heightNotifier;
ValueNotifier<double> dragDownPercentage = ValueNotifier(0);
double _height;
double _prevHeight;
SnapPosition snap;
//Used to set the height after the animation is complete
double _endHeight;
bool _up;
///Current y position of drag gesture
double _dragHeight;
///Used to determine SnapPosition
double _startHeight;
bool dismissed = false;
bool animating = false;
StreamController<double> _heightController =
StreamController<double>.broadcast();
AnimationController _animationController;
Animation<double> _sizeAnimation;
void statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) {
//unblock touch events
animating = false;
_animationController.dispose();
_animationController = AnimationController(
vsync: this,
duration: widget.duration,
);
_animationController.addStatusListener(statusListener);
}
}
@override
void initState() {
@ -57,15 +80,10 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
duration: widget.duration,
);
_animationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_animationController.reset();
heightNotifier.value = _endHeight;
_height = _endHeight;
}
});
_animationController.addStatusListener(statusListener);
_dragHeight = widget.minHeight;
_height = widget.minHeight;
super.initState();
}
@ -78,6 +96,8 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
if (dismissed) return Container();
return ValueListenableBuilder(
builder: (BuildContext context, double value, Widget child) {
var _percentage = ((value - widget.minHeight)) /
@ -90,7 +110,8 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
GestureDetector(
onTap: () => _animateToHeight(widget.minHeight),
child: Opacity(
opacity: _percentage,
opacity: _borderDouble(
minRange: 0, maxRange: 1, value: _percentage),
child: Container(color: widget.backgroundColor),
),
),
@ -99,56 +120,77 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
child: SizedBox(
height: value,
child: GestureDetector(
child: Material(
color: Theme.of(context).canvasColor,
child: Container(
constraints: BoxConstraints.expand(),
child: widget.builder(value, _percentage),
decoration: BoxDecoration(
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black45,
blurRadius: widget.elevation,
offset: Offset(0.0, 4))
],
color: Colors.white,
child: ValueListenableBuilder(
valueListenable: dragDownPercentage,
builder: (context, value, child) {
if (value == 0) return child;
return Opacity(
opacity: _borderDouble(
minRange: 0, maxRange: 1, value: 1 - value),
child: Transform.translate(
offset: Offset(0.0, widget.minHeight * value * 0.5),
child: child,
),
);
},
child: Material(
color: Theme.of(context).canvasColor,
child: Container(
constraints: BoxConstraints.expand(),
child: widget.builder(value, _percentage),
decoration: BoxDecoration(
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black45,
blurRadius: widget.elevation,
offset: Offset(0.0, 4))
],
color: Colors.white,
),
),
),
),
onTap: () {
bool up = _height != widget.maxHeight;
_animateToHeight(up ? widget.maxHeight : widget.minHeight);
},
onTap: () => snapToPosition(_dragHeight != widget.maxHeight
? SnapPosition.MAX
: SnapPosition.MIN),
onPanStart: (details) => _startHeight = _dragHeight,
onPanEnd: (details) async {
if (_up)
_animateToHeight(widget.maxHeight);
else
_animateToHeight(widget.minHeight);
SnapPosition snap = SnapPosition.MIN;
final _percentageMax = _percentageFromValueInRange(
min: widget.minHeight,
max: widget.maxHeight,
value: _dragHeight);
///Started from expanded state
if (_startHeight > widget.minHeight) {
if (_percentageMax > 0.8) snap = SnapPosition.MAX;
}
///Started from minified state
else {
if (_percentageMax > 0.2)
snap = SnapPosition.MAX;
else
///DismissedPercentage > 0.2 -> dismiss
if (_percentageFromValueInRange(
min: widget.minHeight,
max: 0,
value: _dragHeight) >
0.2) snap = SnapPosition.DISMISS;
}
snapToPosition(snap);
},
onPanUpdate: (details) {
_prevHeight = _height;
if (animating) return;
if (dismissed) return;
//details.delta.dy < 0 -> -- = +
var h = _height -= details.delta.dy;
_dragHeight -= details.delta.dy;
//Makes sure that height !> maxHeight && !< minHeight
if (h > widget.maxHeight) h = widget.maxHeight;
if (h < widget.minHeight) h = widget.minHeight;
//Makes sure that the widget wont rebuild unnecessarily
if (_prevHeight == h &&
(h == widget.minHeight || h == widget.maxHeight))
return;
_height = h;
if (_height == widget.maxHeight)
_up = true;
else if (_height == widget.minHeight)
_up = false;
else
_up = _prevHeight < _height;
heightNotifier.value = h;
handleHeightChange();
},
),
),
@ -160,19 +202,76 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
);
}
///Determines whether the panel should be updated in height or discarded
void handleHeightChange() {
if (_dragHeight >= widget.minHeight) {
heightNotifier.value = _dragHeight;
if (dragDownPercentage.value != 0) dragDownPercentage.value = 0;
} else {
var percentageDown = _borderDouble(
minRange: 0,
maxRange: 1,
value: _percentageFromValueInRange(
min: widget.minHeight, max: 0, value: _dragHeight));
if (dragDownPercentage.value != percentageDown)
dragDownPercentage.value = percentageDown;
if (percentageDown >= 1 && !dismissed) {
if (widget.onDismiss != null) widget.onDismiss();
setState(() {
dismissed = true;
});
}
}
}
///Animates the panel height according to a SnapPoint
void snapToPosition(SnapPosition snapPosition) {
switch (snapPosition) {
case SnapPosition.MAX:
_animateToHeight(widget.maxHeight);
return;
case SnapPosition.MIN:
_animateToHeight(widget.minHeight);
return;
case SnapPosition.DISMISS:
_animateToHeight(0);
return;
}
}
///Animates the panel height to a specific value
void _animateToHeight(final double h) {
_endHeight = h;
_sizeAnimation = Tween(
begin: _height,
final startHeight = _dragHeight;
Animation<double> _sizeAnimation = Tween(
begin: startHeight,
end: h,
).animate(
CurvedAnimation(parent: _animationController, curve: widget.curve));
_sizeAnimation.addListener(() {
if (!(_sizeAnimation.value > widget.maxHeight) &&
!(_sizeAnimation.value < widget.minHeight))
heightNotifier.value = _sizeAnimation.value;
if (_sizeAnimation.value == startHeight) return;
_dragHeight = _sizeAnimation.value;
handleHeightChange();
});
_animationController.forward();
animating = true;
_animationController.forward(from: 0);
}
}
//Calculates the percentage of a value within a given range of values
double _percentageFromValueInRange({final double min, max, value}) {
return (value - min) / (max - min);
}
double _borderDouble({double minRange, double maxRange, double value}) {
if (value > maxRange) return maxRange;
if (value < minRange) return minRange;
return value;
}