-
-
Save mingsai/50fd69cf3412cdcefff785d8db9b5af6 to your computer and use it in GitHub Desktop.
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
import 'package:flutter/material.dart'; | |
import 'dart:math' as math; | |
import 'dart:async'; | |
void main() => runApp(new MyApp()); | |
class MyApp extends StatelessWidget { | |
// This widget is the root of your application. | |
@override | |
Widget build(BuildContext context) { | |
return new MaterialApp( | |
title: 'Flutter Demo', | |
theme: new ThemeData( | |
primarySwatch: Colors.blue, | |
), | |
home: new MyHomePage(title: 'Flip Animation'), | |
); | |
} | |
} | |
class MyHomePage extends StatelessWidget { | |
final digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9,]; | |
final String title; | |
MyHomePage({this.title}); | |
@override | |
Widget build(BuildContext context) { | |
return new Scaffold( | |
appBar: new AppBar( | |
title: new Text(title), | |
), | |
body: new Center( | |
child: FlipPanel.builder( | |
itemBuilder: (context, index) => Container( | |
alignment: Alignment.center, | |
width: 96.0, | |
height: 128.0, | |
decoration: BoxDecoration( | |
color: Colors.black, | |
borderRadius: BorderRadius.all(Radius.circular(4.0)), | |
), | |
child: Text( | |
'${digits[index]}', | |
style: TextStyle( | |
fontWeight: FontWeight.bold, | |
fontSize: 80.0, | |
color: Colors.yellow), | |
), | |
), | |
itemsCount: digits.length, | |
period: Duration(milliseconds: 1000), | |
loop: -1, | |
), | |
), | |
); | |
} | |
} | |
/// 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); | |
/// A widget for flip panel with built-in animation | |
/// Content of the panel is built from [IndexedItemBuilder] or [StreamItemBuilder] | |
/// | |
/// Note: the content size should be equal | |
enum FlipDirection { up, down } | |
class FlipPanel<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; | |
FlipPanel({ | |
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, | |
/// if not the animation becomes jerky/stuttery. | |
FlipPanel.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.up, | |
}) : 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] | |
FlipPanel.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.up, | |
}) : assert(itemStream != null), | |
indexedItemBuilder = null, | |
streamItemBuilder = itemBuilder, | |
itemsCount = 0, | |
period = null, | |
loop = 0, | |
startIndex = 0, | |
super(key: key); | |
@override | |
_FlipPanelState<T> createState() => _FlipPanelState<T>(); | |
} | |
class _FlipPanelState<T> extends State<FlipPanel> | |
with TickerProviderStateMixin { | |
AnimationController _controller; | |
Animation _animation; | |
int _currentIndex; | |
bool _isReversePhase; | |
bool _isStreamMode; | |
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. | |
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 = | |
new 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(); | |
} | |
}); | |
} 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 _buildPanel(); | |
} | |
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) { | |
_child1 = _child2 != null | |
? _child2 | |
: _isStreamMode | |
? widget.streamItemBuilder(context, _currentValue) | |
: widget.indexedItemBuilder( | |
context, _currentIndex % widget.itemsCount); | |
_child2 = null; | |
_upperChild1 = | |
_upperChild2 != null ? _upperChild2 : makeUpperClip(_child1); | |
_lowerChild1 = | |
_lowerChild2 != null ? _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 { | |
_child1 = _child2 != null | |
? _child2 | |
: _isStreamMode | |
? widget.streamItemBuilder(context, _currentValue) | |
: widget.indexedItemBuilder( | |
context, _currentIndex % widget.itemsCount); | |
_upperChild1 = | |
_upperChild2 != null ? _upperChild2 : makeUpperClip(_child1); | |
_lowerChild1 = | |
_lowerChild2 != null ? _lowerChild2 : makeLowerClip(_child1); | |
} | |
} | |
Widget _buildUpperFlipPanel() => 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 _buildLowerFlipPanel() => 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 _buildPanel() { | |
return _running | |
? Column( | |
mainAxisSize: MainAxisSize.min, | |
mainAxisAlignment: MainAxisAlignment.center, | |
crossAxisAlignment: CrossAxisAlignment.center, | |
children: [ | |
_buildUpperFlipPanel(), | |
Padding( | |
padding: EdgeInsets.only(top: widget.spacing), | |
), | |
_buildLowerFlipPanel(), | |
], | |
) | |
: _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