mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
1186 lines
38 KiB
Dart
1186 lines
38 KiB
Dart
import 'dart:math';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/rendering.dart';
|
|
|
|
/// This is where the current time and total time labels should appear in
|
|
/// relation to the progress bar.
|
|
enum TimeLabelLocation {
|
|
/// The time is displayed above the progress bar.
|
|
///
|
|
/// | 01:23 05:00 |
|
|
/// | -------O---------------- |
|
|
above,
|
|
|
|
/// The time is displayed below the progress bar.
|
|
///
|
|
/// | -------O---------------- |
|
|
/// | 01:23 05:00 |
|
|
below,
|
|
|
|
/// The time is displayed on the sides of the progress bar.
|
|
///
|
|
/// | 01:23 -------O---------------- 05:00 |
|
|
sides,
|
|
|
|
/// The time is not displayed.
|
|
///
|
|
/// | -------O---------------- |
|
|
none,
|
|
}
|
|
|
|
/// The time label on the right hand side can be shown as the [totalTime] or as
|
|
/// the [remainingTime]. If the choice is [remainingTime] then this will be
|
|
/// shown as a negative number.
|
|
///
|
|
///
|
|
enum TimeLabelType {
|
|
/// The time label on the right shows the total time.
|
|
///
|
|
/// | -------O---------------- |
|
|
/// | 01:23 05:00 |
|
|
totalTime,
|
|
|
|
/// The time label on the right shows the remaining time as a
|
|
/// negative number.
|
|
///
|
|
/// | -------O---------------- |
|
|
/// | 01:23 -03:37 |
|
|
remainingTime,
|
|
}
|
|
|
|
/// The shape of the progress bar at the left and right ends.
|
|
enum BarCapShape {
|
|
/// The left and right ends of the bar are round.
|
|
round,
|
|
|
|
/// The left and right ends of the bar are square.
|
|
square,
|
|
}
|
|
|
|
/// A progress bar widget to show or set the location of the currently
|
|
/// playing audio or video content.
|
|
///
|
|
/// This widget does not itself play audio or video content, but you can
|
|
/// use it in conjunction with an audio plugin. It is a more convenient
|
|
/// replacement for the Flutter Slider widget.
|
|
class ProgressBar extends LeafRenderObjectWidget {
|
|
/// You must set the current audio or video duration [progress] and also
|
|
/// the [total] duration. Optionally set the [buffered] content progress
|
|
/// as well.
|
|
///
|
|
/// When a user drags the thumb to a new location you can be notified
|
|
/// by the [onSeek] callback so that you can update your audio/video player.
|
|
const ProgressBar({
|
|
super.key,
|
|
required this.progress,
|
|
required this.total,
|
|
this.buffered,
|
|
this.onSeek,
|
|
this.onDragStart,
|
|
this.onDragUpdate,
|
|
this.onDragEnd,
|
|
this.barHeight = 5.0,
|
|
this.baseBarColor,
|
|
this.progressBarColor,
|
|
this.bufferedBarColor,
|
|
this.barCapShape = BarCapShape.round,
|
|
this.thumbRadius = 10.0,
|
|
this.thumbColor,
|
|
this.thumbGlowColor,
|
|
this.thumbGlowRadius = 30.0,
|
|
this.thumbCanPaintOutsideBar = true,
|
|
this.timeLabelLocation,
|
|
this.timeLabelType,
|
|
this.timeLabelTextStyle,
|
|
this.timeLabelPadding = 0.0,
|
|
this.textScaleFactor = 1.0,
|
|
});
|
|
|
|
/// The elapsed playing time of the media.
|
|
///
|
|
/// This should not be greater than the [total] time.
|
|
final Duration progress;
|
|
|
|
/// The total duration of the media.
|
|
final Duration total;
|
|
|
|
/// The currently buffered content of the media.
|
|
///
|
|
/// This is useful for streamed content. If you are playing a local file
|
|
/// then you can leave this out.
|
|
final Duration? buffered;
|
|
|
|
/// A callback when user moves the thumb.
|
|
///
|
|
/// When the user moved the thumb on the progress bar this callback will
|
|
/// run. It will not run until after the user has finished the touch event.
|
|
///
|
|
/// You will get the chosen duration to start playing at which you can pass
|
|
/// on to your media player.
|
|
///
|
|
/// If you want continuous duration updates as the user moves the thumb,
|
|
/// see [onDragUpdate], where the provided [ThumbDragDetails] has a
|
|
/// `timeStamp` with the seek duration on it.
|
|
final ValueChanged<Duration>? onSeek;
|
|
|
|
/// A callback when the user starts to move the thumb.
|
|
///
|
|
/// This will be called only once when the drag begins. This provides you
|
|
/// with the [ThumbDragDetails].
|
|
///
|
|
/// This method is useful if you are planning to do something like add a time
|
|
/// label and/or video preview over the thumb and you need to do some
|
|
/// initialization.
|
|
///
|
|
/// Use [onSeek] if you only want to seek to a new audio position when the
|
|
/// drag event has finished.
|
|
final ThumbDragStartCallback? onDragStart;
|
|
|
|
/// A callback when the user is moving the thumb.
|
|
///
|
|
/// This will be called repeatedly as the thumb position changes. This
|
|
/// provides you with the [ThumbDragDetails], which notify you of the global
|
|
/// and local positions of the drag event as well as the current thumb
|
|
/// duration. The current thumb duration will not go beyond [total] or less
|
|
/// that `Duration.zero` so you can use this information to clamp the drag
|
|
/// position values.
|
|
///
|
|
/// This method is useful if you are planning to do something like add a time
|
|
/// label and/or video preview over the thumb and need to update the position
|
|
/// to stay in sync with the thumb position.
|
|
///
|
|
/// Use [onSeek] if you only want to seek to a new audio position when the
|
|
/// drag event has finished.
|
|
final ThumbDragUpdateCallback? onDragUpdate;
|
|
|
|
/// A callback when the user is finished moving the thumb.
|
|
///
|
|
/// This will be called only once when the drag ends.
|
|
///
|
|
/// This method is useful if you are planning to do something like add a time
|
|
/// label and/or video preview over the thumb and you need to dispose of
|
|
/// something when the drag is finished.
|
|
///
|
|
/// This method is called directly before [onSeek].
|
|
final VoidCallback? onDragEnd;
|
|
|
|
/// The vertical thickness of the progress bar.
|
|
final double barHeight;
|
|
|
|
/// The color of the progress bar before playback has started.
|
|
///
|
|
/// By default it is a transparent version of your theme's primary color.
|
|
final Color? baseBarColor;
|
|
|
|
/// The color of the progress bar to the left of the current playing
|
|
/// [progress].
|
|
///
|
|
/// By default it is your theme's primary color.
|
|
final Color? progressBarColor;
|
|
|
|
/// The color of the progress bar between the [progress] location and the
|
|
/// [buffered] location.
|
|
///
|
|
/// By default it is a transparent version of your theme's primary color,
|
|
/// a shade darker than [baseBarColor].
|
|
final Color? bufferedBarColor;
|
|
|
|
/// The shape of the bar at the left and right ends.
|
|
///
|
|
/// This affects the base bar for the total time, the current progress bar,
|
|
/// and the buffered progress bar. The default is [BarCapShape.round].
|
|
final BarCapShape barCapShape;
|
|
|
|
/// The radius of the circle for the moveable progress bar thumb.
|
|
final double thumbRadius;
|
|
|
|
/// The color of the circle for the moveable progress bar thumb.
|
|
///
|
|
/// By default it is your theme's primary color.
|
|
final Color? thumbColor;
|
|
|
|
/// The color of the pressed-down effect of the moveable progress bar thumb.
|
|
///
|
|
/// By default it is [thumbColor] with an alpha value of 80.
|
|
final Color? thumbGlowColor;
|
|
|
|
/// The radius of the circle for the pressed-down effect of the moveable
|
|
/// progress bar thumb.
|
|
///
|
|
/// By default it is 30.
|
|
final double thumbGlowRadius;
|
|
|
|
/// Whether the thumb radius will before the start of the bar when at the
|
|
/// beginning or after the end of the bar when at the end.
|
|
///
|
|
/// The default is `true` and this means that the thumb will be painted
|
|
/// outside of the bounds of the widget if there are no side labels. You can
|
|
/// wrap [ProgressBar] with a `Padding` widget if your layout needs to leave
|
|
/// some extra room for the thumb.
|
|
///
|
|
/// When set to `false` the thumb will be clamped within the width of the
|
|
/// bar. This is nice for aligning the thumb with vertical labels at the start
|
|
/// and end of playback. However, because of the clamping, the thumb won't
|
|
/// move during audio/video playback when near the ends. Depending on the
|
|
/// size of the thumb and the length of the song, this usually only lasts
|
|
/// a few seconds. The progress label still indicates that playback
|
|
/// is happening during this time, though.
|
|
final bool thumbCanPaintOutsideBar;
|
|
|
|
/// The location for the [progress] and [total] duration text labels.
|
|
///
|
|
/// By default the labels appear under the progress bar but you can also
|
|
/// put them above, on the sides, or remove them altogether.
|
|
final TimeLabelLocation? timeLabelLocation;
|
|
|
|
/// What to display for the time label on the right
|
|
///
|
|
/// The right time label can show the total time or the remaining time as a
|
|
/// negative number. The default is [TimeLabelType.totalTime].
|
|
final TimeLabelType? timeLabelType;
|
|
|
|
/// The [TextStyle] used by the time labels.
|
|
///
|
|
/// By default it is [TextTheme.bodyLarge].
|
|
final TextStyle? timeLabelTextStyle;
|
|
|
|
/// The extra space between the time labels and the progress bar.
|
|
///
|
|
/// The default is 0.0. A positive number will move the labels further from
|
|
/// the progress bar and a negative number will move them closer.
|
|
final double timeLabelPadding;
|
|
|
|
final double textScaleFactor;
|
|
|
|
@override
|
|
RenderObject createRenderObject(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
final primaryColor = theme.colorScheme.primary;
|
|
final textStyle = timeLabelTextStyle ?? theme.textTheme.bodyLarge;
|
|
return _RenderProgressBar(
|
|
progress: progress,
|
|
total: total,
|
|
buffered: buffered ?? Duration.zero,
|
|
onSeek: onSeek,
|
|
onDragStart: onDragStart,
|
|
onDragUpdate: onDragUpdate,
|
|
onDragEnd: onDragEnd,
|
|
barHeight: barHeight,
|
|
baseBarColor: baseBarColor ?? primaryColor.withValues(alpha: 0.24),
|
|
progressBarColor: progressBarColor ?? primaryColor,
|
|
bufferedBarColor:
|
|
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24),
|
|
barCapShape: barCapShape,
|
|
thumbRadius: thumbRadius,
|
|
thumbColor: thumbColor ?? primaryColor,
|
|
thumbGlowColor:
|
|
thumbGlowColor ?? (thumbColor ?? primaryColor).withAlpha(80),
|
|
thumbGlowRadius: thumbGlowRadius,
|
|
thumbCanPaintOutsideBar: thumbCanPaintOutsideBar,
|
|
timeLabelLocation: timeLabelLocation ?? TimeLabelLocation.below,
|
|
timeLabelType: timeLabelType ?? TimeLabelType.totalTime,
|
|
timeLabelTextStyle: textStyle,
|
|
timeLabelPadding: timeLabelPadding,
|
|
textScaleFactor: textScaleFactor,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void updateRenderObject(BuildContext context, RenderObject renderObject) {
|
|
final theme = Theme.of(context);
|
|
final primaryColor = theme.colorScheme.primary;
|
|
final textStyle = timeLabelTextStyle ?? theme.textTheme.bodyLarge;
|
|
(renderObject as _RenderProgressBar)
|
|
..total = total
|
|
..progress = progress
|
|
..buffered = buffered ?? Duration.zero
|
|
..onSeek = onSeek
|
|
..onDragStart = onDragStart
|
|
..onDragUpdate = onDragUpdate
|
|
..onDragEnd = onDragEnd
|
|
..barHeight = barHeight
|
|
..baseBarColor = baseBarColor ?? primaryColor.withValues(alpha: 0.24)
|
|
..progressBarColor = progressBarColor ?? primaryColor
|
|
..bufferedBarColor =
|
|
bufferedBarColor ?? primaryColor.withValues(alpha: 0.24)
|
|
..barCapShape = barCapShape
|
|
..thumbRadius = thumbRadius
|
|
..thumbColor = thumbColor ?? primaryColor
|
|
..thumbGlowColor =
|
|
thumbGlowColor ?? (thumbColor ?? primaryColor).withAlpha(80)
|
|
..thumbGlowRadius = thumbGlowRadius
|
|
..thumbCanPaintOutsideBar = thumbCanPaintOutsideBar
|
|
..timeLabelLocation = timeLabelLocation ?? TimeLabelLocation.below
|
|
..timeLabelType = timeLabelType ?? TimeLabelType.totalTime
|
|
..timeLabelTextStyle = textStyle
|
|
..timeLabelPadding = timeLabelPadding
|
|
..textScaleFactor = textScaleFactor;
|
|
}
|
|
|
|
@override
|
|
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
|
super.debugFillProperties(properties);
|
|
properties
|
|
..add(StringProperty('progress', progress.toString()))
|
|
..add(StringProperty('total', total.toString()))
|
|
..add(StringProperty('buffered', buffered.toString()))
|
|
..add(
|
|
ObjectFlagProperty<ValueChanged<Duration>>(
|
|
'onSeek',
|
|
onSeek,
|
|
ifNull: 'unimplemented',
|
|
),
|
|
)
|
|
..add(
|
|
ObjectFlagProperty<ThumbDragStartCallback>(
|
|
'onDragStart',
|
|
onDragStart,
|
|
ifNull: 'unimplemented',
|
|
),
|
|
)
|
|
..add(
|
|
ObjectFlagProperty<ThumbDragUpdateCallback>(
|
|
'onDragUpdate',
|
|
onDragUpdate,
|
|
ifNull: 'unimplemented',
|
|
),
|
|
)
|
|
..add(
|
|
ObjectFlagProperty<VoidCallback>(
|
|
'onDragEnd',
|
|
onDragEnd,
|
|
ifNull: 'unimplemented',
|
|
),
|
|
)
|
|
..add(DoubleProperty('barHeight', barHeight))
|
|
..add(ColorProperty('baseBarColor', baseBarColor))
|
|
..add(ColorProperty('progressBarColor', progressBarColor))
|
|
..add(ColorProperty('bufferedBarColor', bufferedBarColor))
|
|
..add(StringProperty('barCapShape', barCapShape.toString()))
|
|
..add(DoubleProperty('thumbRadius', thumbRadius))
|
|
..add(ColorProperty('thumbColor', thumbColor))
|
|
..add(ColorProperty('thumbGlowColor', thumbGlowColor))
|
|
..add(DoubleProperty('thumbGlowRadius', thumbGlowRadius))
|
|
..add(
|
|
FlagProperty(
|
|
'thumbCanPaintOutsideBar',
|
|
value: thumbCanPaintOutsideBar,
|
|
ifTrue: 'true',
|
|
ifFalse: 'false',
|
|
showName: true,
|
|
),
|
|
)
|
|
..add(StringProperty('timeLabelLocation', timeLabelLocation.toString()))
|
|
..add(StringProperty('timeLabelType', timeLabelType.toString()))
|
|
..add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle))
|
|
..add(DoubleProperty('timeLabelPadding', timeLabelPadding));
|
|
}
|
|
}
|
|
|
|
/// The callback signature for when the thumb begins a horizontal drag.
|
|
typedef ThumbDragStartCallback = void Function(ThumbDragDetails details);
|
|
|
|
/// The callback signature for when the thumb is moving on horizontally and has
|
|
/// new data.
|
|
typedef ThumbDragUpdateCallback = void Function(ThumbDragDetails details);
|
|
|
|
/// Data to pass back on drag callback events
|
|
class ThumbDragDetails {
|
|
const ThumbDragDetails({
|
|
this.timeStamp = Duration.zero,
|
|
this.globalPosition = Offset.zero,
|
|
this.localPosition = Offset.zero,
|
|
});
|
|
|
|
/// The duration position of the thumb on the progress bar
|
|
final Duration timeStamp;
|
|
|
|
/// The global position of the drag event moving the thumb on the progress bar.
|
|
final Offset globalPosition;
|
|
|
|
/// The local position of the drag event moving the thumb on the progress bar.
|
|
final Offset localPosition;
|
|
|
|
@override
|
|
String toString() =>
|
|
'${objectRuntimeType(this, 'ThumbDragDetails')}('
|
|
'time: $timeStamp, '
|
|
'global: $globalPosition, '
|
|
'local: $localPosition)';
|
|
}
|
|
|
|
// Handles all gestures so that it will always win a the gesture arena.
|
|
// Without doing this, if you used this widget in a swipable tab layout,
|
|
// you would cause a swipe rather than a drag when trying to move the thumb.
|
|
class _EagerHorizontalDragGestureRecognizer
|
|
extends HorizontalDragGestureRecognizer {
|
|
@override
|
|
void addAllowedPointer(PointerDownEvent event) {
|
|
super.addAllowedPointer(event);
|
|
resolve(GestureDisposition.accepted);
|
|
}
|
|
|
|
@override
|
|
String get debugDescription => '_EagerHorizontalDragGestureRecognizer';
|
|
}
|
|
|
|
class _RenderProgressBar extends RenderBox {
|
|
_RenderProgressBar({
|
|
required Duration progress,
|
|
required Duration total,
|
|
required Duration buffered,
|
|
ValueChanged<Duration>? onSeek,
|
|
ThumbDragStartCallback? onDragStart,
|
|
ThumbDragUpdateCallback? onDragUpdate,
|
|
VoidCallback? onDragEnd,
|
|
required double barHeight,
|
|
required Color baseBarColor,
|
|
required Color progressBarColor,
|
|
required Color bufferedBarColor,
|
|
required BarCapShape barCapShape,
|
|
double thumbRadius = 20.0,
|
|
required Color thumbColor,
|
|
required Color thumbGlowColor,
|
|
double thumbGlowRadius = 30.0,
|
|
bool thumbCanPaintOutsideBar = true,
|
|
required TimeLabelLocation timeLabelLocation,
|
|
required TimeLabelType timeLabelType,
|
|
TextStyle? timeLabelTextStyle,
|
|
double timeLabelPadding = 0.0,
|
|
double textScaleFactor = 1.0,
|
|
}) : _total = total,
|
|
_buffered = buffered,
|
|
_onSeek = onSeek,
|
|
_onDragStartUserCallback = onDragStart,
|
|
_onDragUpdateUserCallback = onDragUpdate,
|
|
_onDragEndUserCallback = onDragEnd,
|
|
_barHeight = barHeight,
|
|
_baseBarColor = baseBarColor,
|
|
_progressBarColor = progressBarColor,
|
|
_bufferedBarColor = bufferedBarColor,
|
|
_barCapShape = barCapShape,
|
|
_thumbRadius = thumbRadius,
|
|
_thumbColor = thumbColor,
|
|
_thumbGlowColor = thumbGlowColor,
|
|
_thumbGlowRadius = thumbGlowRadius,
|
|
_thumbCanPaintOutsideBar = thumbCanPaintOutsideBar,
|
|
_timeLabelLocation = timeLabelLocation,
|
|
_timeLabelType = timeLabelType,
|
|
_timeLabelTextStyle = timeLabelTextStyle,
|
|
_timeLabelPadding = timeLabelPadding,
|
|
_textScaleFactor = textScaleFactor {
|
|
_drag = _EagerHorizontalDragGestureRecognizer()
|
|
..onStart = _onDragStart
|
|
..onUpdate = _onDragUpdate
|
|
..onEnd = _onDragEnd
|
|
..onCancel = _finishDrag;
|
|
if (!_userIsDraggingThumb) {
|
|
_progress = progress;
|
|
_thumbValue = _proportionOfTotal(_progress);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_drag?.dispose();
|
|
_clearLabelCache();
|
|
super.dispose();
|
|
}
|
|
|
|
// This is the gesture recognizer used to move the thumb.
|
|
_EagerHorizontalDragGestureRecognizer? _drag;
|
|
|
|
// This is a value between 0.0 and 1.0 used to indicate the position on
|
|
// the bar.
|
|
late double _thumbValue;
|
|
|
|
// The thumb can move for two reasons. One is that the [progress] changed.
|
|
// The other is that the user is dragging the thumb. This variable keeps
|
|
// track of that so that while the user is dragging the thumb at the same
|
|
// time as a [progress] update there won't be a conflict.
|
|
bool _userIsDraggingThumb = false;
|
|
|
|
// This padding is always used between the time labels and the progress bar
|
|
// when the time labels are on the sides. Any user defined [timeLabelPadding]
|
|
// is in addition to this.
|
|
double get _defaultSidePadding {
|
|
const minPadding = 5.0;
|
|
return (_thumbCanPaintOutsideBar) ? thumbRadius + minPadding : minPadding;
|
|
}
|
|
|
|
void _onDragStart(DragStartDetails details) {
|
|
if (onDragStart == null) {
|
|
return;
|
|
}
|
|
_userIsDraggingThumb = true;
|
|
_updateThumbPosition(details.localPosition);
|
|
onDragStart?.call(
|
|
ThumbDragDetails(
|
|
timeStamp: _currentThumbDuration(),
|
|
globalPosition: details.globalPosition,
|
|
localPosition: details.localPosition,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onDragUpdate(DragUpdateDetails details) {
|
|
if (onDragUpdate == null) {
|
|
return;
|
|
}
|
|
_updateThumbPosition(details.localPosition);
|
|
onDragUpdate?.call(
|
|
ThumbDragDetails(
|
|
timeStamp: _currentThumbDuration(),
|
|
globalPosition: details.globalPosition,
|
|
localPosition: details.localPosition,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _onDragEnd(DragEndDetails details) {
|
|
if (onSeek == null) {
|
|
return;
|
|
}
|
|
onDragEnd?.call();
|
|
onSeek?.call(_currentThumbDuration());
|
|
_finishDrag();
|
|
}
|
|
|
|
void _finishDrag() {
|
|
_userIsDraggingThumb = false;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Duration _currentThumbDuration() {
|
|
final thumbMilliseconds = _thumbValue * total.inMilliseconds;
|
|
return Duration(milliseconds: thumbMilliseconds.round());
|
|
}
|
|
|
|
// This needs to stay in sync with the layout. This could be a potential
|
|
// source of bugs if there is a layout change but we forget to update this.
|
|
// It might be a good idea to redesign the architecture so that there is
|
|
// only one place to make changes.
|
|
void _updateThumbPosition(Offset localPosition) {
|
|
final dx = localPosition.dx;
|
|
double lengthBefore = 0.0;
|
|
double lengthAfter = 0.0;
|
|
if (_timeLabelLocation == TimeLabelLocation.sides) {
|
|
lengthBefore =
|
|
_leftLabelSize.width + _defaultSidePadding + _timeLabelPadding;
|
|
lengthAfter =
|
|
_rightLabelSize.width + _defaultSidePadding + _timeLabelPadding;
|
|
}
|
|
// The paint used to draw the bar line draws half of the cap before the
|
|
// start of the line (and after the end of the line). The cap radius is
|
|
// equal to half of the line width, which in this case is the bar height.
|
|
final barCapRadius = _barHeight / 2;
|
|
double barStart = lengthBefore + barCapRadius;
|
|
double barEnd = size.width - lengthAfter - barCapRadius;
|
|
final barWidth = barEnd - barStart;
|
|
final position = (dx - barStart).clamp(0.0, barWidth);
|
|
_thumbValue = (position / barWidth);
|
|
_progress = _currentThumbDuration();
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The play location of the media.
|
|
///
|
|
/// This is used to update the thumb value and the left time label.
|
|
Duration get progress => _progress;
|
|
Duration _progress = Duration.zero;
|
|
set progress(Duration value) {
|
|
final clamp = _clampDuration(value);
|
|
if (_progress == clamp) {
|
|
return;
|
|
}
|
|
if (_labelLengthDifferent(_progress, clamp)) {
|
|
_clearLabelCache();
|
|
}
|
|
if (!_userIsDraggingThumb) {
|
|
_progress = clamp;
|
|
_thumbValue = _proportionOfTotal(clamp);
|
|
}
|
|
markNeedsPaint();
|
|
}
|
|
|
|
bool _labelLengthDifferent(Duration first, Duration second) {
|
|
return (first.inMinutes < 10 && second.inMinutes >= 10) ||
|
|
(first.inMinutes >= 10 && second.inMinutes < 10) ||
|
|
(first.inHours == 0 && second.inHours != 0) ||
|
|
(first.inHours != 0 && second.inHours == 0) ||
|
|
(first.inHours < 10 && second.inHours >= 10) ||
|
|
(first.inHours >= 10 && second.inHours < 10);
|
|
}
|
|
|
|
TextPainter? _cachedLeftLabel;
|
|
Size get _leftLabelSize {
|
|
_cachedLeftLabel ??= _leftTimeLabel();
|
|
return _cachedLeftLabel!.size;
|
|
}
|
|
|
|
TextPainter? _cachedRightLabel;
|
|
Size get _rightLabelSize {
|
|
_cachedRightLabel ??= _rightTimeLabel();
|
|
return _cachedRightLabel!.size;
|
|
}
|
|
|
|
void _clearLabelCache() {
|
|
_cachedLeftLabel?.dispose();
|
|
_cachedRightLabel?.dispose();
|
|
_cachedLeftLabel = null;
|
|
_cachedRightLabel = null;
|
|
}
|
|
|
|
TextPainter _leftTimeLabel() {
|
|
final text = _getTimeString(progress);
|
|
return _layoutText(text);
|
|
}
|
|
|
|
TextPainter _rightTimeLabel() {
|
|
switch (timeLabelType) {
|
|
case TimeLabelType.totalTime:
|
|
final text = _getTimeString(total);
|
|
return _layoutText(text);
|
|
case TimeLabelType.remainingTime:
|
|
final remaining = total - progress;
|
|
final text = '-${_getTimeString(remaining)}';
|
|
return _layoutText(text);
|
|
}
|
|
}
|
|
|
|
TextPainter _layoutText(String text) {
|
|
TextPainter textPainter = TextPainter(
|
|
text: TextSpan(text: text, style: _timeLabelTextStyle),
|
|
textDirection: TextDirection.ltr,
|
|
textScaler: TextScaler.linear(textScaleFactor),
|
|
)..layout(minWidth: 0, maxWidth: double.infinity);
|
|
return textPainter;
|
|
}
|
|
|
|
/// The total time length of the media.
|
|
Duration get total => _total;
|
|
Duration _total;
|
|
set total(Duration value) {
|
|
final clamp = (value.isNegative) ? Duration.zero : value;
|
|
if (_total == clamp) {
|
|
return;
|
|
}
|
|
if (_labelLengthDifferent(_total, clamp)) {
|
|
_clearLabelCache();
|
|
}
|
|
_total = clamp;
|
|
if (!_userIsDraggingThumb) {
|
|
_thumbValue = _proportionOfTotal(progress);
|
|
}
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The buffered length of the media when streaming.
|
|
Duration get buffered => _buffered;
|
|
Duration _buffered;
|
|
set buffered(Duration value) {
|
|
final clamp = _clampDuration(value);
|
|
if (_buffered == clamp) {
|
|
return;
|
|
}
|
|
_buffered = clamp;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
Duration _clampDuration(Duration value) {
|
|
if (value.isNegative) return Duration.zero;
|
|
if (value.compareTo(_total) > 0) return _total;
|
|
return value;
|
|
}
|
|
|
|
/// A callback for the audio duration position to where the thumb was moved.
|
|
ValueChanged<Duration>? get onSeek => _onSeek;
|
|
ValueChanged<Duration>? _onSeek;
|
|
set onSeek(ValueChanged<Duration>? value) {
|
|
if (value == _onSeek) {
|
|
return;
|
|
}
|
|
_onSeek = value;
|
|
}
|
|
|
|
/// A callback when the thumb starts being dragged.
|
|
ThumbDragStartCallback? get onDragStart => _onDragStartUserCallback;
|
|
ThumbDragStartCallback? _onDragStartUserCallback;
|
|
set onDragStart(ThumbDragStartCallback? value) {
|
|
if (value == _onDragStartUserCallback) {
|
|
return;
|
|
}
|
|
_onDragStartUserCallback = value;
|
|
}
|
|
|
|
/// A callback when the thumb is being dragged.
|
|
ThumbDragUpdateCallback? get onDragUpdate => _onDragUpdateUserCallback;
|
|
ThumbDragUpdateCallback? _onDragUpdateUserCallback;
|
|
set onDragUpdate(ThumbDragUpdateCallback? value) {
|
|
if (value == _onDragUpdateUserCallback) {
|
|
return;
|
|
}
|
|
_onDragUpdateUserCallback = value;
|
|
}
|
|
|
|
/// A callback when the thumb drag is finished.
|
|
VoidCallback? get onDragEnd => _onDragEndUserCallback;
|
|
VoidCallback? _onDragEndUserCallback;
|
|
set onDragEnd(VoidCallback? value) {
|
|
if (value == _onDragEndUserCallback) {
|
|
return;
|
|
}
|
|
_onDragEndUserCallback = value;
|
|
}
|
|
|
|
/// The vertical thickness of the bar that the thumb moves along.
|
|
double get barHeight => _barHeight;
|
|
double _barHeight;
|
|
set barHeight(double value) {
|
|
if (_barHeight == value) return;
|
|
_barHeight = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The color of the progress bar before any playing or buffering.
|
|
Color get baseBarColor => _baseBarColor;
|
|
Color _baseBarColor;
|
|
set baseBarColor(Color value) {
|
|
if (_baseBarColor == value) return;
|
|
_baseBarColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The color of the played portion of the progress bar.
|
|
Color get progressBarColor => _progressBarColor;
|
|
Color _progressBarColor;
|
|
set progressBarColor(Color value) {
|
|
if (_progressBarColor == value) return;
|
|
_progressBarColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The color of the visible buffered portion of the progress bar.
|
|
Color get bufferedBarColor => _bufferedBarColor;
|
|
Color _bufferedBarColor;
|
|
set bufferedBarColor(Color value) {
|
|
if (_bufferedBarColor == value) return;
|
|
_bufferedBarColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
BarCapShape get barCapShape => _barCapShape;
|
|
BarCapShape _barCapShape;
|
|
set barCapShape(BarCapShape value) {
|
|
if (_barCapShape == value) return;
|
|
_barCapShape = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The color of the moveable thumb.
|
|
Color get thumbColor => _thumbColor;
|
|
Color _thumbColor;
|
|
set thumbColor(Color value) {
|
|
if (_thumbColor == value) return;
|
|
_thumbColor = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The length of the radius for the circular thumb.
|
|
double get thumbRadius => _thumbRadius;
|
|
double _thumbRadius;
|
|
set thumbRadius(double value) {
|
|
if (_thumbRadius == value) return;
|
|
_thumbRadius = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The color of the pressed-down effect of the moveable thumb.
|
|
Color get thumbGlowColor => _thumbGlowColor;
|
|
Color _thumbGlowColor;
|
|
set thumbGlowColor(Color value) {
|
|
if (_thumbGlowColor == value) return;
|
|
_thumbGlowColor = value;
|
|
if (_userIsDraggingThumb) markNeedsPaint();
|
|
}
|
|
|
|
/// The length of the radius of the pressed-down effect of the moveable thumb.
|
|
double get thumbGlowRadius => _thumbGlowRadius;
|
|
double _thumbGlowRadius;
|
|
set thumbGlowRadius(double value) {
|
|
if (_thumbGlowRadius == value) return;
|
|
_thumbGlowRadius = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// Whether the thumb will paint before the start or after the end of the bar.
|
|
bool get thumbCanPaintOutsideBar => _thumbCanPaintOutsideBar;
|
|
bool _thumbCanPaintOutsideBar;
|
|
set thumbCanPaintOutsideBar(bool value) {
|
|
if (_thumbCanPaintOutsideBar == value) return;
|
|
_thumbCanPaintOutsideBar = value;
|
|
markNeedsPaint();
|
|
}
|
|
|
|
/// The position of the duration text labels for the progress and total time.
|
|
TimeLabelLocation get timeLabelLocation => _timeLabelLocation;
|
|
TimeLabelLocation _timeLabelLocation;
|
|
set timeLabelLocation(TimeLabelLocation value) {
|
|
if (_timeLabelLocation == value) return;
|
|
_timeLabelLocation = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// What to display for the time label on the right
|
|
///
|
|
/// The right time label can show the total time or the remaining time as a
|
|
/// negative number. The default is [TimeLabelType.totalTime].
|
|
TimeLabelType get timeLabelType => _timeLabelType;
|
|
TimeLabelType _timeLabelType;
|
|
set timeLabelType(TimeLabelType value) {
|
|
if (_timeLabelType == value) return;
|
|
_timeLabelType = value;
|
|
_clearLabelCache();
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The text style for the duration text labels. By default this style is
|
|
/// taken from the theme's [textStyle.bodyText1].
|
|
TextStyle? get timeLabelTextStyle => _timeLabelTextStyle;
|
|
TextStyle? _timeLabelTextStyle;
|
|
set timeLabelTextStyle(TextStyle? value) {
|
|
if (_timeLabelTextStyle == value) return;
|
|
_timeLabelTextStyle = value;
|
|
_clearLabelCache();
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The length of the radius for the circular thumb.
|
|
double get timeLabelPadding => _timeLabelPadding;
|
|
double _timeLabelPadding;
|
|
set timeLabelPadding(double value) {
|
|
if (_timeLabelPadding == value) return;
|
|
_timeLabelPadding = value;
|
|
markNeedsLayout();
|
|
}
|
|
|
|
/// The text scale factor for the `progress` and `total` text labels.
|
|
/// By default the value is 1.0.
|
|
double get textScaleFactor => _textScaleFactor;
|
|
double _textScaleFactor;
|
|
set textScaleFactor(double value) {
|
|
if (_textScaleFactor == value) return;
|
|
_textScaleFactor = value;
|
|
_clearLabelCache();
|
|
markNeedsLayout();
|
|
}
|
|
|
|
// The smallest that this widget would ever want to be.
|
|
static const _minDesiredWidth = 100.0;
|
|
|
|
@override
|
|
double computeMinIntrinsicWidth(double height) => _minDesiredWidth;
|
|
|
|
@override
|
|
double computeMaxIntrinsicWidth(double height) => _minDesiredWidth;
|
|
|
|
@override
|
|
double computeMinIntrinsicHeight(double width) => _calculateDesiredHeight();
|
|
|
|
@override
|
|
double computeMaxIntrinsicHeight(double width) => _calculateDesiredHeight();
|
|
|
|
@override
|
|
bool hitTestSelf(Offset position) => true;
|
|
|
|
@override
|
|
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
|
assert(debugHandleEvent(event, entry));
|
|
if (event is PointerDownEvent) {
|
|
_drag?.addPointer(event);
|
|
}
|
|
}
|
|
|
|
@override
|
|
void performLayout() {
|
|
size = computeDryLayout(constraints);
|
|
}
|
|
|
|
@override
|
|
Size computeDryLayout(BoxConstraints constraints) {
|
|
final desiredWidth = constraints.maxWidth;
|
|
final desiredHeight = _calculateDesiredHeight();
|
|
final desiredSize = Size(desiredWidth, desiredHeight);
|
|
return constraints.constrain(desiredSize);
|
|
}
|
|
|
|
// When changing these remember to keep the gesture recognizer for the
|
|
// thumb in sync.
|
|
double _calculateDesiredHeight() {
|
|
switch (_timeLabelLocation) {
|
|
case TimeLabelLocation.below:
|
|
case TimeLabelLocation.above:
|
|
return _heightWhenLabelsAboveOrBelow();
|
|
case TimeLabelLocation.sides:
|
|
return _heightWhenLabelsOnSides();
|
|
default:
|
|
return _heightWhenNoLabels();
|
|
}
|
|
}
|
|
|
|
double _heightWhenLabelsAboveOrBelow() {
|
|
return _heightWhenNoLabels() + _textHeight() + _timeLabelPadding;
|
|
}
|
|
|
|
double _heightWhenLabelsOnSides() {
|
|
return max(_heightWhenNoLabels(), _textHeight());
|
|
}
|
|
|
|
double _heightWhenNoLabels() {
|
|
return max(2 * _thumbRadius, _barHeight);
|
|
}
|
|
|
|
double _textHeight() {
|
|
return _leftLabelSize.height;
|
|
}
|
|
|
|
@override
|
|
bool get isRepaintBoundary => true;
|
|
|
|
@override
|
|
void paint(PaintingContext context, Offset offset) {
|
|
final canvas = context.canvas
|
|
..save()
|
|
..translate(offset.dx, offset.dy);
|
|
|
|
switch (_timeLabelLocation) {
|
|
case TimeLabelLocation.above:
|
|
case TimeLabelLocation.below:
|
|
_drawProgressBarWithLabelsAboveOrBelow(canvas);
|
|
break;
|
|
case TimeLabelLocation.sides:
|
|
_drawProgressBarWithLabelsOnSides(canvas);
|
|
break;
|
|
default:
|
|
_drawProgressBarWithoutLabels(canvas);
|
|
}
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
/// Draw the progress bar and labels vertically aligned:
|
|
///
|
|
/// | -------O---------------- |
|
|
/// | 01:23 05:00 |
|
|
///
|
|
/// Or like this:
|
|
///
|
|
/// | 01:23 05:00 |
|
|
/// | -------O---------------- |
|
|
void _drawProgressBarWithLabelsAboveOrBelow(Canvas canvas) {
|
|
// calculate sizes
|
|
final barWidth = size.width;
|
|
final barHeight = _heightWhenNoLabels();
|
|
|
|
// whether to paint the labels below the progress bar or above it
|
|
final isLabelBelow = _timeLabelLocation == TimeLabelLocation.below;
|
|
|
|
// current time label
|
|
final labelDy = (isLabelBelow) ? barHeight + _timeLabelPadding : 0.0;
|
|
final leftLabelOffset = Offset(0, labelDy);
|
|
_leftTimeLabel().paint(canvas, leftLabelOffset);
|
|
|
|
// total or remaining time label
|
|
final rightLabelDx = size.width - _rightLabelSize.width;
|
|
final rightLabelOffset = Offset(rightLabelDx, labelDy);
|
|
_rightTimeLabel().paint(canvas, rightLabelOffset);
|
|
|
|
// progress bar
|
|
final barDy = (isLabelBelow)
|
|
? 0.0
|
|
: _leftLabelSize.height + _timeLabelPadding;
|
|
_drawProgressBar(canvas, Offset(0, barDy), Size(barWidth, barHeight));
|
|
}
|
|
|
|
/// Draw the progress bar and labels horizontally aligned:
|
|
///
|
|
/// | 01:23 -------O---------------- 05:00 |
|
|
///
|
|
void _drawProgressBarWithLabelsOnSides(Canvas canvas) {
|
|
// left time label
|
|
final leftLabelSize = _leftLabelSize;
|
|
final verticalOffset = size.height / 2 - leftLabelSize.height / 2;
|
|
final leftLabelOffset = Offset(0, verticalOffset);
|
|
_leftTimeLabel().paint(canvas, leftLabelOffset);
|
|
|
|
// right time label
|
|
final rightLabelSize = _rightLabelSize;
|
|
final rightLabelWidth = rightLabelSize.width;
|
|
final totalLabelDx = size.width - rightLabelWidth;
|
|
final totalLabelOffset = Offset(totalLabelDx, verticalOffset);
|
|
_rightTimeLabel().paint(canvas, totalLabelOffset);
|
|
|
|
// progress bar
|
|
final leftLabelWidth = leftLabelSize.width;
|
|
final barHeight = _heightWhenNoLabels();
|
|
final barWidth =
|
|
size.width -
|
|
2 * _defaultSidePadding -
|
|
2 * _timeLabelPadding -
|
|
leftLabelWidth -
|
|
rightLabelWidth;
|
|
final barDy = size.height / 2 - barHeight / 2;
|
|
final barDx = leftLabelWidth + _defaultSidePadding + _timeLabelPadding;
|
|
_drawProgressBar(canvas, Offset(barDx, barDy), Size(barWidth, barHeight));
|
|
}
|
|
|
|
/// Draw the progress bar without labels like this:
|
|
///
|
|
/// | -------O---------------- |
|
|
///
|
|
void _drawProgressBarWithoutLabels(Canvas canvas) {
|
|
final barWidth = size.width;
|
|
final barHeight = 2 * _thumbRadius;
|
|
_drawProgressBar(canvas, Offset.zero, Size(barWidth, barHeight));
|
|
}
|
|
|
|
void _drawProgressBar(Canvas canvas, Offset offset, Size localSize) {
|
|
canvas
|
|
..save()
|
|
..translate(offset.dx, offset.dy);
|
|
_drawBaseBar(canvas, localSize);
|
|
_drawBufferedBar(canvas, localSize);
|
|
_drawCurrentProgressBar(canvas, localSize);
|
|
_drawThumb(canvas, localSize);
|
|
canvas.restore();
|
|
}
|
|
|
|
void _drawBaseBar(Canvas canvas, Size localSize) {
|
|
_drawBar(
|
|
canvas: canvas,
|
|
availableSize: localSize,
|
|
widthProportion: 1.0,
|
|
color: baseBarColor,
|
|
);
|
|
}
|
|
|
|
void _drawBufferedBar(Canvas canvas, Size localSize) {
|
|
_drawBar(
|
|
canvas: canvas,
|
|
availableSize: localSize,
|
|
widthProportion: _proportionOfTotal(_buffered),
|
|
color: bufferedBarColor,
|
|
);
|
|
}
|
|
|
|
void _drawCurrentProgressBar(Canvas canvas, Size localSize) {
|
|
_drawBar(
|
|
canvas: canvas,
|
|
availableSize: localSize,
|
|
widthProportion: _proportionOfTotal(_progress),
|
|
color: progressBarColor,
|
|
);
|
|
}
|
|
|
|
void _drawBar({
|
|
required Canvas canvas,
|
|
required Size availableSize,
|
|
required double widthProportion,
|
|
required Color color,
|
|
}) {
|
|
final strokeCap = (_barCapShape == BarCapShape.round)
|
|
? StrokeCap.round
|
|
: StrokeCap.square;
|
|
final baseBarPaint = Paint()
|
|
..color = color
|
|
..strokeCap = strokeCap
|
|
..strokeWidth = _barHeight;
|
|
final capRadius = _barHeight / 2;
|
|
final adjustedWidth = availableSize.width - barHeight;
|
|
final dx = widthProportion * adjustedWidth + capRadius;
|
|
final startPoint = Offset(capRadius, availableSize.height / 2);
|
|
var endPoint = Offset(dx, availableSize.height / 2);
|
|
canvas.drawLine(startPoint, endPoint, baseBarPaint);
|
|
}
|
|
|
|
void _drawThumb(Canvas canvas, Size localSize) {
|
|
final thumbPaint = Paint()..color = thumbColor;
|
|
final barCapRadius = _barHeight / 2;
|
|
final availableWidth = localSize.width - _barHeight;
|
|
var thumbDx = _thumbValue * availableWidth + barCapRadius;
|
|
if (!_thumbCanPaintOutsideBar) {
|
|
thumbDx = thumbDx.clamp(_thumbRadius, localSize.width - _thumbRadius);
|
|
}
|
|
final center = Offset(thumbDx, localSize.height / 2);
|
|
if (_userIsDraggingThumb) {
|
|
final thumbGlowPaint = Paint()..color = thumbGlowColor;
|
|
canvas.drawCircle(center, thumbGlowRadius, thumbGlowPaint);
|
|
}
|
|
canvas.drawCircle(center, thumbRadius, thumbPaint);
|
|
}
|
|
|
|
double _proportionOfTotal(Duration duration) {
|
|
if (total.inMilliseconds == 0) {
|
|
return 0.0;
|
|
}
|
|
return (duration.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0);
|
|
}
|
|
|
|
String _getTimeString(Duration time) {
|
|
final minutes = time.inMinutes
|
|
.remainder(Duration.minutesPerHour)
|
|
.toString();
|
|
final seconds = time.inSeconds
|
|
.remainder(Duration.secondsPerMinute)
|
|
.toString()
|
|
.padLeft(2, '0');
|
|
return time.inHours > 0
|
|
? "${time.inHours}:${minutes.padLeft(2, "0")}:$seconds"
|
|
: "$minutes:$seconds";
|
|
}
|
|
|
|
@override
|
|
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
|
super.describeSemanticsConfiguration(config);
|
|
|
|
// description
|
|
config
|
|
..textDirection = TextDirection.ltr
|
|
..label =
|
|
'进度条' //'Progress bar';
|
|
..value = '${(_thumbValue * 100).round()}%'
|
|
// increase action
|
|
..onIncrease = increaseAction;
|
|
final increased = _thumbValue + _semanticActionUnit;
|
|
config
|
|
..increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%'
|
|
// decrease action
|
|
..onDecrease = decreaseAction;
|
|
final decreased = _thumbValue - _semanticActionUnit;
|
|
config.decreasedValue = '${((decreased).clamp(0.0, 1.0) * 100).round()}%';
|
|
}
|
|
|
|
// This is how much to move the thumb if the move is triggered by a
|
|
// semantic action rather than a touch event.
|
|
static const double _semanticActionUnit = 0.05;
|
|
|
|
void increaseAction() {
|
|
final newValue = _thumbValue + _semanticActionUnit;
|
|
_thumbValue = (newValue).clamp(0.0, 1.0);
|
|
markNeedsPaint();
|
|
markNeedsSemanticsUpdate();
|
|
onSeek?.call(_currentThumbDuration());
|
|
}
|
|
|
|
void decreaseAction() {
|
|
final newValue = _thumbValue - _semanticActionUnit;
|
|
_thumbValue = (newValue).clamp(0.0, 1.0);
|
|
markNeedsPaint();
|
|
markNeedsSemanticsUpdate();
|
|
onSeek?.call(_currentThumbDuration());
|
|
}
|
|
}
|