You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter MarqueeWidget遇ScrollController未绑定错误,求动态文本长度判断方案

Fixing "ScrollController not attached to any scroll views" in Custom Marquee Widget

Alright, let's break down why this error pops up when your text is too short, and fix it properly.

Why the Error Happens

Your current code tries to kick off the scroll animation right in initState, but at that point the ScrollController hasn't been attached to the SingleChildScrollView yet. Even worse, when the text fits entirely within its parent container, the scroll view doesn't create a scrollable area at all—so the controller's position property doesn't exist, leading to that crash when you call animateTo.

Step-by-Step Fix

We need two critical changes:

  1. Only animate when necessary: Run the marquee loop only if the text exceeds the available viewport space.
  2. Wait for the controller to attach: Make sure we don't interact with the controller until it's properly linked to the scroll view.

Here's the revised, robust version of your MarqueeWidget:

import 'package:flutter/material.dart';

class MarqueeWidget extends StatefulWidget {
  final Widget child;
  final Axis direction;
  final Duration animationDuration, backDuration, pauseDuration;
  MarqueeWidget({
    @required this.child,
    this.direction: Axis.horizontal,
    this.animationDuration: const Duration(milliseconds: 7000),
    this.backDuration: const Duration(milliseconds: 2000),
    this.pauseDuration: const Duration(milliseconds: 2000),
  });
  @override
  _MarqueeWidgetState createState() => _MarqueeWidgetState();
}

class _MarqueeWidgetState extends State<MarqueeWidget> {
  ScrollController scrollController = ScrollController();
  bool _isScrollNeeded = false;
  bool _isAnimating = false;

  @override
  void initState() {
    super.initState();
    // Wait until the widget is fully built before checking scroll state
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _checkScrollRequirement();
      if (_isScrollNeeded) _startMarquee();
    });
  }

  @override
  void didUpdateWidget(covariant MarqueeWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    // Re-check if scroll is needed when content changes
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _checkScrollRequirement();
      if (_isScrollNeeded && !_isAnimating) _startMarquee();
    });
  }

  void _checkScrollRequirement() {
    if (scrollController.hasClients) {
      setState(() {
        // Check if content overflows the viewport
        _isScrollNeeded = scrollController.position.maxScrollExtent > 0;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: widget.child,
      scrollDirection: widget.direction,
      controller: scrollController,
      physics: const NeverScrollableScrollPhysics(), // Block user-initiated scroll
    );
  }

  void _startMarquee() async {
    if (!scrollController.hasClients || !_isScrollNeeded) return;
    _isAnimating = true;
    try {
      while (true) {
        await Future.delayed(widget.pauseDuration);
        if (!_isScrollNeeded) break;
        await scrollController.animateTo(
          scrollController.position.maxScrollExtent,
          duration: widget.animationDuration,
          curve: Curves.easeIn,
        );
        await Future.delayed(widget.pauseDuration);
        if (!_isScrollNeeded) break;
        await scrollController.animateTo(
          0.0,
          duration: widget.backDuration,
          curve: Curves.easeOut,
        );
      }
    } catch (e) {
      debugPrint("Marquee animation error: $e");
    } finally {
      _isAnimating = false;
    }
  }

  @override
  void dispose() {
    scrollController.dispose();
    super.dispose();
  }
}

Key Improvements Explained

  • PostFrameCallback: We wait until the widget is fully rendered before checking scroll status—this guarantees the controller is attached to the scroll view.
  • _isScrollNeeded Flag: The animation only runs if maxScrollExtent is greater than 0 (meaning content overflows the viewport).
  • Dynamic Content Handling: didUpdateWidget re-checks scroll requirements if the text changes, so the marquee starts/stops automatically.
  • Safety Checks: We verify scrollController.hasClients before accessing its position, and wrap the loop in a try/catch to handle unexpected detachments.
  • Disabled User Scroll: NeverScrollableScrollPhysics prevents user swipes from interfering with the marquee animation.

Usage Reminder

Your original implementation code stays exactly the same—this revised widget will automatically skip animation when text fits perfectly:

MarqueeWidget(
  direction: Axis.horizontal,
  child: Text(
    '$parsedString', // Text length no longer causes crashes
    style: AppTheme.of(context).caption(),
  ),
)

内容的提问来源于stack exchange,提问作者DolDurma

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:08:34