Flutter : 사용자 정의 페인트에서 모양 드래그 앤 드롭



소개



이 게시물에서는 Flutter CustomPaint Widget을 사용하여 사용자 대화형 Drag & Drop을 구현하는 방법을 공유하고 싶습니다. 플러터와 다트가 제공하는 것만 사용할 것이기 때문에 no packages need to be installed가 있습니다. 자, 뛰어들자!

1. 셰이프 개체 데이터가 포함된 StatefulWidget




class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool isDown = false;
  double x = 0.0;
  double y = 0.0;
  int? targetId;
  Map<int, Map<String, double>> pathList = {
    1: {"x": 100, "y": 100, "r": 50, "color": 0},
    2: {"x": 200, "y": 200, "r": 50, "color": 1},
    3: {"x": 300, "y": 300, "r": 50, "color": 2},
  };

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Container(child: Text('dummy text'))
        );
  }
}



먼저 사용자 상호 작용에 반응하도록 하려면 StatefulWidget가 필요합니다.
  • isDown → 사용자가 아래를 터치하거나 클릭하면 참
  • x, y → 사용자 동작 좌표 포함
  • targetId → 사용자가 현재 가리키고 있는 모양 객체의 id
  • pathList → 캔버스에 표시될 모양 데이터 목록. 이 예에서는 원으로만 테스트하므로 모양 데이터의 x 및 y는 원의 중심 좌표, r은 반지름, 색상은 미리 설정된 색상 목록의 인덱스입니다
  • .

    2. 포함할 위젯




    @override
      Widget build(BuildContext context) {
        return Scaffold(
            appBar: AppBar(
              title: Text(widget.title),
            ),
            body: GestureDetector(
              child: Container(
                  width: MediaQuery.of(context).size.width,
                  height: MediaQuery.of(context).size.height,
                  color: Colors.grey,
                  child: CustomPaint(
                    foregroundPainter:
                        ShapePainter(down: isDown, x: x, y: y, pathList: pathList),
                    size: Size(MediaQuery.of(context).size.width,
                        MediaQuery.of(context).size.height),
                  )),
            ) // This trailing comma makes auto-formatting nicer for build methods.
            );
      }
    


    이제 위젯 트리를 채워야 합니다.
  • GestureDetector → 감지할 사용자 상호 작용과 관련된 모든 위젯을 래핑해야 합니다
  • .
  • CustomPaint → 캔버스 기능을 제공하는 위젯
  • ShapePainter → 칠하고 다시 칠함

  • 3. 사용자 상호 작용 캡처




    // util function
    bool isInObject(Map<String, double> data, double dx, double dy) {
      Path _tempPath = Path()
        ..addOval(Rect.fromCircle(
            center: Offset(data['x']!, data['y']!), radius: data['r']!));
      return _tempPath.contains(Offset(dx, dy));
    }
    // event handler
    void _down(DragStartDetails details) {
        setState(() {
          isDown = true;
          x = details.localPosition.dx;
          y = details.localPosition.dy;
        });
      }
    
      void _up() {
        setState(() {
          isDown = false;
          targetId = null;
        });
      }
    
      void _move(DragUpdateDetails details) {
        if (isDown) {
          setState(() {
            x += details.delta.dx;
            y += details.delta.dy;
            targetId ??= pathList.keys
                .firstWhereOrNull((_id) => isInObject(pathList[_id]!, x, y));
            if (targetId != null) {
              pathList = {
                ...pathList,
                targetId!: {...pathList[targetId!]!, 'x': x, 'y': y}
              };
            }
          });
        }
      }
    
    // map event handler to pan event
    ...
    body: GestureDetector(
              onPanStart: (details) {
                _down(details);
              },
              onPanEnd: (details) {
                _up();
              },
              onPanUpdate: (details) {
                _move(details);
              },
    ...
    


    다음은 이벤트 핸들러 등록입니다. GestureDetector에서 팬 이벤트는 드래그 앤 드롭용입니다.
  • onPanStart → 초기 사용자 작업 좌표 설정 및 isDown을 true로 설정
  • onPanUpdate → isDown이 true인 경우 사용자 작업 좌표를 업데이트하고 대상 항목을 찾고 관련 상태(targetId, pathList)를 업데이트합니다.
  • onPanEnd → isDown을 false로 설정

  • 4. CustomPaint 및 CustomPainter를 사용하여 그리기




    class ShapePainter extends CustomPainter {
      final colors = [Colors.red, Colors.yellow, Colors.lightBlue];
      Path path = Path();
      Paint _paint = Paint()
        ..color = Colors.red
        ..strokeWidth = 5
        ..strokeCap = StrokeCap.round;
    
      final bool down;
      final double x;
      final double y;
      Map<int, Map<String, double>> pathList;
      ShapePainter({
        required this.down,
        required this.x,
        required this.y,
        required this.pathList,
      });
      @override
      void paint(Canvas canvas, Size size) {
        for (var pathData in pathList.values) {
          _paint = _paint..color = colors[pathData['color']! as int];
          path = Path()
            ..addOval(Rect.fromCircle(
                center: Offset(pathData['x']!, pathData['y']!),
                radius: pathData['r']!));
          canvas.drawPath(path, _paint);
        }
      }
    
      @override
      bool shouldRepaint(ShapePainter oldDelegate) => down;
    }
    


    ShapePainter는 페인트 방법을 소유한 것입니다. 별다른 로직은 없고, 위의 위젯이 준 데이터를 그냥 그립니다. shouldRepaint는 사용자가 터치하거나 마우스를 눌렀을 때 항상 호출되므로 사용자가 작업 중일 때만 업데이트를 수행할 수 있습니다.

    결론



    나는 일반적으로 캔버스에 정말 관심이 있습니다. Flutter CustomPainter를 사용하여 Drag & Drop을 구현하려는 분들께 도움이 되었으면 합니다.

    건배!

    좋은 웹페이지 즐겨찾기