Brother Cat said
This article is about how to add motion characteristics, sports balls, gravity, Bezier curves, polygons, and irregular curves to your animation. If you are looking for this information, you should digest this source code well. This is the basis of animation, the front end is to be cool, let's get started.
The best experience is to read the original text (link below).
Old iron remember to forward, Brother Mao will present more Flutter good articles~~~~
WeChat group ducafecat
Station b https://space.bilibili.com/404904528
original
https://preyea-regmi.medium.com/implementing-motion-design-with-flutter-126d06b080ab
Code
https://github.com/PreyeaRegmi/FlutterMotionDesignSamples
reference
- https://pub.flutter-io.cn/packages/get#reactive-state-manager
- https://dart.dev/guides/language/extension-methods
text
Realizing motion design most of the time is a somewhat cumbersome mobile application. This article explains how to implement motion design through Flutter from a more practical perspective. We will take a simple sport design from the dribble as a reference and start building it step by step. All copyrights are reserved to the respective authors, and the complete source code of the implementation can be found on github.
https://github.com/PreyeaRegmi/FlutterMotionDesignSamples
Now we will focus on the login/register interaction. So, just like other interaction design, we will try to break it down into multiple scenarios so that we can have a clear overall concept and link these scenarios together.
Scenario 1: Initial status screen
In this scene, we have a bouncing image and text at the bottom, a curved white background, a brand title surrounding the center of the image and an amoeba-shaped background. Drag the content at the bottom until a certain distance is covered, revealing the animation playback and scene transition to the next scene.
Show animation (intermediate scene)
In this intermediate scene, the curve background height is animated. In addition, in this animation, the cubic Bezier curve of the control points is also translated and restored to provide an acceleration effect. The icons on the side and the amoeba background are also translated in the vertical direction in response to the animated display.
Scene 2: Display the animation status screen later
When the display animation is complete, the brand title is replaced by a circular icon, a label indicator flies over from the left side of the screen, and the corresponding label is loaded.
Now we have an overview of the relevant scenarios involved in the design. In the next step, we try to map these ideas into implementation details. So let's get started.
We will use the stack as the top-level container to host all our scenes, and according to the current scene state, we will add their respective widgets to the stack and animate their geometry.
@override
Widget build(BuildContext context) {
List<Widget> stackChildren = [];
switch (currentScreenState) {
case CURRENT_SCREEN_STATE.INIT_STATE:
stackChildren.addAll(_getBgWidgets());
stackChildren.addAll(_getDefaultWidgets());
stackChildren.addAll(_getInitScreenWidgets());
stackChildren.add(_getBrandTitle());
break;
case CURRENT_SCREEN_STATE.REVEALING_ANIMATING_STATE:
stackChildren.addAll(_getBgWidgets());
stackChildren.addAll(_getDefaultWidgets());
stackChildren.add(_getBrandTitle());
break;
case CURRENT_SCREEN_STATE.POST_REVEAL_STATE:
stackChildren.addAll(_getBgWidgets());
stackChildren.addAll(_getDefaultWidgets());
stackChildren.insert(stackChildren.length - 1, _getCurvedPageSwitcher());
stackChildren.addAll(_getPostRevealAnimationStateWidgets());
stackChildren.add(buildPages());
break;
}
return Stack(children: stackChildren);
}
For scenario 1, all corresponding widgets are positioned and added to the stack. The bounce effect of the "swipe up to start" widget at the bottom also starts immediately.
//Animation Controller for setting bounce animation for "Swipe up" text widget
_swipeUpBounceAnimationController =
AnimationController(duration: Duration(milliseconds: 800), vsync: this)
..repeat(reverse: true);
//Animation for actual bounce effect
_swipeUpBounceAnimation = Tween<double>(begin: 0, end: -20).animate(
CurvedAnimation(
parent: _swipeUpBounceAnimationController,
curve: Curves.easeOutBack))
..addListener(() {
setState(() {
_swipeUpDy = _swipeUpBounceAnimation.value;
});
});
//We want to loop bounce effect until user intercepts with drag touch event.
_swipeUpBounceAnimationController.repeat(reverse: true);
//Animated value used by corresponding "Swipe up to Start" Widget in _getInitScreenWidgets() method
Positioned(
right: 0,
left: 0,
bottom: widget.height * .05,
child: Transform.translate(
offset: Offset(0, _swipeUpDy),
child: IgnorePointer(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.upload_rounded,
color: Colors.deepPurple,
size: 52,
),
Text(
"Swipe up to start",
style: TextStyle(color: Colors.grey.shade800),
)
]),
))),
In order to implement the dragging behavior of this widget, a scrollable widget is also placed on the top, covering the lower half of the screen. "Swipe up to start" will also be translated according to the drag distance. Once the threshold (70% of the height of the scrollable component) is crossed, the display animation will be played.
//A simple container with a SingleChildScrollView. The trick is to set the child of SingleChildScrollView height
//exceed the height of parent scroll widget so it can be scrolled. The BouncingScrollPhysics helps the scroll retain its
//original position if it doesn't cross the threshold to play reveal animation.
//This widget is added by _getInitScreenWidgets() method
Positioned(
right: 0,
left: 0,
bottom: 0,
child: Container(
height: widget.height * .5,
child: SingleChildScrollView(
controller: _scrollController,
physics: BouncingScrollPhysics(),
child: Container(
height: widget.height * .5 + .1,
// color:Colors.yellow,
),
),
),
),
//Intercepts the bounce animation and start dragg animation
void _handleSwipe() {
_swipeUpBounceAnimationController.stop(canceled: true);
double dy = _scrollController.position.pixels;
double scrollRatio =
math.min(1.0, _scrollController.position.pixels / _swipeDistance);
//If user scroll 70% of the scrolling region we proceed towards reveal animation
if (scrollRatio > .7)
_playRevealAnimation();
else
setState(() {
_swipeUpDy = dy * -1;
});
}
In the display animation, use CustomPainter to draw the curve background and the amoeba background. During the animation process, the height of the curve background and the intermediate control points are interpolated to 75% of the screen height. Similarly, the amoeba drawn with Bezier curves also translates vertically.
//Update scene state to "reveal" and start corresponding animation
//This method is called when drag excced our defined threshold
void _playRevealAnimation() {
setState(() {
currentScreenState = CURRENT_SCREEN_STATE.REVEALING_ANIMATING_STATE;
_revealAnimationController.forward();
_amoebaAnimationController.forward();
});
}
//Animation controller for expanding the curve animation
_revealAnimationController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this)
..addStatusListener((status) {
if (status == AnimationStatus.completed)
setState(() {
currentScreenState = CURRENT_SCREEN_STATE.POST_REVEAL_STATE;
_postRevealAnimationController.forward();
});
});
//Animation to translate the brand label
_titleBaseLinePosTranslateAnim = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0,
widget.height -
_initialCurveHeight -
widget.height * .2 -
arcHeight,
0,
_initialCurveHeight),
end: RelativeRect.fromLTRB(
0,
widget.height - _finalCurveHeight - 20 - arcHeight,
0,
_finalCurveHeight))
.animate(CurvedAnimation(
parent: _revealAnimationController, curve: Curves.easeOutBack));
//Animation to translate side icons
_sideIconsTranslateAnim = RelativeRectTween(
begin: RelativeRect.fromLTRB(
0,
widget.height -
_initialCurveHeight -
widget.height * .25 -
arcHeight,
0,
_initialCurveHeight),
end: RelativeRect.fromLTRB(
0,
widget.height -
_finalCurveHeight -
widget.height * .25 -
arcHeight,
0,
_finalCurveHeight))
.animate(CurvedAnimation(
parent: _revealAnimationController, curve: Curves.easeInOutBack));
//Tween for animating height of the curve during reveal process
_swipeArcAnimation =
Tween<double>(begin: _initialCurveHeight, end: _finalCurveHeight)
.animate(CurvedAnimation(
parent: _revealAnimationController, curve: Curves.easeInCubic));
//Animation for the mid control point of cubic bezier curve to show acceleration effect in response to user drag.
_swipeArchHeightAnimation = TweenSequence<double>(
<TweenSequenceItem<double>>[
TweenSequenceItem<double>(
tween: Tween<double>(begin: 0, end: 200),
weight: 50.0,
),
TweenSequenceItem<double>(
tween: Tween<double>(begin: 200, end: 0),
weight: 50.0,
),
],
).animate(CurvedAnimation(
parent: _revealAnimationController, curve: Curves.easeInCubic));
//Animation Controller for amoeba background
_amoebaAnimationController =
AnimationController(duration: Duration(milliseconds: 350), vsync: this);
_amoebaOffsetAnimation =
Tween<Offset>(begin: Offset(0, 0), end: Offset(-20, -70)).animate(
CurvedAnimation(
parent: _amoebaAnimationController,
curve: Curves.easeInOutBack));
After the animation is complete, Scene 2 is set. In this scenario, the brand title is replaced by an icon, and the label indicator is displayed from the left side of the screen.
//Animation controller for showing animation after reveal
_postRevealAnimationController =
AnimationController(duration: Duration(milliseconds: 600), vsync: this);
//Scale animation for showing center logo after reveal is completed
_centerIconScale = Tween<double>(begin: 0, end: .5).animate(CurvedAnimation(
parent: _postRevealAnimationController,
curve: Curves.fastOutSlowIn,
));
//_centerIconScale animation used by FAB in the middle
Positioned.fromRelativeRect(
rect: _titleBaseLinePosTranslateAnim.value.shift(Offset(0, 18)),
child: ScaleTransition(
scale: _centerIconScale,
child: FloatingActionButton(
backgroundColor: Colors.white,
elevation: 5,
onPressed: null,
child: Icon(Icons.monetization_on_outlined,
size: 100,
color: isLeftTabSelected
? Colors.deepPurple
: Colors.pinkAccent))),
),
//Tab selection is done by "CurvePageSwitchIndicator" widget
Positioned(
top: 0,
bottom: _titleBaseLinePosTranslateAnim.value.bottom,
left: 0,
right: 0,
child: CurvePageSwitchIndicator(widget.height, widget.width, arcHeight, 3,
true, _onLeftTabSelectd, _onRightTabSelectd),
);
//The build method of CurvePageSwitchIndicator consisting of "CurvePageSwitcher" CustomPainter to paint tab selection arc
//and Gesture detectors stacked on top to intercept left and right tap event.
///When the reveal scene is completed, left tab is selected and the tab selection fly
//towards from the left side of the screen
@override
Widget build(BuildContext context) {
return Stack(children: [
Transform(
transform: Matrix4.identity()
..setEntry(0, 3, translationDxAnim.value)
..setEntry(1, 3, translationDyAnim.value)
..rotateZ(rotationAnim.value * 3.14 / 180),
alignment: Alignment.bottomLeft,
child: Container(
height: double.infinity,
width: double.infinity,
child: CustomPaint(
painter: CurvePageSwitcher(
widget.arcHeight,
widget.arcBottomOffset,
showLeftAsFirstPage,
pageTabAnimationController!),
),
)),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: Stack(children: [
Positioned(
left: 0,
right: 20,
bottom: 0,
top: 90,
child: Transform.rotate(
angle: -13 * 3.14 / 180,
child: Align(
alignment: Alignment.center,
child: Text(
"Login",
style: TextStyle(
color: showLeftAsFirstPage
? Colors.white
: Colors.white60,
fontSize: 22,
fontWeight: FontWeight.w800),
)))),
GestureDetector(onTap: _handleLeftTab,
)
])),
Expanded(
child: Stack(children: [
Positioned(
left: 20,
right: 0,
bottom: 0,
top: 90,
child: Transform.rotate(
angle: 13 * 3.14 / 180,
child: Align(
alignment: Alignment.center,
child: Text("Signup",
style: TextStyle(
color: !showLeftAsFirstPage
? Colors.white
: Colors.white60,
fontSize: 22,
fontWeight: FontWeight.w800))))),
GestureDetector(onTap: _handleRightTab,
)
])),
],
),
]);
}
The tab indicator is also drawn using Bezier curves and positioned on the surface background of scene 1, but in a separate CustomPainter. In order to achieve the tab stop selection effect, use the clipping path when drawing the tab stop selection curve.
//The paint method of "CurvePageSwitcher" to draw tab selection arc
void _drawSwipeAbleArc(Canvas canvas, Size size) {
Path path = Path();
path.moveTo(-2, size.height - archBottomOffset);
path.cubicTo(
-2,
size.height - archBottomOffset,
size.width / 2,
size.height - arcHeight - archBottomOffset,
size.width + 2,
size.height - archBottomOffset);
path.moveTo(size.width + 2, size.height - archBottomOffset);
path.close();
double left, right;
if (showLeftAsFirstPage) {
left = size.width / 2 - size.width / 2 * animation.value;
right = size.width / 2;
swipeArcPaint.color = Colors.green;
} else {
left = size.width / 2;
right = size.width * animation.value;
swipeArcPaint.color = Colors.deepPurple;
}
canvas.clipRect(Rect.fromLTRB(left, 0, right, size.height));
canvas.drawPath(path, swipeArcPaint);
}
In addition, the two containers are placed on top of each other with their respective label colors. According to the selected tab, keep the corresponding container and translate another container to the opposite end of the x-axis, thereby discarding the other container.
///The background for selected tab. On the basis of tab selected, the foreground container is translated away,
///revealing the underlying background container. If the screen state is just set to reveal, then in the
///initial state no foreground container is added which is signified by _tabSelectionAnimation set to null.
///_tabSelectionAnimation is only set when either of the tab is pressed.
List<Widget> _getBgWidgets() {
List<Widget> widgets = [];
Color foreGroundColor;
Color backgroundColor;
if (isLeftTabSelected) {
foreGroundColor = Colors.deepPurple;
backgroundColor = Colors.pink;
} else {
foreGroundColor = Colors.pink;
backgroundColor = Colors.deepPurple;
}
widgets.add(Positioned.fill(child: Container(color: foreGroundColor)));
if (_tabSelectionAnimation != null)
widgets.add(PositionedTransition(
rect: _tabSelectionAnimation!,
child: Container(
decoration: BoxDecoration(
color: backgroundColor
),
)));
widgets.add(Container(
height: double.infinity,
width: double.infinity,
child: CustomPaint(
painter: AmoebaBg(_amoebaOffsetAnimation),
),
));
return widgets;
}
Because I couldn't get the exact pictures and resources, I used the closest one I could find on the Internet.
So in general, the results we get are as follows.
© Cat brother
Past
Open source
GetX Quick Start
https://github.com/ducafecat/getx_quick_start
News client
https://github.com/ducafecat/flutter_learn_news
strapi manual translation
WeChat discussion group ducafecat
Series collection
Translation
https://ducafecat.tech/categories/%E8%AF%91%E6%96%87/
Open source project
https://ducafecat.tech/categories/%E5%BC%80%E6%BA%90/
Dart programming language basics
https://space.bilibili.com/404904528/channel/detail?cid=111585
Getting started with Flutter zero foundation
https://space.bilibili.com/404904528/channel/detail?cid=123470
Flutter actual combat from scratch news client
https://space.bilibili.com/404904528/channel/detail?cid=106755
Flutter component development
https://space.bilibili.com/404904528/channel/detail?cid=144262
Flutter Bloc
https://space.bilibili.com/404904528/channel/detail?cid=177519
Flutter Getx4
https://space.bilibili.com/404904528/channel/detail?cid=177514
Docker Yapi
https://space.bilibili.com/404904528/channel/detail?cid=130578
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。