mod: marquee use velocity

This commit is contained in:
My-Responsitories
2025-08-30 02:58:53 +08:00
committed by bggRGjQaUbCoE
parent 8d94c0405f
commit 498ab2818e
4 changed files with 162 additions and 142 deletions

View File

@@ -1,22 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
class MarqueeText extends StatelessWidget { class MarqueeText extends StatelessWidget {
final double maxWidth; final double maxWidth;
final String text; final String text;
final TextStyle? style; final TextStyle? style;
final int? count;
final bool bounce;
final double spacing; final double spacing;
final double velocity;
const MarqueeText( const MarqueeText(
this.text, { this.text, {
super.key, super.key,
required this.maxWidth, required this.maxWidth,
this.style, this.style,
this.count,
this.bounce = true,
this.spacing = 0, this.spacing = 0,
this.velocity = 25,
}); });
@override @override
@@ -37,12 +36,10 @@ class MarqueeText extends StatelessWidget {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
); );
if (width > maxWidth) { if (width > maxWidth) {
return SingleWidgetMarquee( return NormalMarquee(
child, velocity: velocity,
duration: Duration(milliseconds: (width / 50 * 1000).round()),
bounce: bounce,
count: count,
spacing: spacing, spacing: spacing,
child: child,
); );
} else { } else {
return child; 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<StatefulWidget> createState() => _SingleWidgetMarqueeState();
}
class _SingleWidgetMarqueeState extends State<SingleWidgetMarquee>
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 { abstract class Marquee extends SingleChildRenderObjectWidget {
final Axis direction; final Axis direction;
final Clip clipBehavior; final Clip clipBehavior;
final double spacing; final double spacing;
final Animation<double> animation; final double velocity;
const Marquee({ const Marquee({
super.key, super.key,
required this.animation, required this.velocity,
required super.child, required super.child,
this.direction = Axis.horizontal, this.direction = Axis.horizontal,
this.clipBehavior = Clip.hardEdge, this.clipBehavior = Clip.hardEdge,
@@ -121,7 +70,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget {
renderObject renderObject
..direction = direction ..direction = direction
..clipBehavior = clipBehavior ..clipBehavior = clipBehavior
..animation = animation ..velocity = velocity
..spacing = spacing; ..spacing = spacing;
} }
} }
@@ -129,7 +78,7 @@ abstract class Marquee extends SingleChildRenderObjectWidget {
class NormalMarquee extends Marquee { class NormalMarquee extends Marquee {
const NormalMarquee({ const NormalMarquee({
super.key, super.key,
required super.animation, required super.velocity,
required super.child, required super.child,
super.direction, super.direction,
super.clipBehavior, super.clipBehavior,
@@ -139,7 +88,7 @@ class NormalMarquee extends Marquee {
@override @override
RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender( RenderObject createRenderObject(BuildContext context) => _NormalMarqueeRender(
direction: direction, direction: direction,
animation: animation, velocity: velocity,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
spacing: spacing, spacing: spacing,
); );
@@ -148,7 +97,7 @@ class NormalMarquee extends Marquee {
class BounceMarquee extends Marquee { class BounceMarquee extends Marquee {
const BounceMarquee({ const BounceMarquee({
super.key, super.key,
required super.animation, required super.velocity,
required super.child, required super.child,
super.direction, super.direction,
super.clipBehavior, super.clipBehavior,
@@ -158,7 +107,7 @@ class BounceMarquee extends Marquee {
@override @override
RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender( RenderObject createRenderObject(BuildContext context) => _BounceMarqueeRender(
direction: direction, direction: direction,
animation: animation, velocity: velocity,
clipBehavior: clipBehavior, clipBehavior: clipBehavior,
spacing: spacing, spacing: spacing,
); );
@@ -168,15 +117,15 @@ abstract class MarqueeRender extends RenderBox
with RenderObjectWithChildMixin<RenderBox> { with RenderObjectWithChildMixin<RenderBox> {
MarqueeRender({ MarqueeRender({
required Axis direction, required Axis direction,
required Animation<double> animation, required double velocity,
required double spacing,
required this.clipBehavior, required this.clipBehavior,
required this.spacing, }) : _spacing = spacing,
}) : _direction = direction, _velocity = velocity,
_animation = animation, _direction = direction,
assert(spacing.isFinite && !spacing.isNaN); assert(spacing.isFinite && !spacing.isNaN);
Clip clipBehavior; Clip clipBehavior;
double spacing;
Axis _direction; Axis _direction;
Axis get direction => _direction; Axis get direction => _direction;
@@ -186,40 +135,61 @@ abstract class MarqueeRender extends RenderBox
markNeedsLayout(); markNeedsLayout();
} }
Animation<double> _animation; double _velocity;
Animation<double> get animation => _animation; set velocity(double value) {
set animation(Animation<double> value) { if (_velocity == value) return;
if (_animation == value) return; _velocity = value;
if (_listened) { _simulation = _simulation?.copyWith(initialValue: _delta, velocity: value);
_animation.removeListener(markNeedsPaint); ticker?.reset();
value.addListener(markNeedsPaint); }
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 @override
void detach() { void detach() {
_removeListener(); ticker?.stop();
super.detach(); super.detach();
} }
bool _listened = false; @override
void _addListener() { void attach(PipelineOwner owner) {
if (!_listened) { super.attach(owner);
_animation.addListener(markNeedsPaint); ticker?.start();
_listened = true;
}
} }
void _removeListener() { @override
if (_listened) { void dispose() {
_animation.removeListener(markNeedsPaint); ticker?.dispose();
_listened = false; ticker = null;
} super.dispose();
} }
late double _distance; late double _distance;
Ticker? ticker;
_MarqueeSimulation? _simulation;
@override @override
void performLayout() { void performLayout() {
final child = this.child; final child = this.child;
@@ -235,7 +205,7 @@ abstract class MarqueeRender extends RenderBox
); );
size = constraints.constrain(child.size); size = constraints.constrain(child.size);
_distance = child.size.width - size.width; _distance = child.size.width - size.width;
if (spacing.isNegative) spacing *= -size.width; if (_spacing.isNegative) _spacing *= -size.width;
} else { } else {
child.layout( child.layout(
BoxConstraints(maxWidth: constraints.maxWidth), BoxConstraints(maxWidth: constraints.maxWidth),
@@ -243,12 +213,15 @@ abstract class MarqueeRender extends RenderBox
); );
size = constraints.constrain(child.size); size = constraints.constrain(child.size);
_distance = child.size.height - size.height; _distance = child.size.height - size.height;
if (spacing.isNegative) spacing *= -size.height; if (_spacing.isNegative) _spacing *= -size.height;
} }
if (_distance > 0) { if (_distance > 0) {
_addListener(); updateSize();
ticker ??= Ticker(_onTick)..start();
} else { } 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)); 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 { class _BounceMarqueeRender extends MarqueeRender {
_BounceMarqueeRender({ _BounceMarqueeRender({
required super.direction, required super.direction,
required super.animation, required super.velocity,
required super.clipBehavior, required super.clipBehavior,
required super.spacing, required super.spacing,
}); });
@override
void updateSize() {
final size = _distance + _spacing;
if (size == _simulation?.size) return;
_simulation = _MarqueeSimulation(_delta, size, false, _velocity);
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child == null) return; if (child == null) return;
final tick = _animation.value;
if (_distance > 0) { if (_distance > 0) {
final helfSpacing = spacing / 2.0; final delta = _spacing / 2.0 - _delta;
void paintChild() { void paintChild() {
if (_direction == Axis.horizontal) { if (_direction == Axis.horizontal) {
context.paintChild( context.paintChild(child!, Offset(offset.dx + delta, offset.dy));
child!,
Offset(
offset.dx + helfSpacing - tick * (_distance + spacing),
offset.dy,
),
);
} else { } else {
context.paintChild( context.paintChild(child!, Offset(offset.dx, offset.dy + delta));
child!,
Offset(
offset.dx,
offset.dy + helfSpacing - tick * (_distance + spacing),
),
);
} }
} }
@@ -315,33 +289,46 @@ class _BounceMarqueeRender extends MarqueeRender {
class _NormalMarqueeRender extends MarqueeRender { class _NormalMarqueeRender extends MarqueeRender {
_NormalMarqueeRender({ _NormalMarqueeRender({
required super.direction, required super.direction,
required super.animation, required super.velocity,
required super.clipBehavior, required super.clipBehavior,
required super.spacing, 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 @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
final child = this.child; final child = this.child;
if (child == null) return; if (child == null) return;
final tick = _animation.value;
if (_distance > 0) { if (_distance > 0) {
void paintChild() { void paintChild() {
if (_direction == Axis.horizontal) { if (_direction == Axis.horizontal) {
final w = child.size.width + spacing; final dx = _delta;
final dx = tick * w;
context.paintChild(child, Offset(offset.dx - dx, offset.dy)); context.paintChild(child, Offset(offset.dx - dx, offset.dy));
if (dx > _distance) { 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 { } else {
final h = child.size.height + spacing; final dy = _delta;
final dy = tick * h;
context.paintChild(child, Offset(offset.dx, offset.dy - dy)); context.paintChild(child, Offset(offset.dx, offset.dy - dy));
if (dy > _distance) { 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();
}
}

View File

@@ -19,18 +19,12 @@ class MusicRecommandPage extends StatefulWidget {
} }
class _MusicRecommandPageState extends State<MusicRecommandPage> class _MusicRecommandPageState extends State<MusicRecommandPage>
with GridMixin, SingleTickerProviderStateMixin { with GridMixin {
late final _controller = Get.put( late final _controller = Get.put(
MusicRecommendController(), MusicRecommendController(),
tag: Utils.generateRandomString(8), tag: Utils.generateRandomString(8),
); );
late final _animation = AnimationController(
vsync: this,
duration: const Duration(seconds: 5),
reverseDuration: const Duration(seconds: 5),
)..repeat(reverse: true);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -68,10 +62,8 @@ class _MusicRecommandPageState extends State<MusicRecommandPage>
response?.isNotEmpty == true response?.isNotEmpty == true
? SliverGrid.builder( ? SliverGrid.builder(
gridDelegate: gridDelegate, gridDelegate: gridDelegate,
itemBuilder: (context, index) => MusicVideoCardH( itemBuilder: (context, index) =>
videoItem: response[index], MusicVideoCardH(videoItem: response[index]),
animation: _animation,
),
itemCount: response!.length, itemCount: response!.length,
) )
: HttpError(onReload: _controller.onReload), : HttpError(onReload: _controller.onReload),
@@ -120,10 +112,4 @@ class _MusicRecommandPageState extends State<MusicRecommandPage>
), ),
); );
} }
@override
void dispose() {
_animation.dispose();
super.dispose();
}
} }

View File

@@ -14,12 +14,10 @@ import 'package:flutter/material.dart';
class MusicVideoCardH extends StatelessWidget { class MusicVideoCardH extends StatelessWidget {
final BgmRecommend videoItem; final BgmRecommend videoItem;
final Animation<double> animation;
const MusicVideoCardH({ const MusicVideoCardH({
super.key, super.key,
required this.videoItem, required this.videoItem,
required this.animation,
}); });
@override @override
@@ -117,7 +115,7 @@ class MusicVideoCardH extends StatelessWidget {
), ),
const SizedBox(height: 3), const SizedBox(height: 3),
BounceMarquee( BounceMarquee(
animation: animation, velocity: 25,
child: Row( child: Row(
spacing: 8, spacing: 8,
children: [ children: [

View File

@@ -1933,8 +1933,6 @@ class HeaderControlState extends TripleState<HeaderControl> {
return MarqueeText( return MarqueeText(
title, title,
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
count: 3,
bounce: false,
spacing: 30, spacing: 30,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,