一切視覺的動效都只是感性的欺騙,如我手中的線,跳動的人偶。她征服著你,我控制著她。--捷特
本文所有程式碼: 【github:https://github.com/toly1994328/flutter_play_bezier】
- 【Flutter高階玩法】 貝塞爾曲線的表象認知
- 【flutter高階玩法】貝塞爾實戰1 - 波浪
- 【Flutter高階玩法】 貝塞爾曲線的本質認知
前言
事項預告: 2020-04-04 晚8:30
【程式設計技術交流聖地-Flutter群】: 圖文直播某個元件原始碼,共同交流學習。我們們有緣再見。
上一篇中通過一些可操作的案例感性地瞭解貝塞爾曲線是什麼東西。
本篇將介紹貝塞爾曲線的一個簡單應用,也是我曾經入門Android繪製的第一個東西
這裡想強調一下:貝塞爾曲線甚至說是繪製的本身和平臺並沒有太大的關聯性,可以很方便的移植。重要的不是api本身,而是你能用這些api做出什麼。
圓形 | 橢圓 | 圓角矩形 |
---|---|---|
一、靜態繪製
1. 繪製單體波
最重要的是知道自己想畫什麼。先看一下曲線怎麼畫。上一篇說過,
二貝最重要的是兩個點控制點
和終點
。如下圖,即可得到一個波峰。
為波的寬高各取一個變數,
waveWidth
,waveHeight
,呢麼很容易得到這三個點的座標
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth/2, -waveHeight*2,
waveWidth, 0);
複製程式碼
這樣就繪製了一個
波
,通過waveWidth
,waveHeight
控制長度和寬度。
2. 二貝的相對繪製
先對繪製
relativeQuadraticBezierTo
,是以當前點為參考點進行繪製。
也就再畫線是剛才的終點相當於0,0。 複製一份就是一個波。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
複製程式碼
我們想要的是類似正弦的波,稍微改一筆即可。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
複製程式碼
再拷貝一份,就又是一個波。值就是相對繪製的好處。
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
複製程式碼
3. 實現波動的原理
接下來是很關鍵的一步,為了好看,我畫了一個輔助的紫色box,並左移兩個波。
canvas.save();
canvas.translate(-2*waveWidth, 0);
_mainPath.moveTo(-2*waveWidth, 0);
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.close();
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill);
canvas.restore();
複製程式碼
然後畫出底部區域,我將下面的波高改為了20.
canvas.save();
canvas.translate(-2*waveWidth, 0);
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth*2 * 2.0, 0);
_mainPath.close();
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill);
canvas.restore();
複製程式碼
這樣靜態的繪製就已經over了。接下來的事情就非常簡單了,讓波不斷的移動即可。
二. 實現動畫
1. 定義動畫器
AnimationController可以讓數字在0~1間不斷變化。在變化時對介面進行重新整理
畫布中接受一個factor
的移動因子,在點選時執行AnimationController#repeat
來不斷執行
class TolyWave extends StatefulWidget {
@override
_TolyWaveState createState() => _TolyWaveState();
}
class _TolyWaveState extends State<TolyWave> with SingleTickerProviderStateMixin{
AnimationController _controller;
@override
void initState() {
//橫屏
SystemChrome.setPreferredOrientations([DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight]);
//全屏顯示
SystemChrome.setEnabledSystemUIOverlays([]);
_controller = AnimationController(vsync: this,duration: Duration(milliseconds: 500))
..addListener((){
setState(() {
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail) => _controller.repeat(),
child: CustomPaint(
painter: BezierPainter(factor: _controller.value),
),
);
}
}
複製程式碼
然後在畫布中移動
2*waveWidth*factor
即可得到一個不斷運動的波。
canvas.save();
canvas.translate(-2*waveWidth+2*waveWidth*factor, 0);
// 英雄所見...
canvas.restore();
複製程式碼
2. 畫布裁剪
可能現在你還沒有看出什麼,那我現在將紫色矩形框裁一下
@override
void paint(Canvas canvas, Size size) {
canvas.clipRect((Rect.fromCenter(
center: Offset( waveWidth, 0),width: waveWidth*2,height: 200.0)));
canvas.save();
// 英雄所見...
複製程式碼
快速 | 慢速 | 寬度 |
---|---|---|
這樣一來,基本的邏輯算是整清了
3. 動畫曲線
既然用了動畫,怎麼能少的了曲線。
fastOutSlowIn | easeInQuad | linear |
---|---|---|
class _TolyWaveState extends State<TolyWave> with SingleTickerProviderStateMixin{
AnimationController _controller;
Animation _anim;
@override
void initState() {
//英雄所見...
_anim = CurveTween(curve: Curves.linear).animate(_controller);
super.initState();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanDown: (detail) => _controller.repeat(reverse: false),
child: CustomPaint(
painter: BezierPainter(factor: _anim.value),
),
);
}
}
複製程式碼
4. 二重波
原理也很簡單,在原來的基礎上,再畫一個移動速度翻倍的波,將原來的透明度變淺即可 由於速度變成兩倍,移動距離邊長,所以波形需要三份。
fastOutSlowIn | easeInQuad | linear |
---|---|---|
@override
void paint(Canvas canvas, Size size) {
center = center.translate(-size.width / 2, 0);
canvas.drawColor(Colors.white, BlendMode.color);
canvas.translate(size.width / 2, size.height / 2);
canvas.clipPath(Path()..addRect(Rect.fromCenter(center: Offset( waveWidth, 0),width: waveWidth*2,height: 200.0)));
// _drawGrid(canvas, size); //繪製格線
// _drawAxis(canvas, size); //繪製軸線
canvas.save();
canvas.save();
canvas.translate(-4*waveWidth+2*waveWidth*factor, 0);
drawWave(canvas);
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill..color=Colors.red.withAlpha(88));
canvas.restore();
canvas.translate(-4*waveWidth+2*waveWidth*factor*2, 0);
drawWave(canvas);
canvas.drawPath(_mainPath, _mainPaint..style=PaintingStyle.fill..color=Colors.red);
canvas.restore();
}
void drawWave(Canvas canvas) {
_mainPath.moveTo(0, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, -waveHeight*2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(waveWidth/2, waveHeight*2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth*3 * 2.0, 0);
_mainPath.close();
}
複製程式碼
下面就來揭密為什麼動畫只是視覺的騙術。
你所見的永動,只是區域性範圍的重複。
把剪裁的區域去掉,也就是下面這醜陋的東西。
5. 圓形剪裁
除了規規整整的矩形,也可以裁成橢圓
圓形 | 橢圓 | 圓角矩形 |
---|---|---|
---->[圓]----
canvas.clipPath(Path()
..addOval(Rect.fromCenter(
center: Offset( waveWidth, 0),width: waveWidth*2,height: waveWidth*2)));
---->[橢圓]----
canvas.clipPath(Path()
..addOval(Rect.fromCenter(
center: Offset( waveWidth, 0),
width: waveWidth*2,height: 200.0)));
---->[圓角矩形]----
canvas.clipPath(Path()
..addRRect(RRect.fromRectXY(Rect.fromCenter(
center: Offset( waveWidth, 0),
width: waveWidth*2,height: 200.0), 30 , 30)));
複製程式碼
到此為止鋪墊結束,大家可以下載用
toly_wave.dart
檔案自己玩玩
三、FlutterWaveLoading
元件
核心的原理和思想都說完了,就不廢話了,下面直接貼原始碼,想研究的自己研究一下。不想研究的可以直接拿去用。
List.generate(9, (v) => 0.1 * v+0.1)
.map((e) => FlutterWaveLoading(
width: 75, //寬
height: 75,//高
isOval: true, // 是否橢圓裁切
progress: e, // 進度
waveHeight: 3, //波浪高
color: Colors.blue, //顏色
))
.toList()
複製程式碼
/// create by 張風捷特烈 on 2020-04-04
/// contact me by email 1981462002@qq.com
/// 說明: 貝塞爾曲線測試畫布
///
class FlutterWaveLoading extends StatefulWidget {
final double width;
final double height;
final double waveHeight;
final Color color;
final double strokeWidth;
final double progress;
final double factor;
final int secondAlpha;
final double borderRadius;
final bool isOval;
FlutterWaveLoading(
{
this.width = 100,
this.height = 100/0.618,
this.factor = 1,
this.waveHeight = 5,
this.progress = 0.5,
this.color = Colors.green,
this.strokeWidth = 3,
this.secondAlpha = 88,
this.isOval = false,
this.borderRadius = 20});
@override
_FlutterWaveLoadingState createState() => _FlutterWaveLoadingState();
}
class _FlutterWaveLoadingState extends State<FlutterWaveLoading>
with SingleTickerProviderStateMixin {
AnimationController _controller;
Animation _anim;
@override
void initState() {
_controller =
AnimationController(vsync: this, duration: Duration(milliseconds: 1200))
..addListener(() {
setState(() {});
})
..repeat();
_anim = CurveTween(curve: Curves.linear).animate(_controller);
super.initState();
}
@override
Widget build(BuildContext context) {
return UnconstrainedBox(
child: Container(
width: widget.width,
height: widget.height,
child: CustomPaint(
painter: BezierPainter(
factor: _anim.value,
waveHeight: widget.waveHeight,
progress: widget.progress,
color: widget.color,
strokeWidth: widget.strokeWidth,
secondAlpha: widget.secondAlpha,
isOval: widget.isOval,
borderRadius: widget.borderRadius),
),
),
);
}
}
class BezierPainter extends CustomPainter {
Paint _mainPaint;
Path _mainPath;
double waveWidth = 80;
double wrapHeight;
final double waveHeight;
final Color color;
final double strokeWidth;
final double progress;
final double factor;
final int secondAlpha;
final double borderRadius;
final bool isOval;
BezierPainter(
{this.factor = 1,
this.waveHeight = 8,
this.progress = 0.5,
this.color = Colors.green,
this.strokeWidth = 3,
this.secondAlpha = 88,
this.isOval = false,
this.borderRadius = 20}) {
_mainPaint = Paint()
..color = Colors.yellow
..style = PaintingStyle.stroke
..strokeWidth = 2;
_mainPath = Path();
}
@override
void paint(Canvas canvas, Size size) {
print(size);
waveWidth = size.width / 2;
wrapHeight = size.height;
Path path = Path();
if (!isOval) {
path.addRRect(
RRect.fromRectXY(Offset(0, 0) & size, borderRadius, borderRadius));
canvas.clipPath(path);
canvas.drawPath(
path,
_mainPaint
..strokeWidth = strokeWidth
..color = color);
}
if (isOval) {
path.addOval(Offset(0, 0) & size);
canvas.clipPath(path);
canvas.drawPath(
path,
_mainPaint
..strokeWidth = strokeWidth
..color = color);
}
canvas.translate(0, wrapHeight);
canvas.save();
canvas.translate(0, waveHeight);
canvas.save();
canvas.translate(-4 * waveWidth + 2 * waveWidth * factor, 0);
drawWave(canvas);
canvas.drawPath(
_mainPath,
_mainPaint
..style = PaintingStyle.fill
..color = color.withAlpha(88));
canvas.restore();
canvas.translate(-4 * waveWidth + 2 * waveWidth * factor * 2, 0);
drawWave(canvas);
canvas.drawPath(
_mainPath,
_mainPaint
..style = PaintingStyle.fill
..color = color);
canvas.restore();
}
void drawWave(Canvas canvas) {
_mainPath.moveTo(0, 0);
_mainPath.relativeLineTo(0, -wrapHeight * progress);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, -waveHeight * 2, waveWidth, 0);
_mainPath.relativeQuadraticBezierTo(
waveWidth / 2, waveHeight * 2, waveWidth, 0);
_mainPath.relativeLineTo(0, wrapHeight);
_mainPath.relativeLineTo(-waveWidth * 3 * 2.0, 0);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
複製程式碼
尾聲
另外本人有一個Flutter微信交流群,歡迎小夥伴加入,共同探討Flutter的問題,期待與你的交流與切磋。
@張風捷特烈 2019.04.04 未允禁轉
我的公眾號:程式設計之王
聯絡我--郵箱:1981462002@qq.com --微信:zdl1994328
~ END ~