OnDismiss Feature
This commit is contained in:
parent
afbc315d87
commit
8d60fda628
|
@ -6,6 +6,8 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
typedef Widget MiniplayerBuilder(double height, double percentage);
|
typedef Widget MiniplayerBuilder(double height, double percentage);
|
||||||
|
|
||||||
|
enum SnapPosition { MAX, MIN, DISMISS }
|
||||||
|
|
||||||
class Miniplayer extends StatefulWidget {
|
class Miniplayer extends StatefulWidget {
|
||||||
final double minHeight, maxHeight, elevation;
|
final double minHeight, maxHeight, elevation;
|
||||||
final MiniplayerBuilder builder;
|
final MiniplayerBuilder builder;
|
||||||
|
@ -13,6 +15,7 @@ class Miniplayer extends StatefulWidget {
|
||||||
final Color backgroundColor;
|
final Color backgroundColor;
|
||||||
final Duration duration;
|
final Duration duration;
|
||||||
final ValueNotifier<double> valueNotifier;
|
final ValueNotifier<double> valueNotifier;
|
||||||
|
final Function onDismiss;
|
||||||
|
|
||||||
const Miniplayer({
|
const Miniplayer({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -24,6 +27,7 @@ class Miniplayer extends StatefulWidget {
|
||||||
this.backgroundColor = const Color(0x70000000),
|
this.backgroundColor = const Color(0x70000000),
|
||||||
this.valueNotifier,
|
this.valueNotifier,
|
||||||
this.duration = const Duration(milliseconds: 300),
|
this.duration = const Duration(milliseconds: 300),
|
||||||
|
this.onDismiss,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -32,18 +36,37 @@ class Miniplayer extends StatefulWidget {
|
||||||
|
|
||||||
class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
||||||
ValueNotifier<double> heightNotifier;
|
ValueNotifier<double> heightNotifier;
|
||||||
|
ValueNotifier<double> dragDownPercentage = ValueNotifier(0);
|
||||||
|
|
||||||
double _height;
|
SnapPosition snap;
|
||||||
double _prevHeight;
|
|
||||||
|
|
||||||
//Used to set the height after the animation is complete
|
///Current y position of drag gesture
|
||||||
double _endHeight;
|
double _dragHeight;
|
||||||
bool _up;
|
|
||||||
|
///Used to determine SnapPosition
|
||||||
|
double _startHeight;
|
||||||
|
|
||||||
|
bool dismissed = false;
|
||||||
|
|
||||||
|
bool animating = false;
|
||||||
|
|
||||||
StreamController<double> _heightController =
|
StreamController<double> _heightController =
|
||||||
StreamController<double>.broadcast();
|
StreamController<double>.broadcast();
|
||||||
AnimationController _animationController;
|
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
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -57,15 +80,10 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
||||||
duration: widget.duration,
|
duration: widget.duration,
|
||||||
);
|
);
|
||||||
|
|
||||||
_animationController.addStatusListener((status) {
|
_animationController.addStatusListener(statusListener);
|
||||||
if (status == AnimationStatus.completed) {
|
|
||||||
_animationController.reset();
|
_dragHeight = widget.minHeight;
|
||||||
heightNotifier.value = _endHeight;
|
|
||||||
_height = _endHeight;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
_height = widget.minHeight;
|
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -78,6 +96,8 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
if (dismissed) return Container();
|
||||||
|
|
||||||
return ValueListenableBuilder(
|
return ValueListenableBuilder(
|
||||||
builder: (BuildContext context, double value, Widget child) {
|
builder: (BuildContext context, double value, Widget child) {
|
||||||
var _percentage = ((value - widget.minHeight)) /
|
var _percentage = ((value - widget.minHeight)) /
|
||||||
|
@ -90,7 +110,8 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => _animateToHeight(widget.minHeight),
|
onTap: () => _animateToHeight(widget.minHeight),
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: _percentage,
|
opacity: _borderDouble(
|
||||||
|
minRange: 0, maxRange: 1, value: _percentage),
|
||||||
child: Container(color: widget.backgroundColor),
|
child: Container(color: widget.backgroundColor),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -99,56 +120,77 @@ class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: value,
|
height: value,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Material(
|
child: ValueListenableBuilder(
|
||||||
color: Theme.of(context).canvasColor,
|
valueListenable: dragDownPercentage,
|
||||||
child: Container(
|
builder: (context, value, child) {
|
||||||
constraints: BoxConstraints.expand(),
|
if (value == 0) return child;
|
||||||
child: widget.builder(value, _percentage),
|
|
||||||
decoration: BoxDecoration(
|
return Opacity(
|
||||||
boxShadow: <BoxShadow>[
|
opacity: _borderDouble(
|
||||||
BoxShadow(
|
minRange: 0, maxRange: 1, value: 1 - value),
|
||||||
color: Colors.black45,
|
child: Transform.translate(
|
||||||
blurRadius: widget.elevation,
|
offset: Offset(0.0, widget.minHeight * value * 0.5),
|
||||||
offset: Offset(0.0, 4))
|
child: child,
|
||||||
],
|
),
|
||||||
color: Colors.white,
|
);
|
||||||
|
},
|
||||||
|
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: () {
|
onTap: () => snapToPosition(_dragHeight != widget.maxHeight
|
||||||
bool up = _height != widget.maxHeight;
|
? SnapPosition.MAX
|
||||||
_animateToHeight(up ? widget.maxHeight : widget.minHeight);
|
: SnapPosition.MIN),
|
||||||
},
|
onPanStart: (details) => _startHeight = _dragHeight,
|
||||||
onPanEnd: (details) async {
|
onPanEnd: (details) async {
|
||||||
if (_up)
|
SnapPosition snap = SnapPosition.MIN;
|
||||||
_animateToHeight(widget.maxHeight);
|
|
||||||
else
|
final _percentageMax = _percentageFromValueInRange(
|
||||||
_animateToHeight(widget.minHeight);
|
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) {
|
onPanUpdate: (details) {
|
||||||
_prevHeight = _height;
|
if (animating) return;
|
||||||
|
if (dismissed) return;
|
||||||
|
|
||||||
//details.delta.dy < 0 -> -- = +
|
_dragHeight -= details.delta.dy;
|
||||||
var h = _height -= details.delta.dy;
|
|
||||||
|
|
||||||
//Makes sure that height !> maxHeight && !< minHeight
|
handleHeightChange();
|
||||||
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;
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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) {
|
void _animateToHeight(final double h) {
|
||||||
_endHeight = h;
|
final startHeight = _dragHeight;
|
||||||
_sizeAnimation = Tween(
|
|
||||||
begin: _height,
|
Animation<double> _sizeAnimation = Tween(
|
||||||
|
begin: startHeight,
|
||||||
end: h,
|
end: h,
|
||||||
).animate(
|
).animate(
|
||||||
CurvedAnimation(parent: _animationController, curve: widget.curve));
|
CurvedAnimation(parent: _animationController, curve: widget.curve));
|
||||||
|
|
||||||
_sizeAnimation.addListener(() {
|
_sizeAnimation.addListener(() {
|
||||||
if (!(_sizeAnimation.value > widget.maxHeight) &&
|
if (_sizeAnimation.value == startHeight) return;
|
||||||
!(_sizeAnimation.value < widget.minHeight))
|
|
||||||
heightNotifier.value = _sizeAnimation.value;
|
_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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue