Flutter - 这么炫酷的App你见过吗😍?

简介: 就是想做一个真正喜欢的APP而非demo!!!这是一个花了半年业余时间做的一款习惯打卡APP,吸取了市面上很多产品经验。
前言:在前几天,我发现了一个动画特别炫酷的一个Flutter项目,一款习惯养成类的App,看了后就真的是爱不释手,功能很丰富,所以我立刻找到了开源作者,向他申请了写作权限。然后开始了对项目的分析(求个赞!!!相信我,看完这篇你会有收获的👍)

我对他项目的代码进行了部分修改,修改的源代码在文章最后~

开源项目地址:https://github.com/designDo/flutter-checkio

先上效果图:

tt0.top-432794.gif
tt0.top-795301.gif

还有很多的功能大家自己下载源码(觉得好的话给开源作者点个star哦,人家不容易!)

本文分析重点:

  • 登录界面的动画、输入框处理以及顶部弹出框
  • 底部导航栏的动画处理
  • 首页动画以及环形进度条处理
  • 适配深色模式(分析一下作者的全局状态管理)

1.登录界面的动画、输入框处理以及顶部弹出框

  • 动画处理

    这里一共有3处动画,输入框的缩放动画,验证码按钮的平移动画,登录界面的缩放动画。

    当我们使用动画时,我们需要定义一个Controller来控制管理动画

    AnimationController _animationController;

    当然使用动画时我们的State是需要混入SingleTickerProviderStateMixin这个类的

    在效果图中我们也不难看出动画直接是有时间间距的,所以我们整个界面仅用一个Controller来控制,使其从上到下逐步显示。

    关于缩放动画呢,在flutter我们需要使用ScaleTransition,其中最重要的一点便是:

    Animation<double> scale //控制widget缩放

    来看看详细使用:

    ScaleTransition(
        //控制缩放从0到1
      scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
          //控制动画的Controller
        parent: _animationController,
          //0,0.3是动画运行的时间
          //curve用来描述曲线运动的动画
        curve: Interval(0, 0.3, curve: Curves.fastOutSlowIn),
      )),
      child:...
    )

    这里关于其他动画也差不多,区别就在于动画和动画的运行时间

    关键区别:

    验证码的输入框:

    curve: Interval(0.3, 0.6, curve: Curves.fastOutSlowIn),

    获取验证码按钮:

    这里主要区别是position用于处理初始时的绝对位置

    SlideTransition(
        //大家可以将begin: Offset(2, 0)的数据更改,这样就会清晰的体验到它的功能
      position: Tween<Offset>(begin: Offset(2, 0), end: Offset.zero)
          .animate(CurvedAnimation(
          parent: _animationController,
          curve:
          Interval(0.6, 0.8, curve: Curves.fastOutSlowIn))),child:...)

    登录按钮:

    ScaleTransition(
      scale: Tween<double>(begin: 0, end: 1).animate(CurvedAnimation(
        parent: _animationController,
        curve: Interval(0.8, 1, curve: Curves.fastOutSlowIn),
      )),child:...)

    关于动画的实现就是这样,是不是非常的简单~

  • 手机号输入框的限制处理

登录输入框处理.png

我觉得这个样式很炫酷,主要是在平时不是很常见,就分析一下

这里我们封装了一个CustomEditField输入框,可以更好的做动画的处理

动画定义

///文本内容
String _value = '';
TextEditingController editingController;
AnimationController numAnimationController;
Animation<double> numAnimation;

且该组件需要混入(Mixin)TickerProviderStateMixin与AutomaticKeepAliveClientMixin,因为AnimationController需要调用TickerProvider里的createTicker方法(感兴趣可以查看flutter源码)

with TickerProviderStateMixin, AutomaticKeepAliveClientMixin

初始化时:

@override
void initState() {
_value = widget.initValue;
  //初始化controller
editingController = TextEditingController(text: widget.initValue);
  //初始化限制框的控制器与动画
numAnimationController =
    AnimationController(duration: Duration(milliseconds: 500), vsync: this);
numAnimation = CurvedAnimation(
    parent: numAnimationController, curve: Curves.easeOutBack);
if (widget.initValue.length > 0) {
  numAnimationController.forward(from: 0.3);
}
super.initState();
}

销毁时:

@override
void dispose() {
editingController.dispose();
numAnimationController.dispose();
super.dispose();
}

UI: 使用Stack用于包裹一个输入框和限制框

Stack(
  children:[
      TextField(),
      //限制框的动画,所以在外面套一层ScaleTransition
      ScaleTransition(
          child:Padding()
      )
  ]
)

使用这个封装的组件时,我们主要处理numDecoration

此处的颜色为全局管理的处理,直接复制该代码需要修改

 numDecoration: BoxDecoration(
  shape: BoxShape.rectangle,
  color: AppTheme.appTheme.cardBackgroundColor(),
  borderRadius: BorderRadius.all(Radius.circular(15)),
  boxShadow: AppTheme.appTheme.containerBoxShadow()),
numTextStyle: AppTheme.appTheme
  .themeText(fontWeight: FontWeight.bold, fontSize: 15),
  • 顶部弹出框的处理

1634777618(1).png

使用了flash这个插件,一个高度可定制、功能强大且易于使用的警告框

为了代码的复用,在这里进行了封装处理

class FlashHelper {
  static Future<T> toast<T>(BuildContext context, String message) async {
    return showFlash<T>(
        context: context,
        //显示两秒
        duration: Duration(milliseconds: 2000),
        builder: (context, controller) {
            //弹出框
          return Flash.bar(
              margin: EdgeInsets.only(left: 24, right: 24),
              position: FlashPosition.top,
              brightness: AppTheme.appTheme.isDark()
                  ? Brightness.light
                  : Brightness.dark,
              backgroundColor: Colors.transparent,
              controller: controller,
              child: Container(
                alignment: Alignment.center,
                padding: EdgeInsets.all(16),
                height: 80,
                decoration: BoxDecoration(
                    shape: BoxShape.rectangle,
                    borderRadius: BorderRadius.all(Radius.circular(16)),
                    gradient: AppTheme.appTheme.containerGradient(),
                    boxShadow: AppTheme.appTheme.coloredBoxShadow()),
                child: Text(
                    //显示的文字
                  message,
                  style: AppTheme.appTheme.headline1(
                      textColor: Colors.white,
                      fontWeight: FontWeight.normal,
                      fontSize: 16),
                ),
              ));
        });
  }
}

2.底部导航栏的动画处理

tt0.top-150276.gif

这里真的是惊艳到我了,Icon都是画出来的,作者真的是脑洞大开,点赞!

  • Icon的绘制

    房子:

static final home = FluidFillIconData([
  //房子
  ui.Path()..addRRect(RRect.fromLTRBXY(-10, -2, 10, 10, 2, 2)),
  ui.Path()
    ..moveTo(-14, -2)
    ..lineTo(14, -2)
    ..lineTo(0, -16)
    ..close(),
]);

四个正方形:

static final window = FluidFillIconData([
//正方形
ui.Path()..addRRect(RRect.fromLTRBXY(-12, -12, -2, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, -12, 12, -2, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(-12, 2, -2, 12, 2, 2)),
ui.Path()..addRRect(RRect.fromLTRBXY(2, 2, 12, 12, 2, 2)),
]);

趋势图:

static final progress = FluidFillIconData([
//趋势图
ui.Path()
  ..moveTo(-10, -10)
  ..lineTo(-10, 8)
  ..arcTo(Rect.fromCircle(center: Offset(-8, 8), radius: 2), -1 * math.pi,
      -0.5 * math.pi, true)
  ..moveTo(-8, 10)
  ..lineTo(10, 10),
ui.Path()
  ..moveTo(-6.5, 2.5)
  ..lineTo(0, -5)
  ..lineTo(4, 0)
  ..lineTo(10, -9),
]);

我的:

static final user = FluidFillIconData([
//我的
ui.Path()..arcTo(Rect.fromLTRB(-5, -16, 5, -6), 0, 1.9 * math.pi, true),
ui.Path()..arcTo(Rect.fromLTRB(-10, 0, 10, 20), 0, -1.0 * math.pi, true),
]);

大佬的思路就是强👍

  • 切换时的波浪动画

    这里主要是两个部分,一个是点击切换时的波浪动画,一个是动画结束后的凹凸效果

    这样的效果我们需要通过CustomPainter来进行绘制

    我们需要定义一些参数(指展示最重要的)

    final double _normalizedY;final double _x;

    然后进行绘制

     @override
     void paint(canvas, size) {
       // 使用基于“_normalizedY”值的各种线性插值绘制两条三次bezier曲线
       final norm = LinearPointCurve(0.5, 2.0).transform(_normalizedY) / 2;
       final radius = Tween<double>(
           begin: _radiusTop,
           end: _radiusBottom
         ).transform(norm);
       // 当动画结束后的凹凸效果
       final anchorControlOffset = Tween<double>(
           begin: radius * _horizontalControlTop,
           end: radius * _horizontalControlBottom
         ).transform(LinearPointCurve(0.5, 0.75).transform(norm));
       final dipControlOffset = Tween<double>(
           begin: radius * _pointControlTop,
           end: radius * _pointControlBottom
         ).transform(LinearPointCurve(0.5, 0.8).transform(norm));
         
         
       final y = Tween<double>(
           begin: _topY,
           end: _bottomY
           ).transform(LinearPointCurve(0.2, 0.7).transform(norm));
       final dist = Tween<double>(
           begin: _topDistance,
           end: _bottomDistance
           ).transform(LinearPointCurve(0.5, 0.0).transform(norm));
       final x0 = _x - dist / 2;
       final x1 = _x + dist / 2;
    
         //绘制工程
       final path = Path()
         ..moveTo(0, 0)
         ..lineTo(x0 - radius, 0)
         ..cubicTo(x0 - radius + anchorControlOffset, 0, x0 - dipControlOffset, y, x0, y)
         ..lineTo(x1, y) //背景的宽高
         ..cubicTo(x1 + dipControlOffset, y, x1 + radius - anchorControlOffset, 0, x1 + radius, 0)
           //背景的宽高
         ..lineTo(size.width, 0)
         ..lineTo(size.width, size.height)
         ..lineTo(0, size.height);
    
       final paint = Paint()
           ..color = _color;
    
       canvas.drawPath(path, paint);
     }
    
     @override
     bool shouldRepaint(_BackgroundCurvePainter oldPainter) {
       return _x != oldPainter._x
           || _normalizedY != oldPainter._normalizedY
           || _color != oldPainter._color;
     }

    这样带波浪动画的背景就完成啦~

  • 按钮的弹跳动画

    其实实现方式与波浪动画相同,也是通过CustomPainter来进行绘制

    (只展示核心代码)

//绘制其他无状态的按钮
final paintBackground = Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 2.4
        ..strokeCap = StrokeCap.round
        ..strokeJoin = StrokeJoin.round
        ..color = AppTheme.iconColor;
//绘制点击该按钮时的颜色
final paintForeground = Paint()
    ..style = PaintingStyle.stroke
    ..strokeWidth = 2.4
    ..strokeCap = StrokeCap.round
    ..strokeJoin = StrokeJoin.round
    ..color = AppTheme.appTheme.selectColor();

Icon的背景以及跳跃我们需要定义AnimationController与Animation,进行跳跃动画的绘制

在初始化时处理动画

@override
void initState() {
  _animationController = AnimationController(
      duration: const Duration(milliseconds: 1666),
      reverseDuration: const Duration(milliseconds: 833),
      vsync: this);
  _animation = Tween<double>(begin: 0.0, end: 1.0).animate(_animationController)
    ..addListener(() {
      setState(() {
      });
    });
  _startAnimation();

  super.initState();
}
final offsetCurve = _selected ? ElasticOutCurve(0.38) : Curves.easeInQuint;
final scaleCurve = _selected ? CenteredElasticOutCurve(0.6) : CenteredElasticInCurve(0.6);

final progress = LinearPointCurve(0.28, 0.0).transform(_animation.value);

final offset = Tween<double>(
  begin: _defaultOffset,
  end: _activeOffset
  ).transform(offsetCurve.transform(progress));
final scaleCurveScale = 0.50;
final scaleY = 0.5 + scaleCurve.transform(progress) * scaleCurveScale + (0.5 - scaleCurveScale / 2);

用于控制动画的运行与销毁:

@override
void didUpdateWidget(oldWidget) {
setState(() {
  _selected = widget._selected;
});
_startAnimation();
super.didUpdateWidget(oldWidget);
}

void _startAnimation() {
if (_selected) {
  _animationController.forward();
} else {
  _animationController.reverse();
}
}

ui布局:

return GestureDetector(
onTap: _onPressed,
behavior: HitTestBehavior.opaque,
child: Container(
  constraints: BoxConstraints.tight(ne),
  alignment: Alignment.center,
  child: Container(
    margin: EdgeInsets.all(ne.width / 2 - _radius),
    constraints: BoxConstraints.tight(Size.square(_radius * 2)),
    decoration: ShapeDecoration(
      color: AppTheme.appTheme.cardBackgroundColor(),
      shape: CircleBorder(),
    ),
    transform: Matrix4.translationValues(0, -offset, 0),
    //Icon的绘制
    child: FluidFillIcon(
        _iconData,
        LinearPointCurve(0.25, 1.0).transform(_animation.value),
        scaleY,
    ),
  ),
),
);

这样底部导航栏就完成啦!

3.首页动画以及环形进度条处理

  • 首页整体列表动画处理

    这一部分数据是最为复杂的

    与其他动画相同,我们需要一个controller来控制,在此页面,我们还需要一个List来存放数据

    final AnimationController mainScreenAnimationController;
    final Animation<dynamic> mainScreenAnimation;
    final List<Habit> habits;

    数据存储在此文章暂时不分析,大家可以自己运行源码~

    初始化动画:

@override
void initState() {
  animationController = AnimationController(
      duration: const Duration(milliseconds: 2000), vsync: this);
  super.initState();
}

因为使用到动画的组件很多,所以我们根节点使用AnimatedBuilder,主要使用的动画FadeTransition与Transform,做法于上面相同,在此就不多赘述了。

  • 环形进度条

    我们封装了一个CircleProgressBar用户绘制圆形进度条

    这部分的ui很简单,主要是动画的绘制较为复杂

屏幕截图 2021-10-23 140905.jpg

ui:

return AspectRatio(
aspectRatio: 1,
child: AnimatedBuilder(
  animation: this.curve,
  child: Container(),
  builder: (context, child) {
    final backgroundColor =
        this.backgroundColorTween?.evaluate(this.curve) ??
            this.widget.backgroundColor;
    final foregroundColor =
        this.foregroundColorTween?.evaluate(this.curve) ??
            this.widget.foregroundColor;
  
    return CustomPaint(
      child: child,
        //重点是这个封装组件,这里是圆形里面的进度条
      foregroundPainter: CircleProgressBarPainter(
        backgroundColor: backgroundColor,
        foregroundColor: foregroundColor,
        percentage: this.valueTween.evaluate(this.curve),
        strokeWidth: widget.strokeWidth
      ),
    );
  },
),
);

详细的绘制:

@override
void paint(Canvas canvas, Size size) {
final Offset center = size.center(Offset.zero);
final Size constrainedSize =
    size - Offset(this.strokeWidth, this.strokeWidth);
final shortestSide =
    Math.min(constrainedSize.width, constrainedSize.height);
final foregroundPaint = Paint()
  ..color = this.foregroundColor
  ..strokeWidth = this.strokeWidth
  ..strokeCap = StrokeCap.round
  ..style = PaintingStyle.stroke;
final radius = (shortestSide / 2);

// Start at the top. 0 radians represents the right edge
final double startAngle = -(2 * Math.pi * 0.25);
final double sweepAngle = (2 * Math.pi * (this.percentage ?? 0));

// Don't draw the background if we don't have a background color
if (this.backgroundColor != null) {
  final backgroundPaint = Paint()
    ..color = this.backgroundColor
    ..strokeWidth = this.strokeWidth
    ..style = PaintingStyle.stroke;
  canvas.drawCircle(center, radius, backgroundPaint);
}

canvas.drawArc(
  Rect.fromCircle(center: center, radius: radius),
  startAngle,
  sweepAngle,
  false,
  foregroundPaint,
);
}

这里还有一个很实用的功能:

时间定义和欢迎词

屏幕截图 2021-10-23 142038.jpg

这个demo包含了大部分对时间的处理

屏幕截图 2021-10-23 142440.jpg
例如:

///根据当前时间获取,[monthIndex]个月的开始结束日期
static Pair<DateTime> getMonthStartAndEnd(DateTime now, int monthIndex) {
  DateTime start = DateTime(now.year, now.month - monthIndex, 1);
  DateTime end = DateTime(now.year, now.month - monthIndex + 1, 0);
  return Pair<DateTime>(start, end);
}

强烈推荐大家学习,开发中比较常用!

关于此app的大部分动画ui都分析完成了,其他都是在复用,大家觉得还不错的话可以自己下载体验一下,养成好习惯~

4.适配深色模式(分析一下作者的全局状态管理)

作者在这里使用了Bloc用于状态管理

///  theme mode
enum AppThemeMode {
  Light,
  Dark,
}
///字体模式
enum AppFontMode {
  ///默认字体
  Roboto,
  ///三方字体
  MaShanZheng,
}
///颜色模式,特定view背景颜色
enum AppThemeColorMode { 
    Indigo, Orange, Pink, Teal, Blue, Cyan, Purple }

在此基础上,定义了颜色,样式,例如:

String fontFamily(AppFontMode fontMode) {
  switch (fontMode) {
    case AppFontMode.MaShanZheng:
      return 'MaShanZheng';
  }
  return 'Roboto';
}

然后在使用样式时多用三元判断,这样就很简单的实现了状态管理

这样对这个项目的ui已经动画就分析完成了,大家也可以通过这个项目来学习本地存储,看到这里了,不妨点个赞吧😘

相关文章
|
6月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
Flutter App混淆加固、保护与优化原理
121 0
|
3月前
|
开发工具 iOS开发
解决Flutter运行报错Could not run build/ios/iphoneos/Runner.app
解决Flutter运行报错Could not run build/ios/iphoneos/Runner.app
154 2
|
3月前
|
JSON 自然语言处理 Android开发
Flutter本地化(国际化)之App名称
Flutter本地化(国际化)之App名称
70 1
|
5月前
|
移动开发 小程序 安全
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
基础入门-APP架构&小程序&H5+Vue语言&Web封装&原生开发&Flutter
|
6月前
|
人工智能 自然语言处理 API
我用 Flutter Gemini 写了一个水贴 APP
本文通过 Flutter 插件 google_generative_ai 快速的集成了 google ai gemini 来实现一个水贴的工具。
我用 Flutter Gemini 写了一个水贴 APP
|
Dart IDE 开发工具
【腾讯云 Cloud Studio 实战训练营】尝鲜体验Flutter编写一个App应用
欢迎参加腾讯云 Cloud Studio 实战训练营!在本次训练营中,我们将通过App项目入口说明,基本文件说明,基础框架搭建,带您一步步编写一个基于 Flutter 的静态App系统。无论您是初学者还是有一定编程经验的开发者,本训练营都将为您提供一个深入了解和掌握 Flutter 技术以及App开发的机会。 使用 Flutter作为UI框架,我们将能够充分利用其强大的功能和优势,快速搭建一个高效、可扩展的App系统。Flutter是Google开源的构建用户界面(UI)工具包,帮助开发者通过一套代码库高效构建多平台精美应用,支持移动、Web、桌面和嵌入式平台。
【腾讯云 Cloud Studio 实战训练营】尝鲜体验Flutter编写一个App应用
|
6月前
|
存储 前端开发 Go
flutter+go构建的即时通讯app,ChatCraft
社交应用程序在世界各地都很流行,例如 Facebook、Line、Whatsapp。如果您渴望打造一款独具个性的社交平台,Chat-Craft项目将是您不可或缺的理想之选。Chat-Craft是一款跨平台移动应用,采用了Golang作为后端服务端技术,以及Flutter作为前端客户端技术。该应用旨在提供高效、稳定且跨平台的用户体验,将现代的移动应用开发技术和高性能的后端服务端技术相结合。在Chat-Craft项目中,作者精心雕琢了客户端UI,灵感汲取于各大即时通讯应用及最新的应用设计规范。
|
6月前
|
存储 容器
Flutter 应用服务:主题、暗黑、国际化、本地化-app_service库
Flutter 应用服务:主题、暗黑、国际化、本地化-app_service库
278 0
|
11月前
|
JSON Dart 安全
Flutter App混淆加固、保护与优化原理
在移动应用程序开发中,保护应用程序的代码和数据安全至关重要。本文将探讨如何对Flutter应用程序进行混淆、优化和保护,以提高应用程序的安全性和隐私。
|
Dart 前端开发 JavaScript
掌握这个关键技术,让你的APP开发事半功倍!——Flutter与其他方案的区别
掌握这个关键技术,让你的APP开发事半功倍!——Flutter与其他方案的区别
83 0
下一篇
无影云桌面