Last active
May 7, 2024 15:35
-
-
Save hnvn/f1094fb4f6902078516cba78de9c868e 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 | |
) | |
], | |
); | |
} | |
} |
😍 🥰 Thanks for the Code its 😊 amazing 🤟 you do but for me How does it work.
I got a lot of error 🥵 in doing so i have update flutter to latest version 2.04.I tried to replace with @required and ? nullable type, but could not solved.
👹 error: The parameter 'title' can't have a value of 'null' because of its type, but the implicit default value is 'null'. (missing_default_value_for_parameter at lib/flutter_flip_animation_4.dart:25)
There is miss mached type '<'T'>' with '<'dynamic'>' type.
salut, je viens d'essayer le code mais ca ne marche pas ... avec beaucoup de rouges!!!!
@satoshi Nakamoto
Can you please make this code null safety?
import 'package:flutter/material.dart';
import 'dart:math' as math;
import 'dart:async';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flip Animation'),
);
}
}
class MyHomePage extends StatelessWidget {
final digits = [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9,
];
final String title;
MyHomePage({Key? key, required this.title}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: FlipPanel.builder(
itemBuilder: (context, index) => Container(
alignment: Alignment.center,
width: 96.0,
height: 128.0,
decoration: const BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.all(Radius.circular(4.0)),
),
child: Text(
'${digits[index]}',
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 80.0,
color: Colors.yellow),
),
),
itemsCount: digits.length,
period: const 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;
const 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]
const 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 = 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 = _loop! + 1;
}
_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 _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) {
if (_child2 != null) {
_child1 = _child2;
} else {
_child1 = _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 {
if (_child2 != null) {
_child1 = _child2;
} else {
_child1 = _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
awesome coding