library auto_scroll_text; // Created by Bomsamdi on 2022 // Copyright © 2022 Bomsamdi. All rights reserved. import 'dart:async'; import 'package:flutter/material.dart'; import 'after_layout_mixin.dart'; /// [AutoScrollText] is a solution when you need /// text widget for long texts without overlaping or overflow.elipsis /// [AutoScrollText] supports both directions [Axis.horizontal] and [Axis.vertical] class AutoScrollText extends StatefulWidget { /// [Text.text] of [AutoScrollText] final String text; /// [TextStyle] of [Text] final TextStyle? textStyle; /// [SingleChildScrollView.scrollDirection] default value [Axis.horizontal] final Axis scrollDirection; /// [Curve] of scroll animation final Curve curve; /// Distance per tick final double moveDistance; /// Reset timer final int timerRest; const AutoScrollText({ super.key, required this.text, this.textStyle, this.scrollDirection = Axis.horizontal, this.curve = Curves.linear, this.moveDistance = 1.0, this.timerRest = 100, }); @override State createState() { return AutoScrollTextState(); } } class AutoScrollTextState extends State with SingleTickerProviderStateMixin, AfterLayoutMixin { /// Final text for scrolling String textToScroll = ""; /// SingleChildScrollView controller final ScrollController _scrollController = ScrollController(); /// Actual position of scroll double position = 0.0; /// Repeatable timer Timer? timer; /// if text is to long for axis, define auto scroll action bool _isScrollable = false; /// SingleChildScrollView key final GlobalKey _scrollKey = GlobalKey(); /// Default [TextStyle] if [widget.textStyle] is null TextStyle get defaultTextStyle => const TextStyle(); @override void initState() { textToScroll = widget.text; super.initState(); } /// Timer for animation void _startTimer() { if (_scrollKey.currentContext != null) { timer = Timer.periodic(Duration(milliseconds: widget.timerRest), (timer) { double maxScrollExtent = _scrollController.position.maxScrollExtent; double pixels = _scrollController.position.pixels; if (pixels + widget.moveDistance >= maxScrollExtent) { position = 0; _scrollController.jumpTo(position); } position += widget.moveDistance; _scrollController.animateTo(position, duration: Duration(milliseconds: widget.timerRest), curve: widget.curve); }); } } /// Check if autoscroll animation is needed void _checkIsAutoScrollNeeded() { setState(() { _isScrollable = _scrollController.position.maxScrollExtent > 0; }); } /// Text builder Widget _text() { if (widget.scrollDirection == Axis.vertical) { String newString = textToScroll.split("").join("\n"); return Text( newString, style: widget.textStyle ?? defaultTextStyle, textAlign: TextAlign.center, ); } return Text( textToScroll, style: widget.textStyle ?? defaultTextStyle, textAlign: TextAlign.justify, ); } @override void dispose() { super.dispose(); // dispose timer when needed timer?.cancel(); } @override Widget build(BuildContext context) { return SingleChildScrollView( key: _scrollKey, scrollDirection: widget.scrollDirection, controller: _scrollController, physics: _isScrollable ? const AlwaysScrollableScrollPhysics() : const NeverScrollableScrollPhysics(), child: _text(), ); } @override FutureOr afterFirstLayout(BuildContext context) { _checkIsAutoScrollNeeded(); if (_isScrollable) { setState(() { textToScroll = " $textToScroll "; }); _startTimer(); } } }