前言

贝塞尔曲线的应用填补了计算机绘制与手绘之前的差距,更能表达人想画出的曲线,为了更好的理解万能的贝塞尔曲线,而海豚是我认为在海洋生物中身体曲线最完美的海洋生物,在海洋中游泳速度最高可达80km/h;比驱逐舰速度还快,学习绘制正好学到了贝塞尔曲线,那么我们今天就用贝塞尔曲线画看看能不能画一只可爱的小海豚呢。

效果图

先上效果图:

实现步骤

path路径绘制贝塞尔曲线的方法非常简单,只需要传入控制点即可,二阶就传1个控制点1个终点,三阶就传2个控制点和1个终点,但是要找到合适控制的点就没那么容易了,这时候我们如果可以用手指在屏幕上不断调试寻找合适的点岂不是非常方便,接下来我们就先实现下面的功能,通过手指不断调试控制点位并将多个贝塞尔曲线进行连接。

可以看到一个三阶贝塞尔需要1个起点、2个控制点和1个终点组成,首先我们需要通过手势识别将这些控制点存储起来然后赋值给绘制组件进行更新就可以了,这里我们需要用到状态管理ChangeNotifier类,它继承Listenable,因为在绘制组件的构造方法里有一个参数repaint接受Listenable类型来控制是否重新绘制,数据变化就重新绘制。

const CustomPainter({ Listenable? repaint }) : _repaint = repaint;

因为CustomPainter的构造方法里的repaint参数就是负责更新绘制的,所以我们先要定义一个类继承ChangeNotifier来存储这些数据。

代码:

class TouchController extends ChangeNotifier {
  List<Offset> _points = []; //点集合
  int _selectIndex = -1;// 选中的点 更新位置用

  int get selectIndex => _selectIndex;

  List<Offset> get points => _points;

  // 选择某一个点 保存index
  set selectIndex(int value) {
    if (_selectIndex == value) return;
    _selectIndex = value;
    notifyListeners();// 通知刷新
  }
   // 选中的点标记
  Offset? get selectPoint => _selectIndex == -1 ? null : _points[_selectIndex];

  // 添加一个点
  void addPoint(Offset point) {
    points.add(point);
    notifyListeners();
  }
   // 手指移动时更新当前点的位置
  void updatePoint(int index, Offset point) {
    points[index] = point;
    notifyListeners();
  }
    // 删除最后一个点 相当于撤回上一步操作
  void removeLast() {
    points.removeLast();
    notifyListeners();
  }

}

有了存储数据的空间之后,我们就需要通过手势去获取这些点,通过手势在画布上的操作获取当前的位置进行存储以及更新。

 GestureDetector(
  child: CustomPaint(
    painter:
        _DolphinPainter(widget.touchController, widget.image),
  ),
  onPanDown: (d) {
    // 按压
    judgeZone(d.localPosition);
  },
  onPanUpdate: (d) {
    // 移动
    if (widget.touchController.selectIndex != -1) {
      widget.touchController.updatePoint(
          widget.touchController.selectIndex, d.localPosition);
    }
  },
)
///判断出是否在某点的半径为r圆范围内
bool judgeCircleArea(Offset src, Offset dst, double r) =>
    (src - dst).distance <= r;
///手指按下触发
void judgeZone(Offset src) {
  /// 循环所有的点
  for (int i = 0; i < widget.touchController.points.length; i  ) {
    // 判断手指按的位置有没有按过的点
    if (judgeCircleArea(src, widget.touchController.points[i], 20)) {
      // 有点 不添加更新选中的点
      widget.touchController.selectIndex = i;
      return;
    }
  }
  // 无点 添加新的点 并将选中的点清空
  widget.touchController.addPoint(src);
  widget.touchController.selectIndex = -1;
}

到这里我们的手势按压和移动就会将数据存储到我们刚才定义的类中,接下来我们需要将这些数据赋予真正的绘制组件 CustomPainter

class _DolphinPainter extends CustomPainter {
  final TouchController touchController;// 存储数据类
//  final ui.Image image;

  _DolphinPainter(this.touchController, this.image)
    // 这个地方传入需要更新的 Listenable
      : super(repaint: touchController);

  List<Offset>? pos; //存储手势按压的点

  @override
  void paint(Canvas canvas, Size size) {
    // 画布原点平移到屏幕中央
    canvas.translate(size.width / 2, size.height / 2);
    // ,因为手势识别的原点是左上角,所以这里将存储的点相对的原点进行偏移到跟画布一致 负值向左上角偏移
    pos = touchController.points
        .map((e) => e.translate(-size.width / 2, -size.height / 2))
        .toList();

// 定义画笔
    var paint = Paint()
      ..strokeWidth = 2
      ..color = Colors.purple
      ..style = PaintingStyle.stroke
      ..isAntiAlias = true;

    // canvas.drawImage(image, Offset(-image.width / 2, -image.height / 2), paint);

    // 如果点小于4个 那么就只绘制点 如果>=4个点 那么就绘制贝塞尔曲线
    if (pos != null && pos!.length >= 4) {
      var path = Path();
      // 设置起点 手指第一个按压的点
      path.moveTo(pos![0].dx, (pos![0].dy));
      // path添加第一个贝塞尔曲线
      path.cubicTo(pos![1].dx,pos![1].dy, pos![2].dx, pos![2].dy, pos![3].dx,
          pos![3].dy);
          //绘制辅助线
      _drawHelpLine(canvas, size, paint, 0);
      // 绘制首个贝塞尔曲线
      canvas.drawPath(path, paint..color = Colors.purple);
      
      // for循环 绘制第2个以后的曲线 以上个终点为下一个的起点
      for (int i = 1; i < (pos!.length - 1) ~/ 3; i  ) {
          //之后贝塞尔曲线的起点都是上一个贝塞尔曲线的终点
          // 比如第一个曲线 1,2,3,4.第二个就是4,5,6,7...以此类推,这样我们才能把线连接起来绘制图案
        // 这里把绘制之前的颜色覆盖
      // canvas.drawPath(path, paint..color = Colors.white);
        // 绘制辅助线
        _drawHelpLine(canvas, size, paint, i);
        //绘制贝塞尔曲线
        path.cubicTo(
          pos![i * 3   1].dx,
          pos![i * 3   1].dy,
          pos![i * 3   2].dx,
          pos![i * 3   2].dy,
          pos![i * 3   3].dx,
          pos![i * 3   3].dy,
        );

        if (i == 8) {
          path.close();
        }
        canvas.drawPath(path, paint..color = Colors.purple);
      }

      // 绘制辅助点
      _drawHelpPoint(canvas, paint);
      // 选中点
      _drawHelpSelectPoint(canvas, size, paint);
    } else {
      // 绘制辅助点
      _drawHelpPoint(canvas, paint);
    }


    // 画眼睛 眼睛位于起点的左侧,所以中心点向左偏移
    canvas.drawCircle(
        pos!.first.translate(-50, 5),
        10,
        paint
          ..color = Colors.black87
          ..style = PaintingStyle.stroke
          ..strokeWidth = 2);
    canvas.drawCircle(
        pos!.first.translate(-53, 5),
        7,
        paint
          ..color = Colors.black87
          ..style = PaintingStyle.fill);
  }
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
  return false;
}

void _drawHelpPoint(Canvas canvas, Paint paint) {
  canvas.drawPoints(
      PointMode.points,
      pos ?? [],
      paint
        ..strokeWidth = 10
        ..strokeCap = StrokeCap.round
        ..color = Colors.redAccent);
}

void _drawHelpSelectPoint(Canvas canvas, Size size, Paint paint) {
  Offset? selectPos = touchController.selectPoint;
  selectPos = selectPos?.translate(-size.width / 2, -size.height / 2);
  if (selectPos == null) return;
  canvas.drawCircle(
      selectPos,
      10,
      paint
        ..color = Colors.green
        ..strokeWidth = 2);
}

void _drawHelpLine(Canvas canvas, Size size, Paint paint, int i) {
  canvas.drawLine(
      Offset(pos![i * 3].dx, pos![i * 3].dy),
      Offset(pos![i * 3   1].dx, pos![i * 3   1].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);

  canvas.drawLine(
      Offset(pos![i * 3   1].dx, pos![i * 3   1].dy),
      Offset(pos![i * 3   2].dx, pos![i * 3   2].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);

  canvas.drawLine(
      Offset(pos![i * 3   2].dx, pos![i * 3   2].dy),
      Offset(pos![i * 3   3].dx, pos![i * 3   3].dy),
      paint
        ..color = Colors.redAccent
        ..strokeWidth = 2);
}

最终在我们的手指的控制以及辅助线的帮助下,图案就慢慢的绘制出来了。

去掉辅助线和点

然后将画笔改为填充,那么就得到我们一开始那副可爱的小海豚了。

总结

通过这个小海豚图案我们可以更加的理解贝塞尔曲线的绘制机制,通过你的手势控制,你也可以画出任何曲线和任何图案,可以说贝塞尔曲线就是绘制中的灵魂,掌握了贝塞尔曲线就相当于掌握了所有绘制组件,因为理论上来说,所有的二维图形都可以被贝塞尔曲线画出来,只要我们能准确的找到控制的点,就可以绘制无限可能的图案。

以上就是Android Flutter利用贝塞尔曲线画一个小海豚的详细内容,更多关于Android Flutter海豚的资料请关注Devmax其它相关文章!

Android Flutter利用贝塞尔曲线画一个小海豚的更多相关文章

  1. 基于canvas使用贝塞尔曲线平滑拟合折线段的方法

    这篇文章主要介绍了基于canvas使用贝塞尔曲线平滑拟合折线段的方法的相关资料,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. html5 canvas合成海报所遇问题及解决方案总结

    这篇文章主要介绍了html5 canvas合成海报所遇问题及解决方案总结,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  3. 使用SVG实现提示框功能的示例代码

    这篇文章主要介绍了使用SVG实现提示框功能的示例代码,代码简单易懂,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  4. canvas进阶之贝塞尔公式推导与物体跟随复杂曲线的轨迹运动

    这篇文章主要介绍了canvas进阶之贝塞尔公式推导与物体跟随复杂曲线的轨迹运动,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  5. Html5 video标签视频的最佳实践

    这篇文章主要介绍了Html5 video标签视频的最佳实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  6. canvas仿写贝塞尔曲线的示例代码

    这篇文章主要介绍了canvas仿写贝塞尔曲线的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  7. HTML5在微信内置浏览器下右上角菜单的调整字体导致页面显示错乱的问题

    HTML5在微信内置浏览器下,在右上角菜单的调整字体导致页面显示错乱的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧

  8. ios – containerURLForSecurityApplicationGroupIdentifier:在iPhone和Watch模拟器上给出不同的结果

    我使用默认的XCode模板创建了一个WatchKit应用程序.我向iOSTarget,WatchkitAppTarget和WatchkitAppExtensionTarget添加了应用程序组权利.(这是应用程序组名称:group.com.lombax.fiveminutes)然后,我尝试使用iOSApp和WatchKitExtension访问共享文件夹URL:延期:iOS应用:但是,测试NSURL

  9. Ionic – Splash Screen适用于iOS,但不适用于Android

    我有一个离子应用程序,其中使用CLI命令离子资源生成的启动画面和图标iOS版本与正在渲染的启动画面完美配合,但在Android版本中,只有在加载应用程序时才会显示白屏.我检查了config.xml文件,所有路径看起来都是正确的,生成的图像出现在相应的文件夹中.(我使用了splash.psd模板来生成它们.我错过了什么?这是config.xml文件供参考,我觉得我在这里做错了–解决方法在config.xml中添加以下键:它对我有用!

  10. ios – 无法启动iPhone模拟器

    /Library/Developer/CoreSimulator/Devices/530A44CB-5978-4926-9E91-E9DBD5BFB105/data/Containers/Bundle/Application/07612A5C-659D-4C04-ACD3-D211D2830E17/ProductName.app/ProductName然后,如果您在Xcode构建设置中选择标准体系结构并再次构建和运行,则会产生以下结果:dyld:lazysymbolbindingFailed:Symbol

随机推荐

  1. Flutter 网络请求框架封装详解

    这篇文章主要介绍了Flutter 网络请求框架封装详解,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

  2. Android单选按钮RadioButton的使用详解

    今天小编就为大家分享一篇关于Android单选按钮RadioButton的使用详解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧

  3. 解决android studio 打包发现generate signed apk 消失不见问题

    这篇文章主要介绍了解决android studio 打包发现generate signed apk 消失不见问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧

  4. Android 实现自定义圆形listview功能的实例代码

    这篇文章主要介绍了Android 实现自定义圆形listview功能的实例代码,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  5. 详解Android studio 动态fragment的用法

    这篇文章主要介绍了Android studio 动态fragment的用法,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

  6. Android用RecyclerView实现图标拖拽排序以及增删管理

    这篇文章主要介绍了Android用RecyclerView实现图标拖拽排序以及增删管理的方法,帮助大家更好的理解和学习使用Android,感兴趣的朋友可以了解下

  7. Android notifyDataSetChanged() 动态更新ListView案例详解

    这篇文章主要介绍了Android notifyDataSetChanged() 动态更新ListView案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下

  8. Android自定义View实现弹幕效果

    这篇文章主要为大家详细介绍了Android自定义View实现弹幕效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  9. Android自定义View实现跟随手指移动

    这篇文章主要为大家详细介绍了Android自定义View实现跟随手指移动,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Android实现多点触摸操作

    这篇文章主要介绍了Android实现多点触摸操作,实现图片的放大、缩小和旋转等处理,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

返回
顶部