Flutter 仿英雄联盟客户端详解

3,381 阅读4分钟

371shots_so.png

最近在用Flutter 模仿英雄联盟客户端,

制作顶部菜单的时候,发现了之前没有注意的细节效果,写个文章分享和记录一下。


3ae51f1da5262dbe4f2c71d4ed081553_v2-6e44af480b53a6e3bcded2917319a25c_1440w.jpg

分析:鼠标移动到菜单上,会产生一个光晕,从底部向上扇形扩散,光晕的中心点就是鼠标的X轴坐标。

现在尝试来复现这个效果。

我们需要用到的第一个组件是: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是系统定义的 3.1415926535897932 , pi + pi 就是从左侧水平  0°到 右侧 180°
    canvas.drawArc(
      Rect.fromLTWH(0, 0, radius, radius),
      pi,
      pi,
      true,
      paint,
    );
  }

4d55a482d5db7a262fea1c7553cbadde_v2-2e4686f2ba72c040ed198f8ea8aef3c2_1440w.webp 绘制出大小80直径的圆

9b01be4556e38ae986f3a701f178fdb2_v2-fba386e8689ac015fb0394644ee38d84_1440w.webp 绘制出80直径半圆


接下来就是让这个圆在鼠标的位置中心点绘制,所以需要把鼠标传进去,一开始我使用的是AnimatedBuilder+CustomPaint, 这种方案每次绘制都会重新创建新的painter对象,不是特别的好。

我参考了Flutter 绘制探索 1 | CustomPainter 正确刷新姿势 | 七日打卡这篇文章,里面有讲到CustomPaint的几种刷新机制,当前这个Menu 比较适合采用 ChangeNotifier的方式进行刷新,让当前TopMenuShadowPainter继承ChangeNotifier,并且实现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,
        ),
      ),
);

内部更新鼠标的坐标和进出状态。

TopMenuShadowPainter extends ChangeNotifier implements CustomPainter

// 使用方法刷新鼠标的进入离开状态 以及 鼠标的 坐标 =-=
// notifyListeners 触发的时候,没有用RepaintBoundary,会导致其他部分组件重绘。

/// 鼠标进入
  bool mouseEnter = false;

  // 鼠标的坐标点
  double mouseX = 0;

  /// 更新
  void updateEnter(bool newMouseEnter) {
    mouseEnter = newMouseEnter;
    //debugPrint("鼠标进入退出:$mouseEnter");
    notifyListeners();
  }

  /// 更新
  void updatePoint(double newMouseX) {
    mouseX = newMouseX;
    notifyListeners();
  }

重新计算绘制的圆位置。

// 计算鼠标的坐标 产生圆的位置
// 圆起点的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,
);

好的,我们现在来看看效果。

97965e7f76c0c77f861655dc657820bc_v2-c04b443685c90d406d98648521296adf_1440w.webp

ops?咋回事,溢出了。。

这时候我们设置一下裁剪区域,

// 从0,0坐标 扩展到画板尺寸size的Rect区域
canvas.clipRect(Offset.zero  & size);

eac545b935403a9b56dd35b651afca3d_v2-13aebf677de3cb0cd34ef93fe0875909_1440w.webp Y轴溢出问题解决

33c8a873376040731d8347eefb125d6c_v2-c1858438746090cb4f02dfc710c4a7bd_1440w.webp Y轴不溢出了,但是X轴上面的被裁剪掉了

调整一下裁剪区域

// 裁剪区域 向左移动 圆的大小 向右也移动圆的的大小,radius
Rect clipRect = Rect.fromLTRB(- radius, 0, size.width + radius, size.height);
canvas.clipRect(clipRect);

3abf584b0b08324b8d6d749d788667e0_v2-b00518f47069343f4d4bd6cbd74e0e5c_1440w.webp

接下来,再给光晕增加一个高斯模糊的效果。

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,
    );

5f919f72cf41c7df9bd09101d8cbd52d_v2-384127bc0dd7b81daec4eb575aab8910_1440w.webp

PS:最开始我是给每个菜单包一个这个渲染,因为我是模仿英雄联盟做的,后面封装了一个完整的容器,这样多个菜单共享一个背景刷,效果会好很多。

另外一个 PLAY按钮的具体做法,目前有点LOW,大概讲一下思路,看看后面有没有更好的方案

a4c85abc94d9bd735c6b1c7914b3179d_v2-0a382b04ebfef5c6c6cbbd25aa0d51ce_1440w.webp 默认效果

e6ac5335cecfc3bd1fa1c7878c175e03_v2-e487b80c6e017b216fa096ee368c0205_1440w.webp 随机圆点

6cc38e7614c63b4b1a74b873db97d9bf_v2-29a13f79c0911b9530d7f9c73da79808_1440w.webp 开启模糊

鼠标移入的时候,开启一个循环动画。绘制随机的光斑(模糊),每一帧向左侧偏移0~1px,最终效果就还能接受,等优化了。

最后附上源码地址,可以下载编译 或者 下载 编译好的包 体验。 github.com/944095635/l…