最近看到一个关于App UI 界面中的毛玻璃特效,给人一种高级的设计感,强烈的视觉冲击力,感觉非常赞,用上他可以为我们的App增色不少,于是我用Flutter复刻了一把,最后送上完整代码,学会了可以直接拿去用哦,快来看看吧
先看成品
- 用到的主要技术点有:
- Flutter中的高斯模糊滤镜
- 自定义渐变色边框
下面我们开始制作吧
1. 先找个底图做背景
核心代码
Stack(
children: [
/// 底图
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Image.asset(
'assets/images/test.png',
fit: BoxFit.fitWidth,
),
),
///
],
)
2. 再画个矩形
核心代码
/// 画个矩形
Container(
width: 300,
height: 200,
colosr: Colosr.white,
)
3.给矩形加个渐变色
这里注意不是简单的纯色。
选择纯白色(#FFFFFF)作为渐变颜色,但不同之处在于两端的不透明度不同:
- 渐变起点颜色:不透明度 40%
- 渐变终点颜色:不透明度 10%
这样的渐变让卡片显得更加细腻、光滑,模拟出玻璃表面的微妙变化。 核心代码
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.1),
],
begin: const Alignment(-1, -1),
end: const Alignment(0.3, 0.5),
),
),
4. 添加背景模糊
这里我们实用了Flutter中的BackdropFilter组件,同时配合ClipRRect剪出圆角矩形的形状,同时只让卡片部分是模糊的,其他部分是清晰的,这点很重要。为了模仿玻璃的质感,我们需要添加 背景模糊 效果。模糊能为设计增添空间感,模拟出真实玻璃的视觉效果。建议将模糊值设置在 20px 左右,你可以根据实际需求进行微调。模糊会让背景元素呈现朦胧感,增强设计的深度。
核心代码
ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 20,
sigmaY: 20,
),
///省略...
),
),
5. 加个阴影
boxShadow: [
BoxShadow(
color: const Color(0x000000).withOpacity(0.1),
offset: const Offset(0, 1),
blurRadius: 24,
spreadRadius: -1,
)
],
6. 渐变色边框
这一步是点睛之笔,加了边框后会模拟出璃材质的边缘通常具有光滑的反射感。为卡片添加一个优雅的 3px 边框,并使用渐变填充,以模拟定向光源的反射效果。这里实用到了Flutter中的自定义画布,再自定义画布上画出渐变色边框
CustomPaint(
painter: GradientBoundPainter(
colors: [
const Color(0xFFFFFFFF).withOpacity(0.5),
const Color(0xFFFF48DB).withOpacity(0.5),
],
width: bc.maxWidth,
height: bc.maxHeight,
),
child: ///省略...
)
7. 加点文字
卡片的基础已经完成,现在是时候为其添加内容了。这里注意要使用白色文字,设置不透明度为 50%,创造出一种 压印效果。
最后,送上完整代码,欢迎关注微信公众号 【技术万有引力】
import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
class Test extends StatefulWidget {
const Test({Key? key}) : super(key: key);
@override
State<Test> createState() => _TestState();
}
class _TestState extends State<Test> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
SizedBox(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
child: Image.asset(
'assets/images/test.png',
fit: BoxFit.fitWidth,
),
),
Align(
alignment: const Alignment(0, -0.1),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(
sigmaX: 20,
sigmaY: 20,
),
child: SizedBox(
width: 300,
height: 200,
child: LayoutBuilder(
builder: (BuildContext _, BoxConstraints bc) {
return CustomPaint(
painter: GradientBoundPainter(
colors: [
const Color(0xFFFFFFFF).withOpacity(0.5),
const Color(0xFFFF48DB).withOpacity(0.5),
],
width: bc.maxWidth,
height: bc.maxHeight,
),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 30,
vertical: 40,
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.1),
],
begin: const Alignment(-1, -1),
end: const Alignment(0.3, 0.5),
),
boxShadow: [
BoxShadow(
color: const Color(0x000000).withOpacity(0.1),
offset: const Offset(0, 1),
blurRadius: 24,
spreadRadius: -1)
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
'MEMBERSHIP',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Expanded(child: SizedBox()),
],
),
Expanded(child: SizedBox()),
Text(
'GRAVITY TECHNOLOGY',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
'SOFTMAX.TOOL',
style: TextStyle(
color: Colors.white.withOpacity(0.5),
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
],
),
),
);
},
),
),
),
),
)
],
),
);
}
}
class GradientBoundPainter extends CustomPainter {
final List<Color> colors;
final double width;
final double height;
final double strokeWidth;
const GradientBoundPainter({
Key? key,
required this.colors,
required this.width,
required this.height,
this.strokeWidth = 3.0,
});
@override
void paint(Canvas canvas, Size size) {
final rectWidth = width, rectHeight = height;
final radius = 20.0;
//定义矩形的位置和尺寸
Rect rect = Offset(
size.width / 2 - rectWidth / 2, size.height / 2 - rectHeight / 2) &
Size(rectWidth, rectHeight);
//RRect.fromRectAndRadius一个具有圆角的矩形
RRect rRect = RRect.fromRectAndRadius(rect, Radius.circular(radius));
//绘制
final paint = Paint()
//创建线性渐变着色器
..shader = LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: colors,
).createShader(rect)
..strokeWidth = strokeWidth
//只绘制边框而不填充
..style = PaintingStyle.stroke;
canvas.drawRRect(rRect, paint);
}
@override
bool shouldRepaint(covariant GradientBoundPainter oldDelegate) {
return oldDelegate.colors != colors;
}
}