diff --git a/lib/common/widgets/marquee.dart b/lib/common/widgets/marquee.dart index d2f621657..4cc0676bc 100644 --- a/lib/common/widgets/marquee.dart +++ b/lib/common/widgets/marquee.dart @@ -1,22 +1,21 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart'; class MarqueeText extends StatelessWidget { final double maxWidth; final String text; final TextStyle? style; - final int? count; - final bool bounce; final double spacing; + final double velocity; const MarqueeText( this.text, { super.key, required this.maxWidth, this.style, - this.count, - this.bounce = true, this.spacing = 0, + this.velocity = 25, }); @override @@ -37,12 +36,10 @@ class MarqueeText extends StatelessWidget { textDirection: TextDirection.ltr, ); if (width > maxWidth) { - return SingleWidgetMarquee( - child, - duration: Duration(milliseconds: (width / 50 * 1000).round()), - bounce: bounce, - count: count, + return NormalMarquee( + velocity: velocity, spacing: spacing, + child: child, ); } else { return child; @@ -50,63 +47,15 @@ class MarqueeText extends StatelessWidget { } } -class SingleWidgetMarquee extends StatefulWidget { - final Widget child; - final Duration? duration; - final bool bounce; - final double spacing; - final int? count; - - const SingleWidgetMarquee( - this.child, { - super.key, - this.duration, - this.bounce = false, - this.spacing = 0, - this.count, - }); - - @override - State createState() => _SingleWidgetMarqueeState(); -} - -class _SingleWidgetMarqueeState extends State - with SingleTickerProviderStateMixin { - late final _controller = AnimationController( - vsync: this, - duration: widget.duration, - reverseDuration: widget.duration, - )..repeat(reverse: widget.bounce, count: widget.count); - - @override - Widget build(BuildContext context) => widget.bounce - ? BounceMarquee( - animation: _controller, - spacing: widget.spacing, - child: widget.child, - ) - : NormalMarquee( - animation: _controller, - spacing: widget.spacing, - child: widget.child, - ); - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } -} - abstract class Marquee extends SingleChildRenderObjectWidget { final Axis direction; final Clip clipBehavior; final double spacing; - final Animation animation; + final double velocity; const Marquee({ super.key, - required this.animation, + required this.velocity, required super.child, this.direction = Axis.horizontal, this.clipBehavior = Clip.hardEdge, @@ -121,7 +70,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget { renderObject ..direction = direction ..clipBehavior = clipBehavior - ..animation = animation + ..velocity = velocity ..spacing = spacing; } } @@ -129,7 +78,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget { class NormalMarquee extends Marquee { const NormalMarquee({ super.key, - required super.animation, + required super.velocity, required super.child, super.direction, super.clipBehavior, @@ -139,7 +88,7 @@ class NormalMarquee extends Marquee { @override RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender( direction: direction, - animation: animation, + velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, ); @@ -148,7 +97,7 @@ class NormalMarquee extends Marquee { class BounceMarquee extends Marquee { const BounceMarquee({ super.key, - required super.animation, + required super.velocity, required super.child, super.direction, super.clipBehavior, @@ -158,7 +107,7 @@ class BounceMarquee extends Marquee { @override RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender( direction: direction, - animation: animation, + velocity: velocity, clipBehavior: clipBehavior, spacing: spacing, ); @@ -168,15 +117,15 @@ abstract class MarqueeRender extends RenderBox with RenderObjectWithChildMixin { MarqueeRender({ required Axis direction, - required Animation animation, + required double velocity, + required double spacing, required this.clipBehavior, - required this.spacing, - }) : _direction = direction, - _animation = animation, + }) : _spacing = spacing, + _velocity = velocity, + _direction = direction, assert(spacing.isFinite && !spacing.isNaN); Clip clipBehavior; - double spacing; Axis _direction; Axis get direction => _direction; @@ -186,40 +135,61 @@ abstract class MarqueeRender extends RenderBox markNeedsLayout(); } - Animation _animation; - Animation get animation => _animation; - set animation(Animation value) { - if (_animation == value) return; - if (_listened) { - _animation.removeListener(markNeedsPaint); - value.addListener(markNeedsPaint); + double _velocity; + set velocity(double value) { + if (_velocity == value) return; + _velocity = value; + _simulation = _simulation?.copyWith(initialValue: _delta, velocity: value); + ticker?.reset(); + } + + double _spacing; + set spacing(double value) { + if (value.isNegative) { + value *= _direction == Axis.horizontal ? -size.width : -size.height; } - _animation = value; + if (_spacing == value) return; + + _simulation = _simulation?.copyWith( + initialValue: _delta, + addSize: value - _spacing, + ); + _spacing = value; + ticker?.reset(); + } + + double _delta = 0; + set delta(double value) { + if (_delta == value) return; + _delta = value; + markNeedsPaint(); } @override void detach() { - _removeListener(); + ticker?.stop(); super.detach(); } - bool _listened = false; - void _addListener() { - if (!_listened) { - _animation.addListener(markNeedsPaint); - _listened = true; - } + @override + void attach(PipelineOwner owner) { + super.attach(owner); + ticker?.start(); } - void _removeListener() { - if (_listened) { - _animation.removeListener(markNeedsPaint); - _listened = false; - } + @override + void dispose() { + ticker?.dispose(); + ticker = null; + super.dispose(); } late double _distance; + Ticker? ticker; + + _MarqueeSimulation? _simulation; + @override void performLayout() { final child = this.child; @@ -235,7 +205,7 @@ abstract class MarqueeRender extends RenderBox ); size = constraints.constrain(child.size); _distance = child.size.width - size.width; - if (spacing.isNegative) spacing *= -size.width; + if (_spacing.isNegative) _spacing *= -size.width; } else { child.layout( BoxConstraints(maxWidth: constraints.maxWidth), @@ -243,12 +213,15 @@ abstract class MarqueeRender extends RenderBox ); size = constraints.constrain(child.size); _distance = child.size.height - size.height; - if (spacing.isNegative) spacing *= -size.height; + if (_spacing.isNegative) _spacing *= -size.height; } + if (_distance > 0) { - _addListener(); + updateSize(); + ticker ??= Ticker(_onTick)..start(); } else { - _removeListener(); + ticker?.dispose(); + ticker = null; } } @@ -262,41 +235,42 @@ abstract class MarqueeRender extends RenderBox context.paintChild(child!, Offset(offset.dx, offset.dy - _distance / 2)); } } + + void _onTick(Duration elapsed) { + delta = _simulation!.x( + elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond, + ); + } + + void updateSize(); } class _BounceMarqueeRender extends MarqueeRender { _BounceMarqueeRender({ required super.direction, - required super.animation, + required super.velocity, required super.clipBehavior, required super.spacing, }); + @override + void updateSize() { + final size = _distance + _spacing; + if (size == _simulation?.size) return; + _simulation = _MarqueeSimulation(_delta, size, false, _velocity); + } + @override void paint(PaintingContext context, Offset offset) { if (child == null) return; - final tick = _animation.value; - if (_distance > 0) { - final helfSpacing = spacing / 2.0; + final delta = _spacing / 2.0 - _delta; void paintChild() { if (_direction == Axis.horizontal) { - context.paintChild( - child!, - Offset( - offset.dx + helfSpacing - tick * (_distance + spacing), - offset.dy, - ), - ); + context.paintChild(child!, Offset(offset.dx + delta, offset.dy)); } else { - context.paintChild( - child!, - Offset( - offset.dx, - offset.dy + helfSpacing - tick * (_distance + spacing), - ), - ); + context.paintChild(child!, Offset(offset.dx, offset.dy + delta)); } } @@ -315,33 +289,46 @@ class _BounceMarqueeRender extends MarqueeRender { class _NormalMarqueeRender extends MarqueeRender { _NormalMarqueeRender({ required super.direction, - required super.animation, + required super.velocity, required super.clipBehavior, required super.spacing, }); + @override + void updateSize() { + final size = + (_direction == Axis.horizontal + ? child!.size.width + : child!.size.height) + + _spacing; + if (size == _simulation?.size) return; + _simulation = _MarqueeSimulation(_delta, size, true, _velocity); + } + @override void paint(PaintingContext context, Offset offset) { final child = this.child; if (child == null) return; - final tick = _animation.value; - if (_distance > 0) { void paintChild() { if (_direction == Axis.horizontal) { - final w = child.size.width + spacing; - final dx = tick * w; + final dx = _delta; context.paintChild(child, Offset(offset.dx - dx, offset.dy)); if (dx > _distance) { - context.paintChild(child, Offset(offset.dx + w - dx, offset.dy)); + context.paintChild( + child, + Offset(offset.dx + _simulation!.size - dx, offset.dy), + ); } } else { - final h = child.size.height + spacing; - final dy = tick * h; + final dy = _delta; context.paintChild(child, Offset(offset.dx, offset.dy - dy)); if (dy > _distance) { - context.paintChild(child, Offset(offset.dx, offset.dy + h - dy)); + context.paintChild( + child, + Offset(offset.dx, offset.dy + _simulation!.size - dy), + ); } } } @@ -357,3 +344,54 @@ class _NormalMarqueeRender extends MarqueeRender { } } } + +class _MarqueeSimulation extends Simulation { + _MarqueeSimulation( + this.initialValue, + this.size, + this.notBounce, + this.velocity, + ); + + final double initialValue; + final double size; + final bool notBounce; + final double velocity; + + @override + double x(double timeInSeconds) { + assert(timeInSeconds >= 0.0); + final totalX = initialValue + velocity * timeInSeconds; + if (notBounce) return totalX % size; + + final doublePeriod = 2.0 * size; + final doubleX = totalX % doublePeriod; + return doubleX < size ? doubleX : doublePeriod - doubleX; + } + + @override + double dx(double timeInSeconds) => velocity; + + @override + bool isDone(double timeInSeconds) => false; + + _MarqueeSimulation copyWith({ + final double? initialValue, + final double? addSize, + final bool? notBounce, + final double? velocity, + }) => _MarqueeSimulation( + initialValue ?? this.initialValue, + addSize == null ? size : size + addSize, + notBounce ?? this.notBounce, + velocity ?? this.velocity, + ); +} + +extension on Ticker { + void reset() { + this + ..stop() + ..start(); + } +} diff --git a/lib/pages/music/video/view.dart b/lib/pages/music/video/view.dart index d0a57e51b..0d1cdfc26 100644 --- a/lib/pages/music/video/view.dart +++ b/lib/pages/music/video/view.dart @@ -19,18 +19,12 @@ class MusicRecommandPage extends StatefulWidget { } class _MusicRecommandPageState extends State - with GridMixin, SingleTickerProviderStateMixin { + with GridMixin { late final _controller = Get.put( MusicRecommendController(), tag: Utils.generateRandomString(8), ); - late final _animation = AnimationController( - vsync: this, - duration: const Duration(seconds: 5), - reverseDuration: const Duration(seconds: 5), - )..repeat(reverse: true); - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -68,10 +62,8 @@ class _MusicRecommandPageState extends State response?.isNotEmpty == true ? SliverGrid.builder( gridDelegate: gridDelegate, - itemBuilder: (context, index) => MusicVideoCardH( - videoItem: response[index], - animation: _animation, - ), + itemBuilder: (context, index) => + MusicVideoCardH(videoItem: response[index]), itemCount: response!.length, ) : HttpError(onReload: _controller.onReload), @@ -120,10 +112,4 @@ class _MusicRecommandPageState extends State ), ); } - - @override - void dispose() { - _animation.dispose(); - super.dispose(); - } } diff --git a/lib/pages/music/widget/music_video_card_h.dart b/lib/pages/music/widget/music_video_card_h.dart index ce8ed46bf..4cb7d7c08 100644 --- a/lib/pages/music/widget/music_video_card_h.dart +++ b/lib/pages/music/widget/music_video_card_h.dart @@ -14,12 +14,10 @@ import 'package:flutter/material.dart'; class MusicVideoCardH extends StatelessWidget { final BgmRecommend videoItem; - final Animation animation; const MusicVideoCardH({ super.key, required this.videoItem, - required this.animation, }); @override @@ -117,7 +115,7 @@ class MusicVideoCardH extends StatelessWidget { ), const SizedBox(height: 3), BounceMarquee( - animation: animation, + velocity: 25, child: Row( spacing: 8, children: [ diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index ef9889c0c..c782299b7 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -1933,8 +1933,6 @@ class HeaderControlState extends TripleState { return MarqueeText( title, maxWidth: constraints.maxWidth, - count: 3, - bounce: false, spacing: 30, style: const TextStyle( color: Colors.white,