Flutter實現(xiàn)文字鏤空效果的詳細步驟
引言
哈哈,2019年初我剛?cè)肼殨r,遇到了一個特別的需求:學(xué)校的卡片上要有個分類標簽,文字部分還得鏤空。當時我剛開始接觸Flutter,對很多功能都不熟悉,這個需求就一直沒能實現(xiàn),成了我的一個小執(zhí)念?,F(xiàn)在我早已不在那兒工作了,可這兩天閑來無事,突然想起了這個事。趁著五一假期,我開始琢磨畫筆功能,終于把當年實現(xiàn)不了的功能給實現(xiàn)了。

Tip: 這時候可能會有人說:啊,這道題我會,用
ShaderMask配置blendMode: BlendMode.srcOut就能實現(xiàn),但實際上這個組件不能設(shè)置圓角,內(nèi)邊距等相關(guān)內(nèi)容,如果這時候添加一個Container那么鏤空效果也只能看到Container的顏色,而不能看到最底部的圖片
實現(xiàn)原理
文字鏤空效果的核心是使用Canvas和自定義繪制(CustomPainter)來創(chuàng)建一個矩形,然后從中"切出"文字形狀。我們將使用Flutter的BlendMode.dstOut混合模式來實現(xiàn)這一效果。
開始實現(xiàn)
步驟1:創(chuàng)建基礎(chǔ)應(yīng)用結(jié)構(gòu)
首先,我們需要設(shè)置基本的應(yīng)用結(jié)構(gòu):
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rectangle Text Cutout',
theme: ThemeData(
primarySwatch: Colors.teal,
useMaterial3: true,
),
home: const RectangleDrawingScreen(),
);
}
}
這里我們創(chuàng)建了一個基本的MaterialApp,并設(shè)置了主題顏色為teal(青色),啟用了Material 3設(shè)計。
步驟2:創(chuàng)建主屏幕
接下來,我們創(chuàng)建主屏幕,這是一個StatefulWidget,因為我們需要管理多個可變狀態(tài):
class RectangleDrawingScreen extends StatefulWidget {
const RectangleDrawingScreen({super.key});
@override
State<RectangleDrawingScreen> createState() => _RectangleDrawingScreenState();
}
class _RectangleDrawingScreenState extends State<RectangleDrawingScreen> {
// 定義狀態(tài)變量
double _cornerRadius = 20.0;
String _text = "FLUTTER";
double _fontSize = 60.0;
Color _rectangleColor = Colors.teal;
Color _backgroundColor = Colors.white;
// 構(gòu)建UI...
}
我們定義了幾個關(guān)鍵狀態(tài)變量:
_cornerRadius:矩形的圓角半徑_text:要鏤空的文字_fontSize:文字大小_rectangleColor:矩形的顏色_backgroundColor:背景顏色
步驟3:實現(xiàn)自定義繪制器
這是實現(xiàn)鏤空效果的核心部分 - 自定義繪制器:
class RectangleTextCutoutPainter extends CustomPainter {
final double cornerRadius;
final String text;
final double fontSize;
final Color rectangleColor;
RectangleTextCutoutPainter({
required this.cornerRadius,
required this.text,
required this.fontSize,
required this.rectangleColor,
});
@override
void paint(Canvas canvas, Size size) {
// 創(chuàng)建矩形區(qū)域
final Rect rect = Rect.fromLTWH(
20,
20,
size.width - 40,
size.height - 40,
);
// 創(chuàng)建圓角矩形
final RRect roundedRect = RRect.fromRectAndRadius(
rect,
Radius.circular(cornerRadius),
);
// 設(shè)置文字樣式
final textStyle = TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
);
final textSpan = TextSpan(
text: text,
style: textStyle,
);
// 創(chuàng)建文字繪制器
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
// 計算文字位置
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final double xCenter = (size.width - textPainter.width) / 2;
final double yCenter = (size.height - textPainter.height) / 2;
// 使用圖層和混合模式實現(xiàn)鏤空效果
canvas.saveLayer(rect.inflate(20), Paint());
final Paint rectanglePaint = Paint()
..color = rectangleColor
..style = PaintingStyle.fill;
canvas.drawRRect(roundedRect, rectanglePaint);
final Paint cutoutPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
canvas.saveLayer(rect.inflate(20), cutoutPaint);
textPainter.paint(canvas, Offset(xCenter, yCenter));
canvas.restore();
canvas.restore();
}
@override
bool shouldRepaint(covariant RectangleTextCutoutPainter oldDelegate) {
return oldDelegate.cornerRadius != cornerRadius ||
oldDelegate.text != text ||
oldDelegate.fontSize != fontSize ||
oldDelegate.rectangleColor != rectangleColor;
}
}
這個自定義繪制器的工作原理是:
- 創(chuàng)建一個圓角矩形
- 使用
saveLayer和BlendMode.dstOut創(chuàng)建一個混合圖層 - 在矩形上"切出"文字形狀
- 使用
shouldRepaint方法優(yōu)化重繪性能
步驟4:構(gòu)建UI界面
現(xiàn)在,讓我們實現(xiàn)主界面,包括預(yù)覽區(qū)域和控制面板:
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Rectangle Text Cutout'),
backgroundColor: Colors.teal.shade100,
),
body: Column(
children: [
// 預(yù)覽區(qū)域
Expanded(
child: Container(
color: Colors.grey[200],
child: Center(
child: Stack(
children: [
// 背景圖片
Positioned.fill(
child: Image.network(
"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5reh5YaZ5oiQ54Gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eVeu8bI%2BhDJkVUHe7U%3D",
fit: BoxFit.cover,
),
),
// 自定義繪制
CustomPaint(
size: const Size(double.infinity, double.infinity),
painter: RectangleTextCutoutPainter(
cornerRadius: _cornerRadius,
text: _text,
fontSize: _fontSize,
rectangleColor: _rectangleColor,
),
),
// 額外的ShaderMask效果
ShaderMask(
blendMode: BlendMode.srcOut,
child: Text(
_text,
),
shaderCallback: (bounds) =>
LinearGradient(colors: [Colors.black], stops: [0.0])
.createShader(bounds),
),
],
),
),
),
),
// 控制面板
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[200],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 圓角控制
const Text('Corner Radius:', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _cornerRadius,
min: 0,
max: 100,
divisions: 100,
label: _cornerRadius.round().toString(),
activeColor: Colors.teal,
onChanged: (value) {
setState(() {
_cornerRadius = value;
});
},
),
// 字體大小控制
const SizedBox(height: 10),
const Text('Font Size:', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _fontSize,
min: 20,
max: 120,
divisions: 100,
label: _fontSize.round().toString(),
activeColor: Colors.teal,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
// 文字輸入
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
labelText: 'Text to Cut Out',
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.teal),
),
),
onChanged: (value) {
setState(() {
_text = value;
});
},
controller: TextEditingController(text: _text),
),
// 矩形顏色選擇
const SizedBox(height: 16),
Row(
children: [
const Text('Rectangle Color: ', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
_buildColorButton(Colors.teal),
_buildColorButton(Colors.blue),
_buildColorButton(Colors.red),
_buildColorButton(Colors.purple),
_buildColorButton(Colors.orange),
],
),
// 背景顏色選擇
const SizedBox(height: 16),
Row(
children: [
const Text('Background Color: ', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
_buildBackgroundColorButton(Colors.white),
_buildBackgroundColorButton(Colors.grey.shade300),
_buildBackgroundColorButton(Colors.yellow.shade100),
_buildBackgroundColorButton(Colors.blue.shade100),
_buildBackgroundColorButton(Colors.pink.shade100),
],
),
],
),
),
],
),
);
}
步驟5:實現(xiàn)顏色選擇按鈕
最后,我們實現(xiàn)顏色選擇按鈕的構(gòu)建方法:
Widget _buildColorButton(Color color) {
return GestureDetector(
onTap: () {
setState(() {
_rectangleColor = color;
});
},
child: Container(
margin: const EdgeInsets.only(right: 8),
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _rectangleColor == color ? Colors.black : Colors.transparent,
width: 2,
),
),
),
);
}
Widget _buildBackgroundColorButton(Color color) {
return GestureDetector(
onTap: () {
setState(() {
_backgroundColor = color;
});
},
child: Container(
margin: const EdgeInsets.only(right: 8),
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _backgroundColor == color ? Colors.black : Colors.transparent,
width: 2,
),
),
),
);
}
關(guān)鍵技術(shù)點解析
1. 混合模式(BlendMode)的應(yīng)用
在這個效果中,最關(guān)鍵的技術(shù)是使用BlendMode.dstOut混合模式。這個混合模式會從目標圖像(矩形)中"減去"源圖像(文字),從而創(chuàng)建出文字形狀的"洞"。
final Paint cutoutPaint = Paint() ..color = Colors.white ..style = PaintingStyle.fill ..blendMode = BlendMode.dstOut;
2. Canvas圖層(Layer)的使用
我們使用canvas.saveLayer()和canvas.restore()來創(chuàng)建和管理圖層,這是實現(xiàn)復(fù)雜繪制效果的關(guān)鍵:
canvas.saveLayer(rect.inflate(20), Paint()); // 繪制矩形 canvas.saveLayer(rect.inflate(20), cutoutPaint); // 繪制文字 canvas.restore(); canvas.restore();
3. 文字居中處理
為了讓文字在矩形中居中顯示,我們需要計算正確的位置:
final double xCenter = (size.width - textPainter.width) / 2; final double yCenter = (size.height - textPainter.height) / 2;
code
為了方便大家查閱,下面貼出完整代碼
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Rectangle Text Cutout',
theme: ThemeData(
primarySwatch: Colors.teal,
useMaterial3: true,
),
home: const RectangleDrawingScreen(),
);
}
}
class RectangleDrawingScreen extends StatefulWidget {
const RectangleDrawingScreen({super.key});
@override
State<RectangleDrawingScreen> createState() => _RectangleDrawingScreenState();
}
class _RectangleDrawingScreenState extends State<RectangleDrawingScreen> {
double _cornerRadius = 20.0;
String _text = "FLUTTER";
double _fontSize = 60.0;
Color _rectangleColor = Colors.teal;
Color _backgroundColor = Colors.white;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Rectangle Text Cutout'),
backgroundColor: Colors.teal.shade100,
),
body: Column(
children: [
Expanded(
child: Container(
color: Colors.grey[200],
child: Center(
child: Stack(
children: [
Positioned.fill(
child: Image.network(
"https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d11fee3a97464bca82c9291435cc2a89~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5reh5YaZ5oiQ54Gw:q75.awebp?rk3s=f64ab15b&x-expires=1746213056&x-signature=ylstmk1m2eVeu8bI%2BhDJkVUHe7U%3D",
fit: BoxFit.cover,
),
),
CustomPaint(
size: const Size(double.infinity, double.infinity),
painter: RectangleTextCutoutPainter(
cornerRadius: _cornerRadius,
text: _text,
fontSize: _fontSize,
rectangleColor: _rectangleColor,
),
),
ShaderMask(
blendMode: BlendMode.srcOut,
child: Text(
_text,
),
shaderCallback: (bounds) =>
LinearGradient(colors: [Colors.black], stops: [0.0])
.createShader(bounds),
),
],
),
),
),
),
Container(
padding: const EdgeInsets.all(16),
color: Colors.grey[200],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Corner Radius:', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _cornerRadius,
min: 0,
max: 100,
divisions: 100,
label: _cornerRadius.round().toString(),
activeColor: Colors.teal,
onChanged: (value) {
setState(() {
_cornerRadius = value;
});
},
),
const SizedBox(height: 10),
const Text('Font Size:', style: TextStyle(fontWeight: FontWeight.bold)),
Slider(
value: _fontSize,
min: 20,
max: 120,
divisions: 100,
label: _fontSize.round().toString(),
activeColor: Colors.teal,
onChanged: (value) {
setState(() {
_fontSize = value;
});
},
),
const SizedBox(height: 10),
TextField(
decoration: const InputDecoration(
labelText: 'Text to Cut Out',
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: BorderSide(color: Colors.teal),
),
),
onChanged: (value) {
setState(() {
_text = value;
});
},
controller: TextEditingController(text: _text),
),
const SizedBox(height: 16),
Row(
children: [
const Text('Rectangle Color: ', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
_buildColorButton(Colors.teal),
_buildColorButton(Colors.blue),
_buildColorButton(Colors.red),
_buildColorButton(Colors.purple),
_buildColorButton(Colors.orange),
],
),
const SizedBox(height: 16),
Row(
children: [
const Text('Background Color: ', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(width: 10),
_buildBackgroundColorButton(Colors.white),
_buildBackgroundColorButton(Colors.grey.shade300),
_buildBackgroundColorButton(Colors.yellow.shade100),
_buildBackgroundColorButton(Colors.blue.shade100),
_buildBackgroundColorButton(Colors.pink.shade100),
],
),
],
),
),
],
),
);
}
Widget _buildColorButton(Color color) {
return GestureDetector(
onTap: () {
setState(() {
_rectangleColor = color;
});
},
child: Container(
margin: const EdgeInsets.only(right: 8),
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _rectangleColor == color ? Colors.black : Colors.transparent,
width: 2,
),
),
),
);
}
Widget _buildBackgroundColorButton(Color color) {
return GestureDetector(
onTap: () {
setState(() {
_backgroundColor = color;
});
},
child: Container(
margin: const EdgeInsets.only(right: 8),
width: 30,
height: 30,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
border: Border.all(
color: _backgroundColor == color ? Colors.black : Colors.transparent,
width: 2,
),
),
),
);
}
}
class RectangleTextCutoutPainter extends CustomPainter {
final double cornerRadius;
final String text;
final double fontSize;
final Color rectangleColor;
RectangleTextCutoutPainter({
required this.cornerRadius,
required this.text,
required this.fontSize,
required this.rectangleColor,
});
@override
void paint(Canvas canvas, Size size) {
final Rect rect = Rect.fromLTWH(
20,
20,
size.width - 40,
size.height - 40,
);
final RRect roundedRect = RRect.fromRectAndRadius(
rect,
Radius.circular(cornerRadius),
);
final textStyle = TextStyle(
fontSize: fontSize,
fontWeight: FontWeight.bold,
);
final textSpan = TextSpan(
text: text,
style: textStyle,
);
final textPainter = TextPainter(
text: textSpan,
textDirection: TextDirection.ltr,
);
textPainter.layout(
minWidth: 0,
maxWidth: size.width,
);
final double xCenter = (size.width - textPainter.width) / 2;
final double yCenter = (size.height - textPainter.height) / 2;
canvas.saveLayer(rect.inflate(20), Paint());
final Paint rectanglePaint = Paint()
..color = rectangleColor
..style = PaintingStyle.fill;
canvas.drawRRect(roundedRect, rectanglePaint);
final Paint cutoutPaint = Paint()
..color = Colors.white
..style = PaintingStyle.fill
..blendMode = BlendMode.dstOut;
canvas.saveLayer(rect.inflate(20), cutoutPaint);
textPainter.paint(canvas, Offset(xCenter, yCenter));
canvas.restore();
canvas.restore();
}
@override
bool shouldRepaint(covariant RectangleTextCutoutPainter oldDelegate) {
return oldDelegate.cornerRadius != cornerRadius ||
oldDelegate.text != text ||
oldDelegate.fontSize != fontSize ||
oldDelegate.rectangleColor != rectangleColor;
}
}
以上就是Flutter實現(xiàn)文字鏤空效果的詳細步驟的詳細內(nèi)容,更多關(guān)于Flutter文字鏤空效果的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android Shader應(yīng)用開發(fā)之雷達掃描效果
這篇文章主要為大家詳細介紹了Android Shader應(yīng)用開發(fā)之雷達掃描效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-07-07
Android實現(xiàn)TextView兩端對齊的方法
這篇文章主要介紹了Android實現(xiàn)TextView兩端對齊的方法,需要的朋友可以參考下2016-01-01
Android編程實現(xiàn)Gallery中每次滑動只顯示一頁的方法
這篇文章主要介紹了Android編程實現(xiàn)Gallery中每次滑動只顯示一頁的方法,涉及Android擴展Gallery控件實現(xiàn)翻頁效果控制的功能,涉及Android事件響應(yīng)及屬性控制的相關(guān)技巧,需要的朋友可以參考下2015-11-11
Android開發(fā)兩個activity之間傳值示例詳解
這篇文章主要為大家介紹了Android開發(fā)兩個activity之間傳值示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-07-07
理解關(guān)于Android系統(tǒng)中輕量級指針的實現(xiàn)
由于android系統(tǒng)底層的很大的一部分是用C++實現(xiàn)的,C++的開發(fā)就難免會使用到指針的這個知識 點。而C++的難點和容易出問題的也在于指針。使用指針出錯,常常會引發(fā)帶來對項目具有毀滅性的錯誤,內(nèi)存泄漏、邏輯錯誤、系統(tǒng)崩潰2021-10-10
Android自定義View實現(xiàn)音頻播放圓形進度條
這篇文章主要為大家詳細介紹了Android自定義View實現(xiàn)音頻播放圓形進度條,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-06-06

