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

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

https://ducafecat.tech/

https://github.com/ducafecat

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

https://getstrapi.cn

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


独立开发者_猫哥
666 声望126 粉丝