Flutter内阴影
创始人
2025-06-01 05:03:12
0

前言

在前几天的业务需求中,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;}// 遍历所有的BoxShadowfor (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 {// 计算内部矩形的RRectvar 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 和需要调整的颜色量 amount
static 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,则将当前数值设为 0if (value + amount < 0) return MapEntry(key, 0);// 如果 value + amount > 255,则将当前数值设为 255if (value + amount > 255) return MapEntry(key, 255);// 否则,将当前数值设为 value + amountreturn MapEntry(key, value + amount);});// 返回根据调整后的 red、green、blue 数值创建的颜色对象return Color.fromRGBO(colors["red"], colors["green"], colors["blue"], 1);
}

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

关于我

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

相关内容

热门资讯

周代音乐内容分为几类 周代音乐内容分为几类A.3 B.4 C.5 D.6答案错误,务信,被坑了
古代的叙事诗 古代的叙事诗那个《孔雀东南飞》可能是,还有《木兰诗》、《长恨歌》、《琵琶行》应该也是的。其实是不是叙...
最新放假通知!连休8天 今天是端午节假期的最后一天下次休假在啥时候?根据国务院办公厅关于2025年部分节假日安排的通知今年的...
白头发男主角是王爷的小说 白头发男主角是王爷的小说不嫁妖孽王爷 白发王妃(女主白了头发,男主为了女主喝了一种药也白了头发)
用物理学原理解释彩虹为什么是弯... 用物理学原理解释彩虹为什么是弯的,中午为何很难看到彩虹是空气中水蒸气浓度不同造成的色散,所以必须在比...
大幅超去年同期 2025端午档... 据网络平台数据,截至6月2日12时47分,2025年端午档电影票房破4亿,大幅超去年同期。合家欢电影...
东方海洋:全资子公司取得医疗器... 每经AI快讯,6月2日,东方海洋公告,全资子公司艾维可生物科技有限公司自主研发的铁蛋白(Fer)测定...
晶科能源:收到政府补助9800... 晶科能源(688223)公告,5月29日收到政府补助人民币9800万元,属于与收益相关的政府补助款项...
北汽蓝谷:子公司5月销量同比增... 6月2日,北汽蓝谷公告称,子公司北京新能源汽车股份有限公司5月份产量为13,433辆,同比增长216...
采访体育记者可以问什么问题 采访体育记者可以问什么问题要做一个体育记者的人物专访,请问有什么问题可以问啊?求助啊.....为什么...
高凌信息:终止发行股份及支付现... 高凌信息(688175.SH)公告称,公司决定终止发行股份及支付现金购买资产并募集配套资金事项,主要...
为了培养学生吃苦耐劳、坚强自立... 为了培养学生吃苦耐劳、坚强自立的精神,教师给学生们讲了“卧薪尝胆”的故事。这体现了哪项德育方法?()...
送你一片浪漫花海 和薰衣草一起... 位于新疆伊犁河谷中部的伊宁县天山花海景区是国内薰衣草连片种植面积最大的薰衣草基地。这两天,当地2.2...
奇瑞集团5月销售汽车20.57... 人民财讯6月2日电,奇瑞集团消息,奇瑞集团2025年5月销售汽车20.57万辆,同比增长9.1%。其...
【特稿】一堂课、一座桥、一程游... “天津的企业给我们出资建设温室大棚,又手把手地教我们种植技术,还主动为我们的农产品找销路,实在太感谢...
一堂课、一座桥、一程游、一束菇... 转自:津云庆阳与天津相隔千山万水,但“对口帮扶”让两座城市亲如兄弟。海河之水悄然润泽着董志塬的沟壑,...
华仁药业:持股5%以上股东华仁... 华仁药业6月2日公告,公司持股5%以上股东华仁世纪集团计划自2025年6月24日至2025年9月23...
“苏超”火了!网友:没有人情世... 转自:宁波晚报足球世界里,太多夹杂着底蕴比拼与恩怨情仇的高手过招,被冠以“德比”之名而享誉全球——意...
A股千亿级研发投入行业:建筑行...   在科技自立自强与产业链升级的双重驱动下,A股上市公司的研发投入正成为观察中国经济动能转换的关键窗...
适合一个人干的小生意,夫妻店创...   嗨,我的朋友们。夫妻可以做生意吗      一起?对于做小生意,没问题。      但是做大生意...