The cat said

This article is about how to add motion characteristics, motion ball, gravity, Bezier curves, polygons, irregular curves in your animation, if you are looking for this information, this source code you can have a good digestion. This is all the basics of animation. The front end is all about cool. Here we go.

The best experience is to read the original article (link below).

Old iron remember to forward, Brother Cat will present more Flutter good articles ~~~~

Ducafecat WeChat group

B stationspace.bilibili.com/404904528

The original

Preyea-regmi.medium.com/implementin…

code

Github.com/PreyeaRegmi…

reference

  • Pub. Flutter – IO. Cn/packages/ge…
  • The dart. Dev/guides/lang…

The body of the

Most of the time implementing motion design is a bit of a drag on mobile applications. This paper explains how to achieve motion design through Flutter from a more practical perspective. We will take a simple motion design from dribbling as a reference and start building it step by step. All copyright is reserved to the respective authors, and the full source code of the implementation can be found on Github.

Github.com/PreyeaRegmi…

Now let’s focus on the login/registration interaction. So, just like any other interaction design, we’ll try to break it down into multiple scenarios so we can have a clear overall idea 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 bottom content until a certain distance is covered, revealing the animation to play and the scene to switch to the next scene.

Show animation (middle scene)

In this intermediate scene, the curvilinear background height is animated. In addition, in this animation, the control point’s cubic Bezier curves are also shifted and restored to provide an acceleration effect. The side ICONS and amoeba background are also translated vertically in response to the animation.

Scene 2: The animation status screen is displayed later

When the display animation is complete, the brand title is replaced by a circular icon, a label indicator flies from the left side of the screen, and the corresponding label is loaded.

We now have an overview of the relevant scenarios involved in the design. Next, we try to map these ideas into implementation details. So let’s get started.

We’ll use the Stack as the top-level container to host all of our scenes, and based on the current scene state, we’ll add individual 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);
  }
Copy the code

For scenario 1, all the corresponding widgets are located and added to the stack. The bouncing effect of the “slide Up 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),
                      )
                    ]),
              ))),
Copy the code

To enable the dragging behavior of the widget, a scrollable widget is also placed at the top, covering the bottom half of the screen. The “Slide up start” is also translated according to the distance you drag. Once you cross the threshold (70% of the height of the part that can be rolled), a display animation is 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;
      });
  }
Copy the code

In the display animation, use CustomPainter to draw the curve background and the amoeba background. During animation, the height of the curved background and the control points in between were interpolated to 75% of the screen height. Similarly, an amoeba drawn with a Bezier curve is vertically translated.

//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));
Copy the code

Once the animation is complete, scene 2 is set up. In this scenario, brand titles are replaced by ICONS and label indicators are 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,
            )
          ])),
        ],
      ),
    ]);
  }
Copy the code

The TAB indicator is also drawn using bezier curves and positioned above the curved background of Scene 1, but in a separate CustomPainter. To achieve the effect of TAB selection, clipping paths are used when drawing TAB selection curves.

//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);
  }
Copy the code

In addition, the two containers are placed on top of each other in their respective label colors. According to the selected TAB, retain the corresponding container, translate the other container to the opposite end of the X axis, and thus discard 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;
  }
Copy the code

Since I couldn’t get exact images and resources, I used the closest one I could find online.

So in general, what we get is the following.


The elder brother of the © cat

ducafecat.tech/

github.com/ducafecat

The issue of

Open source

GetX Quick Start

Github.com/ducafecat/g…

News client

Github.com/ducafecat/f…

Strapi manual translation

getstrapi.cn

Wechat discussion group Ducafecat

A series of collections

The translation

Ducafecat. Tech/categories /…

The open source project

Ducafecat. Tech/categories /…

Dart programming language basics

Space.bilibili.com/404904528/c…

Start Flutter with zero basics

Space.bilibili.com/404904528/c…

Flutter combat news client from scratch

Space.bilibili.com/404904528/c…

Flutter component development

Space.bilibili.com/404904528/c…

Flutter Bloc

Space.bilibili.com/404904528/c…

Flutter Getx4

Space.bilibili.com/404904528/c…

Docker Yapi

Space.bilibili.com/404904528/c…