完好作用如上图

作为一个晚年Android,我现已一年多没写Android代码了,所以最近在复健、可是Android真实没搞头,所以决定学下Flutter。学Flutter大约一个多星期、这个算是咱第一个比较完好的功用代码了,在这儿给我们分享下我的思路和要害代码

设计稿来源

秉承着能搬就搬的原则,咱从dribbble上查找weather要害字、找到了这个酷炫的作用图,所以看看能不能复刻出来 # Mobile | Weather app

Flutter练习第一弹-酷炫入场动画

布局完成

Flutter练习第一弹-酷炫入场动画

代码不杂乱,这儿我就不详细讲了。作为Flutter新手的我、能快速搞定布局、下面两个工具真是出了大力。

  • 大佬张风捷特烈的FlutterUnit
  • copilot

这儿我随便举个copilot牛皮的比如

Flutter练习第一弹-酷炫入场动画

上面这个布局,我才写了个类名

class ForecastLayout extends StatefulWidget {

接下来奇特的copilot就给我生成了下面一大堆代码

Flutter练习第一弹-酷炫入场动画

数据类都给我写好了,他为什么知道我要用这三个字段、是因为forecast是猜测的意思么!!! 这儿ai知道我在写天气相关的ui、给我生成了day和temperature两个字段就算了,竟然还知道给我加个icon,你说奇特不奇特!!!

Flutter练习第一弹-酷炫入场动画

除了边框、间隔、背景色根本都和最终结果八九不离十

上升动画

Flutter练习第一弹-酷炫入场动画

进场动画中大部分都是这种上升+渐现的组合动画,Flutter供给了很多简化动画写法的封装类,因为咱刚学也在摸索,所以写了好几种完成

办法一:SlideTransition+FadeTransition(引荐)

  1. SlideTransition 操控y方向上移动的间隔,这儿的0.5就表明child高度的一半
  2. FadeTransition 操控child的透明度从0到1

Flutter练习第一弹-酷炫入场动画

class UpAnimationLayout extends StatefulWidget {
  final AnimateCallback? callback;
  final AnimationStatusListener? statusListener;
  final Widget child;
  final double? upOffset;
  final Duration? duration;
  const UpAnimationLayout(
      {super.key,
      required this.child,
      this.callback,
      this.statusListener,
      this.upOffset,
      this.duration});
  @override
  State<StatefulWidget> createState() => _UpAnimationLayoutState();
}
class _UpAnimationLayoutState extends State<UpAnimationLayout> with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
        vsync: this, duration: widget.duration ?? const Duration(milliseconds: 500))
      ..addListener(() {
        widget.callback?.call(_controller);
      })
      ..addStatusListener(widget.statusListener ?? (status) {});
  }
  void start() {
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return SlideTransition(
        position:
            Tween(begin: Offset(0, widget.upOffset ?? 0.5), end: Offset.zero).animate(_controller),
        child: FadeTransition(
            opacity: Tween(begin: 0.0, end: 1.0).animate(_controller), child: widget.child));
  }
}

办法二:Stack布局+AnimatedBuilder(不引荐)

  1. AnimatedBuilder+Tween操控值的改变
  2. Stack + Positioned:改动top属性值完成上组件上移的动画
  3. Opacity:改动opacity属性值完成透明度的改变

难点:需求知道top的组件高度值height才能精确操控上移动画,例如假如要求组件呈现时能看到一半的高度、那么top的改变就必须为height/2~0

可是目前咱没发现能build之前获取height的办法(我看有人写了个AfterLayout来做,可是我还没尝试)

只要文本可以使用TextPainter来获取文本的size,详细代码如下

// 这儿一定需求用组件的style生成一个style传入painter
final style = DefaultTextStyle.of(context).style.merge(widget.textStyle);
final painter = TextPainter(
  text: TextSpan(text: widget.value, style: style),
  textDirection: TextDirection.ltr,
  textScaleFactor: MediaQuery.of(context).textScaleFactor,
)..layout();
final size = painter.size;

我最开端便是参阅网上Flutter关于文字动画的源码写的,因为抄漏了个DefaultTextStyle.merge的逻辑,导致size的宽度一向偏小,原因是widget.textStyle没有设置letterSpacing的,可是Flutter的Text组件画文字时、是用DefaultTextStyle加上了这个space所以实际的宽度会比核算出来的大

动画先后顺序操控

这个页面有非常多的动画,所以就需求操控动画呈现的时机和先后顺序

办法一:AnimationController动画回调+自动调用forward

首先经过addListeneraddStatusListener得到动画的回调

void initState() {
  super.initState();
  _controller = AnimationController(vsync: this, duration: widget.duration)
    ..addListener(() {
      widget.callback?.call(_controller);
    })
    ..addStatusListener(widget.statusListener ?? (status) {});
}

然后经过传参的办法把动画回调露出给外部

typedef AnimateCallback = void Function(AnimationController controller);
class UpAnimationLayout extends StatefulWidget {
  final AnimateCallback? callback;
  final AnimationStatusListener? statusListener;
  .......

外部得到动画的回调后、经过GlobalKey得到需求开端动画的组件、然后调用forward敞开第二个动画

final GlobalKey<_UpAnimationLayoutState> _iconsLayoutKey = GlobalKey();
void summaryAnimationCallback(controller) {
  if (animationIndex < 6 && controller.value > 0.8) {
    // 第一个动画进展80%时、animationIndex确保只走一次
    animationIndex++;
    // 经过key获取第二个组件的实例
    _iconsLayoutKey.currentState?.start();
  }
}

假如是StatelessWidget的组件、可以经过currentWidget来调用

(_dateTextKey.currentWidget as UpTextAnimation).start();
void start() {
  _controller.forward();
}
  • 长处:组件之间彼此解耦
  • 缺陷:要写很多重复的代码

办法二:Interval操控每个动画的占比

Tween(begin: 0.0, end: 1.0).animate(_controller)

Flutter中给Animation设置AnimationController时还可以这么写

_opacityAnimation = Tween(begin: 0.0, end: 1.0).animate(CurvedAnimation(
  parent: _controller,
  curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
));

上面的Interval表明这个动画的时刻段为**_controller操控的整个动画时刻的0~0.5**

假如有三个不同的动画ani1、ani2、ani3,都设置Interval分别为

  • ani1:0.0~0.5
  • ani2:0.2~0.7
  • ani3:0.5~1.0

并且他们绑定同一个AnimationController,此刻假如调用_controller.forward(),就可以做到三个动画交错运行了

  • 长处:组件之间耦合、需求统一看动画的时刻
  • 缺陷:代码相对较少

明显经过Interval组合的动画,更适合列表一类的动画

办法三:pub上找动画插件写组合动画

flutter为啥不供给AnimationSet相似的组件呢?网上应该有、我找了几个相似的、感觉并不太好用就没试、我们有什么引荐的么

图标动画

Flutter练习第一弹-酷炫入场动画

为了完成上面的动画、我查找了一圈、大约准备了三种办法

  • 办法一:Flutter自带的AnimatedIcon:自带的一些矢量图动画、可是不能定制、所以只能抛弃了
  • 办法二:rive,看了老半天感觉不简略、所以暂时抛弃了
  • 办法三:canvas画图标(实际使用):经过CustomPaint画图+Tween动画完成

波涛动画

画动画之前,强烈引荐先看完大佬写的文章# Flutter 制作探究 1 | CustomPainter 正确改写姿势 | 七日打卡

完成波涛动画、上一大堆、原理咱就不赘述了,我参阅的这个Flutter完成圆形波涛进展球

这儿说下不同的地方

  • 我这儿是图标、所以宽度、高度需求经过参数传入,波长、波的高度都需求依据size来核算
  • 我这儿是画线、并且是画三条
  • 一起需求用clipPath只保存size内的波涛线

Flutter练习第一弹-酷炫入场动画

Flutter练习第一弹-酷炫入场动画

@override
Widget build(BuildContext context) {
  return CustomPaint(
    size: Size(widget.size, widget.size),
    painter: WavePainter(
      waveOffsetX: _animation,
      strokeWidth: widget.strokeWidth,
      waveColor: widget.color ?? Theme.of(context).primaryColor,
    ),
  );
}
class WavePainter extends CustomPainter {
  WavePainter({
    required this.waveOffsetX,
    required this.waveColor,
    required this.strokeWidth,
  }) : super(repaint: waveOffsetX);
  final Animation<double> waveOffsetX;
  final Color waveColor;
  final double strokeWidth;
  void drawWave(Canvas canvas, Size size, Paint paint) {
    var waveHeight = size.height / 4;
    var waveWidth = size.width;
    final offsetX = waveOffsetX.value;
    var offsetY = size.height * 0.2;
    Path wavePath = Path();
    for (int i = 0; i < 3; i++) {
      Offset point1 = Offset(offsetX, offsetY);
      Offset point2 = Offset(offsetX + waveWidth, offsetY);
      Offset point3 = Offset(offsetX + waveWidth * 2, offsetY);
      Offset point4 = Offset(offsetX + waveWidth * 3, offsetY);
      Offset ct1 = Offset(offsetX + waveWidth / 2, offsetY - waveHeight);
      Offset ct2 = Offset(offsetX + waveWidth * 3 / 2, offsetY + waveHeight);
      Offset ct3 = Offset(offsetX + waveWidth * 5 / 2, offsetY - waveHeight);
      wavePath.moveTo(point1.dx, point1.dy);
      wavePath.quadraticBezierTo(ct1.dx, ct1.dy, point2.dx, point2.dy);
      wavePath.quadraticBezierTo(ct2.dx, ct2.dy, point3.dx, point3.dy);
      wavePath.quadraticBezierTo(ct3.dx, ct3.dy, point4.dx, point4.dy);
      offsetY += waveHeight;
    }
    Path clipPath = Path()..addRect(Rect.fromLTWH(0, 0, size.width, size.height));
    canvas.clipPath(clipPath);
    canvas.drawPath(wavePath, paint);
  }
  @override
  void paint(Canvas canvas, Size size) {
    // 2. configure the paint and drawing properties
    Paint paint = Paint()
      ..isAntiAlias = true
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..color = waveColor;
    drawWave(canvas, size, paint);
  }
  // 7. only return true if the old progress value
  // is different from the new one
  @override
  bool shouldRepaint(covariant WavePainter oldDelegate) {
    return oldDelegate.waveOffsetX != waveOffsetX;
  }
}

实际上水波动画用三阶贝塞尔曲线完成更简略的,不知道为啥上的文章都是用的二阶完成的波涛、我深刻怀疑都在转移

别的可以用这个网站调试贝塞尔曲线:cubic-bezier.com/#.46,.04,.8…

水滴动画

画水滴

Flutter练习第一弹-酷炫入场动画

这儿需求用三阶贝塞尔曲线画线、否则会很突兀

一共三个曲线、分别为左上、底部、右上三条线

  • 黑点为曲线的起点和结尾
  • 红点为操控点

flutter供给了办法来画三阶贝塞尔曲线、我简略封装下

void _cubicTo(Path path, Offset ctrl1, Offset ctrl2, Offset offset) {
  path.cubicTo(ctrl1.dx, ctrl1.dy, ctrl2.dx, ctrl2.dy, offset.dx, offset.dy);
}
// dy为极点的y坐标
Offset point1 = Offset(size.width / 2, dy * size.height);
Offset point2 = Offset(size.width * 0.2, size.height * 0.6);
Offset ct1_1 = Offset(size.width * 0.35, size.height * 0.2);
Offset ct1_2 = Offset(size.width * 0.2, size.height * 0.35);
Offset point3 = Offset(size.width * 0.8, size.height * 0.6);
Offset ct2_1 = Offset(size.width * 0.25, size.height * 1);
Offset ct2_2 = Offset(size.width * 0.75, size.height * 1);
Offset ct3_1 = Offset(size.width * 0.8, size.height * 0.35); // 对应ct1_2
Offset ct3_2 = Offset(size.width * 0.65, size.height * 0.2); // 对应ct1_1
Path path = Path()..moveTo(point1.dx, point1.dy);
_cubicTo(path, ct1_1, ct1_2, point2);
_cubicTo(path, ct2_1, ct2_2, point3);
_cubicTo(path, ct3_1, ct3_2, point1);
canvas.drawPath(path, paint);

画中心圆弧

Flutter练习第一弹-酷炫入场动画

这个就不用贝塞尔曲线了,直接用圆或者椭圆就行

确定好中心点和曲线的开始视点即可(留意flutter圆的开始度数为90度

Flutter练习第一弹-酷炫入场动画

代码很简略吧,趁便提一下奇特的copilot、这儿度数的核算、我就在注释里写上了起点、结尾,ai就帮我想好了度数的核算了。这个操作在动画代码里我用到了很屡次

动画操控

Flutter练习第一弹-酷炫入场动画

  • 水滴上升动画:操控顶部点y的坐标即可
  • 圆弧动画:操控开始视点即可
  • 圆弧动画相似荡秋千、每次水滴的极点到最高时、秋千也到最高,因为有左右两次最高的时刻点、所以圆弧动画的总时刻是水滴动画总时刻的2倍

这儿我没找到好办法直接完成2倍的操作、我是这么写的

@override
void initAnimations() {
  _offsetAnimation = Tween(begin: 0.0, end: 1.0).animate(_controller);
  _startAngleAnimation = Tween(begin: 0.0, end: 1.0)
      .animate(CurvedAnimation(parent: _controller, curve: Curves.ease));
}

因为要做循环、所以不能直接经过interval或者时刻操控,下面的代码其实仍是ai帮我写好的

double dy;
// 因为startAngle的动画时刻是offset动画时刻的两倍,这儿对半处理
if (firstOffsetYFactor.value < 0.5) {
  dy = firstOffsetYFactor.value * 0.2;
} else {
  dy = (1 - firstOffsetYFactor.value) * 0.2;
}

眼睛动画

Flutter练习第一弹-酷炫入场动画

  • 经过两条三阶贝塞尔曲线画眼眶
  • 经过圆画一个眼球
  • 眨眼作用:经过clipPath确保眼球在眼眶之内
  • 动画操控:改动贝塞尔曲线操控点y轴坐标即可

Flutter练习第一弹-酷炫入场动画

动画循环

flutter没有供给设置动画循环的api,所以还得自己写 思路也不杂乱

  • 动画结束后:经过AnimationController.addStatusListener监听status完成
  • 再次敞开动画
    • reset:从0到1
    • reverse:从1到0

Flutter练习第一弹-酷炫入场动画

我这儿实际情况是眼球和水滴需求reverse、波涛线需求reset

因为我需求的循环动画有点多、所以简略封装了一下

mixin LoopAnimation<T extends StatefulWidget> on State<T> {
  late AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = initAnimationController();
    // 循环动画
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        if (isResetAnimation()) {
          _controller.reset();
        } else {
          _controller.reverse();
        }
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });
    initAnimations();
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  bool isResetAnimation() => true;
  AnimationController initAnimationController();
  void initAnimations();
}

页面翻转动画

参阅的这个源码:github.com/aeyrium/cub…

大致原理也不杂乱,咱画个图就理解了

Flutter练习第一弹-酷炫入场动画

要害代码

  • 假如是页面内回转:需求使用PageView
  • 假如是页面间跳转:需求使用PageRouteBuilder
  • 旋转代码

Flutter练习第一弹-酷炫入场动画

详细代码咱就不贴了,我也是搬的,他这个源码是flutter2+的,所以要略微改一下空安全相关代码