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({ Key? 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, }) : super(key: key); /// 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? 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; @override RenderObject createRenderObject(BuildContext context) { final theme = Theme.of(context); final primaryColor = theme.colorScheme.primary; final textStyle = timeLabelTextStyle ?? theme.textTheme.bodyLarge; final textScaleFactor = MediaQuery.textScaleFactorOf(context); return _RenderProgressBar( progress: progress, total: total, buffered: buffered ?? Duration.zero, onSeek: onSeek, onDragStart: onDragStart, onDragUpdate: onDragUpdate, onDragEnd: onDragEnd, barHeight: barHeight, baseBarColor: baseBarColor ?? primaryColor.withOpacity(0.24), progressBarColor: progressBarColor ?? primaryColor, bufferedBarColor: bufferedBarColor ?? primaryColor.withOpacity(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; final textScaleFactor = MediaQuery.textScaleFactorOf(context); (renderObject as _RenderProgressBar) ..progress = progress ..total = total ..buffered = buffered ?? Duration.zero ..onSeek = onSeek ..onDragStart = onDragStart ..onDragUpdate = onDragUpdate ..onDragEnd = onDragEnd ..barHeight = barHeight ..baseBarColor = baseBarColor ?? primaryColor.withOpacity(0.24) ..progressBarColor = progressBarColor ?? primaryColor ..bufferedBarColor = bufferedBarColor ?? primaryColor.withOpacity(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())); properties.add(StringProperty('total', total.toString())); properties.add(StringProperty('buffered', buffered.toString())); properties.add(ObjectFlagProperty>('onSeek', onSeek, ifNull: 'unimplemented')); properties.add(ObjectFlagProperty( 'onDragStart', onDragStart, ifNull: 'unimplemented')); properties.add(ObjectFlagProperty( 'onDragUpdate', onDragUpdate, ifNull: 'unimplemented')); properties.add(ObjectFlagProperty('onDragEnd', onDragEnd, ifNull: 'unimplemented')); properties.add(DoubleProperty('barHeight', barHeight)); properties.add(ColorProperty('baseBarColor', baseBarColor)); properties.add(ColorProperty('progressBarColor', progressBarColor)); properties.add(ColorProperty('bufferedBarColor', bufferedBarColor)); properties.add(StringProperty('barCapShape', barCapShape.toString())); properties.add(DoubleProperty('thumbRadius', thumbRadius)); properties.add(ColorProperty('thumbColor', thumbColor)); properties.add(ColorProperty('thumbGlowColor', thumbGlowColor)); properties.add(DoubleProperty('thumbGlowRadius', thumbGlowRadius)); properties.add( FlagProperty( 'thumbCanPaintOutsideBar', value: thumbCanPaintOutsideBar, ifTrue: 'true', ifFalse: 'false', showName: true, ), ); properties .add(StringProperty('timeLabelLocation', timeLabelLocation.toString())); properties.add(StringProperty('timeLabelType', timeLabelType.toString())); properties .add(DiagnosticsProperty('timeLabelTextStyle', timeLabelTextStyle)); properties.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? 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); } } // 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) { _userIsDraggingThumb = true; _updateThumbPosition(details.localPosition); onDragStart?.call(ThumbDragDetails( timeStamp: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, )); } void _onDragUpdate(DragUpdateDetails details) { _updateThumbPosition(details.localPosition); onDragUpdate?.call(ThumbDragDetails( timeStamp: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, )); } void _onDragEnd(DragEndDetails details) { 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 = 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, textScaleFactor: textScaleFactor, ); textPainter.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? get onSeek => _onSeek; ValueChanged? _onSeek; set onSeek(ValueChanged? 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; canvas.save(); canvas.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(); canvas.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; } 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; config.label = '进度条';//'Progress bar'; config.value = '${(_thumbValue * 100).round()}%'; // increase action config.onIncrease = increaseAction; final increased = _thumbValue + _semanticActionUnit; config.increasedValue = '${((increased).clamp(0.0, 1.0) * 100).round()}%'; // decrease action config.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()); } }