This article has participated in the “Digitalstar Project” and won a creative gift package to challenge the creative incentive money.
Foreword: today is 1024, wish everybody brothers festival happiness first, never baldness, never Bug😜. Get down to business: A few days ago, I found a cool animation of a Flutter project, a habit building App. After watching it, I couldn’t put it down. It has many functions, so I immediately found an open source author and applied for his writing permission. Then began the analysis of the project (thumbs up!! Believe me, after reading this article you will have a harvest 👍)
I have partially modified the code of his project, and the modified source code is at the end of the article
Open source project address: github.com/designDo/fl…
First, the effect picture:
There are a lot of functions we download the source code (feel good to open source author point a star oh, people are not easy!)
Key points of this paper:
- Login interface animation, input box processing and top pop-up box
- Bottom navigation bar animation processing
- Home animation and annular progress bar processing
- Adapt dark mode (analyze the author’s global state management)
1. Animation, input box processing, and top pop-up box of the login interface
-
Animation processing
There are altogether three animations: zooming animation of input box, panning animation of verification code button and zooming animation of login interface.
When we use animation, we need to define a Controller to control and manage the animation
AnimationController _animationController; Copy the code
When using the animation of our State is, of course, need to be mixed with SingleTickerProviderStateMixin of this class
In the renderings, it is not difficult to see that the animation is directly time-spaced, so we only use a Controller to control the entire interface, making it gradually displayed from top to bottom.
For scaling animations, we need to use ScaleTransition to flutter. One of the most important things about flutter is:
Animation<double> scale // Control the zoom of the widget Copy the code
Let’s see how it works in detail:
ScaleTransition( // Control scaling from 0 to 1 scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation( // The Controller that controls the animation parent: _animationController, // 0,0.3 is the animation run time //curve is used to describe the motion of a curve curve: Interval(0.0.3, curve: Curves.fastOutSlowIn), )), child:... ) Copy the code
The same is true for other animations, but the difference is the runtime of the animation and the animation
Key differences:
Verification code input box:
curve: Interval(0.3.0.6, curve: Curves.fastOutSlowIn), Copy the code
Get the verification code button:
The main difference here is that position is used to deal with the initial absolute position
SlideTransition( // You can change begin: Offset(2, 0) so that you can experience its function clearly position: Tween<Offset>(begin: Offset(2.0), end: Offset.zero) .animate(CurvedAnimation( parent: _animationController, curve: Interval(0.6.0.8, curve: Curves.fastOutSlowIn))),child:...) Copy the code
Login button:
ScaleTransition( scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation( parent: _animationController, curve: Interval(0.8.1, curve: Curves.fastOutSlowIn), )),child:...) Copy the code
The realization of animation is like this, is not very simple ~
-
Mobile phone number input box limit processing
I think this style is very cool, mainly in peacetime is not very common, on the analysis
Here we’ve wrapped a CustomEditField input box to make it easier to animate
Animation definition
///The text content
String _value = ' ';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;
Copy the code
And the component need to be mixed with mixins TickerProviderStateMixin and AutomaticKeepAliveClientMixin, Because the AnimationController needs to call the createTicker method in the TickerProvider.
with TickerProviderStateMixin, AutomaticKeepAliveClientMixin
Copy the code
Initialization:
@override
void initState() {
_value = widget.initValue;
// Initialize the controller
editingController = TextEditingController(text: widget.initValue);
// Initialize the controller and animation of the bounding box
numAnimationController =
AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
numAnimationController.forward(from: 0.3);
}
super.initState();
}
Copy the code
When destruction:
@override
void dispose() {
editingController.dispose();
numAnimationController.dispose();
super.dispose();
}
Copy the code
UI: Use Stack to wrap an input box and a limiter box
Stack(children:[TextField(), // Restrict the animation of the box, so there is a layer on the outside of ScaleTransition ScaleTransition(child:Padding())])Copy the code
When using this encapsulated component, we mainly deal with NumDecorations
The color here is handled globally and the code needs to be modified to copy directly
numDecoration: BoxDecoration(
shape: BoxShape.rectangle,
color: AppTheme.appTheme.cardBackgroundColor(),
borderRadius: BorderRadius.all(Radius.circular(15)),
boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
.themeText(fontWeight: FontWeight.bold, fontSize: 15),
Copy the code
- Top pop-up box handling
Use flash, a highly customizable, powerful, and easy to use warning box
In order to reuse the code, encapsulation is carried out here
class FlashHelper {
static Future<T> toast<T>(BuildContext context, String message) async {
return showFlash<T>(
context: context,
// Display two seconds
duration: Duration(milliseconds: 2000),
builder: (context, controller) {
/ / the pop-up box
return Flash.bar(
margin: EdgeInsets.only(left: 24, right: 24),
position: FlashPosition.top,
brightness: AppTheme.appTheme.isDark()
? Brightness.light
: Brightness.dark,
backgroundColor: Colors.transparent,
controller: controller,
child: Container(
alignment: Alignment.center,
padding: EdgeInsets.all(16),
height: 80,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: AppTheme.appTheme.containerGradient(),
boxShadow: AppTheme.appTheme.coloredBoxShadow()),
child: Text(
// Display text
message,
style: AppTheme.appTheme.headline1(
textColor: Colors.white,
fontWeight: FontWeight.normal,
fontSize: 16),),)); }); }}Copy the code
2. Animation processing of the bottom navigation bar
Here is really amazing to me, Icon are all drawn, the author is really imaginative, thumbs up!
-
Icon in the drawing
House:
static final home = FluidFillIconData([
/ / houseui.Path().. addRRect(RRect.fromLTRBXY(- 10.2 -.10.10.2.2)), ui.Path() .. moveTo(- 14.2 -)
..lineTo(14.2 -)
..lineTo(0.- 16)
..close(),
]);
Copy the code
Four squares:
static final window = FluidFillIconData([
/ / squareui.Path().. addRRect(RRect.fromLTRBXY(- 12.- 12.2 -.2 -.2.2)), ui.Path().. addRRect(RRect.fromLTRBXY(2.- 12.12.2 -.2.2)), ui.Path().. addRRect(RRect.fromLTRBXY(- 12.2.2 -.12.2.2)), ui.Path().. addRRect(RRect.fromLTRBXY(2.2.12.12.2.2)));Copy the code
Trend diagram:
static final progress = FluidFillIconData([
/ / trend chartui.Path() .. moveTo(- 10.- 10)
..lineTo(- 10.8)
..arcTo(Rect.fromCircle(center: Offset(- 8 -.8), radius: 2), - 1 * math.pi,
0.5 * math.pi, true)
..moveTo(- 8 -.10)
..lineTo(10.10), ui.Path() .. moveTo(6.5.2.5)
..lineTo(0.- 5)
..lineTo(4.0)
..lineTo(10.9 -),]);Copy the code
My:
static final user = FluidFillIconData([
/ / to meui.Path().. arcTo(Rect.fromLTRB(- 5.- 16.5.- 6), 0.1.9 * math.pi, true), ui.Path().. arcTo(Rect.fromLTRB(- 10.0.10.20), 0.1.0 * math.pi, true),]);Copy the code
Big guy’s idea is strong 👍
-
Wave animation when switching
Here are mainly two parts, one is the wave animation when clicking the switch, the other is the bump effect after the animation
We need to use CustomPainter to render this effect
We need to define some parameters.
final double _normalizedY;final double _x; Copy the code
And then I’m gonna draw it
@override
void paint(canvas, size) {
Draw two cubic Bezier curves using various linear interpolations based on the value of "_normalizedY"
final norm = LinearPointCurve(0.5.2.0).transform(_normalizedY) / 2;
final radius = Tween<double>(
begin: _radiusTop,
end: _radiusBottom
).transform(norm);
// Bump effect after animation
final anchorControlOffset = Tween<double>(
begin: radius * _horizontalControlTop,
end: radius * _horizontalControlBottom
).transform(LinearPointCurve(0.5.0.75).transform(norm));
final dipControlOffset = Tween<double>(
begin: radius * _pointControlTop,
end: radius * _pointControlBottom
).transform(LinearPointCurve(0.5.0.8).transform(norm));
final y = Tween<double>(
begin: _topY,
end: _bottomY
).transform(LinearPointCurve(0.2.0.7).transform(norm));
final dist = Tween<double>(
begin: _topDistance,
end: _bottomDistance
).transform(LinearPointCurve(0.5.0.0).transform(norm));
final x0 = _x - dist / 2;
final x1 = _x + dist / 2;
// Draw the project
finalpath = Path() .. moveTo(0.0)
..lineTo(x0 - radius, 0)
..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y) .. lineTo(x1, y)// Width and height of the background
..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
// Width and height of the background
..lineTo(size.width, 0).. lineTo(size.width, size.height) .. lineTo(0, size.height);
finalpaint = Paint() .. color = _color; canvas.drawPath(path, paint); }@override
bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
return_x ! = oldPainter._x || _normalizedY ! = oldPainter._normalizedY || _color ! = oldPainter._color; }Copy the code
The background with the wave animation is complete
-
Button bounce animation
In fact, the implementation method is the same as wave animation, also through CustomPainter to draw
(Core code only)
// Draw other stateless buttons
finalpaintBackground = Paint() .. style = PaintingStyle.stroke .. strokeWidth =2.4. strokeCap = StrokeCap.round .. strokeJoin = StrokeJoin.round .. color = AppTheme.iconColor;// Draw the color when the button is clicked
finalpaintForeground = Paint() .. style = PaintingStyle.stroke .. strokeWidth =2.4. strokeCap = StrokeCap.round .. strokeJoin = StrokeJoin.round .. color = AppTheme.appTheme.selectColor();Copy the code
We need to define the AnimationController and Animation to draw the jump Animation
Process animation at initialization time
@override
void initState() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 1666),
reverseDuration: const Duration(milliseconds: 833),
vsync: this);
_animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController) .. addListener(() { setState(() { }); }); _startAnimation();super.initState();
}
Copy the code
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);
final progress = LinearPointCurve(0.28.0.0).transform(_animation.value);
final offset = Tween<double>(
begin: _defaultOffset,
end: _activeOffset
).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);
Copy the code
Used to control the operation and destruction of animation:
@override
void didUpdateWidget(oldWidget) {
setState(() {
_selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}
void _startAnimation() {
if (_selected) {
_animationController.forward();
} else{ _animationController.reverse(); }}Copy the code
The UI layout:
return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
constraints: BoxConstraints.tight(ne),
alignment: Alignment.center,
child: Container(
margin: EdgeInsets.all(ne.width / 2 - _radius),
constraints: BoxConstraints.tight(Size.square(_radius * 2)),
decoration: ShapeDecoration(
color: AppTheme.appTheme.cardBackgroundColor(),
shape: CircleBorder(),
),
transform: Matrix4.translationValues(0, -offset, 0),
/ / draw the Icon
child: FluidFillIcon(
_iconData,
LinearPointCurve(0.25.1.0).transform(_animation.value),
scaleY,
),
),
),
);
Copy the code
The bottom navigation bar is complete!
3. Home page animation and annular progress bar processing
-
Home page overall list animation processing
This part of the data is the most complicated
Like the other animations, we need a Controller to control, and on this page, we need a List to hold the data
final AnimationController mainScreenAnimationController; final Animation<dynamic> mainScreenAnimation; final List<Habit> habits; Copy the code
Data stored in this article is not temporarily analyzed, we can run their own source ~
Initialize animation:
@override
void initState() {
animationController = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
super.initState();
}
Copy the code
Since there are many components that use animation, we use AnimatedBuilder for the root node, and mainly use FadeTransition and Transform animations, which are the same as above, so we won’t go into details here.
-
Circular progress bar
We encapsulate a CircleProgressBar with a user-drawn circular progress bar
This part of the UI is very simple, mainly the animation drawing is more complicated
ui:
return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
animation: this.curve,
child: Container(),
builder: (context, child) {
final backgroundColor =
this.backgroundColorTween? .evaluate(this.curve) ??
this.widget.backgroundColor;
final foregroundColor =
this.foregroundColorTween? .evaluate(this.curve) ??
this.widget.foregroundColor;
return CustomPaint(
child: child,
// Here is the progress bar inside the circle
foregroundPainter: CircleProgressBarPainter(
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
percentage: this.valueTween.evaluate(this.curve), strokeWidth: widget.strokeWidth ), ); },),);Copy the code
Detailed drawing:
@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
Math.min(constrainedSize.width, constrainedSize.height);
finalforegroundPaint = Paint() .. color =this.foregroundColor .. strokeWidth =this.strokeWidth .. strokeCap = StrokeCap.round .. style = PaintingStyle.stroke;final radius = (shortestSide / 2);
// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));
// Don't draw the background if we don't have a background color
if (this.backgroundColor ! =null) {
finalbackgroundPaint = Paint() .. color =this.backgroundColor .. strokeWidth =this.strokeWidth .. style = PaintingStyle.stroke; canvas.drawCircle(center, radius, backgroundPaint); } canvas.drawArc( Rect.fromCircle(center: center, radius: radius), startAngle, sweepAngle,false,
foregroundPaint,
);
}
Copy the code
Here’s another useful feature:
Time definition and speech of welcome
This demo covers most of the handling of time
Such as:
///Obtain the value based on the current time. [monthIndex] Start and end date of a month
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
DateTime start = DateTime(now.year, now.month - monthIndex, 1);
DateTime end = DateTime(now.year, now.month - monthIndex + 1.0);
return Pair<DateTime>(start, end);
}
Copy the code
We strongly recommend learning, development is more commonly used!
Most of the animation UI of this app has been analyzed, and the rest are being reused. If you think it is good, you can download and experience it by yourself to form a good habit
4. Adapt dark mode (analyze the author’s global state management)
Bloc is used here for state management
/// theme mode
enum AppThemeMode {
Light,
Dark,
}
///Font pattern
enum AppFontMode {
///The default font
Roboto,
///The three fonts
MaShanZheng,
}
///Color mode, specific view background color
enum AppThemeColorMode {
Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }
Copy the code
On this basis, define the color, style, such as:
String fontFamily(AppFontMode fontMode) {
switch (fontMode) {
case AppFontMode.MaShanZheng:
return 'MaShanZheng';
}
return 'Roboto';
}
Copy the code
Then use ternary judgment when using styles, which makes state management easy
So the UI of this project has been animated analysis is completed, we can also learn local storage through this project, see here, might as well click a like 😘
Source code here oh, here talent gathering ~