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/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<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 {
final Axis direction;
final Clip clipBehavior;
final double spacing;
final Animation<double> 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<RenderBox> {
MarqueeRender({
required Axis direction,
required Animation<double> 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<double> _animation;
Animation<double> get animation => _animation;
set animation(Animation<double> 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();
}
}

View File

@@ -19,18 +19,12 @@ class MusicRecommandPage extends StatefulWidget {
}
class _MusicRecommandPageState extends State<MusicRecommandPage>
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<MusicRecommandPage>
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<MusicRecommandPage>
),
);
}
@override
void dispose() {
_animation.dispose();
super.dispose();
}
}

View File

@@ -14,12 +14,10 @@ import 'package:flutter/material.dart';
class MusicVideoCardH extends StatelessWidget {
final BgmRecommend videoItem;
final Animation<double> 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: [

View File

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