Android?貝塞爾曲線繪制一個(gè)波浪球
前言
當(dāng) flutter 的現(xiàn)有組件無(wú)法滿足產(chǎn)品要求的 UI 效果時(shí),我們就需要通過(guò)自繪組件的方式來(lái)進(jìn)行實(shí)現(xiàn)了。本篇文章就來(lái)介紹如何用 flutter 自定義實(shí)現(xiàn)一個(gè)帶文本的波浪球,效果如下所示:
先來(lái)總結(jié)下 WaveLoadingWidget 的特點(diǎn),這樣才能歸納出實(shí)現(xiàn)該效果所需要的步驟:
- widget 的主體是一個(gè)不規(guī)則的半圓形,頂部曲線以類似于波浪的形式從左往右上下起伏運(yùn)行
- 波浪球可以自定義顏色,此處以 waveColor 命名
- 波浪球的起伏線將嵌入的文本分為上下兩種顏色,上半部分顏色以 backgroundColor 命名,下半部分顏色以 foregroundColor 命名,文本的整體顏色一直在根據(jù)波浪的運(yùn)行而動(dòng)態(tài)變化中
雖然文本的整體顏色是在不斷變化的,但只要能夠繪制出其中一幀的圖形,其動(dòng)態(tài)效果就能通過(guò)不斷改變波浪曲線的位置參數(shù)來(lái)實(shí)現(xiàn),所以這里先把該 widget 當(dāng)成靜態(tài)的,先實(shí)現(xiàn)其靜態(tài)效果即可
將繪制步驟拆解為以下幾步:
- 繪制顏色為 backgroundColor 的文本,將其繪制在 canvas 的最底層
- 根據(jù) widget 的寬高信息構(gòu)建一個(gè)不超出范圍的最大圓形路徑 circlePath
- 以 circlePath 的水平中間線作為波浪的基準(zhǔn)起伏線,在起伏線的上邊和下邊分別用貝塞爾曲線繪制一段連續(xù)的波浪 path,將 path 的首尾兩端以矩形的方式連接在一起,構(gòu)成 wavePath,wavePath 的底部會(huì)與 circlePath 的最底部相交
- 取 circlePath 和 wavePath 的交集 combinePath,用 waveColor 填充, 此時(shí)就得到了半圓形的球形波浪了
- 利用
canvas.clipPath(combinePath)
方法裁切畫布,再繪制顏色為 foregroundColor 的文本,此時(shí)繪制的 foregroundColor 文本只會(huì)顯示 combinePath 范圍內(nèi)的部分,也即只會(huì)顯示下半部分,使得兩次不同時(shí)間繪制的文本重疊在了一起,從而得到了有不同顏色范圍的文本 - 利用 AnimationController 不斷改變 wavePath 的起始點(diǎn)的 X 坐標(biāo),同時(shí)重新刷新 UI,從而得到波浪不斷從左往右起伏運(yùn)行的動(dòng)態(tài)效果
現(xiàn)在就來(lái)一步步實(shí)現(xiàn)以上的繪制步驟吧
一、繪制 backgroundColor 文本
flutter 通過(guò) CustomPainter 為開發(fā)者提供了自繪 UI 的入口,其內(nèi)部的 void paint(Canvas canvas, Size size)
方法提供了畫布 canvas 對(duì)象以及包含 widget 寬高信息的 size 對(duì)象
這里就來(lái)繼承 CustomPainter 類,在 paint
方法中先來(lái)繪制顏色為 backgroundColor 的文本。flutter 的 canvas 對(duì)象沒有提供直接 drawText
的 API,所以其繪制文本的步驟相對(duì)原生的自定義 View 要稍微麻煩一點(diǎn)
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; } }
二、構(gòu)建 circlePath
取 widget 的寬度和高度的最小值作為圓的直徑大小,以此構(gòu)建出一個(gè)不超出 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); }
三、繪制波浪線
波浪的寬度和高度就根據(jù)一個(gè)固定的比例值來(lái)求值,以 circlePath 的中間分隔線作為水平線,在水平線的上下根據(jù)貝塞爾曲線繪制出連續(xù)的波浪線
@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 繪制出來(lái),實(shí)際上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint); }
此時(shí)繪制的曲線還處于非閉合狀態(tài),需要將 wavePath 的首尾兩端連接起來(lái),這樣后面才可以和 circlePath 取交集
wavePath.relativeLineTo(0, radius); wavePath.lineTo(-waveWidth, side); wavePath.close(); //為了方便讀者理解,這里把 wavePath 繪制出來(lái),實(shí)際上不需要 final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; canvas.drawPath(wavePath, paint);
wavePath 閉合后,此時(shí)半圓的顏色就會(huì)鋪滿了
四、取交集
取 circlePath 和 wavePath 的交集,就得到一個(gè)半圓形波浪球了
final paint = Paint() ..isAntiAlias = true ..style = PaintingStyle.fill ..strokeWidth = 3 ..color = waveColor; final combinePath = Path.combine(PathOperation.intersect, circlePath, wavePath); canvas.drawPath(combinePath, paint);
五、繪制 foregroundColor 文本
文本的顏色是分為上下兩部分的,上半部分顏色為 backgroundColor,下半部分為 foregroundColor。在第一步的時(shí)候已經(jīng)繪制了顏色為 backgroundColor 的文本了,foregroundColor 文本不需要顯示上半部分,所以在繪制 foregroundColor 文本之前需要先把繪制區(qū)域限定在 combinePath 內(nèi),使得兩次不同時(shí)間繪制的文本重疊在了一起,從而得到有不同顏色范圍的文本
canvas.clipPath(combinePath); _drawText(canvas: canvas, side: side, color: foregroundColor);
六、添加動(dòng)畫
現(xiàn)在已經(jīng)繪制好靜態(tài)時(shí)的效果了,可以考慮如何使 widget 動(dòng)起來(lái)了
要實(shí)現(xiàn)動(dòng)態(tài)效果也很簡(jiǎn)單,只要不斷改變貝塞爾曲線的起始點(diǎn)坐標(biāo),使之不斷從左往右移動(dòng),就可以營(yíng)造出波浪從左往右前進(jìn)的效果了。_WaveLoadingPainter 根據(jù)外部傳入的動(dòng)畫值 animatedValue 來(lái)設(shè)置 wavePath 的起始坐標(biāo)點(diǎn)即可,生成 animatedValue 的邏輯和其它繪制參數(shù)均由 _WaveLoadingState 來(lái)提供
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 根據(jù) animatedValue 來(lái)設(shè)置 wavePath 的起始坐標(biāo)點(diǎn)
wavePath.moveTo((animatedValue - 1) * waveWidth, radius);
七、使用
最后將 _WaveLoadingState 包裹到 StatefulWidget 中,在 StatefulWidget 中開放可以自定義配置的參數(shù)就可以了
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, )
源代碼看這里:WaveLoadingWidget
以上就是Android 貝塞爾曲線繪制一個(gè)波浪球的詳細(xì)內(nèi)容,更多關(guān)于Android貝塞爾曲線的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
安卓(Android)開發(fā)之統(tǒng)計(jì)App啟動(dòng)時(shí)間
當(dāng)大家要改善APP啟動(dòng)速度優(yōu)化的時(shí)候,首先要知道App的啟動(dòng)時(shí)間,那么改如何統(tǒng)計(jì)時(shí)間呢,下面我們一起來(lái)看看。2016-08-08Android通過(guò)RemoteViews實(shí)現(xiàn)跨進(jìn)程更新UI示例
本篇文章主要介紹了Android通過(guò)RemoteViews實(shí)現(xiàn)跨進(jìn)程更新UI示例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02android 開發(fā)教程之日歷項(xiàng)目實(shí)踐(二)
決定開始學(xué)習(xí) Android 平臺(tái)下的軟件開發(fā),以日歷作為實(shí)踐項(xiàng)目,進(jìn)行一周后,基本完成,有需要的朋友可以參考下2013-01-01Android實(shí)現(xiàn)下拉放大圖片松手自動(dòng)反彈效果
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)下拉放大圖片松手自動(dòng)反彈效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-03-03輕松實(shí)現(xiàn)功能強(qiáng)大的Android刮獎(jiǎng)效果控件(ScratchView)
這篇文章主要為大家詳細(xì)介紹了ScratchView如何一步步打造萬(wàn)能的Android刮獎(jiǎng)效果控件,,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-09-09Android網(wǎng)絡(luò)判斷知識(shí)小結(jié)
本文通過(guò)兩段實(shí)例代碼分別給大家介紹Android中判斷當(dāng)前網(wǎng)絡(luò)是否可用和Android 關(guān)于判斷應(yīng)用是否有網(wǎng)絡(luò)的相關(guān)知識(shí),對(duì)android網(wǎng)絡(luò)判斷相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2015-12-12Android 詳解ThreadLocal及InheritableThreadLocal
這篇文章主要介紹了Android 詳解ThreadLocal及InheritableThreadLocal的相關(guān)資料,需要的朋友可以參考下2017-01-01Android控件ListView用法(讀取聯(lián)系人示例代碼)
本文以一個(gè)讀取聯(lián)系人的代碼為大家講解下Android控件中ListView的使用方法,這個(gè)listView有個(gè)setAdapter 適配器,里面可以直接實(shí)現(xiàn)接口,或者寫個(gè)類2013-06-06詳解Android應(yīng)用開發(fā)中Intent的作用及使用方法
這篇文章主要介紹了Android應(yīng)用開發(fā)中Intent的作用與用法,包括如何激活A(yù)ctivity組件與Intent的投遞等,需要的朋友可以參考下2016-03-03