Browse Source

update

Bouncing animation added
pull/1/head
Bartosz Wróbel 2 years ago
parent
commit
aa4b860e16
  1. 4
      CHANGELOG.md
  2. 91
      example/lib/main.dart
  3. 2
      example/pubspec.lock
  4. 18
      lib/after_layout_mixin.dart
  5. 483
      lib/auto_scroll_text.dart
  6. 4
      lib/auto_scroll_text_impl.dart
  7. 11
      lib/auto_scroll_text_mode.dart
  8. 2
      pubspec.yaml

4
CHANGELOG.md

@ -1,3 +1,7 @@
## 0.0.2
* Bouncing animation added
## 0.0.1 ## 0.0.1
* Initial release. * Initial release.

91
example/lib/main.dart

@ -1,4 +1,4 @@
import 'package:auto_scroll_text/auto_scroll_text.dart'; import 'package:auto_scroll_text/auto_scroll_text_impl.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
// Created by Bomsamdi on 2022 // Created by Bomsamdi on 2022
@ -49,6 +49,12 @@ class _MyHomePageState extends State<MyHomePage> {
ElevatedButton( ElevatedButton(
onPressed: _openVertical, onPressed: _openVertical,
child: const Text("Open VERTICAL example")), child: const Text("Open VERTICAL example")),
ElevatedButton(
onPressed: _openBouncingHorizontal,
child: const Text("Open BOUNCING HORIZONTAL example")),
ElevatedButton(
onPressed: _openBouncingVertical,
child: const Text("Open BOUNCING VERTICAL example")),
], ],
), ),
), ),
@ -66,6 +72,18 @@ class _MyHomePageState extends State<MyHomePage> {
builder: (context) => const VerticalExample(), builder: (context) => const VerticalExample(),
)); ));
} }
void _openBouncingHorizontal() {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const BouncingHorizontalExample(),
));
}
void _openBouncingVertical() {
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => const BouncingVerticalExample(),
));
}
} }
class HorizontalExample extends StatefulWidget { class HorizontalExample extends StatefulWidget {
@ -88,9 +106,8 @@ class _HorizontalExampleState extends State<HorizontalExample> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[ children: const <Widget>[
AutoScrollText( AutoScrollText(
text:
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
textStyle: TextStyle(fontSize: 24), style: TextStyle(fontSize: 24),
), ),
], ],
), ),
@ -119,10 +136,74 @@ class _VerticalExampleState extends State<VerticalExample> {
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[ children: const <Widget>[
AutoScrollText( AutoScrollText(
text:
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.", "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
textStyle: TextStyle(fontSize: 24), style: TextStyle(fontSize: 24),
scrollDirection: Axis.vertical,
),
],
),
),
);
}
}
class BouncingHorizontalExample extends StatefulWidget {
const BouncingHorizontalExample({Key? key}) : super(key: key);
@override
State<BouncingHorizontalExample> createState() =>
_BouncingHorizontalExampleState();
}
class _BouncingHorizontalExampleState extends State<BouncingHorizontalExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Bouncing Horizontal Example"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[
AutoScrollText(
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
style: TextStyle(fontSize: 24),
mode: AutoScrollTextMode.bouncing,
),
],
),
),
);
}
}
class BouncingVerticalExample extends StatefulWidget {
const BouncingVerticalExample({Key? key}) : super(key: key);
@override
State<BouncingVerticalExample> createState() =>
_BouncingVerticalExampleState();
}
class _BouncingVerticalExampleState extends State<BouncingVerticalExample> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("Bouncing Vertical Example"),
),
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[
AutoScrollText(
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
style: TextStyle(fontSize: 24),
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
mode: AutoScrollTextMode.bouncing,
), ),
], ],
), ),

2
example/pubspec.lock

@ -14,7 +14,7 @@ packages:
path: ".." path: ".."
relative: true relative: true
source: path source: path
version: "0.0.1" version: "0.0.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:

18
lib/after_layout_mixin.dart

@ -1,18 +0,0 @@
import 'dart:async';
// Created by Bomsamdi on 2022
// Copyright © 2022 Bomsamdi. All rights reserved.
import 'package:flutter/widgets.dart';
mixin AfterLayoutMixin<T extends StatefulWidget> on State<T> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.endOfFrame.then(
(_) {
if (mounted) afterFirstLayout(context);
},
);
}
FutureOr<void> afterFirstLayout(BuildContext context);
}

483
lib/auto_scroll_text.dart

@ -1,148 +1,429 @@
library auto_scroll_text;
// Created by Bomsamdi on 2022 // Created by Bomsamdi on 2022
// Copyright © 2022 Bomsamdi. All rights reserved. // Copyright © 2022 Bomsamdi. All rights reserved.
import 'dart:async'; import 'dart:async';
import 'package:auto_scroll_text/auto_scroll_text_mode.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'after_layout_mixin.dart';
/// [AutoScrollText] is a solution when you need /// AutoScrollText widget automatically scrolls provided [text]
/// text widget for long texts without overlaping or overflow.elipsis ///
/// [AutoScrollText] supports both directions [Axis.horizontal] and [Axis.vertical] /// ### Example:
///
/// ```dart
/// AutoScrollText(
/// "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book.",
/// mode: AutoScrollTextMode.bouncing,
/// velocity: Velocity(pixelsPerSecond: Offset(150, 0)),
/// delayBefore: Duration(milliseconds: 500),
/// numberOfReps: 5,
/// pauseBetween: Duration(milliseconds: 50),
/// style: TextStyle(color: Colors.green),
/// textAlign: TextAlign.right,
/// selectable: true,
/// scrollDirection: Axis.horizontal,
/// curve: Curves.linear,
/// )
/// ```
class AutoScrollText extends StatefulWidget { class AutoScrollText extends StatefulWidget {
/// [Text.text] of [AutoScrollText] const AutoScrollText(
this.text, {
Key? key,
this.style,
this.textAlign,
this.textDirection = TextDirection.ltr,
this.numberOfReps,
this.delayBefore,
this.pauseBetween,
this.mode = AutoScrollTextMode.endless,
this.velocity = const Velocity(pixelsPerSecond: Offset(80, 0)),
this.selectable = false,
this.intervalSpaces,
this.scrollDirection = Axis.horizontal,
this.curve = Curves.linear,
}) : super(key: key);
/// The text string, that would be scrolled.
/// In case text does fit into allocated space, it wouldn't be scrolled
/// and would be shown as is.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText('A sample text for AutoScrollText widget.')
/// ```
final String text; final String text;
/// [TextStyle] of [Text] /// Provides [TextAlign] alignment if text string is not long enough to be scrolled.
final TextStyle? textStyle; ///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Short text',
/// textAlign: TextAlign.right,
/// )
/// ```
final TextAlign? textAlign;
/// [SingleChildScrollView.scrollDirection] default value [Axis.horizontal] /// Provides [TextDirection] - a direction in which text flows.
final Axis scrollDirection; /// Default is [TextDirection.ltr].
/// Default scrolling direction would be opposite to [textDirection],
/// e.g. for [TextDirection.rtl] scrolling would be from left to right
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'This is a RTL text. This is a RTL text. This is a RTL text. This is a RTL text. ',
/// textDirection: TextDirection.rtl,
/// )
/// ```
final TextDirection textDirection;
/// [Curve] of scroll animation /// Allows to apply custom [TextStyle] to [text].
final Curve curve; ///
/// `null` by default.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with TextStyle',
/// style: TextStyle(
/// color: Colors.white,
/// ),
/// )
/// ```
final TextStyle? style;
/// Distance per tick /// Limits number of scroll animation rounds.
final double moveDistance; ///
/// Default is [infinity].
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Limit scroll rounds to 10',
/// numberOfReps: 10,
/// )
/// ```
final int? numberOfReps;
/// Reset timer /// Delay before first animation round.
final int timerRest; ///
/// Default is [Duration.zero].
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Start animation after 1 sec delay',
/// delayBefore: Duration(seconds: 1),
/// )
/// ```
final Duration? delayBefore;
const AutoScrollText({ /// Determines pause interval between animation rounds.
super.key, ///
required this.text, /// Only allowed if [mode] is set to [AutoScrollTextMode.bouncing].
this.textStyle, ///
this.scrollDirection = Axis.horizontal, /// Default is [Duration.zero].
this.curve = Curves.linear, ///
this.moveDistance = 3.0, /// ### Example:
this.timerRest = 100, ///
}); /// ```dart
/// AutoScrollText(
@override /// 'Text with pause between animations',
State<StatefulWidget> createState() { /// mode: AutoScrollTextMode.bouncing,
return AutoScrollTextState(); /// pauseBetween: Duration(milliseconds: 300),
} /// )
} /// ```
final Duration? pauseBetween;
class AutoScrollTextState extends State<AutoScrollText> /// Sets one of two different types of scrolling behavior.
with SingleTickerProviderStateMixin, AfterLayoutMixin { /// [AutoScrollTextMode.endless] - default, scrolls text in one direction endlessly.
/// Final text for scrolling /// [AutoScrollTextMode.bouncing] - when [text] string is scrolled to its end,
String textToScroll = ""; /// starts animation to opposite direction.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Animate text string back and forth',
/// mode: AutoScrollTextMode.bouncing,
/// )
/// ```
final AutoScrollTextMode mode;
/// SingleChildScrollView controller /// Allows to customize animation speed.
final ScrollController _scrollController = ScrollController(); ///
/// Default is `Velocity(pixelsPerSecond: Offset(80, 0))`
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with animation of 100px per second',
/// velocity: Velocity(pixelsPerSecond: Offset(100, 0)),
/// )
final Velocity velocity;
/// Actual position of scroll /// Allows users to select provided [text], copy it to clipboard etc.
double position = 0.0; ///
/// Default is `false`.
///
/// Example:
///
/// ```dart
/// AutoScrollText(
/// 'This text is has possibility to select and copy it to clipboard',
/// selectable: true,
/// )
/// ```
final bool selectable;
/// Repeatable timer /// Adds blank spaces between two nearby text sentences
Timer? timer; /// in case of [AutoScrollTextMode.endless]
///
/// Default is `1`.
///
/// Example:
///
/// ```dart
/// AutoScrollText(
/// 'This is the sample text for AutoScrollText widget. ',
/// blankSpaces: 10,
/// )
/// ```
final int? intervalSpaces;
/// if text is to long for axis, define auto scroll action /// Allows users to define scrollDirection of [AutoScrollText]
bool _isScrollable = false; ///
/// Default is [Axis.horizontal].
///
/// Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with vertical scroll direction',
/// scrollDirection: Axis.vertical,
/// )
/// ```
final Axis scrollDirection;
/// SingleChildScrollView key /// [Curve] of scroll animation
final GlobalKey _scrollKey = GlobalKey(); /// Allows users to define [Curve] of animation for [AutoScrollText]
///
/// Default is [Curves.linear].
///
/// Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with linear animation,
/// curve: Curves.linear,
/// )
/// ```
final Curve curve;
/// Default [TextStyle] if [widget.textStyle] is null @override
TextStyle get defaultTextStyle => const TextStyle(); State<AutoScrollText> createState() => _AutoScrollTextState();
}
class _AutoScrollTextState extends State<AutoScrollText> {
final _scrollController = ScrollController();
String _text = "";
String? _endlessText;
double? _originalTextWidth;
Timer? _timer;
bool _running = false;
int _counter = 0;
@override @override
void initState() { void initState() {
textToScroll = widget.text;
super.initState(); super.initState();
if (widget.scrollDirection == Axis.vertical) {
String newString = widget.text.split("").join("\n");
_text = newString;
} else {
_text = widget.text;
}
final WidgetsBinding binding = WidgetsBinding.instance;
binding.addPostFrameCallback(_initScroll);
}
@override
void didUpdateWidget(covariant AutoScrollText oldWidget) {
_onUpdate(oldWidget);
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
assert(
widget.pauseBetween == null ||
widget.mode == AutoScrollTextMode.bouncing,
'pauseBetween is only available in AutoScrollTextMode.bouncing mode');
assert(
widget.intervalSpaces == null ||
widget.mode == AutoScrollTextMode.endless,
'intervalSpaces is only available in AutoScrollTextMode.endless mode');
return Directionality(
textDirection: widget.textDirection,
child: Scrollbar(
controller: _scrollController,
thickness: 0,
child: SingleChildScrollView(
controller: _scrollController,
scrollDirection: widget.scrollDirection,
child: widget.selectable
? SelectableText(
_endlessText ?? _text,
style: widget.style,
textAlign: widget.textAlign,
)
: Text(
_endlessText ?? _text,
style: widget.style,
textAlign: widget.textAlign,
),
),
),
);
} }
/// Timer for animation Future<void> _initScroll(_) async {
void _startTimer() { await _delayBeforeStartAnimation();
if (_scrollKey.currentContext != null) { _timer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
timer = Timer.periodic(Duration(milliseconds: widget.timerRest), (timer) { if (!_available) {
double maxScrollExtent = _scrollController.position.maxScrollExtent; timer.cancel();
double pixels = _scrollController.position.pixels; return;
if (pixels + widget.moveDistance >= maxScrollExtent) { }
position = 0; final int? maxReps = widget.numberOfReps;
_scrollController.jumpTo(position); if (maxReps != null && _counter >= maxReps) {
} timer.cancel();
position += widget.moveDistance; return;
_scrollController.animateTo(position, }
duration: Duration(milliseconds: widget.timerRest), if (!_running) _runAnimation();
curve: widget.curve);
}); });
} }
Future<void> _runAnimation() async {
_running = true;
final int? maxReps = widget.numberOfReps;
if (maxReps == null || _counter < maxReps) {
_counter++;
switch (widget.mode) {
case AutoScrollTextMode.bouncing:
{
await _animateBouncing();
break;
}
default:
{
await _animateEndless();
}
}
}
_running = false;
} }
/// Check if autoscroll animation is needed Future<void> _animateEndless() async {
void _checkIsAutoScrollNeeded() { if (!_available) return;
final ScrollPosition position = _scrollController.position;
final bool needsScrolling = position.maxScrollExtent > 0;
if (!needsScrolling) {
if (_endlessText != null) setState(() => _endlessText = null);
return;
}
if (_endlessText == null || _originalTextWidth == null) {
setState(() { setState(() {
_isScrollable = _scrollController.position.maxScrollExtent > 0; _originalTextWidth =
position.maxScrollExtent + position.viewportDimension;
_endlessText = _text + _getSpaces(widget.intervalSpaces ?? 1) + _text;
}); });
return;
} }
final double endlessTextWidth =
/// Text builder position.maxScrollExtent + position.viewportDimension;
Widget _text() { final double singleRoundExtent = endlessTextWidth - _originalTextWidth!;
if (widget.scrollDirection == Axis.vertical) { final Duration duration = _getDuration(singleRoundExtent);
String newString = textToScroll.split("").join("\n"); if (duration == Duration.zero) return;
return Text( if (!_available) return;
newString, await _scrollController.animateTo(
style: widget.textStyle ?? defaultTextStyle, singleRoundExtent,
textAlign: TextAlign.center, duration: duration,
curve: widget.curve,
); );
if (!_available) return;
_scrollController.jumpTo(position.minScrollExtent);
} }
return Text(
textToScroll, Future<void> _animateBouncing() async {
style: widget.textStyle ?? defaultTextStyle, final double maxExtent = _scrollController.position.maxScrollExtent;
textAlign: TextAlign.justify, final double minExtent = _scrollController.position.minScrollExtent;
final double extent = maxExtent - minExtent;
final Duration duration = _getDuration(extent);
if (duration == Duration.zero) return;
if (!_available) return;
await _scrollController.animateTo(
maxExtent,
duration: duration,
curve: widget.curve,
);
if (!_available) return;
await _scrollController.animateTo(
minExtent,
duration: duration,
curve: widget.curve,
); );
if (!_available) return;
if (widget.pauseBetween != null) {
await Future<dynamic>.delayed(widget.pauseBetween!);
}
} }
@override Future<void> _delayBeforeStartAnimation() async {
void dispose() { final Duration? delayBefore = widget.delayBefore;
super.dispose(); if (delayBefore == null) return;
// dispose timer when needed await Future<dynamic>.delayed(delayBefore);
timer?.cancel();
} }
@override Duration _getDuration(double extent) {
Widget build(BuildContext context) { final int milliseconds =
return SingleChildScrollView( (extent * 1000 / widget.velocity.pixelsPerSecond.dx).round();
key: _scrollKey, return Duration(milliseconds: milliseconds);
scrollDirection: widget.scrollDirection,
controller: _scrollController,
physics: _isScrollable
? const AlwaysScrollableScrollPhysics()
: const NeverScrollableScrollPhysics(),
child: _text(),
);
} }
@override void _onUpdate(AutoScrollText oldWidget) {
FutureOr<void> afterFirstLayout(BuildContext context) { if (widget.text != oldWidget.text && _endlessText != null) {
_checkIsAutoScrollNeeded();
if (_isScrollable) {
setState(() { setState(() {
textToScroll = " $textToScroll "; _endlessText = null;
_originalTextWidth = null;
if (widget.scrollDirection == Axis.vertical) {
String newString = widget.text.split("").join("\n");
_text = newString;
} else {
_text = widget.text;
}
}); });
_startTimer(); _scrollController.jumpTo(_scrollController.position.minScrollExtent);
} }
} }
String _getSpaces(int number) {
String spaces = '';
for (int i = 0; i < number; i++) {
spaces += '\u{00A0}';
}
return spaces;
}
bool get _available => mounted && _scrollController.hasClients;
} }

4
lib/auto_scroll_text_impl.dart

@ -0,0 +1,4 @@
library auto_scroll_text;
export 'package:auto_scroll_text/auto_scroll_text.dart';
export 'package:auto_scroll_text/auto_scroll_text_mode.dart';

11
lib/auto_scroll_text_mode.dart

@ -0,0 +1,11 @@
// Created by Bomsamdi on 2022
// Copyright © 2022 Bomsamdi. All rights reserved.
/// Animation types for [AutoScrollText] widget.
/// [endless] - scrolls text in one direction endlessly.
/// [bouncing] - when text is scrolled to its end,
/// starts animation to opposite direction.
enum AutoScrollTextMode {
bouncing,
endless,
}

2
pubspec.yaml

@ -1,6 +1,6 @@
name: auto_scroll_text name: auto_scroll_text
description: AutoScrollText is package for users which need a single line text widget without overlaping or TextOverflow.elipsis for long texts. description: AutoScrollText is package for users which need a single line text widget without overlaping or TextOverflow.elipsis for long texts.
version: 0.0.1 version: 0.0.2
homepage: https://github.com/Bomsamdi/auto_scroll_text homepage: https://github.com/Bomsamdi/auto_scroll_text
environment: environment:

Loading…
Cancel
Save