Browse Source

update

Bouncing animation added
pull/1/head
Bartosz Wróbel 2 years ago
parent
commit
aa4b860e16
  1. 4
      CHANGELOG.md
  2. 95
      example/lib/main.dart
  3. 2
      example/pubspec.lock
  4. 18
      lib/after_layout_mixin.dart
  5. 489
      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.

95
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.", style: TextStyle(fontSize: 24),
textStyle: 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.", style: TextStyle(fontSize: 24),
textStyle: 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);
}

489
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(
/// 'Text with pause between animations',
/// mode: AutoScrollTextMode.bouncing,
/// pauseBetween: Duration(milliseconds: 300),
/// )
/// ```
final Duration? pauseBetween;
@override /// Sets one of two different types of scrolling behavior.
State<StatefulWidget> createState() { /// [AutoScrollTextMode.endless] - default, scrolls text in one direction endlessly.
return AutoScrollTextState(); /// [AutoScrollTextMode.bouncing] - when [text] string is scrolled to its end,
} /// starts animation to opposite direction.
} ///
/// ### Example:
class AutoScrollTextState extends State<AutoScrollText> ///
with SingleTickerProviderStateMixin, AfterLayoutMixin { /// ```dart
/// Final text for scrolling /// AutoScrollText(
String textToScroll = ""; /// '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");
/// Timer for animation _text = newString;
void _startTimer() { } else {
if (_scrollKey.currentContext != null) { _text = widget.text;
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);
});
} }
final WidgetsBinding binding = WidgetsBinding.instance;
binding.addPostFrameCallback(_initScroll);
} }
/// Check if autoscroll animation is needed @override
void _checkIsAutoScrollNeeded() { void didUpdateWidget(covariant AutoScrollText oldWidget) {
setState(() { _onUpdate(oldWidget);
_isScrollable = _scrollController.position.maxScrollExtent > 0; super.didUpdateWidget(oldWidget);
});
}
/// 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 @override
void dispose() { void dispose() {
_timer?.cancel();
super.dispose(); super.dispose();
// dispose timer when needed
timer?.cancel();
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SingleChildScrollView( assert(
key: _scrollKey, widget.pauseBetween == null ||
scrollDirection: widget.scrollDirection, widget.mode == AutoScrollTextMode.bouncing,
controller: _scrollController, 'pauseBetween is only available in AutoScrollTextMode.bouncing mode');
physics: _isScrollable assert(
? const AlwaysScrollableScrollPhysics() widget.intervalSpaces == null ||
: const NeverScrollableScrollPhysics(), widget.mode == AutoScrollTextMode.endless,
child: _text(), '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,
),
),
),
); );
} }
@override Future<void> _initScroll(_) async {
FutureOr<void> afterFirstLayout(BuildContext context) { await _delayBeforeStartAnimation();
_checkIsAutoScrollNeeded(); _timer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
if (_isScrollable) { if (!_available) {
timer.cancel();
return;
}
final int? maxReps = widget.numberOfReps;
if (maxReps != null && _counter >= maxReps) {
timer.cancel();
return;
}
if (!_running) _runAnimation();
});
}
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;
}
Future<void> _animateEndless() async {
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(() {
textToScroll = " $textToScroll "; _originalTextWidth =
position.maxScrollExtent + position.viewportDimension;
_endlessText = _text + _getSpaces(widget.intervalSpaces ?? 1) + _text;
}); });
_startTimer(); return;
}
final double endlessTextWidth =
position.maxScrollExtent + position.viewportDimension;
final double singleRoundExtent = endlessTextWidth - _originalTextWidth!;
final Duration duration = _getDuration(singleRoundExtent);
if (duration == Duration.zero) return;
if (!_available) return;
await _scrollController.animateTo(
singleRoundExtent,
duration: duration,
curve: widget.curve,
);
if (!_available) return;
_scrollController.jumpTo(position.minScrollExtent);
}
Future<void> _animateBouncing() async {
final double maxExtent = _scrollController.position.maxScrollExtent;
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!);
} }
} }
Future<void> _delayBeforeStartAnimation() async {
final Duration? delayBefore = widget.delayBefore;
if (delayBefore == null) return;
await Future<dynamic>.delayed(delayBefore);
}
Duration _getDuration(double extent) {
final int milliseconds =
(extent * 1000 / widget.velocity.pixelsPerSecond.dx).round();
return Duration(milliseconds: milliseconds);
}
void _onUpdate(AutoScrollText oldWidget) {
if (widget.text != oldWidget.text && _endlessText != null) {
setState(() {
_endlessText = null;
_originalTextWidth = null;
if (widget.scrollDirection == Axis.vertical) {
String newString = widget.text.split("").join("\n");
_text = newString;
} else {
_text = widget.text;
}
});
_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