猫哥说

这篇文章讲的是如何在你的动画中退出静止个性、静止球、重力、贝塞尔曲线、多边形、不规则曲线,如果你正在找这方面材料,这个源码你可要好好消化了。这都是动画中的根底,前端就是要酷炫,开始吧。

最佳体验还是浏览原文(链接在上面)。

老铁记得 转发 ,猫哥会出现更多 Flutter 好文~~~~

微信群 ducafecat

b站 https://space.bilibili.com/40...

原文

https://preyea-regmi.medium.c...

代码

https://github.com/PreyeaRegm...

参考

  • https://pub.flutter-io.cn/pac...
  • https://dart.dev/guides/langu...

注释

大部分工夫实现静止设计是一个有点累赘的挪动应用程序。本文从更加实用的角度论述了如何通过 Flutter 实现静止设计。咱们将采取一个简略的静止设计从运球作为一个参考,并开始建设它一步一步。所有版权保留给各自的作者,实现的残缺源代码能够在 github 上找到。

https://github.com/PreyeaRegm...

当初咱们将重点放在登录/注册交互上。所以,就像其余的交互设计一样,咱们将尝试把它分解成多个场景,这样咱们就能够有一个清晰的整体概念,并将这些场景链接在一起。

场景 1: 初始状态屏幕

在这个场景中,咱们在底部有一个弹跳的图像和文字,一个蜿蜒的红色背景,一个品牌题目突围着图像的核心和变形虫形态的背景。拖动底部的内容,直到肯定的间隔被笼罩,揭示动画播放和场景转换到下一个场景。

展现动画(两头场景)

在这个两头场景中,曲线背景高度是动画的。此外,在这个动画,控制点的三次贝塞尔曲线也被平移和还原,以提供减速成果。侧面的图标和变形虫背景也在垂直方向上 translated 以响应动画的显示。

场景 2: 前期显示动画状态屏幕

当显示动画实现后,品牌题目被一个圆形图标取代,一个标签指示器从屏幕左侧飞过来,相应的标签被加载。

当初咱们有了设计中波及的相干场景的概述。下一步,咱们尝试将这些想法映射到实现细节中。那么让咱们开始吧。

咱们将应用 stack 作为顶级容器来托管咱们所有的场景,并依据以后场景状态,咱们将向 stack 增加各自的小部件,并动画他们的几何图形。

@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);  }

对于场景 1,所有相应的小部件都被定位并增加到 stack 中。底部“向上滑动开始”小部件的弹跳成果也立刻开始。

//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),                      )                    ]),              ))),

为了实现这个小部件的拖动行为,一个可滚动的小部件也被搁置在顶部,笼罩屏幕的下半局部。“向上滑动开始”也会依据拖动间隔进行 translated,一旦跨过阈值(可滚动部件高度的 70%) ,就会播放显示动画。

//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() methodPositioned(        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;      });  }

在显示动画中,应用 CustomPainter 绘制曲线背景和变形虫背景。在动画制作过程中,曲线背景的高度以及两头控制点都被内插到了屏幕高度的 75% 。相似地,用贝塞尔曲线绘制的变形虫也是垂直平移的。

//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));

实现动画后,场景 2 就设置好了。在这个场景中,品牌题目被图标所取代,标签指示器从屏幕左侧显示。

//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" widgetPositioned(      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,            )          ])),        ],      ),    ]);  }

制表符指示器也应用贝塞尔曲线绘制,并定位在场景 1 的曲面背景之上,但在独自的 CustomPainter 中。为了实现制表位抉择成果,在绘制制表位抉择曲线时应用剪辑门路。

//The paint method of "CurvePageSwitcher" to draw tab selection arcvoid _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);  }

除此之外,两个容器以各自的标签色彩互相顶部搁置。依据选定的选项卡,保留相应的容器,将另一个容器 translated 到 x 轴的相同端,从而抛弃另一个容器。

///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;  }

因为我不能失去确切的图片和资源,我应用了我能在网上找到的最靠近的一个。

所以总的来说,咱们失去的后果如下。


© 猫哥

https://ducafecat.tech/

https://github.com/ducafecat

往期

开源

GetX Quick Start

https://github.com/ducafecat/...

新闻客户端

https://github.com/ducafecat/...

strapi 手册译文

https://getstrapi.cn

微信探讨群 ducafecat

系列汇合

译文

https://ducafecat.tech/catego...

开源我的项目

https://ducafecat.tech/catego...

Dart 编程语言根底

https://space.bilibili.com/40...

Flutter 零根底入门

https://space.bilibili.com/40...

Flutter 实战从零开始 新闻客户端

https://space.bilibili.com/40...

Flutter 组件开发

https://space.bilibili.com/40...

Flutter Bloc

https://space.bilibili.com/40...

Flutter Getx4

https://space.bilibili.com/40...

Docker Yapi

https://space.bilibili.com/40...