您好,登錄后才能下訂單哦!
本篇內容介紹了“怎么用Android貝塞爾曲線繪制一個波浪球”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
先來總結下 WaveLoadingWidget 的特點,這樣才能歸納出實現該效果所需要的步驟:
widget 的主體是一個不規則的半圓形,頂部曲線以類似于波浪的形式從左往右上下起伏運行
波浪球可以自定義顏色,此處以 waveColor 命名
波浪球的起伏線將嵌入的文本分為上下兩種顏色,上半部分顏色以 backgroundColor 命名,下半部分顏色以 foregroundColor 命名,文本的整體顏色一直在根據波浪的運行而動態變化中
雖然文本的整體顏色是在不斷變化的,但只要能夠繪制出其中一幀的圖形,其動態效果就能通過不斷改變波浪曲線的位置參數來實現,所以這里先把該 widget 當成靜態的,先實現其靜態效果即可
將繪制步驟拆解為以下幾步:
繪制顏色為 backgroundColor 的文本,將其繪制在 canvas 的最底層
根據 widget 的寬高信息構建一個不超出范圍的最大圓形路徑 circlePath
以 circlePath 的水平中間線作為波浪的基準起伏線,在起伏線的上邊和下邊分別用貝塞爾曲線繪制一段連續的波浪 path,將 path 的首尾兩端以矩形的方式連接在一起,構成 wavePath,wavePath 的底部會與 circlePath 的最底部相交
取 circlePath 和 wavePath 的交集 combinePath,用 waveColor 填充, 此時就得到了半圓形的球形波浪了
利用 canvas.clipPath(combinePath)
方法裁切畫布,再繪制顏色為 foregroundColor 的文本,此時繪制的 foregroundColor 文本只會顯示 combinePath 范圍內的部分,也即只會顯示下半部分,使得兩次不同時間繪制的文本重疊在了一起,從而得到了有不同顏色范圍的文本
利用 AnimationController 不斷改變 wavePath 的起始點的 X 坐標,同時重新刷新 UI,從而得到波浪不斷從左往右起伏運行的動態效果
現在就來一步步實現以上的繪制步驟吧
flutter 通過 CustomPainter 為開發者提供了自繪 UI 的入口,其內部的 void paint(Canvas canvas, Size size)
方法提供了畫布 canvas 對象以及包含 widget 寬高信息的 size 對象
這里就來繼承 CustomPainter 類,在 paint
方法中先來繪制顏色為 backgroundColor 的文本。flutter 的 canvas 對象沒有提供直接 drawText
的 API,所以其繪制文本的步驟相對原生的自定義 View 要稍微麻煩一點
class _WaveLoadingPainter extends CustomPainter { final String text; final double fontSize; final double animatedValue; final Color backgroundColor; final Color foregroundColor; final Color waveColor; _WaveLoadingPainter({ required this.text, required this.fontSize, required this.animatedValue, required this.backgroundColor, required this.foregroundColor, required this.waveColor, }); @override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); } void _drawText( {required Canvas canvas, required double side, required Color color}) { ParagraphBuilder paragraphBuilder = ParagraphBuilder(ParagraphStyle( textAlign: TextAlign.center, fontStyle: FontStyle.normal, fontSize: fontSize, )); paragraphBuilder.pushStyle(ui.TextStyle(color: color)); paragraphBuilder.addText(text); ParagraphConstraints pc = ParagraphConstraints(width: fontSize); Paragraph paragraph = paragraphBuilder.build()..layout(pc); canvas.drawParagraph( paragraph, Offset((side - paragraph.width) / 2.0, (side - paragraph.height) / 2.0), ); } @override bool shouldRepaint(CustomPainter oldDelegate) { return animatedValue != (oldDelegate as _WaveLoadingPainter).animatedValue; } }
取 widget 的寬度和高度的最小值作為圓的直徑大小,以此構建出一個不超出 widget 范圍的最大圓形路徑 circlePath
@override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); final circlePath = Path(); circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); }
波浪的寬度和高度就根據一個固定的比例值來求值,以 circlePath 的中間分隔線作為水平線,在水平線的上下根據貝塞爾曲線繪制出連續的波浪線
@override void paint(Canvas canvas, Size size) { final side = min(size.width, size.height); _drawText(canvas: canvas, side: side, color: backgroundColor); final circlePath = Path(); circlePath.addArc(Rect.fromLTWH(0, 0, side, side), 0, 2 * pi); final waveWidth = side * 0.8; final waveHeight = side / 6; final wavePath = Path(); final radius = side / 2.0; 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); } //為了方便讀者理解,這里把 wavePath 繪制出來,實際上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint); }
此時繪制的曲線還處于非閉合狀態,需要將 wavePath 的首尾兩端連接起來,這樣后面才可以和 circlePath 取交集
wavePath.relativeLineTo(0, radius); wavePath.lineTo(-waveWidth, side); wavePath.close(); //為了方便讀者理解,這里把 wavePath 繪制出來,實際上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint);
wavePath 閉合后,此時半圓的顏色就會鋪滿了
取 circlePath 和 wavePath 的交集,就得到一個半圓形波浪球了
final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; final combinePath = Path.combine(PathOperation.intersect, circlePath, wavePath); canvas.drawPath(combinePath, paint);
文本的顏色是分為上下兩部分的,上半部分顏色為 backgroundColor,下半部分為 foregroundColor。在第一步的時候已經繪制了顏色為 backgroundColor 的文本了,foregroundColor 文本不需要顯示上半部分,所以在繪制 foregroundColor 文本之前需要先把繪制區域限定在 combinePath 內,使得兩次不同時間繪制的文本重疊在了一起,從而得到有不同顏色范圍的文本
canvas.clipPath(combinePath); _drawText(canvas: canvas, side: side, color: foregroundColor);
現在已經繪制好靜態時的效果了,可以考慮如何使 widget 動起來了
要實現動態效果也很簡單,只要不斷改變貝塞爾曲線的起始點坐標,使之不斷從左往右移動,就可以營造出波浪從左往右前進的效果了。_WaveLoadingPainter 根據外部傳入的動畫值 animatedValue 來設置 wavePath 的起始坐標點即可,生成 animatedValue 的邏輯和其它繪制參數均由 _WaveLoadingState 來提供
class _WaveLoadingState extends State<WaveLoading> with SingleTickerProviderStateMixin { String get _text => widget.text; double get _fontSize => widget.fontSize; Color get _backgroundColor => widget.backgroundColor; Color get _foregroundColor => widget.foregroundColor; Color get _waveColor => widget.waveColor; late AnimationController _controller; late Animation<double> _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 700), vsync: this); _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 RepaintBoundary( child: CustomPaint( painter: _WaveLoadingPainter( text: _text, fontSize: _fontSize, animatedValue: _animation.value, backgroundColor: _backgroundColor, foregroundColor: _foregroundColor, waveColor: _waveColor, ), ), ); } }
_WaveLoadingPainter 根據 animatedValue 來設置 wavePath 的起始坐標點
wavePath.moveTo((animatedValue - 1) * waveWidth, radius);
最后將 _WaveLoadingState 包裹到 StatefulWidget 中,在 StatefulWidget 中開放可以自定義配置的參數就可以了
class WaveLoading extends StatefulWidget { final String text; final double fontSize; final Color backgroundColor; final Color foregroundColor; final Color waveColor; WaveLoading({ Key? key, required this.text, required this.fontSize, required this.backgroundColor, required this.foregroundColor, required this.waveColor, }) : super(key: key) { assert(text.isNotEmpty && fontSize > 0); } @override State<StatefulWidget> createState() { return _WaveLoadingState(); } }
使用方式:
SizedBox( width: 300, height: 300, child: WaveLoading( text: "開", fontSize: 210, backgroundColor: Colors.lightBlue, foregroundColor: Colors.white, waveColor: Colors.lightBlue, )
“怎么用Android貝塞爾曲線繪制一個波浪球”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。