写点什么

打造炫酷时尚的 Neumorphism 设计!

  • 2023-03-21
    浙江
  • 本文字数:3951 字

    阅读完需:约 13 分钟

打造炫酷时尚的 Neumorphism 设计!

前言

在前几天的业务需求中,UI 给出的页面中有新拟态的按钮,就是带内部阴影的按钮,如果是利用cssbox-shadow的属性,那么实现起来很简单,但是奈何 Flutter 中的ContainerBoxShadow不具备inset内部阴影的功能,那么本文就来解决这个问题,在解决的过程中,我发现了Neumorphism.io,这可是一个神奇的网站,能满足各种圆角矩形 icon 图表立体化效果要求,同时给出了css代码。那么咱用 Flutter 简单模仿一个,并给出 Flutter Container 按钮对应的代码。


源码地址:https://github.com/taxze6/flutter_neumorphism


效果图:适配了手机端与网页


实现 Flutter 内部阴影

在 Flutter 中,可以使用以下几种方式来实现阴影:


  • BoxShadow


BoxShadow(  color: Colors.grey.withOpacity(0.5),  spreadRadius: 5,  blurRadius: 7,  offset: Offset(0, 3), ),
复制代码


  • 使用 Material 组件的 elevation 属性来添加阴影


Material(  elevation: 5.0,  child: Text('Material Shadow'),),
复制代码


  • 使用 PhysicalModel 组件来添加阴影


PhysicalModel(  color: Colors.white,  elevation: 5.0,  shadowColor: Colors.grey.withOpacity(0.5),  borderRadius: BorderRadius.circular(10),  child: const Text('PhysicalModel Shadow'),),
复制代码


  • 其他方法


总的来说,Flutter 实现阴影的方法有很多。但是,在这么多实现的方法中,却没有能实现这样内部阴影的方法。



而在前几天的需求中,就遇到了需要这样的按钮 UI,第一时间想到的是,通过 Stack 来实现多层阴影,从而达到内部阴影的效果,下面是简单的例子,实现出来的效果还可以,但是不够优雅。



Stack(  alignment: AlignmentDirectional.center,  children: [    Container(      width: 63,      height: 63,      decoration: const BoxDecoration(        color: Color(0xFF949494),        shape: BoxShape.circle,        boxShadow: [          BoxShadow(            color: Color(0xFFE8E8E8),            offset: Offset(8, 8),            blurRadius: 10,            spreadRadius: 1,          ),        ],      ),    ),    ClipRRect(      borderRadius: BorderRadius.circular(63),      child: Container(        width: 63,        height: 63,        decoration: const BoxDecoration(          shape: BoxShape.circle,          boxShadow: [            BoxShadow(              color: Color(0xFFF7F7F7),              offset: Offset(3, 3),              blurRadius: 3,              spreadRadius: 1,            ),          ],        ),      ),    ),  ],)
复制代码


这时我想到了BoxShadow实现外阴影的功能,我想它既然能实现外部的阴影,那么把它的源码拉出来,模仿它外阴影的实现逻辑去绘制内阴影是否可行呢?我觉得可以,那么理论方案已经出现,开始实践。

将 box_shadow 与 box_decoration 的源码拷贝


先看 box_decoration,看绘制的方法,这个 paint 方法主要就是绘制 BoxDecoration 中的各种装饰,例如背景颜色。而_paintShadows 方法就是本文关注的重点,



  //绘制阴影效果的函数  void _paintShadows(Canvas canvas, Rect rect, TextDirection? textDirection) {    // 检查是否需要绘制阴影    if (_decoration.boxShadow == null) {      // 如果不需要,直接返回       return;    }
// 遍历阴影效果列表中的每个阴影配置 for (final BoxShadow boxShadow in _decoration.boxShadow!) { // 根据阴影配置创建 Paint 对象 final Paint paint = boxShadow.toPaint(); // 根据阴影配置计算出阴影绘制区域 final Rect bounds = rect.shift(boxShadow.offset).inflate(boxShadow.spreadRadius); //绘制阴影 _paintBox(canvas, bounds, paint, textDirection); } }
复制代码


根据外阴影的绘制逻辑,我们要做的就是在BoxShadow添加一个是否是绘制内阴影的属性,用于判断,因为如果需要内阴影就不再绘制外阴影了。


for (final painting.BoxShadow boxShadow in _decoration.boxShadow!) {  //添加判断  if (boxShadow is! BoxShadow || !boxShadow.inset) {    continue;  }  ...}
复制代码


既然判断了,如果需要绘制内阴影,就跳过外阴影的绘制逻辑,那么我们就需要自己添加内阴影的绘制逻辑。


void _paintInnerShadows(  Canvas canvas,  Rect rect,  TextDirection? textDirection,) {  // 检查是否有需要绘制的阴影,如果没有则直接返回  if (_decoration.boxShadow == null) {    return;  }  // 遍历所有的BoxShadow  for (final painting.BoxShadow boxShadow in _decoration.boxShadow!) {    // 如果BoxShadow不是BoxShadow类型,或者不是内阴影,跳过本次循环    if (boxShadow is! BoxShadow || !boxShadow.inset) {      continue;    }
// 获取BoxShadow的颜色 final color = boxShadow.color;
// 计算圆角 final borderRadiusGeometry = _decoration.borderRadius ?? (_decoration.shape == BoxShape.circle ? BorderRadius.circular(rect.longestSide) : BorderRadius.zero); // 解决文本方向 final borderRadius = borderRadiusGeometry.resolve(textDirection);
// 使用RRect剪切画布 final clipRRect = borderRadius.toRRect(rect);
// 计算内部矩形 final innerRect = rect.deflate(boxShadow.spreadRadius);
// 如果内部矩形为空,则绘制整个矩形 if (innerRect.isEmpty) { final paint = Paint()..color = color; canvas.drawRRect(clipRRect, paint); } // 否则,绘制内阴影 else { // 计算内部矩形的RRect var innerRRect = borderRadius.toRRect(innerRect); // 保存画布状态 canvas.save(); // 在剪切区域内绘制内阴影 canvas.clipRRect(clipRRect); // 计算包含内阴影和剪切区域的矩形 final outerRect = _areaCastingShadowInHole(rect, boxShadow); // 绘制内阴影 canvas.drawDRRect( RRect.fromRectAndRadius(outerRect, Radius.zero), innerRRect.shift(boxShadow.offset), Paint() ..color = color ..colorFilter = ColorFilter.mode(color, BlendMode.srcIn) ..maskFilter = MaskFilter.blur(BlurStyle.normal, boxShadow.blurSigma), ); // 恢复画布状态 canvas.restore(); } }}
复制代码


其中_areaCastingShadowInHole方法就是用来计算 box 中阴影的区域:


///holeRect:表示阴影的位置和大小///shadow:表示阴影的颜色、大小、位置Rect _areaCastingShadowInHole(Rect holeRect, BoxShadow shadow) {  var bounds = holeRect;  //将bounds沿着所有方向膨胀shadow.blurRadius的距离  //确保生成的阴影图像元素不会被截断  bounds = bounds.inflate(shadow.blurRadius);
//BoxShadow.spreadRadius用于控制阴影扩展的距离 //如果值小于0,则阴影会从矩形边界开始,向内收缩。 if (shadow.spreadRadius < 0) { bounds = bounds.inflate(-shadow.spreadRadius); }
//Rect.shift 方法用于将矩形的位置偏移指定的距离 final offsetBounds = bounds.shift(shadow.offset);
return _unionRects(bounds, offsetBounds);}
复制代码


返回的_unionRects作用主要是先检查boundsoffsetBounds两个矩形是否有空矩形,如果有,则直接返回非空矩形。否则,它计算出包含这两个矩形的最小矩形,并返回该矩形。


Rect _unionRects(Rect a, Rect b) {  if (a.isEmpty) {    return b;  }
if (b.isEmpty) { return a; }
final left = math.min(a.left, b.left); final top = math.min(a.top, b.top); final right = math.max(a.right, b.right); final bottom = math.max(a.bottom, b.bottom);
return Rect.fromLTRB(left, top, right, bottom);}
复制代码


然后就能在 paint 方法中参与绘制的过程:



更多的细节可以参考源码,注释都很全。


讲完了如何实现 Flutter 的内部阴影,本文也没有其他重要的知识点了,不过有一些有趣的东西:


在实现Flutter_Neumorphism中,计算方块上下两个阴影的颜色的过程:


// 定义静态函数 getAdjustColor,接收基础颜色 baseColor 和需要调整的颜色量 amountstatic Color getAdjustColor(Color baseColor, int amount) {  // 将 baseColor 的 red、green、blue 数值存储在一个 Map 对象 colors 中  Map colors = {    "red": baseColor.red,    "green": baseColor.green,    "blue": baseColor.blue  };
// 使用 map 函数对 colors 中的每一个键值对进行处理 colors = colors.map((key, value) { // 如果 value + amount < 0,则将当前数值设为 0 if (value + amount < 0) return MapEntry(key, 0); // 如果 value + amount > 255,则将当前数值设为 255 if (value + amount > 255) return MapEntry(key, 255); // 否则,将当前数值设为 value + amount return MapEntry(key, value + amount); });
// 返回根据调整后的 red、green、blue 数值创建的颜色对象 return Color.fromRGBO(colors["red"], colors["green"], colors["blue"], 1);}
复制代码


更多的细节请看源码,建议大家运行体验看看~

关于我

Hello,我是 Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝

发布于: 刚刚阅读数: 4
用户头像

他日若遂凌云志 敢笑黄巢不丈夫 2020-10-15 加入

曾许少年凌云志,誓做人间第一流. 一起加入Flutter技术交流群532403442 有好多好多滴学习资料喔~ 小T目前主攻Android与Flutter, 通常会搞搞人工智能、SpringBoot 、Mybatiys等.

评论

发布
暂无评论
打造炫酷时尚的 Neumorphism 设计!_flutter_编程的平行世界_InfoQ写作社区