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
* 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';
// Created by Bomsamdi on 2022
@ -49,6 +49,12 @@ class _MyHomePageState extends State<MyHomePage> {
ElevatedButton(
onPressed: _openVertical,
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(),
));
}
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 {
@ -88,9 +106,8 @@ class _HorizontalExampleState extends State<HorizontalExample> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[
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.",
textStyle: TextStyle(fontSize: 24),
"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),
),
],
),
@ -119,10 +136,74 @@ class _VerticalExampleState extends State<VerticalExample> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: const <Widget>[
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.",
textStyle: TextStyle(fontSize: 24),
"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,
),
],
),
),
);
}
}
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,
mode: AutoScrollTextMode.bouncing,
),
],
),

2
example/pubspec.lock

@ -14,7 +14,7 @@ packages:
path: ".."
relative: true
source: path
version: "0.0.1"
version: "0.0.2"
boolean_selector:
dependency: transitive
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
// Copyright © 2022 Bomsamdi. All rights reserved.
import 'dart:async';
import 'package:auto_scroll_text/auto_scroll_text_mode.dart';
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]
/// AutoScrollText widget automatically scrolls provided [text]
///
/// ### 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 {
/// [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;
/// [TextStyle] of [Text]
final TextStyle? textStyle;
/// Provides [TextAlign] alignment if text string is not long enough to be scrolled.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Short text',
/// textAlign: TextAlign.right,
/// )
/// ```
final TextAlign? textAlign;
/// [SingleChildScrollView.scrollDirection] default value [Axis.horizontal]
final Axis scrollDirection;
/// Provides [TextDirection] - a direction in which text flows.
/// 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
final Curve curve;
/// Allows to apply custom [TextStyle] to [text].
///
/// `null` by default.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with TextStyle',
/// style: TextStyle(
/// color: Colors.white,
/// ),
/// )
/// ```
final TextStyle? style;
/// Distance per tick
final double moveDistance;
/// Limits number of scroll animation rounds.
///
/// Default is [infinity].
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Limit scroll rounds to 10',
/// numberOfReps: 10,
/// )
/// ```
final int? numberOfReps;
/// Reset timer
final int timerRest;
/// Delay before first animation round.
///
/// Default is [Duration.zero].
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Start animation after 1 sec delay',
/// delayBefore: Duration(seconds: 1),
/// )
/// ```
final Duration? delayBefore;
const AutoScrollText({
super.key,
required this.text,
this.textStyle,
this.scrollDirection = Axis.horizontal,
this.curve = Curves.linear,
this.moveDistance = 3.0,
this.timerRest = 100,
});
/// Determines pause interval between animation rounds.
///
/// Only allowed if [mode] is set to [AutoScrollTextMode.bouncing].
///
/// Default is [Duration.zero].
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with pause between animations',
/// mode: AutoScrollTextMode.bouncing,
/// pauseBetween: Duration(milliseconds: 300),
/// )
/// ```
final Duration? pauseBetween;
@override
State<StatefulWidget> createState() {
return AutoScrollTextState();
}
}
class AutoScrollTextState extends State<AutoScrollText>
with SingleTickerProviderStateMixin, AfterLayoutMixin {
/// Final text for scrolling
String textToScroll = "";
/// Sets one of two different types of scrolling behavior.
/// [AutoScrollTextMode.endless] - default, scrolls text in one direction endlessly.
/// [AutoScrollTextMode.bouncing] - when [text] string is scrolled to its end,
/// starts animation to opposite direction.
///
/// ### Example:
///
/// ```dart
/// AutoScrollText(
/// 'Animate text string back and forth',
/// mode: AutoScrollTextMode.bouncing,
/// )
/// ```
final AutoScrollTextMode mode;
/// SingleChildScrollView controller
final ScrollController _scrollController = ScrollController();
/// Allows to customize animation speed.
///
/// 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
double position = 0.0;
/// Allows users to select provided [text], copy it to clipboard etc.
///
/// 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
Timer? timer;
/// Adds blank spaces between two nearby text sentences
/// 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
bool _isScrollable = false;
/// Allows users to define scrollDirection of [AutoScrollText]
///
/// Default is [Axis.horizontal].
///
/// Example:
///
/// ```dart
/// AutoScrollText(
/// 'Text with vertical scroll direction',
/// scrollDirection: Axis.vertical,
/// )
/// ```
final Axis scrollDirection;
/// SingleChildScrollView key
final GlobalKey _scrollKey = GlobalKey();
/// [Curve] of scroll animation
/// 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
TextStyle get defaultTextStyle => const TextStyle();
@override
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
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);
});
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);
}
/// 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 didUpdateWidget(covariant AutoScrollText oldWidget) {
_onUpdate(oldWidget);
super.didUpdateWidget(oldWidget);
}
@override
void dispose() {
_timer?.cancel();
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(),
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,
),
),
),
);
}
@override
FutureOr<void> afterFirstLayout(BuildContext context) {
_checkIsAutoScrollNeeded();
if (_isScrollable) {
Future<void> _initScroll(_) async {
await _delayBeforeStartAnimation();
_timer = Timer.periodic(const Duration(milliseconds: 50), (timer) {
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(() {
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
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
environment:

Loading…
Cancel
Save