flutter_miniplayer/lib/miniplayer.dart

456 lines
14 KiB
Dart
Raw Permalink Normal View History

2020-07-29 22:13:39 +02:00
library miniplayer;
2020-07-29 22:32:28 +02:00
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:miniplayer/src/miniplayer_will_pop_scope.dart';
2020-09-26 15:12:50 +02:00
import 'package:miniplayer/src/utils.dart';
2020-07-29 22:32:28 +02:00
export 'package:miniplayer/src/miniplayer_will_pop_scope.dart';
2020-09-26 15:13:06 +02:00
///Type definition for the builder function
2020-07-29 22:32:28 +02:00
typedef Widget MiniplayerBuilder(double height, double percentage);
2020-11-21 11:32:25 +01:00
///Type definition for onDismiss. Will be used in a future version.
typedef void DismissCallback(double percentage);
2020-11-11 10:57:47 +01:00
///Miniplayer class
2020-07-29 22:32:28 +02:00
class Miniplayer extends StatefulWidget {
2020-09-26 15:13:06 +02:00
///Required option to set the minimum and maximum height
final double minHeight, maxHeight;
///Option to enable and set elevation for the miniplayer
final double elevation;
///Central API-Element
///Provides a builder with useful information
2020-07-29 22:32:28 +02:00
final MiniplayerBuilder builder;
2020-09-26 15:13:06 +02:00
///Option to set the animation curve
2020-07-29 22:32:28 +02:00
final Curve curve;
2020-09-26 15:13:06 +02:00
///Sets the background-color of the miniplayer
final Color backgroundColor;
2020-09-26 15:13:06 +02:00
///Option to set the animation duration
2020-08-26 16:40:39 +02:00
final Duration duration;
2020-09-26 15:13:06 +02:00
///Allows you to use a global ValueNotifier with the current progress.
///This can be used to hide the BottomNavigationBar.
2020-11-21 11:52:52 +01:00
final ValueNotifier<double>? valueNotifier;
2020-09-26 15:13:06 +02:00
2020-11-21 11:32:25 +01:00
///Deprecated
@Deprecated(
"Migrate onDismiss to onDismissed as onDismiss will be used differently in a future version.")
2020-11-21 11:52:52 +01:00
final Function? onDismiss;
2020-07-29 22:32:28 +02:00
2020-11-21 11:32:25 +01:00
///If onDismissed is set, the miniplayer can be dismissed
2020-11-21 11:52:52 +01:00
final Function? onDismissed;
2020-11-21 11:32:25 +01:00
2020-11-11 10:57:47 +01:00
//Allows you to manually control the miniplayer in code
2020-11-21 11:52:52 +01:00
final MiniplayerController? controller;
2020-10-06 20:57:24 +02:00
2020-07-30 17:01:37 +02:00
const Miniplayer({
2020-11-21 11:52:52 +01:00
Key? key,
required this.minHeight,
required this.maxHeight,
required this.builder,
2020-09-21 13:30:22 +02:00
this.curve = Curves.easeOut,
2020-07-30 17:01:37 +02:00
this.elevation = 0,
this.backgroundColor = const Color(0x70000000),
this.valueNotifier,
2020-08-26 16:40:39 +02:00
this.duration = const Duration(milliseconds: 300),
2020-09-20 18:42:43 +02:00
this.onDismiss,
2020-11-21 11:32:25 +01:00
this.onDismissed,
2020-10-06 20:57:24 +02:00
this.controller,
2020-07-30 17:01:37 +02:00
}) : super(key: key);
2020-07-29 22:32:28 +02:00
@override
_MiniplayerState createState() => _MiniplayerState();
}
class _MiniplayerState extends State<Miniplayer> with TickerProviderStateMixin {
2020-11-21 11:52:52 +01:00
late ValueNotifier<double> heightNotifier;
2020-09-20 18:42:43 +02:00
ValueNotifier<double> dragDownPercentage = ValueNotifier(0);
2020-11-21 11:32:25 +01:00
///Temporary variable as long as onDismiss is deprecated. Will be removed in a future version.
2020-11-21 11:52:52 +01:00
Function? onDismissed;
2020-09-20 18:42:43 +02:00
///Current y position of drag gesture
2020-11-21 11:52:52 +01:00
late double _dragHeight;
2020-09-20 18:42:43 +02:00
///Used to determine SnapPosition
2021-03-06 15:26:00 +01:00
late double _startHeight;
2020-07-29 22:32:28 +02:00
2020-09-20 18:42:43 +02:00
bool dismissed = false;
bool animating = false;
2020-07-29 22:32:28 +02:00
2020-09-21 00:12:58 +02:00
///Counts how many updates were required for a distance (onPanUpdate) -> necessary to calculate the drag speed
int updateCount = 0;
2020-07-29 22:32:28 +02:00
StreamController<double> _heightController =
StreamController<double>.broadcast();
2020-11-21 11:52:52 +01:00
AnimationController? _animationController;
2020-09-20 18:42:43 +02:00
2020-09-23 08:06:05 +02:00
void _statusListener(AnimationStatus status) {
if (status == AnimationStatus.completed) _resetAnimationController();
}
2020-11-21 11:52:52 +01:00
void _resetAnimationController({Duration? duration}) {
2021-11-10 23:01:47 +01:00
if (_animationController != null) {
_animationController!.dispose();
}
_animationController = AnimationController(
vsync: this,
2021-03-06 15:26:00 +01:00
duration: duration ?? widget.duration,
);
2020-11-21 11:52:52 +01:00
_animationController!.addStatusListener(_statusListener);
animating = false;
2020-09-20 18:42:43 +02:00
}
2020-07-29 22:32:28 +02:00
@override
void initState() {
2021-11-10 23:01:47 +01:00
if (widget.valueNotifier == null) {
heightNotifier = ValueNotifier(widget.minHeight);
2021-11-10 23:01:47 +01:00
} else {
2020-11-21 11:52:52 +01:00
heightNotifier = widget.valueNotifier!;
2021-11-10 23:01:47 +01:00
}
_resetAnimationController();
2020-09-20 18:42:43 +02:00
_dragHeight = heightNotifier.value;
2020-07-29 22:32:28 +02:00
2021-11-10 23:01:47 +01:00
if (widget.controller != null) {
2020-11-21 11:52:52 +01:00
widget.controller!.addListener(controllerListener);
2021-11-10 23:01:47 +01:00
}
2020-10-06 20:57:24 +02:00
2021-11-10 23:01:47 +01:00
if (widget.onDismissed != null) {
2020-11-21 11:32:25 +01:00
onDismissed = widget.onDismissed;
2021-11-10 23:01:47 +01:00
} else {
2020-11-21 11:32:25 +01:00
// ignore: deprecated_member_use_from_same_package
onDismissed = widget.onDismiss;
2021-11-10 23:01:47 +01:00
}
2020-11-21 11:32:25 +01:00
2020-07-29 22:32:28 +02:00
super.initState();
}
@override
void dispose() {
_heightController.close();
2020-10-09 20:52:45 +02:00
2021-11-10 23:01:47 +01:00
if (_animationController != null) {
_animationController!.dispose();
}
if (widget.controller != null) {
2020-11-21 11:52:52 +01:00
widget.controller!.removeListener(controllerListener);
2021-11-10 23:01:47 +01:00
}
2020-10-09 20:52:45 +02:00
2020-07-29 22:32:28 +02:00
super.dispose();
}
@override
Widget build(BuildContext context) {
2021-11-10 23:01:47 +01:00
if (dismissed) {
return Container();
}
2020-09-20 18:42:43 +02:00
return MiniplayerWillPopScope(
2020-12-15 13:14:16 +01:00
onWillPop: () async {
if (heightNotifier.value > widget.minHeight) {
_snapToPosition(PanelState.MIN);
return false;
}
return true;
},
child: ValueListenableBuilder(
2021-03-06 15:26:00 +01:00
valueListenable: heightNotifier,
builder: (BuildContext context, double height, Widget? _) {
var _percentage = ((height - widget.minHeight)) /
2020-12-15 13:14:16 +01:00
(widget.maxHeight - widget.minHeight);
return Stack(
alignment: Alignment.bottomCenter,
children: [
if (_percentage > 0)
GestureDetector(
onTap: () => _animateToHeight(widget.minHeight),
child: Opacity(
opacity: borderDouble(
minRange: 0.0, maxRange: 1.0, value: _percentage),
2020-12-15 13:14:16 +01:00
child: Container(color: widget.backgroundColor),
),
2020-07-29 22:32:28 +02:00
),
2020-12-15 13:14:16 +01:00
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
2021-03-06 15:26:00 +01:00
height: height,
2020-12-15 13:14:16 +01:00
child: GestureDetector(
child: ValueListenableBuilder(
valueListenable: dragDownPercentage,
2021-03-06 15:26:00 +01:00
builder:
(BuildContext context, double value, Widget? child) {
2020-12-15 13:14:16 +01:00
return Opacity(
opacity: borderDouble(
minRange: 0.0,
maxRange: 1.0,
value: 1 - value * 0.8),
2020-12-15 13:14:16 +01:00
child: Transform.translate(
offset: Offset(0.0, widget.minHeight * value * 0.5),
child: child,
),
);
},
child: Material(
child: Container(
constraints: BoxConstraints.expand(),
2021-03-06 15:26:00 +01:00
child: widget.builder(height, _percentage),
2020-12-15 13:14:16 +01:00
decoration: BoxDecoration(
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black45,
blurRadius: widget.elevation,
offset: Offset(0.0, 4))
],
2022-04-29 13:06:31 +02:00
color: widget.backgroundColor, //kopa
2020-12-15 13:14:16 +01:00
),
2020-09-20 18:42:43 +02:00
),
2020-08-27 11:41:09 +02:00
),
2020-07-30 17:01:37 +02:00
),
2020-12-15 13:14:16 +01:00
onTap: () => _snapToPosition(_dragHeight != widget.maxHeight
? PanelState.MAX
: PanelState.MIN),
onPanStart: (details) {
_startHeight = _dragHeight;
updateCount = 0;
2021-11-10 23:01:47 +01:00
if (animating) {
_resetAnimationController();
}
2020-12-15 13:14:16 +01:00
},
onPanEnd: (details) async {
///Calculates drag speed
double speed = (_dragHeight - _startHeight * _dragHeight <
_startHeight
? 1
: -1) /
updateCount *
100;
///Define the percentage distance depending on the speed with which the widget should snap
double snapPercentage = 0.005;
2021-11-10 23:01:47 +01:00
if (speed <= 4) {
2020-12-15 13:14:16 +01:00
snapPercentage = 0.2;
2021-11-10 23:01:47 +01:00
} else if (speed <= 9) {
2020-12-15 13:14:16 +01:00
snapPercentage = 0.08;
2021-11-10 23:01:47 +01:00
} else if (speed <= 50) {
snapPercentage = 0.01;
}
2020-12-15 13:14:16 +01:00
///Determine to which SnapPosition the widget should snap
PanelState snap = PanelState.MIN;
final _percentageMax = percentageFromValueInRange(
min: widget.minHeight,
max: widget.maxHeight,
value: _dragHeight);
///Started from expanded state
if (_startHeight > widget.minHeight) {
2021-11-10 23:01:47 +01:00
if (_percentageMax > 1 - snapPercentage) {
2020-12-15 13:14:16 +01:00
snap = PanelState.MAX;
2021-11-10 23:01:47 +01:00
}
2020-12-15 13:14:16 +01:00
}
///Started from minified state
else {
2021-11-10 23:01:47 +01:00
if (_percentageMax > snapPercentage) {
2020-12-15 13:14:16 +01:00
snap = PanelState.MAX;
2021-11-10 23:01:47 +01:00
}
2020-12-15 13:14:16 +01:00
///DismissedPercentage > 0.2 -> dismiss
2021-11-10 23:01:47 +01:00
else if (onDismissed != null &&
2020-12-15 13:14:16 +01:00
percentageFromValueInRange(
2021-11-10 23:01:47 +01:00
min: widget.minHeight,
max: 0,
value: _dragHeight,
) >
snapPercentage) {
snap = PanelState.DISMISS;
}
2020-12-15 13:14:16 +01:00
}
///Snap to position
_snapToPosition(snap);
},
onPanUpdate: (details) {
if (dismissed) return;
_dragHeight -= details.delta.dy;
updateCount++;
_handleHeightChange();
},
2020-07-30 17:01:37 +02:00
),
2020-07-29 22:32:28 +02:00
),
),
2020-12-15 13:14:16 +01:00
],
);
},
),
2020-07-29 22:32:28 +02:00
);
}
2020-09-20 18:42:43 +02:00
///Determines whether the panel should be updated in height or discarded
2020-09-23 08:06:05 +02:00
void _handleHeightChange({bool animation = false}) {
///Drag above minHeight
2020-09-20 18:42:43 +02:00
if (_dragHeight >= widget.minHeight) {
2021-11-10 23:01:47 +01:00
if (dragDownPercentage.value != 0) {
dragDownPercentage.value = 0;
}
2020-09-21 14:10:15 +02:00
if (_dragHeight > widget.maxHeight) return;
2020-09-21 14:10:15 +02:00
heightNotifier.value = _dragHeight;
}
///Drag below minHeight
2020-11-21 11:32:25 +01:00
else if (onDismissed != null) {
2021-11-10 23:01:47 +01:00
final percentageDown = borderDouble(
minRange: 0.0,
maxRange: 1.0,
2020-09-26 15:12:50 +02:00
value: percentageFromValueInRange(
2020-09-20 18:42:43 +02:00
min: widget.minHeight, max: 0, value: _dragHeight));
2021-11-10 23:01:47 +01:00
if (dragDownPercentage.value != percentageDown) {
2020-09-20 18:42:43 +02:00
dragDownPercentage.value = percentageDown;
2021-11-10 23:01:47 +01:00
}
2020-09-20 18:42:43 +02:00
2020-09-21 13:58:35 +02:00
if (percentageDown >= 1 && animation && !dismissed) {
2021-11-10 23:01:47 +01:00
if (onDismissed != null) {
onDismissed!();
}
setState(() => dismissed = true);
2020-09-20 18:42:43 +02:00
}
}
}
///Animates the panel height according to a SnapPoint
2020-10-06 20:57:24 +02:00
void _snapToPosition(PanelState snapPosition) {
2020-09-20 18:42:43 +02:00
switch (snapPosition) {
2020-10-06 20:57:24 +02:00
case PanelState.MAX:
2020-09-20 18:42:43 +02:00
_animateToHeight(widget.maxHeight);
return;
2020-10-06 20:57:24 +02:00
case PanelState.MIN:
2020-09-20 18:42:43 +02:00
_animateToHeight(widget.minHeight);
return;
2020-10-06 20:57:24 +02:00
case PanelState.DISMISS:
2020-09-20 18:42:43 +02:00
_animateToHeight(0);
return;
}
}
///Animates the panel height to a specific value
2020-11-21 11:52:52 +01:00
void _animateToHeight(final double h, {Duration? duration}) {
2021-03-06 15:26:00 +01:00
if (_animationController == null) return;
2020-09-20 18:42:43 +02:00
final startHeight = _dragHeight;
2021-11-10 23:01:47 +01:00
if (duration != null) {
_resetAnimationController(duration: duration);
}
2020-10-06 20:57:24 +02:00
2020-09-20 18:42:43 +02:00
Animation<double> _sizeAnimation = Tween(
begin: startHeight,
2020-07-29 22:32:28 +02:00
end: h,
).animate(
2020-11-21 11:52:52 +01:00
CurvedAnimation(parent: _animationController!, curve: widget.curve));
2020-07-29 22:32:28 +02:00
_sizeAnimation.addListener(() {
2020-09-20 18:42:43 +02:00
if (_sizeAnimation.value == startHeight) return;
_dragHeight = _sizeAnimation.value;
2020-09-23 08:06:05 +02:00
_handleHeightChange(animation: true);
2020-07-29 22:32:28 +02:00
});
2020-09-20 18:42:43 +02:00
animating = true;
2020-11-21 11:52:52 +01:00
_animationController!.forward(from: 0);
2020-07-29 22:32:28 +02:00
}
2020-10-09 20:52:45 +02:00
2020-11-11 10:57:47 +01:00
//Listener function for the controller
2020-10-09 20:52:45 +02:00
void controllerListener() {
2021-03-06 15:26:00 +01:00
if (widget.controller == null) return;
if (widget.controller!.value == null) return;
2020-11-21 11:52:52 +01:00
switch (widget.controller!.value!.height) {
2020-10-09 20:52:45 +02:00
case -1:
_animateToHeight(
widget.minHeight,
2020-11-21 11:52:52 +01:00
duration: widget.controller!.value!.duration,
2020-10-09 20:52:45 +02:00
);
break;
case -2:
_animateToHeight(
widget.maxHeight,
2020-11-21 11:52:52 +01:00
duration: widget.controller!.value!.duration,
2020-10-09 20:52:45 +02:00
);
break;
case -3:
_animateToHeight(
0,
2020-11-21 11:52:52 +01:00
duration: widget.controller!.value!.duration,
2020-10-09 20:52:45 +02:00
);
break;
default:
_animateToHeight(
2020-11-21 11:52:52 +01:00
widget.controller!.value!.height.toDouble(),
duration: widget.controller!.value!.duration,
2020-10-09 20:52:45 +02:00
);
break;
}
}
2020-07-29 22:13:39 +02:00
}
2020-10-06 20:57:24 +02:00
///-1 Min, -2 Max, -3 Dismiss
enum PanelState { MAX, MIN, DISMISS }
2020-11-11 10:57:47 +01:00
//ControllerData class. Used for the controller
2020-10-06 20:57:24 +02:00
class ControllerData {
final int height;
2020-11-21 11:52:52 +01:00
final Duration? duration;
2020-10-06 20:57:24 +02:00
const ControllerData(this.height, this.duration);
}
2020-11-11 10:57:47 +01:00
//MiniplayerController class
2020-11-21 11:52:52 +01:00
class MiniplayerController extends ValueNotifier<ControllerData?> {
2020-10-06 20:57:24 +02:00
MiniplayerController() : super(null);
2020-11-11 10:57:47 +01:00
//Animates to a given height or state(expanded, dismissed, ...)
2020-11-21 11:52:52 +01:00
void animateToHeight(
{double? height, PanelState? state, Duration? duration}) {
2021-11-10 23:01:47 +01:00
if (height == null && state == null) {
2020-10-07 16:48:23 +02:00
throw ("Miniplayer: One of the two parameters, height or status, is required.");
2021-11-10 23:01:47 +01:00
}
2020-10-07 16:48:23 +02:00
2021-11-10 23:01:47 +01:00
if (height != null && state != null) {
2020-10-07 16:48:23 +02:00
throw ("Miniplayer: Only one of the two parameters, height or status, can be specified.");
2021-11-10 23:01:47 +01:00
}
2020-10-06 20:57:24 +02:00
2020-11-21 11:52:52 +01:00
ControllerData? valBefore = value;
2020-10-06 20:57:24 +02:00
2021-11-10 23:01:47 +01:00
if (state != null) {
2020-10-06 20:57:24 +02:00
value = ControllerData(state.heightCode, duration);
2021-11-10 23:01:47 +01:00
} else {
2020-11-21 11:52:52 +01:00
if (height! < 0) return;
2020-10-06 20:57:24 +02:00
value = ControllerData(height.round(), duration);
}
2021-11-10 23:01:47 +01:00
if (valBefore == value) {
notifyListeners();
}
2020-10-06 20:57:24 +02:00
}
}