完好作用如上图
作为一个晚年Android,我现已一年多没写Android代码了,所以最近在复健、可是Android真实没搞头,所以决定学下Flutter。学Flutter大约一个多星期、这个算是咱第一个比较完好的功用代码了,在这儿给我们分享下我的思路和要害代码
设计稿来源
秉承着能搬就搬的原则,咱从dribbble上查找weather要害字、找到了这个酷炫的作用图,所以看看能不能复刻出来 # Mobile | Weather app
布局完成
代码不杂乱,这儿我就不详细讲了。作为Flutter新手的我、能快速搞定布局、下面两个工具真是出了大力。
- 大佬张风捷特烈的FlutterUnit
- copilot
这儿我随便举个copilot牛皮的比如
上面这个布局,我才写了个类名
class ForecastLayout extends StatefulWidget {
接下来奇特的copilot就给我生成了下面一大堆代码
数据类都给我写好了,他为什么知道我要用这三个字段、是因为forecast是猜测的意思么!!! 这儿ai知道我在写天气相关的ui、给我生成了day和temperature两个字段就算了,竟然还知道给我加个icon,你说奇特不奇特!!!
除了边框、间隔、背景色根本都和最终结果八九不离十
上升动画
进场动画中大部分都是这种上升+渐现的组合动画,Flutter供给了很多简化动画写法的封装类,因为咱刚学也在摸索,所以写了好几种完成
办法一:SlideTransition+FadeTransition(引荐)
-
SlideTransition
操控y方向上移动的间隔,这儿的0.5就表明child
的高度的一半 -
FadeTransition
操控child的透明度从0到1
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(不引荐)
- AnimatedBuilder+Tween操控值的改变
- Stack + Positioned:改动top属性值完成上组件上移的动画
- 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
首先经过addListener
、addStatusListener
得到动画的回调
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自带的
AnimatedIcon
:自带的一些矢量图动画、可是不能定制、所以只能抛弃了 - 办法二:rive,看了老半天感觉不简略、所以暂时抛弃了
- 办法三:canvas画图标(实际使用):经过
CustomPaint
画图+Tween
动画完成
波涛动画
画动画之前,强烈引荐先看完大佬写的文章# Flutter 制作探究 1 | CustomPainter 正确改写姿势 | 七日打卡
完成波涛动画、上一大堆、原理咱就不赘述了,我参阅的这个Flutter完成圆形波涛进展球
这儿说下不同的地方
- 我这儿是图标、所以宽度、高度需求经过参数传入,波长、波的高度都需求依据size来核算
- 我这儿是画线、并且是画三条
- 一起需求用
clipPath
只保存size内的波涛线
@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供给了办法来画三阶贝塞尔曲线、我简略封装下
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圆的开始度数为90度)
代码很简略吧,趁便提一下奇特的copilot、这儿度数的核算、我就在注释里写上了起点、结尾,ai就帮我想好了度数的核算了。这个操作在动画代码里我用到了很屡次
动画操控
- 水滴上升动画:操控顶部点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;
}
眼睛动画
- 经过两条三阶贝塞尔曲线画眼眶
- 经过圆画一个眼球
- 眨眼作用:经过
clipPath
确保眼球在眼眶之内 - 动画操控:改动贝塞尔曲线操控点y轴坐标即可
动画循环
flutter没有供给设置动画循环的api,所以还得自己写 思路也不杂乱
- 动画结束后:经过AnimationController.addStatusListener监听status完成
- 再次敞开动画
- reset:从0到1
- reverse:从1到0
我这儿实际情况是眼球和水滴需求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…
大致原理也不杂乱,咱画个图就理解了
要害代码
- 假如是页面内回转:需求使用
PageView
- 假如是页面间跳转:需求使用
PageRouteBuilder
- 旋转代码
详细代码咱就不贴了,我也是搬的,他这个源码是flutter2+的,所以要略微改一下空安全相关代码