Created
August 21, 2022 05:49
-
-
Save massimilianochiodi/5cfc62681320d43040d1752b6cb7a311 to your computer and use it in GitHub Desktop.
Per creare pannelli del tipo orario dei treni
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
library fliptextpanel_flutter; | |
import 'package:flutter/material.dart'; | |
import 'dart:async'; | |
import 'dart:math' as math; | |
/// | |
/// Signature for a function that creates a widget for a given index, e.g., in a | |
/// list. | |
/// | |
typedef Widget IndexedItemBuilder(BuildContext, int); | |
/// | |
/// Signature for a function that creates a widget for a value emitted from a [Stream] | |
/// | |
typedef Widget StreamItemBuilder<T>(BuildContext, T); | |
/// | |
/// An enum defines all supported directions of [FlipTextPanel] | |
/// | |
enum FlipDirection { up, down } | |
/// | |
/// A [Widget] provides 3D flipp animation on a panel. The content of each panel | |
/// is built from [IndexedItemBuilder] or [StreamItemBuilder] | |
/// | |
/// Note: The size of every panel should be the same and the panel should be | |
/// a static [Widget] (which is an instance of [StatelessWidget]) | |
/// | |
class FlipTextPanel<T> extends StatefulWidget { | |
final IndexedItemBuilder? indexedItemBuilder; | |
final StreamItemBuilder<T>? streamItemBuilder; | |
final Stream<T>? itemStream; | |
final int? itemsCount; | |
final Duration? period; | |
final Duration? duration; | |
final int? loop; | |
final int? startIndex; | |
final T? initValue; | |
final double? spacing; | |
final FlipDirection? direction; | |
FlipTextPanel({ | |
Key? key, | |
this.indexedItemBuilder, | |
this.streamItemBuilder, | |
this.itemStream, | |
this.itemsCount, | |
this.period, | |
this.duration, | |
this.loop, | |
this.startIndex, | |
this.initValue, | |
this.spacing, | |
this.direction, | |
}) : super(key: key); | |
/// | |
/// Create a flip panel from iterable source | |
/// | |
/// * [itemBuilder] is called periodically in each time of [period] | |
/// * The animation is looped in [loop] times before finished. | |
/// Setting [loop] to -1 makes flip animation run forever. | |
/// * The [period] should be two times greater than [duration] of flip animation, | |
/// otherwise the animation becomes jerky/stuttery. | |
/// | |
FlipTextPanel.builder({ | |
Key? key, | |
required IndexedItemBuilder itemBuilder, | |
required this.itemsCount, | |
required this.period, | |
this.duration = const Duration(milliseconds: 500), | |
this.loop = 1, | |
this.startIndex = 0, | |
this.spacing = 0.5, | |
this.direction = FlipDirection.down, | |
}) : assert(itemBuilder != null), | |
assert(itemsCount != null), | |
assert(startIndex! < itemsCount!), | |
assert(period == null || | |
period.inMilliseconds >= 2 * duration!.inMilliseconds), | |
indexedItemBuilder = itemBuilder, | |
streamItemBuilder = null, | |
itemStream = null, | |
initValue = null, | |
super(key: key); | |
/// | |
/// Create a flip panel from stream source | |
/// | |
/// * [itemBuilder] is called whenever a new value is emitted from [itemStream] | |
/// | |
FlipTextPanel.stream({ | |
Key? key, | |
required this.itemStream, | |
required StreamItemBuilder<T> itemBuilder, | |
this.initValue, | |
this.duration = const Duration(milliseconds: 500), | |
this.spacing = 0.5, | |
this.direction = FlipDirection.down, | |
}) : assert(itemStream != null), | |
indexedItemBuilder = null, | |
streamItemBuilder = itemBuilder, | |
itemsCount = 0, | |
period = null, | |
loop = 0, | |
startIndex = 0, | |
super(key: key); | |
@override | |
_FlipTextPanelState<T> createState() => _FlipTextPanelState<T>(); | |
} | |
class _FlipTextPanelState<T> extends State<FlipTextPanel> | |
with SingleTickerProviderStateMixin { | |
late AnimationController _controller; | |
late Animation _animation; | |
late int? _currentIndex; | |
late bool _isReversePhase; | |
late bool _isStreamMode; | |
late bool _running; | |
final _perspective = 0.003; | |
final _zeroAngle = | |
0.0001; // There's something wrong in the perspective transform, I use a very small value instead of zero to temporarily get it around. | |
late int _loop; | |
T? _currentValue, _nextValue; | |
Timer? _timer; | |
StreamSubscription<T>? _subscription; | |
Widget? _child1, _child2; | |
Widget? _upperChild1, _upperChild2; | |
Widget? _lowerChild1, _lowerChild2; | |
@override | |
void initState() { | |
super.initState(); | |
_currentIndex = widget.startIndex; | |
_isStreamMode = widget.itemStream != null; | |
_isReversePhase = false; | |
_running = false; | |
_loop = 0; | |
_controller = AnimationController(duration: widget.duration, vsync: this) | |
..addStatusListener((status) { | |
if (status == AnimationStatus.completed) { | |
_isReversePhase = true; | |
_controller.reverse(); | |
} | |
if (status == AnimationStatus.dismissed) { | |
_currentValue = _nextValue; | |
_running = false; | |
} | |
}) | |
..addListener(() { | |
setState(() { | |
_running = true; | |
}); | |
}); | |
_animation = | |
Tween(begin: _zeroAngle, end: math.pi / 2).animate(_controller); | |
if (widget.period != null) { | |
_timer = Timer.periodic(widget.period!, (_) { | |
if (widget.loop! < 0 || _loop < widget.loop!) { | |
if (_currentIndex! + 1 == widget.itemsCount! - 2) { | |
_loop++; | |
} | |
_currentIndex = (_currentIndex! + 1) % widget.itemsCount!; | |
_child1 = null; | |
_isReversePhase = false; | |
_controller.forward(); | |
} else { | |
_timer!.cancel(); | |
_currentIndex = (_currentIndex! + 1) % widget.itemsCount!; | |
setState(() { | |
_running = false; | |
}); | |
} | |
}); | |
} | |
if (_isStreamMode) { | |
_currentValue = widget.initValue; | |
_subscription = widget.itemStream!.distinct().listen((value) { | |
if (_currentValue == null) { | |
_currentValue = value; | |
} else if (value != _currentValue) { | |
_nextValue = value; | |
_child1 = null; | |
_isReversePhase = false; | |
_controller.forward(); | |
} | |
}) as StreamSubscription<T>; | |
} else if (widget.loop! < 0 || _loop < widget.loop!) { | |
_controller.forward(); | |
} | |
} | |
@override | |
void dispose() { | |
_controller.dispose(); | |
if (_subscription != null) _subscription!.cancel(); | |
if (_timer != null) _timer!.cancel(); | |
super.dispose(); | |
} | |
@override | |
Widget build(BuildContext context) { | |
_buildChildWidgetsIfNeed(context); | |
return _buildTextPanel(); | |
} | |
void _buildChildWidgetsIfNeed(BuildContext context) { | |
Widget makeUpperClip(Widget widget) { | |
return ClipRect( | |
child: Align( | |
alignment: Alignment.topCenter, | |
heightFactor: 0.5, | |
child: widget, | |
), | |
); | |
} | |
Widget makeLowerClip(Widget widget) { | |
return ClipRect( | |
child: Align( | |
alignment: Alignment.bottomCenter, | |
heightFactor: 0.5, | |
child: widget, | |
), | |
); | |
} | |
if (_running) { | |
if (_child1 == null) { | |
if (_child2 != null) { | |
_child1 = _child2; | |
} else { | |
_child1 = _isStreamMode | |
? widget.streamItemBuilder!(context, _currentValue) | |
: widget.indexedItemBuilder!( | |
context, _currentIndex! % widget.itemsCount!); | |
} | |
_child2 = null; | |
_upperChild1 = _upperChild2 ?? makeUpperClip(_child1!); | |
_lowerChild1 = _lowerChild2 ?? makeLowerClip(_child1!); | |
} | |
if (_child2 == null) { | |
_child2 = _isStreamMode | |
? widget.streamItemBuilder!(context, _nextValue) | |
: widget.indexedItemBuilder!( | |
context, (_currentIndex! + 1) % widget.itemsCount!); | |
_upperChild2 = makeUpperClip(_child2!); | |
_lowerChild2 = makeLowerClip(_child2!); | |
} | |
} else { | |
if (_child2 != null) { | |
_child1 = _child2; | |
} else { | |
_child1 = _isStreamMode | |
? widget.streamItemBuilder!(context, _currentValue) | |
: widget.indexedItemBuilder!( | |
context, _currentIndex! % widget.itemsCount!); | |
} | |
_upperChild1 = _upperChild2 ?? makeUpperClip(_child1!); | |
_lowerChild1 = _lowerChild2 ?? makeLowerClip(_child1!); | |
} | |
} | |
Widget _buildUpperFlipTextPanel() => widget.direction == FlipDirection.up | |
? Stack( | |
children: [ | |
Transform( | |
alignment: Alignment.bottomCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _upperChild1), | |
Transform( | |
alignment: Alignment.bottomCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_isReversePhase ? _animation.value : math.pi / 2), | |
child: _upperChild2, | |
), | |
], | |
) | |
: Stack( | |
children: [ | |
Transform( | |
alignment: Alignment.bottomCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _upperChild2), | |
Transform( | |
alignment: Alignment.bottomCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_isReversePhase ? math.pi / 2 : _animation.value), | |
child: _upperChild1, | |
), | |
], | |
); | |
Widget _buildLowerFlipTextPanel() => widget.direction == FlipDirection.up | |
? Stack( | |
children: [ | |
Transform( | |
alignment: Alignment.topCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _lowerChild2), | |
Transform( | |
alignment: Alignment.topCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_isReversePhase ? math.pi / 2 : -_animation.value), | |
child: _lowerChild1, | |
) | |
], | |
) | |
: Stack( | |
children: [ | |
Transform( | |
alignment: Alignment.topCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _lowerChild1), | |
Transform( | |
alignment: Alignment.topCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_isReversePhase ? -_animation.value : math.pi / 2), | |
child: _lowerChild2, | |
) | |
], | |
); | |
Widget _buildTextPanel() { | |
return _running | |
? Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
_buildUpperFlipTextPanel(), | |
Padding( | |
padding: EdgeInsets.only(top: widget.spacing!), | |
), | |
_buildLowerFlipTextPanel(), | |
], | |
) | |
: _isStreamMode && _currentValue == null | |
? Container() | |
: Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
Transform( | |
alignment: Alignment.bottomCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _upperChild1), | |
Padding( | |
padding: EdgeInsets.only(top: widget.spacing!), | |
), | |
Transform( | |
alignment: Alignment.topCenter, | |
transform: Matrix4.identity() | |
..setEntry(3, 2, _perspective) | |
..rotateX(_zeroAngle), | |
child: _lowerChild1) | |
], | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment