Flutter——四:连一连(手势+绘图)

文中只讲解关键代码和具体思路,完整代码放文末需要的自取。话不多说,先上效果

在这里插入图片描述

规则:

1.左边的选项与右边的选项相连
2.按下或松开时在选项内若已有连线,移除原有连线。

实现过程:

    首先捋一下画线的过程,即提供起点和终点的坐标,就可以画出一条线,而要画出多条线,就得提供一个数组。而每条线的两点坐标就是左侧选项的右边中间点右侧选项的左侧中间点
实体类(用于记录起点坐标和终点坐标):

class PositionModel{
  double startX;
  double startY;
  double endX;
  double endY;

  PositionModel({this.startX, this.startY, this.endX, this.endY});
}

画线工具(创建一个画笔,将每一组坐标传入通过canvas.drawLine画线即可):

import 'package:flutter/material.dart';
import 'package:link_demo/position_model.dart';
class LinePainter extends CustomPainter {

  final List<PositionModel> lines;

  LinePainter({this.lines});

  var line = Paint()
    ..style = PaintingStyle.stroke
    ..color = Colors.blue
    ..strokeWidth = 1.0;

  @override
  void paint(Canvas canvas, Size size) {
    if(lines.length == 0) return;
    print("线的数量:${lines.length}");
    for(PositionModel draw in lines){
      canvas.drawLine(Offset(draw.startX, draw.startY- 80.0), Offset(draw.endX,draw.endY - 80.0), line);
    }
  }

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

}

最后将需要画线的区域上套上CustomPaint,并使用刚才的画线工具,传入需要画的线即可实现画线:

CustomPaint(
  foregroundPainter: LinePainter(lines: lines),
  child: child
)

-----------------------------------接下来需要思考的就是如何产生这些线-----------------------------------
产生线的意思就是产生起点坐标和终点坐标,那么这两个坐标产生需要满足什么要求:
1.先判断触摸点是否在选项内部,若触摸点的坐标在选项的左上角的右下部分,并且在右下角的左上部分,则可以确定在选项内部。

  // 判断触摸点是否在选项内
  bool _isInOption({@required double x, @required double y, @required RenderBox renderBox}){
    return x >= renderBox.localToGlobal(Offset.zero).dx &&
        x <= renderBox.localToGlobal(Offset.zero).dx + renderBox.size.width &&
        y >= renderBox.localToGlobal(Offset.zero).dy &&
        y <= renderBox.localToGlobal(Offset.zero).dy + renderBox.size.height;
  }

2.如果按下的时候在一个选项内,若这个选项在左边,那么起点坐标就是这个选项的右侧中间点坐标,若在右边,就是左侧中间点坐标。
3.如果松开的时候在一个选项内,若这个选项在左边,那么终点就是这个选项的右侧中间点坐标,若在右边,就是左侧中间点坐标。

在这里插入图片描述
这里提到的按下松开通过GestureDetector中的三个方法来处理:

// 按下时
onPanDown: (details){
}
// 移动时
onPanUpdate: (details){
}
// 松开时
onPanEnd: (_){
}

    通过方法中提供的details可以获取触摸点的坐标,onPanEnd中没有(没有具体了解过),就通过onPanUpdate来记录触摸点实时的坐标,因为要记录,所以需要创建一个实体:

 PositionModel touchPosition = PositionModel();

然后赋值:

  // 记录触摸点位置
  void _setTouchPosition({@required double x, @required double y}){
    setState(() {
      touchPosition.startX = x;
      touchPosition.startY = y;
    });
  }

接下来提供两组选项:

  List answers = ["A","B","C","D"];
  List questions = ["鸡","鸭","鱼","肉"];

一个记录产生中的线的起点坐标和终点坐标的实体:

  PositionModel line = PositionModel();

和一个记录需要画出来的线的数组:

 List<PositionModel> lines = List<PositionModel>();

★和两组用来获取选项位置的key★:

  List<GlobalKey> _answerKeys = [GlobalKey(),GlobalKey(),GlobalKey(),GlobalKey()];
  List<GlobalKey> _questionKeys = [GlobalKey(),GlobalKey(),GlobalKey(),GlobalKey()];

key设置在每个选项内,可以通过key.currentContext.findRenderObject()来获取该Container的位置

Container(
                  key: keys[index],
                  width: 80,
                  height: 80,
                  alignment: Alignment.center,
                  color: Colors.red,
                  child: Text(list[index],style: TextStyle(
                      color: Colors.white
                  ),),
                ),

关于GlobalKey其实并不怎么了解,网络上只能搜到这是个昂贵的东西,具体怎么个昂贵法没了解到。先不管了,在这里面没什么影响。

-----------------------------------------------准备结束,最后阶段-----------------------------------------------
按下:
    每次按下的时候,先将产生中的线条的每个坐标值都设为null,再循环选项,判断触摸点位置和每一个选项,若在里面则记录起点坐标在line中,记录的时候需判断一下lines中已有的线中是否有起点和要记录的一样,一样的话就要删除原来的线。

 onPanDown: (details){
   _initLine();

   double x = details.globalPosition.dx;
   double y = details.globalPosition.dy;
   print("按下时x:$x,y:$y");
   // 按下时坐标与问题项位置比较
   for(int i = 0; i<questions.length; i++){
     RenderBox renderBox = _questionKeys[i].currentContext.findRenderObject();
     if(_isInOption(x: x, y: y, renderBox: renderBox)){
       double startX = renderBox.localToGlobal(Offset(renderBox.size.width,renderBox.size.height)).dx;
       double startY = renderBox.localToGlobal(Offset(renderBox.size.width,renderBox.size.height)).dy - (renderBox.size.height / 2);

       _setStartPosition(startX: startX, startY: startY);
       return;
     }
   }
   // 按下时坐标与答案项位置比较
   for(int i = 0; i<answers.length; i++){
     RenderBox renderBox = _answerKeys[i].currentContext.findRenderObject();
     if(_isInOption(x: x, y: y, renderBox: renderBox)){
       double startX = renderBox.localToGlobal(Offset.zero).dx;
       double startY = renderBox.localToGlobal(Offset.zero).dy + (renderBox.size.height / 2);

       _setStartPosition(startX: startX, startY: startY);
       return;
     }
   }
 }
 
 // 记录起始点坐标
void _setStartPosition({@required double startX, @required double startY}){
  setState(() {
    line.startX = startX;
    line.startY = startY;
    if(startX != null && startY != null) {
      // 判断已画线中的起始点或结束点是否和要传入的起始点坐标相同 若相同移除已画线
      lines.removeWhere((line){
        return line.startX == startX && line.startY == startY || line.endX == startX && line.endY == startY;
      });
    }
  });
}

松开:
    先判断有没有记录到起始点,没有的话就没必要在进行后面的逻辑了。有的话,也和按下时一样,循环两组选项,看松开的点是否在这些选项中,是的话也还要判断一下原有的线中是否有这个坐标,有的话删去,并且还要判断横坐标是否和按下时的一样,不然就变成两个问题相连或两个答案相连了。都没问题就能把这个拥有起点坐标和终点坐标的line加入到lines中了。

onPanEnd: (_){
   // 存在起始坐标再执行记录结束坐标代码
   if(line.startX != null && line.startY != null){
     double x = touchPosition.startX;
     double y = touchPosition.startY;
     // 松开时坐标与答案项位置比较
     for(int i = 0; i< widget.answers.length; i++){
       RenderBox renderBox = _answerKeys[i].currentContext.findRenderObject();
       if(_isInOption(x: x, y: y, renderBox: renderBox)){
         print("松开时在${widget.answers[i]}里面!");
         double endX = renderBox.localToGlobal(Offset.zero).dx;
         double endY = renderBox.localToGlobal(Offset.zero).dy + (renderBox.size.height / 2);
         // 如果起始点和结束点的横坐标不同则记录结束点坐标
         if(line.startX != endX){
           _addLine(endX: endX, endY: endY);
         }else{
           print("不能将两个答案相连");
         }
         return;
       }
     }
     // 松开时坐标与问题项位置比较
     for(int i = 0; i< widget.questions.length; i++){
       RenderBox renderBox = _questionKeys[i].currentContext.findRenderObject();
       if(_isInOption(x: x, y: y, renderBox: renderBox)){
         print("松开时在${widget.questions[i]}里面!");
         double endX = renderBox.localToGlobal(Offset(renderBox.size.width,renderBox.size.height)).dx;
         double endY = renderBox.localToGlobal(Offset(renderBox.size.width,renderBox.size.height)).dy - (renderBox.size.height / 2);
         // 如果起始点和结束点的横坐标不同则记录结束点坐标
         if(line.startX != endX){
           _addLine(endX: endX, endY: endY);
         }else{
           print("不能将两个问题相连");
         }
         return;
       }
     }
   }else{
     print("没有按下时的坐标");
   }
 }
 
 // 获取结束点后添加画线
void _addLine({@required double endX, @required double endY}){
  setState(() {
    // 判断已画线中的起始点或结束点是否和要传入的结束点坐标相同 若相同移除已画线
    lines.removeWhere((line){
      return line.startX == endX && line.startY == endY || line.endX == endX && line.endY == endY;
    });
    PositionModel model = PositionModel();
    model.startX = line.startX;
    model.startY = line.startY;
    model.endX = endX;
    model.endY = endY;
    lines.add(model);
  });
}

最后:
源码奉上,文章可能理的不怎么顺,如果有问题再留言。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

starcrius

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值