Flutter刮刮乐仿滑动互动广告

灵感来源

仿照在刷科目一时的软件广告,觉得很有意思,故而仿之; 滑动到一定范围后,全部清除,并且弹窗(这里gif没有录制到) 项目地址杉木笙/ads_drawing (gitee.com)

实现效果

思路分析

一开始要实现这个效果,初步分析是Stack中CustomPaint和图片还有阴影叠在一起;

但是这样下来距离目标相差甚远,仔细想了一下问题出在哪里

  • 灰色半透明遮罩从何而来
  • 会采用绘制吗,应该绘制什么
  • 如果采用绘制,怎么判断绘制到一定程度来实现弹窗或其他效果?

思路实现

这个章节里的代码即为最终实现代码.一点一线皆是河山

在这里先放下几个问题,在目录问题解决中解决

  1. NewPoint类的对比问题
  2. 绘制是Rect,为了美观怎么转换成RRect
  3. 点太过密集,导致卡顿

如何绘制

因为绘制的目标不是单纯的纯色,而是图片;所以这里要用到绘制方法采用drawImageRect.

drawImageRect可以理解为把原图的一部分(srcRect)填充到绘制的地方(dstRect)中.

arduino 复制代码
canvas.drawImageRect(image!, srcRect, dstRect, paint);
//image是图片源
//srcRect是来自图片的区域
//dstRect是实际画的位置
//paint是画笔

于是有了下面这个例子

暂且忽视point.toOffset()在这里的作用只是一个坐标点,比如Offset(10,10)

arduino 复制代码
final srcRect = Rect.fromLTWH(  
  point.toOffset().dx * 2,  
  point.toOffset().dy * 2,  
  20,  
  20,  
);  
final dstRect = Rect.fromLTWH(  
  point.toOffset().dx,  
  point.toOffset().dy,  
  10,  
  10,  
);   
canvas.drawImageRect(image!, srcRect, dstRect, paint);

用这个方法,就可以画出2:1的图片 此时上面的方法还不能画出下面这个效果,上面只是一个区块,下图则是很多个点拼在一起的

这里有个技巧

srcRect与dstRect的比例就是绘制出来的比例,或者也可以看成缩放,2:1相当于缩放了一倍

一点一线

根据上面已经可以实现绘画出图片的一部分,也可以叫做一个点. 下面则是从一点到一线之间的变化,下面的代码会用到状态管理插件官网 provider | Flutter package (pub.dev) 从一点到一线这里采用的是GestureDetector来获取手势位移配合绘制,来绘制图片

一点

在这里才是正式开始代码的编写,从理论到实践 新建一个dart文件,在文件中创建类NewPoint 在这里直接一步到底(最后实现的代码),不过在后面会逐步讲到为什么会用到某些地方

dart 复制代码
enum NewPaintState { doing, none }

class NewPoint extends Equatable {
  final int x;
  final int y;
  NewPoint({required this.x, required this.y});
  Offset toOffset() => Offset(x.toDouble(), y.toDouble());

  double get distance => sqrt(x * x + y * y);

  factory NewPoint.fromOffset(Offset offset) {
    return NewPoint(
      x: offset.dx.toInt(),
      y: offset.dy.toInt(),
    );
  }
  NewPoint operator -(NewPoint other) =>
      NewPoint(x: x - other.x, y: y - other.y);

  @override
  // TODO: implement props
  List<Object?> get props => [x, y];
}

一线

创建完点后,再接着创建线 在这里也是直接一步到底(最后实现的代码),不过在后面会逐步讲到为什么会用到某些地方

ini 复制代码
class NewLine {
  List<NewPoint> points = [];
  NewPaintState state;
  double strokeWidth;
  Color color;
  ui.Image? image;
  NewLine({
    this.color = Colors.lightBlue,
    this.strokeWidth = 1,
    this.state = NewPaintState.doing,
    this.image,
  });
  void paint(Canvas canvas, Paint paint) {
    if (image != null) {
      for (var point in points) {
        canvas.save();
        final srcRect = Rect.fromLTWH(
          point.toOffset().dx * 4,
          point.toOffset().dy * 4,
          40,
          40,
        );
        final dstRect = Rect.fromLTWH(
          point.toOffset().dx,
          point.toOffset().dy,
          10,
          10,
        );
        // 定义圆角矩形区域
        final rrect = RRect.fromRectAndRadius(
          dstRect,
          Radius.circular(5),
        );
        // canvas.drawImage(image!, point.toOffset(), paint);
        canvas.clipRRect(rrect);
        canvas.drawImageRect(image!, srcRect, dstRect, paint);
        canvas.restore();
      }
    }
  }
}

一点一线皆是河山

现在有了点和线后,就可以更好的去实现绘制,这里同样给出最终版,并且开始讲解用处

ini 复制代码
class NewAdsProvider extends ChangeNotifier {
  // List<NewLine> _lines = [];//废弃
  // List<NewLine> get lines => _lines;//废弃
  // int allPointsLength = 0;//废弃
  // List<NewPoint> allNewPoints = [];//废弃
  final double tolerance = 5.0;
  Map<String, bool> ignorings = {};
  Map<String, int> allPointsLengths = {};
  Map<String, bool> bgShadows = {};
  Map<String, List<NewLine>> _linesMap = {}; //
  Map<String, ui.Image> _images = {};
  List<NewLine> getLines(String key) => _linesMap[key] ?? [];
  ui.Image? getImage(String key) => _images[key];
  bool getBgShadows(String key) => bgShadows[key] ?? true;
  bool getIgnoring(String key) => ignorings[key] ?? false;
  int getAllPointsLength(String key) => allPointsLengths[key] ?? 0;

  ///获取目前的线
  NewLine getActiveLine(String key) =>
      _linesMap[key]?.singleWhere(
          (element) => element.state == NewPaintState.doing,
          orElse: () => NewLine(state: NewPaintState.none)) ??
      NewLine(state: NewPaintState.none);

  ///添加点
  void addPoint(String key, NewPoint point, {bool force = false}) {
    if (getActiveLine(key).points.isNotEmpty && !force) {
      if ((point - getActiveLine(key).points.last).distance < tolerance) return;
    }
    getActiveLine(key).points.add(point);
    allPointsLengths[key] = getAllPointsLength(key) + 1;
    print(allPointsLengths[key]);
    notifyListeners();
  }

  ///开始一条新线
  void startNewLine(String key, String imageKey) {
    if (_images.containsKey(imageKey)) {
      if (_linesMap[key] == null) {
        _linesMap[key] = [];
        bgShadows[key] = true;
        ignorings[key] = false;
        allPointsLengths[key] = 0;
      }
      _linesMap[key]!.add(NewLine(image: _images[imageKey]));
      notifyListeners();
    }
  }

  ///结束一条新线
  void endLine(String key) {
    if (getActiveLine(key).state == NewPaintState.doing) {
      getActiveLine(key).state = NewPaintState.none;
    }
    if (getAllPointsLength(key) > 300) {
      bgShadows[key] = false;
      ignorings[key] = true;
      _linesMap[key] = [];
      allPointsLengths[key] = 0;
    }
    notifyListeners();
  }

  /// 加载图片
  Future<void> loadImage(String key, String img) async {
    final ByteData data = await rootBundle.load(img);
    final ui.Image image = await _loadImage(Uint8List.view(data.buffer));
    _images[key] = image;
    notifyListeners();
  }

  Future<ui.Image> _loadImage(Uint8List img) async {
    final Completer<ui.Image> completer = Completer();
    ui.decodeImageFromList(img, (ui.Image img) {
      completer.complete(img);
    });
    return completer.future;
  }
//清除
  void clearAll(String key) {
    _linesMap[key]?.clear();
    notifyListeners();
  }
}

同时再附上页面实现的代码

less 复制代码
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => NewAdsProvider(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: DrawingScreen(),
    );
  }
}

class DrawingScreen extends StatefulWidget {
  @override
  _DrawingScreenState createState() => _DrawingScreenState();
}

class _DrawingScreenState extends State<DrawingScreen> {
  @override
  void initState() {
    super.initState();
    final providerInit = Provider.of<NewAdsProvider>(context, listen: false);
    providerInit.loadImage('img2', 'assets/vex1.jpg');
    providerInit.loadImage('img1', 'assets/saduzi.jpg');
  }

  @override
  Widget build(BuildContext context) {
    final adsProvider = Provider.of<NewAdsProvider>(context);
    return Scaffold(
      appBar: AppBar(
        title: Text('Gesture Drawing'),
        actions: [
          IconButton(
            icon: Icon(Icons.clear),
            onPressed: () {
              Provider.of<NewAdsProvider>(context, listen: false)
                  .clearAll('drawing1');
            },
          ),
        ],
      ),
      body: Column(
        children: [
          Expanded(
            child: Stack(
              children: [
                IgnorePointer(
                  ignoring: adsProvider.getIgnoring('drawing1'),
                  child: GestureDetector(
                    onPanStart: (details) {
                      adsProvider.startNewLine('drawing1', 'img1');
                      Offset localPosition = details.localPosition;
                      NewPoint newPoint = NewPoint.fromOffset(localPosition);
                      adsProvider.addPoint('drawing1', newPoint);
                    },
                    onPanUpdate: (details) {
                      Offset localPosition = details.localPosition;
                      NewPoint newPoint = NewPoint.fromOffset(localPosition);
                      adsProvider.addPoint('drawing1', newPoint);
                    },
                    onPanEnd: (details) {
                      adsProvider.endLine('drawing1');
                    },
                    child: Consumer<NewAdsProvider>(
                      builder: (context, adsProvider, child) {
                        return CustomPaint(
                          size: Size.infinite,
                          painter: MyPainter(adsProvider.getLines('drawing1'),
                              adsProvider.getBgShadows('drawing1')),
                        );
                      },
                    ),
                  ),
                ),
              ],
            ),
          ),
          Expanded(
            child: Stack(
              children: [
                Image.asset(
                  'assets/vex1.jpg',
                  width: 300,
                  fit: BoxFit.fitWidth,
                ),
                Visibility(
                  visible: adsProvider.getBgShadows('drawing2'),
                  child: Container(
                    width: 300,
                    height: 300,
                    color: Colors.grey.withOpacity(0.9),
                  ),
                ),
                GestureDetector(
                  onPanStart: (details) {
                    adsProvider.startNewLine('drawing2', 'img2');
                    Offset localPosition = details.localPosition;
                    NewPoint newPoint = NewPoint.fromOffset(localPosition);
                    adsProvider.addPoint('drawing2', newPoint);
                  },
                  onPanUpdate: (details) {
                    Offset localPosition = details.localPosition;
                    NewPoint newPoint = NewPoint.fromOffset(localPosition);
                    adsProvider.addPoint('drawing2', newPoint);
                  },
                  onPanEnd: (details) {
                    adsProvider.endLine('drawing2');
                  },
                  child: Consumer<NewAdsProvider>(
                    builder: (context, adsProvider, child) {
                      return CustomPaint(
                        size: Size.infinite,
                        painter: MyPainter(adsProvider.getLines('drawing2'),
                            adsProvider.getBgShadows('drawing2')),
                      );
                    },
                  ),
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

class MyPainter extends CustomPainter {
  final List<NewLine> lines;
  final bool bg_shadow;
  MyPainter(this.lines, this.bg_shadow);
  int allPointsLength = 0;
  List<NewPoint> allNewPoints = [];
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint();
    for (var line in lines) {
      line.paint(canvas, paint);
    }
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true;
  }
}

问题解决

基于以上的代码已经实现刮刮乐目的,下面则是分析为什么代码中一些部分会采用别的写法,或是问题的解决

问题1 NewPoint类的对比问题

在这个代码块里allNewPoints.contains(point)是失效的

在NewAdsProvider第四行

// List< NewPoint > allNewPoints = [];//废弃

原因是List.contains 方法通过调用对象的 == 运算符来检查列表是否包含指定元素。如果对象没有正确实现 == 运算符,那么 List.contains 方法可能不会按预期工作。

List.contains 方法没有按预期工作,因为 NewPoint 类没有正确实现 == 运算符和 hashCode 方法。默认情况下,Dart 使用对象的内存地址来比较两个对象是否相等,所以即使两个 NewPoint 对象有相同的 xy 值,它们仍然被认为是不同的对象。

这个问题也可以用插件# equatable | Dart package (pub.dev) 来优化.

一点中的代码采用的是插件来优化,如果不想要插件也可以改为

dart 复制代码
import 'package:flutter/material.dart';

class NewPoint {
  final double x;
  final double y;

  NewPoint(this.x, this.y);

  Offset toOffset() => Offset(x, y);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is NewPoint &&
          runtimeType == other.runtimeType &&
          x == other.x &&
          y == other.y;

  @override
  int get hashCode => x.hashCode ^ y.hashCode;
}

对==运算符重载来解决

问题2 绘制是Rect,为了美观怎么转换成RRect

因为绘制的是图片,所以只能采用canvas.drawImageRect这个方法,这个方法绘制出来的是Rect,并不是RRect,所以绘制出来会有一块一块的效果.

为了避免Rect带来的不美观,则需要换成RRct,但是canvas并没有canvas.drawImageRRect,所以需要RRect的话,需要另寻他路,

这里采用了裁剪的方法来获取RRect canvas.clipRRect

dstRect是我们绘制的Rect,把这个转换成RRect,则得到了需要的圆角矩形

ini 复制代码
// 定义圆角矩形区域  
final rrect = RRect.fromRectAndRadius(  
  dstRect,  
  Radius.circular(20),  
);

接下来只需要在绘制图片之前加上canvas.clipRRect就可以实现更好的绘制.

但是如果这样的话,会发现只有一个点可以绘制出来?

这是因为clipRRect后并没有还原画布,画布大小被局限为rrect的大小.

解决方法则是

在绘制前加上canvas.save();绘制后加上canvas.restore();

scss 复制代码
//目前的绘制方法
void paint(Canvas canvas, Paint paint) {  
  if (image != null) {  
    for (var point in points) {  
      canvas.save();  
      final srcRect = Rect.fromLTWH(  
        point.toOffset().dx * 4,  
        point.toOffset().dy * 4,  
        40,  
        40,  
      );  
      final dstRect = Rect.fromLTWH(  
        point.toOffset().dx,  
        point.toOffset().dy,  
        10,  
        10,  
      );  
      // 定义圆角矩形区域  
      final rrect = RRect.fromRectAndRadius(  
        dstRect,  
        Radius.circular(5),  
      );  
      canvas.clipRRect(rrect);  
      canvas.drawImageRect(image!, srcRect, dstRect, paint);  
      canvas.restore();  
    }  
  }  
}

这样就解决了Rect的问题

问题3 点太过密集,导致卡顿

如果不是绘制图片的话,这里的解决方法应为

对曲线的点收集采取每隔一定距离收集一个点,然后曲线拟合成一条曲线.

但是这里是绘制图片,所以曲线拟合就用不到了,

只需要对曲线的点收集采取每隔一定距离收集一个点

回到NewPoint类,添加获取距离和

dart 复制代码
class NewPoint extends Equatable {  
  final int x;  
  final int y;  
  NewPoint({required this.x, required this.y});  
  Offset toOffset() => Offset(x.toDouble(), y.toDouble());  
  ///开始  获取距离
  double get distance => sqrt(x * x + y * y);  
  
  factory NewPoint.fromOffset(Offset offset) {  
    return NewPoint(  
      x: offset.dx.toInt(),  
      y: offset.dy.toInt(),  
    );  
  }  
  NewPoint operator -(NewPoint other) =>  
      NewPoint(x: x - other.x, y: y - other.y);  
    ///结束  运算符重载 operator '-'
  @override  
  // TODO: implement props  
  List<Object?> get props => [x, y];  
}

然后在NewAdsProvider里的添加点方法addPoint进行修改,

scss 复制代码
final double tolerance = 5.0;

///添加点
void addPoint(String key, NewPoint point, {bool force = false}) {
  if (getActiveLine(key).points.isNotEmpty && !force) {
    if ((point - getActiveLine(key).points.last).distance < tolerance) return;
  }
  getActiveLine(key).points.add(point);
  allPointsLengths[key] = getAllPointsLength(key) + 1;
  print(allPointsLengths[key]);
  notifyListeners();
}

扩展

这里还有一些因为时间没有实现的功能,但是思路已经拟出来了

NewLine中dstRect和srcRect之间的倍率关系

这里根据原图的大小和想实现的大小进行等比缩放即可. 如果比例不同则在NewLine中的paint方法中修改

arduino 复制代码
final srcRect = Rect.fromLTWH(
  point.toOffset().dx * 4,
  point.toOffset().dy * 4,
  40,
  40,
);
相关推荐
getapi3 小时前
flutter底部导航代码解释
前端·javascript·flutter
初遇你时动了情3 小时前
安装fvm可以让电脑同时管理多个版本的flutter、flutter常用命令、vscode连接模拟器
flutter
RichardLai8819 小时前
[Flutter学习之Dart基础] - 控制语句
android·flutter
louisgeek1 天前
Flutter Channel 通信机制
flutter
浅忆无痕1 天前
Flutter空安全最小必备知识
android·前端·flutter
亚洲小炫风1 天前
flutter 打包mac程序 dmg教程
flutter·macos
亚洲小炫风1 天前
flutter 桌面应用之系统托盘
flutter·系统托盘
亚洲小炫风2 天前
flutter 桌面应用之右键菜单
flutter·桌面端·右键菜单·contextmenu
louisgeek2 天前
Flutter Widget、Element 和 RenderObject 的区别
flutter
顾林海2 天前
Flutter 文本组件深度剖析:从基础到高级应用
android·前端·flutter