阅读 5901

Flutter 手把手写出超漂亮的登录注册 UI - 饮料食谱App - Speed Code

我是 Zero,今天我们要用 Flutter 敲出上面👆的效果

粗略一看
嗯🤔... 很漂亮... 很笔优特佛(Beautiful)👍
再粗略一看
🤣小老弟,你唬我呢?不就是个登录注册页面吗?
再仔细一看
不对😱,居然有下面👇这么多的知识点

核心知识点

看完文章,点赞后再点击对应的 Widget 可直接进入 Flutter 官方文档😏,(超级贴心💗)

  • 先赞后看,更新永不断👏
  • 好的,我们进入正题

📖 项目介绍 📖

这是我的第 2 个 Speed Code 视频项目文章,通过此文章你可以学习到如上 Widget基础进阶用法,更重要的你可以学习到如何将这些 Widget 灵活的组合,最终实现上面👆的效果。

如果觉得对你有帮助可以点个赞👍 ,我会更有动力录制分享更多 Flutter 优质内容,谢谢你的赞

定义通用主题

  • 大小
/// theme/app_size.dart
import 'package:flutter/material.dart';

// 标题文字大小
const double kTitleTextSize = 24;
// 内容体文字大小
const double kBodyTextSize = 14;
// 按钮文字大小
const double kBtnTextSize = 18;
// 按钮圆角半径
const double kBtnRadius = 24;
// 输入框边框圆角半径
const double kInputBorderRadius = 5;
// icon 大小
const double kIconSize = 24;
// icon 盒子大小
const double kIconBoxSize = 56;
// Light 字重
const FontWeight kLightFontWeight = FontWeight.w300;
// Medium 字重
const FontWeight kMediumFontWeight = FontWeight.w500;

复制代码
  • 颜色
/// theme/app_colors.dart
import 'package:flutter/widgets.dart';

// 背景颜色
const Color kBgColor = Color(0xFFFEDCE0);
// 文字颜色
const Color kTextColor = Color(0xFF3D0007);
// 按钮开始颜色
const Color kBtnColorStart = Color(0xFFF89500);
// 按钮结束颜色
const Color kBtnColorEnd = Color(0xFFFA6B74);
// 按钮投影颜色
const Color kBtnShadowColor = Color(0x33D83131);
// 输入框边框颜色
const Color kInputBorderColor = Color(0xFFECECEC);

// 按钮渐变背景色
const LinearGradient kBtnLinearGradient = LinearGradient(
  colors: [
    kBtnColorStart,
    kBtnColorEnd,
  ],
);

复制代码
  • 样式
/// theme/app_style.dart
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'app_colors.dart';
import 'app_size.dart';

// 按钮投影
const List<BoxShadow> kBtnShadow = [
  BoxShadow(
    color: kBtnShadowColor,
    offset: Offset(0, 8),
    blurRadius: 20,
  )
];

// 按钮文字样式
const TextStyle kBtnTextStyle = TextStyle(
  color: kBtnColorStart,
  fontSize: kBtnTextSize,
  fontWeight: kMediumFontWeight,
);

// 标题文字样式
const TextStyle kTitleTextStyle = TextStyle(
  fontSize: kTitleTextSize,
  color: kTextColor,
  fontWeight: kMediumFontWeight,
);

// 内容文字样式
const TextStyle kBodyTextStyle = TextStyle(
  fontSize: kBodyTextSize,
  color: kTextColor,
  fontWeight: kLightFontWeight,
);

// 输入框边框
OutlineInputBorder kInputBorder = OutlineInputBorder(
  borderRadius: BorderRadius.circular(5),
  borderSide: BorderSide(
    color: kInputBorderColor,
    width: 1,
  ),
);

复制代码

构建欢迎页面

绘制头部内容

  • 1、绘制头部背景
// WelBgHeader
Image.asset(
  'assets/images/bg_welcome_header.png'
)
复制代码

img_01.png

  • 2、绘制 App Icon
// AppIconWidget
Container(
  width: kIconBoxSize,
  height: kIconBoxSize,
  decoration: BoxDecoration(
    color: Colors.white,
    shape: BoxShape.circle,
  ),
  alignment: Alignment.center,
  child: Image.asset(
    'assets/icons/app_icon.png',
    width: 24,
    height: 32,
  ),
)
复制代码

img_02.png

  • 3、绘制 Icon 下的文字
/// Icon Text
Positioned(
  top: 194,
  left: 40,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      AppIconWidget(),
      SizedBox(height: 8),
      Text(
        'Sour',
        style: kTitleTextStyle,
      ),
      SizedBox(height: 8),
      Text(
        'Best drink\nreceipes',
        style: kBodyTextStyle,
      ),
    ],
  ),
),
复制代码

img_03.png

  • 4、设置整体背景色
/// WelcomePage
Scaffold(
  backgroundColor: kBgColor,
  body: Column(
    children: [
      WelcomeHeaderWidget(),
    ],
  ),
)
复制代码

img_04.png

绘制底部内容

仔细看注释,很重要

  • 1、绘制按钮渐变色背景
// 按钮渐变背景色
const LinearGradient kBtnLinearGradient = LinearGradient(
  colors: [
    kBtnColorStart,
    kBtnColorEnd,
  ],
);
// 按钮投影
const List<BoxShadow> kBtnShadow = [
  BoxShadow(
    color: kBtnShadowColor,
    offset: Offset(0, 8),
    blurRadius: 20,
  )
];
// 渐变色按钮
// GradientBtnWidget
SizedBox(
  width: width,
  height: 48,
  child: GestureDetector(
    onTap: onTap,
    child: Container(
      decoration: BoxDecoration(
        // 设置渐变色
        gradient: kBtnLinearGradient,
        // 设置投影
        boxShadow: kBtnShadow,
		    // 设置圆角半径
        borderRadius: BorderRadius.circular(kBtnRadius),
      ),
      alignment: Alignment.center,
      child: child,
    ),
  ),
)
复制代码
  • 2、绘制文字
// 按钮文字样式
const TextStyle kBtnTextStyle = TextStyle(
  color: kBtnColorStart,
  fontSize: kBtnTextSize,
  fontWeight: kMediumFontWeight,
);
// 白色按钮文字
// BtnTextWhiteWidget
Text(
  text,
  style: kBtnTextStyle.copyWith(
    color: Colors.white,
  ),
)
复制代码
  • 3、组合
// Sign up 按钮
GradientBtnWidget(
  width: 208,
  child: BtnTextWhiteWidget(text: 'Sign up'),
  onTap: () {},
)
复制代码

img_05.png

  • 4、绘制登录按钮
// LoginBtnWidget
Container(
  height: 48,
  width: 208,
  decoration: BoxDecoration(
    // 设置白色
    color: Colors.white,
    // 设置圆角半径
    borderRadius: BorderRadius.circular(kBtnRadius),
    // 设置投影
    boxShadow: kBtnShadow,
  ),
  alignment: Alignment.center,
  child: Text(
    'Login',
    style: kBtnTextStyle,
  ),
)
复制代码

img_06.png

  • 5、添加忘记密码文字
// 忘记密码
Text(
  'Forgot password?',
  style: TextStyle(
    fontSize: 18,
    color: kTextColor,
  ),
)
复制代码
  • 6、绘制底部社交媒体第三方登录

// 登录方式图标
class LoginTypeIconWidget extends StatelessWidget {
  const LoginTypeIconWidget({
    Key key,
    this.icon,
  }) : super(key: key);
  final String icon;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 10),
      child: Image.asset(
        icon,
        width: 16,
        height: 16,
      ),
    );
  }
}

// 横线
class LineWidget extends StatelessWidget {
  const LineWidget({
    Key key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      child: Divider(color: kTextColor),
      width: 80,
    );
  }
}
/// 组合起来
Row(
  children: [
    Spacer(),
    LineWidget(),
    LoginTypeIconWidget(icon: 'assets/icons/logo_ins.png'),
    LoginTypeIconWidget(icon: 'assets/icons/logo_fb.png'),
    LoginTypeIconWidget(icon: 'assets/icons/logo_twitter.png'),
    LineWidget(),
    Spacer(),
  ],
)
复制代码

img_08.png

登录页面

绘制头部区域

  • 1、绘制背景
Scaffold(
  // 设置背景为白色
  backgroundColor: Colors.white,
  body: Stack(
    children: [
      Image.asset(
        'assets/images/bg_login_header.png'
      ),
    ],
  ),
);
复制代码
  • 2、绘制返回按钮
// BackIcon
GestureDetector(
  onTap: () {
    // 返回
    Navigator.pop(context);
  },
  child: Container(
    width: 56,
    height: 56,
    decoration: BoxDecoration(
      // 白色背景
      color: Colors.white,
      // 设置圆形
      shape: BoxShape.circle,
    ),
    // 【这里很重要】设置居中
    alignment: Alignment.center,
    child: Image.asset(
      'assets/icons/icon_back.png',
      width: 24,
      height: 24,
    ),
  ),
)
复制代码

img_10.png

绘制输入区域内容

  • 1、绘制文字
// 登录文字内容,可以看上面全局定义的样式
Text(
  'Login',
  style: kTitleTextStyle,
),
SizedBox(height: 20),
Text(
  'Your Email',
  style: kBodyTextStyle,
),
复制代码
  • 2、绘制输入框
// LoginInput
TextField(
  decoration: InputDecoration(
    // 缺省文字
    hintText: hintText,
    // 边框
    border: kInputBorder,
    focusedBorder: kInputBorder,
    enabledBorder: kInputBorder,
    // 输入框前面的邮件图标
    prefixIcon: Container(
      width: kIconBoxSize,
      height: kIconBoxSize,
      // 【这里很重要,再次强调】不然会拉升
      alignment: Alignment.center,
      child: Image.asset(
        prefixIcon,
        width: kIconSize,
        height: kIconSize,
      ),
    ),
    // 设置是否为密码样式
    obscureText: obscureText,
    // 设置文字样式
    style: kBodyTextStyle.copyWith(
      fontSize: 18,
    ),
)
复制代码
  • 3、组合样式
// LoginBodyWidget - Column
SizedBox(height: 4),
LoginInput(
  hintText: 'Email',
  prefixIcon: 'assets/icons/icon_email.png',
),
SizedBox(height: 16),
Text(
  'Your Password',
  style: kBodyTextStyle,
),
SizedBox(height: 4),
LoginInput(
  hintText: 'Password',
  prefixIcon: 'assets/icons/icon_pwd.png',
),
复制代码

img_11.png

  • 4、绘制登录按钮
// 登录按钮
Row(
  children: [
    Spacer(),
    // 渐变背景组件
    GradientBtnWidget(
      child: Row(
        children: [
          SizedBox(width: 34),
          // 白色文字
          BtnTextWhiteWidget(text: 'Login'),
          Spacer(),
          // 向右图标
          Image.asset(
            'assets/icons/icon_arrow_right.png',
            width: kIconSize,
            height: kIconSize,
          ),
          SizedBox(width: 24),
        ],
      ),
      width: 160,
      onTap: () {
        // 点击登录,这里模拟返回了
        Navigator.pop(context);
      },
    ),
  ],
)
复制代码

img_12.png**

绘制曲线剪裁

可以先看看这个曲线设计的整体路径,找出6个控制点4个坐标点
img_13.png

  • p:坐标点
  • c:控制点
// 我们是用路径剪裁
ClipPath(
  clipper: LoginCliper(),
  child: LoginBodyWidget(),
),

// 登录页面剪裁曲线
class LoginCliper extends CustomClipper<Path> {
  // 第一个点
  Point p1 = Point(0.0, 54.0);
  Point c1 = Point(20.0, 25.0);
  Point c2 = Point(81.0, -8.0);
  // 第二个点
  Point p2 = Point(160.0, 20.0);
  Point c3 = Point(216.0, 38.0);
  Point c4 = Point(280.0, 73.0);
  // 第三个点
  Point p3 = Point(280.0, 44.0);
  Point c5 = Point(280.0, -11.0);
  Point c6 = Point(330.0, 8.0);

  @override
  Path getClip(Size size) {
    // 第四个点
    Point p4 = Point(size.width, .0);

    Path path = Path();
    // 移动到起始点
    path.moveTo(p1.x, p1.y);
    // 第 1 段三阶贝塞尔曲线
    path.cubicTo(c1.x, c1.y, c2.x, c2.y, p2.x, p2.y);
    // 第 2 段三阶贝塞尔曲线
    path.cubicTo(c3.x, c3.y, c4.x, c4.y, p3.x, p3.y);
    // 第 3 段三阶贝塞尔曲线
    path.cubicTo(c5.x, c5.y, c6.x, c6.y, p4.x, p4.y);
    // 右下角
    path.lineTo(size.width, size.height);
    // 左下角
    path.lineTo(0, size.height);
    // 闭合
    path.close();
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
    return oldClipper.hashCode != this.hashCode;
  }
}
复制代码

最终效果

img_14.png

这些坐标点怎么来的?

B 站有小伙伴问,是我疏忽了,补充下面的图和说明,你就明白了。

image.png

image.png

  • 整个被剪裁组件的宽高为:375x491
  • 最后根据设计稿标注的各个点的位置,进行计算得出坐标点相对于被剪裁区域组件的坐标点位置。
  • 计算如下
// 第一个点
p1 = Point(0.0, 54.0);
c1 = Point(20.0, 25.0);
c2 = Point(81.0, -8.0);
// 第二个点
p2 = Point(160.0, 20.0);
c3 = Point(216.0, 38.0);
c4 = Point(280.0, 73.0);
// 第三个点
p3 = Point(280.0, 44.0);
c5 = Point(280.0, -11.0);
c6 = Point(330.0, 8.0);
// 第四个点
Point p4 = Point(size.width, .0);
//右下角
p5 = Point(size.width, size.height);
//左下角
p6 = Point(.0, size.height);
复制代码

源码

  • base 分支:定义了基础全局样式、图片,自己练习使用
  • master 分支:视频内容对应的完整代码
  • update 分支:会持续更新优化

最近更新 [2021-04-24]

视频

建议克隆下来 base 分支代码,按照视频自己动手敲几遍,才可以变成自己的东西 动手实战是快速学习的最好方法

关于我

  • 15 年~18 年,使用 Android 原生做智能硬件相关的 App 研发
  • 18 年 5 月,一次偶然的机会接触到了 Flutter ,然后开始自学,可以看 weather_flutter 是我练习 Flutter 的入门实战项目(我现在依然觉得他非常适合 Flutter 入门练习使用)
  • 18 年 8 月,顶着巨大的压力(Flutter 当时还没有 Release 1.0)开始使用 Flutter 开发企业级项目,并且开发维护了十几个 Flutter 插件包(因为当时插件资源非常的匮乏)
  • 截止目前主导并参与上线了 4 款企业级 Flutter App,当前正在负责的一款 App 累计用户 120W+,使用 Flutter 得到了极佳的体验

致谢

  • 感谢 Elizabeth Arostegui 提供的非常漂亮的设计图,这是她的 Figma 主页
  • 如果你也有很棒的设计图,那么可以联系我制作出 App 分享给大家

关注专栏

  • 此文章已收录到下面👇 的专栏,可以直接关注
  • 更多文章继续阅读|系列文章持续更新

👏 欢迎点赞➕关注➕转发,有任何问题随时在下面👇评论,我会第一时间回复哦

文章分类
前端
文章标签