Files
PiliPlus/lib/common/widgets/progress_bar/audio_video_progress_bar.dart
bggRGjQaUbCoE 54c3c314e1 tweak
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
2025-10-03 19:56:18 +08:00

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());
}
}