flutter 中的自定義 Widget 算做是 flutter 體系中比較高階的知識點之一了,至關於原生開發中的自定義 View,以我我的的感覺來講,自定義 widget 的難度要低於自定義 View,不過因爲當前 flutter 的開源庫還不算多豐富,因此有些效果仍是須要開發者本身動手來實現,而本篇文章就來介紹如何用 flutter 來實現一個帶文本的波浪球 Widget,實現的的效果以下所示:git
源代碼點擊這裏下載:github.com/leavesC/flu…github
先來總結下該 WaveLoadingWidget 的特色,這樣才能概括出實現該效果所需的步驟canvas
雖然波浪是不斷運動的,但只要可以繪製出其中一幀的圖形,其動態效果就能經過不斷改變波浪的位置參數來完成,因此這裏先把該 widget 當成靜態的,先實現其靜態效果便可ide
將繪製步驟拆解爲如下幾步:佈局
canvas.clipPath(targetPath)
方法裁切畫布,再繪製顏色爲 foregroundColor 的文本,此時繪製的 foregroundColor 文本只會顯示 targetPath 範圍內的部分,從而使兩次不一樣時間繪製的文本重疊在了一塊兒,獲得了有不一樣顏色範圍的文本如今就來一步步實現以上的繪製步驟吧字體
flutter 經過抽象類 CustomPainter
爲開發者提供了自繪 UI 的入口,其內部的抽象方法 void paint(Canvas canvas, Size size)
提供了畫布對象 canvas 以及包含 widget 寬高信息的 size 對象動畫
此處就來繼承 CustomPainter 類,初始化畫筆對象以及各個配置參數(要繪製的文本,顏色值等)ui
class WaveLoadingPainter extends CustomPainter {
//若是外部沒有指定顏色值,則使用此默認顏色值
static final Color defaultColor = Colors.lightBlue;
//畫筆對象
var _paint = Paint();
//圓形路徑
Path _circlePath = Path();
//波浪路徑
Path _wavePath = Path();
//要顯示的文本
final String text;
//字體大小
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
WaveLoadingPainter(
{this.text,
this.fontSize,
this.backgroundColor,
this.foregroundColor,
this.waveColor}) {
_paint
..isAntiAlias = true
..style = PaintingStyle.fill
..strokeWidth = 3
..color = waveColor ?? defaultColor;
}
@override
void paint(Canvas canvas, Size size) {
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
複製代碼
flutter 的 canvas 對象沒有提供直接 drawText
的 API,其繪製文本的步驟相對原生的自定義 View 要比較麻煩this
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
···
}
void _drawText({Canvas canvas, double side, Color colors}) {
ParagraphBuilder pb = ParagraphBuilder(ParagraphStyle(
textAlign: TextAlign.center,
fontStyle: FontStyle.normal,
fontSize: fontSize ?? 0,
));
pb.pushStyle(ui.TextStyle(color: colors ?? defaultColor));
pb.addText(text);
ParagraphConstraints pc = ParagraphConstraints(width: fontSize ?? 0);
Paragraph paragraph = pb.build()..layout(pc);
canvas.drawParagraph(
paragraph,
Offset(
(side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0));
}
複製代碼
取 widget 的寬和高的最小值做爲圓的直徑大小,以此構建出一個不超出 widget 範圍的最大圓形路徑spa
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
···
}
複製代碼
此處波浪的寬度和高度就根據一個固定的比例值來求值,以 _circlePath 的中間分隔線做爲水平線,在水平線上下根據貝塞爾曲線繪製出連續的波浪線
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
double waveWidth = side * 0.8;
double waveHeight = side / 6;
_wavePath.reset();
_wavePath.moveTo(-waveWidth, radius);
for (double i = -waveWidth; i < side; i += waveWidth) {
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, -waveHeight, waveWidth / 2, 0);
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
//爲了方便讀者理解,這裏把路徑繪製出來,實際上不須要
canvas.drawPath(_wavePath, _paint);
}
複製代碼
此時繪製的曲線還處於非閉合狀態,須要將 _wavePath 的首尾兩端鏈接起來,這樣才能夠和 _circlePath 作交集
_wavePath.relativeLineTo(0, radius);
_wavePath.lineTo(-waveWidth, side);
_wavePath.close();
複製代碼
_wavePath 閉合後,此時繪製出來的圖形就以下所示
_circlePath 和 _wavePath 的交集就是一個半圓形波浪了
var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath);
canvas.drawPath(combine, _paint);
//爲了方便讀者理解,這裏把路徑繪製出來,實際上不須要
canvas.drawPath(combine, _paint);
複製代碼
文本的顏色是分爲上下兩部分的,foregroundColor 顏色的文本不須要顯示上半部分,因此在繪製 foregroundColor 文本的時候須要把上半部分文本給裁切掉,使兩次不一樣時間繪製的文本重疊在了一塊兒,獲得了有不一樣顏色範圍的文本
canvas.clipPath(combine);
_drawText(canvas: canvas, side: side, colors: foregroundColor);
複製代碼
如今已經繪製好了單獨一幀時的效果圖了,能夠考慮使 widget 動起來了
只要不斷改變貝塞爾曲線的起始點座標,使之不斷從左往右移動,就能夠營造出波浪從左往右前進的效果了。WaveLoadingPainter 只負責根據外部傳入的動畫值 animatedValue 來繪製 UI,構造 animatedValue 的邏輯則由外部的 _WaveLoadingWidgetState 進行處理,這裏規定 animatedValue 的值是從 0 遞增到 1,在開始構建 _wavePath 前只須要移動其起始座標點便可
@override
void paint(Canvas canvas, Size size) {
double side = min(size.width, size.height);
double radius = side / 2.0;
_drawText(canvas: canvas, side: side, colors: backgroundColor);
_circlePath.reset();
_circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi);
double waveWidth = side * 0.8;
double waveHeight = side / 6;
_wavePath.reset();
_wavePath.moveTo((animatedValue - 1) * waveWidth, radius);
for (double i = -waveWidth; i < side; i += waveWidth) {
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, -waveHeight, waveWidth / 2, 0);
_wavePath.relativeQuadraticBezierTo(
waveWidth / 4, waveHeight, waveWidth / 2, 0);
}
_wavePath.relativeLineTo(0, radius);
_wavePath.lineTo(-waveWidth, side);
_wavePath.close();
var combine = Path.combine(PathOperation.intersect, _circlePath, _wavePath);
canvas.drawPath(combine, _paint);
canvas.clipPath(combine);
_drawText(canvas: canvas, side: side, colors: foregroundColor);
}
複製代碼
class _WaveLoadingWidgetState extends State<WaveLoadingWidget> with SingleTickerProviderStateMixin {
final String text;
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
AnimationController controller;
Animation<double> animation;
_WaveLoadingWidgetState(
{@required this.text,
@required this.fontSize,
@required this.backgroundColor,
@required this.foregroundColor,
@required this.waveColor});
@override
void initState() {
super.initState();
controller =
AnimationController(duration: const Duration(seconds: 1), vsync: this);
controller.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
print("dismissed");
break;
case AnimationStatus.forward:
print("forward");
break;
case AnimationStatus.reverse:
print("reverse");
break;
case AnimationStatus.completed:
print("completed");
break;
}
});
animation = Tween(
begin: 0.0,
end: 1.0,
).animate(controller)
..addListener(() {
setState(() => {});
});
controller.repeat();
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: WaveLoadingPainter(
text: text,
fontSize: fontSize,
animatedValue: animation.value,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
waveColor: waveColor,
),
);
}
}
複製代碼
以後只要將 WaveLoadingPainter 包裹到 StatefulWidget 中便可,在 StatefulWidget 中開放能夠自定義配置的參數就能夠了
class WaveLoadingWidget extends StatefulWidget {
final String text;
final double fontSize;
final Color backgroundColor;
final Color foregroundColor;
final Color waveColor;
WaveLoadingWidget(
{@required this.text,
@required this.fontSize,
@required this.backgroundColor,
@required this.foregroundColor,
@required this.waveColor}) {
assert(text != null && text.length == 1);
assert(fontSize != null && fontSize > 0);
}
@override
_WaveLoadingWidgetState createState() => _WaveLoadingWidgetState(
text: text,
fontSize: fontSize,
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
waveColor: waveColor,
);
}
複製代碼
使用方式就相似於通常的系統 widget
Container(
width: 300,
height: 300,
child: WaveLoadingWidget(
text: "鍥",
fontSize: 215,
backgroundColor: Colors.lightBlue,
foregroundColor: Colors.white,
waveColor: Colors.lightBlue,
),
),
Container(
width: 250,
height: 250,
child: WaveLoadingWidget(
text: "而",
fontSize: 175,
backgroundColor: Colors.indigoAccent,
foregroundColor: Colors.white,
waveColor: Colors.indigoAccent,
),
),
複製代碼
此外該項目也提供了 N 多個經常使用 Widget 和自定義 Widget 的使用及實現方法,涵蓋了系統 Widget 、佈局容器、動畫、高階功能、自定義 Widget 等內容,歡迎 star