
最近在用Flutter 模仿英雄聯(lián)盟客戶端,
制作頂部菜單的時候,發(fā)現(xiàn)了之前沒有注意的細節(jié)效果,寫個文章分享和記錄一下。

分析:鼠標移動到菜單上,會產(chǎn)生一個光暈,從底部向上扇形擴散,光暈的中心點就是鼠標的X軸坐標。
現(xiàn)在嘗試來復現(xiàn)這個效果。
我們需要用到的第一個組件是:CustomPaint
它是一個自己處理繪制的組件。
一定要用RepaintBoundary包住,不然它會被其他組件的notifyListeners()影響導致CustomPaint重繪,如果沒有用RepaintBoundary包裹,它自身的notifyListeners()行為也會影響其他組件導致重繪。
RepaintBoundary(
//底層光圈繪制層
child:CustomPaint(painter:_painter,
// 上層元素child:widget.child,
),)
class TopMenuShadowPainter extends ChangeNotifier implements CustomPainter {
? TopMenuShadowPainter();
? @override
? void paint(Canvas canvas, Size size) {
? ? // 圓的尺寸
? ? double radius = 80;
? ? // 繪制一個 圓形
? ? canvas.drawOval(
? ? ? Rect.fromLTWH(0, 0, radius, radius),
? ? ? paint,
? ? );
? ? // 或者繪制一個 扇形? pi是系統(tǒng)定義的 3.1415926535897932 , pi + pi 就是從左側(cè)水平? 0°到 右側(cè) 180°
? ? canvas.drawArc(
? ? ? Rect.fromLTWH(0, 0, radius, radius),
? ? ? pi,
? ? ? pi,
? ? ? true,
? ? ? paint,
? ? );
? }


接下來就是讓這個圓在鼠標的位置中心點繪制,所以需要把鼠標傳進去,一開始我使用的是AnimatedBuilder+CustomPaint,這種方案每次繪制都會重新創(chuàng)建新的painter對象,不是特別的好。
我參考了Flutter 繪制探索 1 | CustomPainter 正確刷新姿勢 | 七日打卡這篇文章,里面有講到CustomPaint的幾種刷新機制,當前這個Menu 比較適合采用 ChangeNotifier的方式進行刷新,讓當前TopMenuShadowPainter繼承ChangeNotifier,并且實現(xiàn)CustomPainter,這樣它就帶有通知刷新和繪制能力。
我們需要使用MouseRegion捕獲鼠標的位置和移出移入事件。
/// 繪制器
late TopMenuShadowPainter _painter;
MouseRegion(
? ? ? /// 顯示為手指的效果
? ? ? cursor: SystemMouseCursors.click,
? ? ? onEnter: (event) {
? ? ? ? // 鼠標進入
? ? ? ? _painter.updateEnter(true);
? ? ? },
? ? ? onExit: (event) {
? ? ? ? // 鼠標離開
? ? ? ? _painter.updateEnter(false);
? ? ? },
? ? ? onHover: (event) {
? ? ? ? // 鼠標坐標移動
? ? ? ? _painter.updatePoint(event.localPosition.dx);
? ? ? },
? ? ? child: RepaintBoundary(
? ? ? ? //底層光圈繪制層
? ? ? ? child: CustomPaint(
? ? ? ? ? painter: _painter,
? ? ? ? ? child: widget.child,
? ? ? ? ),
? ? ? ),
);
內(nèi)部更新鼠標的坐標和進出狀態(tài)。
TopMenuShadowPainter extends ChangeNotifier implements CustomPainter
// 使用方法刷新鼠標的進入離開狀態(tài) 以及 鼠標的 坐標 =-=
// notifyListeners 觸發(fā)的時候,沒有用RepaintBoundary,會導致其他部分組件重繪。
/// 鼠標進入
? bool mouseEnter = false;
? // 鼠標的坐標點
? double mouseX = 0;
? /// 更新
? void updateEnter(bool newMouseEnter) {
? ? mouseEnter = newMouseEnter;
? ? //debugPrint("鼠標進入退出:$mouseEnter");
? ? notifyListeners();
? }
? /// 更新
? void updatePoint(double newMouseX) {
? ? mouseX = newMouseX;
? ? notifyListeners();
? }
重新計算繪制的圓位置。
// 計算鼠標的坐標 產(chǎn)生圓的位置
// 圓起點的X坐標 計算方式 = 鼠標的X軸坐標(鼠標是居中位置) 減去 圓的一半
double x =? mouseX - radius / 2,
// 圓起點的Y坐標 計算方式 = 繪圖Canvs的高度的一半
// 向下偏移 10px(使光暈更貼底,這個值也可以不加或者更大)
double y =? size.height / 2 + 10,
// 繪制圓
canvas.drawOval(
? ? Rect.fromLTWH(
? ? ? ? mouseX - radius? / 2,
? ? ? ? size.height / 2 + 10,
? ? ? ? radius,
? ? ? ? radius,
? ? ),
? ? paint,
);
這時候我們設(shè)置一下裁剪區(qū)域,
// 從0,0坐標 擴展到畫板尺寸size的Rect區(qū)域
canvas.clipRect(Offset.zero? & size);
調(diào)整一下裁剪區(qū)域
// 裁剪區(qū)域 向左移動 圓的大小 向右也移動圓的的大小,radius
Rect clipRect = Rect.fromLTRB(- radius, 0, size.width + radius, size.height);
canvas.clipRect(clipRect);
接下來,再給光暈增加一個高斯模糊的效果。
var paint = Paint()
? ? ? // 背景漸變刷子
? ? ? ..shader = gradient
? ? ? // 模糊效果
? ? ? ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 20);
// 繪制底部的金線
canvas.drawLine(
? ? ? Offset(mouseX - 50, size.height),
? ? ? Offset(mouseX + 50, size.height),
? ? ? Paint()
? ? ? ? ..strokeCap = StrokeCap.round
? ? ? ? // 從 中間向兩邊 漸變的背景刷
? ? ? ? ..shader = lineColor
? ? ? ? ..strokeWidth = 1.5,
? ? );
