[Flutter] Canvas에서 Drag and Drop 구현하기

RGLie,·2022년 4월 5일
2
post-thumbnail

Drag and Drop

Drag and Drop(드래그앤드랍, 이하 DnD)은 UI를 구성할 때 인터렉션을 위해 많이 쓰인다. Flutter에서는 Drag and Drop을 Draggable 이라는 위젯으로 지원한다.
Flutter Dev. Draggable Widget
Flutter 공식 유튜브에 Draggable Widget 사용법이 친절하게 나온다.

DnD in Canvas

Widget을 DnD 하기 위해서는 Draggable이라는 위젯을 지원하지만, 나는 위젯이 아닌 CustomPaint의 Canvas위의 하나의 Path를 DnD하고 싶었다. 이때는 어떻게 해야할까? StackOverFlow등을 찾아본 결과 pub dev library를 사용하지 않는 이상 이것만을 위한 위젯 또는 메소드는 없는것 같았다. 그래서 gestureDetector로 직접 구현하기로 했다.

Code


구현결과이다. Canvas 내에 원을 그렸고, 원을 클릭하면 DnD가 된다.

Skeleton Code

  dynamic _balls;
  double xPos = 100;
  double yPos = 100;

 
  Widget build(BuildContext context) {
    _balls=_paint(
        xPosition: xPos,
        yPosition: yPos,
        ballRad: 20
    );

    return Scaffold(
      appBar: AppBar(
        title: Text("Drag and Drop"),
      ),
      body: Center(
        child: Container(
          width: 300,
          height: 300,
          color: Colors.lightBlueAccent,
          child: CustomPaint(
            painter: _balls,
          ),
         ),
        )
      );
  }
class _paint extends CustomPainter {
  final xPosition;
  final yPosition;
  final ballRad;

  _paint({
    required this.xPosition,
    required this.yPosition,
    required this.ballRad,
  });

  
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = Colors.indigoAccent
      ..style = PaintingStyle.stroke
      ..strokeWidth = 2;

    Path path = Path();

    for(double i=0; i<ballRad-1; i++){
      path.addOval(Rect.fromCircle(
          center: Offset(
            xPosition,
            yPosition
          ),
          radius: i
      ));
    }
    path.close();

    canvas.drawPath(path, paint);
  }

  
  bool shouldRepaint(CustomPainter oldDelegate) => true;

}

기본적으로 Canvas와 Canvas 위에 공(물체)를 그려야한다. CustomPaint를 상속받는 _paint class를 만들어 주고, _balls 라는 dynamic 타입 _paint 객체를 선언한다. 이때, _paint는 xPosition, yPosition, ballRadius를 required로 지정한다.

위 코드의 결과 화면에는 반지름 20의 공이 하나 그려진다. 이제 본격적으로 DnD를 구현해보자.

Is Ball Region?

  bool isBallRegion(double checkX, double checkY){
    if((pow(xPosition-checkX, 2)+pow(yPosition-checkY, 2))<=pow(ballRad, 2)){
      return true;
    }
    return false;
  }

간단하게 Canvas 내의 특정 좌표가 공인지를 판단할 수 있는 인스턴스 메소드를 만들자. 이는 고등학교 수학과정에 나오는 원의 부등식을 통해 쉽게 만들 수 있다.

GestureDetector

내가 DnD 구현을 위해 사용할 위젯이다. GestureDetector Widget은 유저의 여러가지 종류의 제스쳐를 인식할 수 있는 위젯이다. flutter docs를 살펴보면 정말 많은 method를 알 수 있다.

GestureDetector(
          onHorizontalDragDown: (details) {

          },
          onHorizontalDragEnd: (details) {

          },
          onHorizontalDragUpdate: (details) {

          },

          child: Container(
            width: 300,
            height: 300,
            color: Colors.lightBlueAccent,
            child: CustomPaint(
              painter: _balls,
            ),
          ),
        )

위의 CustomPaint Widget 위에 Gesture Dectector를 감싸주면 된다. GestureDetector에서 우리가 사용할 것은 onHorizontalDragDown, onHorizontalDragEnd, onHorizontalDragUpdate 이다.

onHorizontalDragDown은 Drag를 시작할 때,
onHorizontalDragEnd는 Drag를 끝낼 때 (Drop 할 때),
onHorizontalDragUpdate는 Drag 할 때 실행된다.

details.localPosition.dx
details.localPosition.dy

각각의 함수에서 details의 정보를 통해 마우스 또는 터치의 좌표값을 알 수 있다. 나는 이 정보를 이용할 예정이다. 먼저, 클릭(터치)중인지를 판단할 수 있는 변수를 하나 선언하자.

bool isClick = false;

이 변수는 onHorizontalDragDown 일 때 true가 되고, onHorizontalDragEnd 일 때 다시 false가 되게 하여 터치 중인지를 판단할 수 있게한다.

          onHorizontalDragDown: (details) {
            setState(() {
              if (_balls.isBallRegion(details.localPosition.dx, details.localPosition.dy)) {
                isClick=true;
              }
            });
          },
          onHorizontalDragEnd: (details) {
            setState(() {
              isClick=false;
            });
          },
          onHorizontalDragUpdate: (details) {
            if(isClick){
              setState(() {
                xPos=details.localPosition.dx;
                yPos=details.localPosition.dy;
              });
            }
          },

onHorizontalDragDown에서 공이 아닌 다른 부분을 Drag 했을 때는 아무 일도 발생하면 안되므로 if문에 위에서 만들어준 isBallRegion 메소드를 부르자. 메소드의 파라미터로는 details.localPosition 즉, 터치 좌표값을 넘겨준다.
그리고 onHorizontalDragUpdate에서는 Drag를 하는 순간마다 새로운 x 좌표값과 y 좌표값을 업데이트한다.

Conclusion

완성이다. 이것을 구현하고자 flutter docs 에서 draggable, gestureDetector, Inkwell 등 터치와 관련된 다양한 Widget들의 속성들을 봤는데 gestureDectector는 앞으로도 쓸일이 많을것 같다는 생각이 든다. 그리고 이 것을 통해 더 재밌는것들 많이 만들수 있을것 같아 흥분된다!

profile
Flutter Programmer (github: RGLie)

0개의 댓글