自动滚动文本

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);
  }
}

 

posted @ 2026-07-03 11:48  呢哇哦比较  阅读(4)  评论(0)    收藏  举报