import 'package:flutter/material.dart';
///滚动文本
class MarqueeWidget extends StatefulWidget {
final Widget child;
final Axis direction;
final Duration animationDuration, backDuration, pauseDuration;
const MarqueeWidget({
Key? key,
required this.child,
this.direction = Axis.horizontal,
this.animationDuration = const Duration(milliseconds: 3000),
this.backDuration = const Duration(milliseconds: 800),
this.pauseDuration = const Duration(milliseconds: 800),
}) : super(key: key);
@override
_MarqueeWidgetState createState() => _MarqueeWidgetState();
}
class _MarqueeWidgetState extends State<MarqueeWidget> {
final ScrollController scrollController = ScrollController();
int _scrollToken = 0;
bool _scrolling = false;
bool _postFrameScheduled = false;
bool _resumeOnNextCheck = false;
bool? _tickerEnabled;
bool? _routeCurrent;
Object? _contextSignature;
double _lastMaxScrollExtent = 0;
@override
void initState() {
super.initState();
_scheduleScrollCheck();
}
@override
void didUpdateWidget(covariant MarqueeWidget oldWidget) {
super.didUpdateWidget(oldWidget);
final contentChanged = oldWidget.direction != widget.direction ||
!_isSameChild(oldWidget.child, widget.child);
if (contentChanged ||
oldWidget.animationDuration != widget.animationDuration ||
oldWidget.backDuration != widget.backDuration ||
oldWidget.pauseDuration != widget.pauseDuration) {
_stopScroll();
_lastMaxScrollExtent = 0;
if (contentChanged) {
_jumpToStart();
}
}
_scheduleScrollCheck();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateContextSignature();
_updateTickerMode();
_updateRouteCurrent();
_scheduleScrollCheck();
}
@override
void dispose() {
_stopScroll();
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_updateContextSignature();
_updateTickerMode();
_updateRouteCurrent();
_scheduleScrollCheck();
return SingleChildScrollView(
child: widget.child,
scrollDirection: widget.direction,
controller: scrollController,
physics: const NeverScrollableScrollPhysics(),
);
}
void _scheduleScrollCheck() {
if (_postFrameScheduled || !mounted) {
return;
}
_postFrameScheduled = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
_postFrameScheduled = false;
if (!mounted || !scrollController.hasClients) {
return;
}
final maxScrollExtent = scrollController.position.maxScrollExtent;
_clampOffset(maxScrollExtent);
if (maxScrollExtent <= 0) {
_stopScroll();
_lastMaxScrollExtent = 0;
_resumeOnNextCheck = false;
return;
}
if (_resumeOnNextCheck ||
!_scrolling ||
_lastMaxScrollExtent != maxScrollExtent) {
final skipInitialPause = _resumeOnNextCheck;
_resumeOnNextCheck = false;
_lastMaxScrollExtent = maxScrollExtent;
_startScroll(skipInitialPause: skipInitialPause);
}
});
}
void _startScroll({bool skipInitialPause = false}) {
_stopScroll();
_scrolling = true;
final token = ++_scrollToken;
_runScrollLoop(token, skipInitialPause: skipInitialPause);
}
void _stopScroll() {
_scrolling = false;
_scrollToken++;
}
void _jumpToStart() {
if (!scrollController.hasClients || scrollController.position.pixels == 0) {
return;
}
_jumpTo(0);
}
void _clampOffset(double maxScrollExtent) {
if (!scrollController.hasClients) {
return;
}
final pixels = scrollController.position.pixels;
if (pixels < 0) {
_jumpTo(0);
} else if (pixels > maxScrollExtent) {
_jumpTo(maxScrollExtent);
}
}
void _jumpTo(double offset) {
try {
scrollController.jumpTo(offset);
} catch (_) {}
}
bool _isSameChild(Widget oldChild, Widget newChild) {
if (identical(oldChild, newChild)) {
return true;
}
final oldSignature = _childSignature(oldChild);
final newSignature = _childSignature(newChild);
if (oldSignature == null || newSignature == null) {
return oldChild.runtimeType == newChild.runtimeType &&
oldChild.key == newChild.key;
}
return oldSignature == newSignature;
}
String? _childSignature(Widget child) {
if (child is Text) {
return [
child.runtimeType,
child.key,
child.data,
child.textSpan?.toPlainText(),
child.style,
child.strutStyle,
child.textAlign,
child.textDirection,
child.locale,
child.softWrap,
child.overflow,
child.textScaler,
child.maxLines,
child.semanticsLabel,
child.textWidthBasis,
child.textHeightBehavior,
].join('|');
}
if (child is Container) {
return _singleChildSignature(
child,
child.child,
extra: [
child.alignment,
child.padding,
child.color,
child.decoration,
child.foregroundDecoration,
child.constraints,
child.margin,
child.transform,
child.clipBehavior,
],
);
}
if (child is Center) {
return _singleChildSignature(
child,
child.child,
extra: [child.widthFactor, child.heightFactor],
);
}
if (child is Align) {
return _singleChildSignature(
child,
child.child,
extra: [child.alignment, child.widthFactor, child.heightFactor],
);
}
if (child is Padding) {
return _singleChildSignature(child, child.child, extra: [child.padding]);
}
if (child is SizedBox) {
return _singleChildSignature(
child,
child.child,
extra: [child.width, child.height],
);
}
if (child is ConstrainedBox) {
return _singleChildSignature(
child,
child.child,
extra: [child.constraints],
);
}
if (child is DefaultTextStyle) {
return _singleChildSignature(
child,
child.child,
extra: [
child.style,
child.textAlign,
child.softWrap,
child.overflow,
child.maxLines,
child.textWidthBasis,
child.textHeightBehavior,
],
);
}
return null;
}
String? _singleChildSignature(
Widget widget,
Widget? child, {
List<Object?> extra = const [],
}) {
final childSignature = child == null ? '' : _childSignature(child);
if (child != null && childSignature == null) {
return null;
}
return [widget.runtimeType, widget.key, ...extra, childSignature].join('|');
}
void _updateTickerMode() {
final enabled = TickerMode.of(context);
final wasEnabled = _tickerEnabled;
_tickerEnabled = enabled;
if (wasEnabled == false && enabled) {
_resumeOnNextCheck = true;
_scheduleScrollCheck();
}
}
void _updateRouteCurrent() {
final route = ModalRoute.of(context);
if (route == null) {
return;
}
final current = route.isCurrent;
final wasCurrent = _routeCurrent;
_routeCurrent = current;
if (wasCurrent == false && current) {
_resumeOnNextCheck = true;
_scheduleScrollCheck();
}
}
void _updateContextSignature() {
final signature = _buildContextSignature();
final oldSignature = _contextSignature;
_contextSignature = signature;
if (oldSignature != null && oldSignature != signature) {
_stopScroll();
_lastMaxScrollExtent = 0;
_resumeOnNextCheck = false;
_jumpToStart();
_scheduleScrollCheck();
}
}
Object _buildContextSignature() {
final defaultTextStyle = DefaultTextStyle.of(context);
return Object.hashAll([
Localizations.maybeLocaleOf(context),
Directionality.maybeOf(context),
MediaQuery.maybeTextScalerOf(context),
defaultTextStyle.style,
defaultTextStyle.textAlign,
defaultTextStyle.softWrap,
defaultTextStyle.overflow,
defaultTextStyle.maxLines,
defaultTextStyle.textWidthBasis,
defaultTextStyle.textHeightBehavior,
]);
}
Future<void> _runScrollLoop(
int token, {
bool skipInitialPause = false,
}) async {
while (_isCurrentScroll(token)) {
if (!_isTickerEnabled()) {
if (!await _wait(_tickerPausedCheckDuration, token)) {
break;
}
continue;
}
if (!skipInitialPause && !await _wait(widget.pauseDuration, token)) {
break;
}
skipInitialPause = false;
final maxScrollExtent = _maxScrollExtent;
if (maxScrollExtent <= 0) {
break;
}
if (!await _animateTo(
maxScrollExtent, widget.animationDuration, Curves.easeIn, token)) {
break;
}
if (!await _wait(widget.pauseDuration, token)) {
break;
}
if (!await _animateTo(0, widget.backDuration, Curves.easeOut, token)) {
break;
}
}
if (mounted && token == _scrollToken) {
_scrolling = false;
_scheduleScrollCheck();
}
}
Duration get _tickerPausedCheckDuration {
if (widget.pauseDuration > Duration.zero) {
return widget.pauseDuration;
}
return const Duration(milliseconds: 100);
}
bool _isCurrentScroll(int token) {
return mounted && token == _scrollToken && scrollController.hasClients;
}
bool _isTickerEnabled() {
return mounted && TickerMode.of(context);
}
double get _maxScrollExtent {
if (!scrollController.hasClients) {
return 0;
}
return scrollController.position.maxScrollExtent;
}
Future<bool> _wait(Duration duration, int token) async {
if (duration > Duration.zero) {
await Future.delayed(duration);
}
return _isCurrentScroll(token);
}
Future<bool> _animateTo(
double offset,
Duration duration,
Curve curve,
int token,
) async {
if (!_isCurrentScroll(token)) {
return false;
}
try {
await scrollController.animateTo(
offset,
duration: duration,
curve: curve,
);
} catch (_) {
return false;
}
return _isCurrentScroll(token);
}
}