mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-28 20:32:15 +02:00
fix(player): loop marquee forward with longer reading pauses
This commit is contained in:
@@ -19,12 +19,17 @@ class OverflowMarquee extends StatefulWidget {
|
||||
|
||||
class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
with WidgetsBindingObserver {
|
||||
static const _gap = 32.0;
|
||||
|
||||
final _scroll = ScrollController();
|
||||
final _contentKey = GlobalKey();
|
||||
Timer? _pause;
|
||||
int _generation = 0;
|
||||
bool _restartQueued = false;
|
||||
bool _motionEnabled = false;
|
||||
bool _appActive = true;
|
||||
bool _looping = false;
|
||||
double _loopDistance = 0;
|
||||
double? _viewport;
|
||||
double? _extent;
|
||||
|
||||
@@ -69,10 +74,18 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
if (!mounted || !_scroll.hasClients) return;
|
||||
// Also cancels an in-flight animation after a track/width change.
|
||||
_scroll.jumpTo(0);
|
||||
if (_motionEnabled &&
|
||||
_appActive &&
|
||||
_scroll.position.maxScrollExtent > 0) {
|
||||
_scheduleLeg(towardEnd: true, generation: _generation);
|
||||
final content = _contentKey.currentContext?.findRenderObject();
|
||||
if (content is! RenderBox || !content.hasSize) return;
|
||||
final overflowing =
|
||||
content.size.width > _scroll.position.viewportDimension + 0.5;
|
||||
_loopDistance = content.size.width + _gap;
|
||||
if (_looping != overflowing) {
|
||||
setState(() => _looping = overflowing);
|
||||
_queueRestart();
|
||||
return;
|
||||
}
|
||||
if (_motionEnabled && _appActive && _looping) {
|
||||
_scheduleCycle(_generation);
|
||||
}
|
||||
});
|
||||
WidgetsBinding.instance.ensureVisualUpdate();
|
||||
@@ -85,28 +98,22 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
_appActive &&
|
||||
_scroll.hasClients;
|
||||
|
||||
void _scheduleLeg({required bool towardEnd, required int generation}) {
|
||||
_pause = Timer(
|
||||
Duration(milliseconds: towardEnd ? 1500 : 1200),
|
||||
() => _scrollLeg(towardEnd: towardEnd, generation: generation),
|
||||
);
|
||||
void _scheduleCycle(int generation) {
|
||||
_pause = Timer(const Duration(seconds: 3), () => _scrollCycle(generation));
|
||||
}
|
||||
|
||||
Future<void> _scrollLeg({
|
||||
required bool towardEnd,
|
||||
required int generation,
|
||||
}) async {
|
||||
Future<void> _scrollCycle(int generation) async {
|
||||
if (!_canScroll(generation)) return;
|
||||
final target = towardEnd ? _scroll.position.maxScrollExtent : 0.0;
|
||||
final distance = (target - _scroll.offset).abs();
|
||||
if (distance < 0.5) return;
|
||||
await _scroll.animateTo(
|
||||
target,
|
||||
duration: Duration(milliseconds: (distance / 28 * 1000).round()),
|
||||
_loopDistance,
|
||||
duration: Duration(milliseconds: (_loopDistance / 28 * 1000).round()),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
if (_canScroll(generation)) {
|
||||
_scheduleLeg(towardEnd: !towardEnd, generation: generation);
|
||||
// The next copy is now exactly where the first started, so resetting
|
||||
// the offset changes neither the visible text nor its direction.
|
||||
_scroll.jumpTo(0);
|
||||
_scheduleCycle(generation);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,7 +145,16 @@ class _OverflowMarqueeState extends State<OverflowMarquee>
|
||||
controller: _scroll,
|
||||
scrollDirection: Axis.horizontal,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: widget.child,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
KeyedSubtree(key: _contentKey, child: widget.child),
|
||||
if (_looping) ...[
|
||||
const SizedBox(width: _gap),
|
||||
ExcludeSemantics(child: IgnorePointer(child: widget.child)),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -53,6 +53,7 @@ void main() {
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
ScrollController controller(WidgetTester tester) => tester
|
||||
@@ -60,7 +61,7 @@ void main() {
|
||||
.controller!;
|
||||
|
||||
Future<void> startScrolling(WidgetTester tester) async {
|
||||
await tester.pump(const Duration(milliseconds: 1500));
|
||||
await tester.pump(const Duration(seconds: 3));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
}
|
||||
|
||||
@@ -75,25 +76,37 @@ void main() {
|
||||
expect(tester.binding.transientCallbackCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('long titles pause, reveal the end, and return smoothly', (
|
||||
testWidgets('long titles loop forward without reversing or a visible jump', (
|
||||
tester,
|
||||
) async {
|
||||
await pumpTitle(tester, explicit: true);
|
||||
final scroll = controller(tester);
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
await tester.pump(const Duration(milliseconds: 2500));
|
||||
expect(scroll.offset, 0);
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(scroll.offset, closeTo(28, 1));
|
||||
await tester.pump(const Duration(minutes: 1));
|
||||
final end = scroll.position.maxScrollExtent;
|
||||
expect(scroll.offset, end);
|
||||
expect(find.byType(ExplicitBadge), findsOneWidget);
|
||||
final titles = find.byType(ExplicitTrackTitle);
|
||||
expect(titles, findsNWidgets(2));
|
||||
final distance =
|
||||
tester.getTopLeft(titles.last).dx - tester.getTopLeft(titles.first).dx;
|
||||
final travelMs = (distance / 28 * 1000).round();
|
||||
await tester.pump(Duration(milliseconds: travelMs - 1000 - 10));
|
||||
expect(scroll.offset, greaterThan(distance - 1));
|
||||
final nextCopyPosition = tester.getTopLeft(titles.last);
|
||||
await tester.pump(const Duration(milliseconds: 11));
|
||||
await tester.pump();
|
||||
expect(scroll.offset, 0);
|
||||
expect(
|
||||
(tester.getTopLeft(titles.first) - nextCopyPosition).distance,
|
||||
lessThan(0.5),
|
||||
);
|
||||
expect(find.byType(ExplicitBadge), findsNWidgets(2));
|
||||
await tester.pump(const Duration(milliseconds: 2500));
|
||||
expect(scroll.offset, 0);
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(scroll.offset, end);
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(scroll.offset, closeTo(end - 28, 1));
|
||||
expect(scroll.offset, closeTo(28, 1));
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
});
|
||||
|
||||
@@ -207,6 +220,8 @@ void main() {
|
||||
);
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await startScrolling(tester);
|
||||
final scrollViews = tester.widgetList<SingleChildScrollView>(
|
||||
find.descendant(
|
||||
@@ -218,7 +233,7 @@ void main() {
|
||||
for (final view in scrollViews) {
|
||||
expect(view.controller!.offset, greaterThan(0));
|
||||
}
|
||||
expect(find.byType(ExplicitBadge), findsOneWidget);
|
||||
expect(find.byType(ExplicitBadge), findsNWidgets(2));
|
||||
expect(tester.takeException(), isNull);
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user