mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
mod: marquee use velocity
This commit is contained in:
committed by
bggRGjQaUbCoE
parent
8d94c0405f
commit
498ab2818e
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user