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 ~