Draggable

Draggable은 하위 위젯을 마우스로 드래그할 수 있게 만드는 위젯입니다. 내부적으로 TransformGestureDetector를 사용하여 드래그 기능을 구현합니다.

개요

Draggable 위젯은 사용자 인터랙션을 통해 위젯을 화면상에서 이동할 수 있게 합니다. 드래그 시작, 진행, 종료 이벤트를 제공하여 세밀한 제어가 가능합니다.

언제 사용하나요?

  • 드래그 앤 드롭 인터페이스: 파일, 카드, 아이템을 이동할 때
  • 대화형 다이어그램: 노드나 요소를 자유롭게 배치할 때
  • 위젯 재배치: 사용자가 UI 요소의 위치를 커스터마이징할 때
  • 게임 요소: 퍼즐 조각이나 게임 오브젝트를 움직일 때
  • 시각적 편집기: 디자인 툴에서 요소를 배치할 때

기본 사용법

import { Draggable, Container } from '@meursyphus/flitter';

Draggable({
  onDragUpdate: ({ delta, movement }) => {
    console.log('드래그 중:', { delta, movement });
  },
  onDragStart: () => {
    console.log('드래그 시작');
  },
  onDragEnd: () => {
    console.log('드래그 종료');
  },
  child: Container({
    width: 100,
    height: 100,
    color: 'blue'
  })
})

Props

속성타입설명
childWidget?드래그 가능하게 만들 하위 위젯
onDragUpdate(event: { delta: Offset, movement: Offset }) => void?드래그 중 호출되는 콜백
onDragStart() => void?드래그 시작 시 호출되는 콜백
onDragEnd() => void?드래그 종료 시 호출되는 콜백
feedbackWidget?드래그 중 표시할 피드백 위젯

이벤트 객체

  • delta: 이전 프레임 대비 이동량
  • movement: 드래그 시작점 대비 총 이동량

실제 사용 예제

1. 기본 드래그 박스

import { Draggable, Container, Text } from '@meursyphus/flitter';

Draggable({
  onDragUpdate: ({ movement }) => {
    console.log(`이동: x=${movement.x}, y=${movement.y}`);
  },
  child: Container({
    width: 120,
    height: 80,
    color: '#3498db',
    child: Text('드래그하세요', {
      style: { color: 'white', textAlign: 'center' }
    })
  })
})

2. 상태 관리와 함께 사용

import { StatefulWidget, State, Draggable, Container, Transform } from '@meursyphus/flitter';

class DraggableBox extends StatefulWidget {
  createState() {
    return new DraggableBoxState();
  }
}

class DraggableBoxState extends State<DraggableBox> {
  position = { x: 0, y: 0 };
  isDragging = false;

  handleDragStart = () => {
    this.setState(() => {
      this.isDragging = true;
    });
  }

  handleDragUpdate = ({ movement }) => {
    this.setState(() => {
      this.position = { x: movement.x, y: movement.y };
    });
  }

  handleDragEnd = () => {
    this.setState(() => {
      this.isDragging = false;
    });
  }

  build() {
    return Transform.translate({
      offset: this.position,
      child: Draggable({
        onDragStart: this.handleDragStart,
        onDragUpdate: this.handleDragUpdate,
        onDragEnd: this.handleDragEnd,
        child: Container({
          width: 100,
          height: 100,
          color: this.isDragging ? '#e74c3c' : '#2ecc71'
        })
      })
    });
  }
}

3. 드래그 영역 제한

import { Draggable, Container } from '@meursyphus/flitter';

class BoundedDraggable extends StatefulWidget {
  createState() {
    return new BoundedDraggableState();
  }
}

class BoundedDraggableState extends State<BoundedDraggable> {
  position = { x: 0, y: 0 };
  bounds = { minX: -100, maxX: 100, minY: -50, maxY: 50 };

  handleDragUpdate = ({ movement }) => {
    const clampedX = Math.max(this.bounds.minX, 
                      Math.min(this.bounds.maxX, movement.x));
    const clampedY = Math.max(this.bounds.minY, 
                      Math.min(this.bounds.maxY, movement.y));
    
    this.setState(() => {
      this.position = { x: clampedX, y: clampedY };
    });
  }

  build() {
    return Transform.translate({
      offset: this.position,
      child: Draggable({
        onDragUpdate: this.handleDragUpdate,
        child: Container({
          width: 60,
          height: 60,
          color: '#9b59b6'
        })
      })
    });
  }
}

4. 드래그 피드백 위젯

import { Draggable, Container, Text, Opacity } from '@meursyphus/flitter';

Draggable({
  child: Container({
    width: 100,
    height: 100,
    color: '#1abc9c',
    child: Text('원본')
  }),
  feedback: Opacity({
    opacity: 0.7,
    child: Container({
      width: 100,
      height: 100,
      color: '#16a085',
      child: Text('드래그 중', {
        style: { color: 'white' }
      })
    })
  })
})

5. 다중 드래그 객체

import { Column, Draggable, Container, Text, EdgeInsets } from '@meursyphus/flitter';

const colors = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12'];
const items = colors.map((color, index) => 
  Draggable({
    onDragUpdate: ({ movement }) => {
      console.log(`Item ${index + 1} 이동:`, movement);
    },
    child: Container({
      width: 80,
      height: 60,
      color,
      margin: EdgeInsets.all(5),
      child: Text(`${index + 1}`, {
        style: { color: 'white', textAlign: 'center' }
      })
    })
  })
);

Column({
  children: items
})

6. 드래그 상태 시각화

import { Draggable, Container, Text, Column } from '@meursyphus/flitter';

class DragVisualizer extends StatefulWidget {
  createState() {
    return new DragVisualizerState();
  }
}

class DragVisualizerState extends State<DragVisualizer> {
  status = '대기 중';
  position = { x: 0, y: 0 };

  build() {
    return Column({
      children: [
        Text(`상태: ${this.status}`),
        Text(`위치: (${this.position.x.toFixed(1)}, ${this.position.y.toFixed(1)})`),
        
        Draggable({
          onDragStart: () => {
            this.setState(() => {
              this.status = '드래그 중';
            });
          },
          onDragUpdate: ({ movement }) => {
            this.setState(() => {
              this.position = movement;
            });
          },
          onDragEnd: () => {
            this.setState(() => {
              this.status = '완료';
            });
          },
          child: Container({
            width: 100,
            height: 100,
            color: '#8e44ad'
          })
        })
      ]
    });
  }
}

주의사항

  1. 성능: 드래그 이벤트는 빈번하게 발생하므로 onDragUpdate에서 무거운 연산을 피하세요.

  2. 상태 업데이트: 드래그 중 상태 변경 시 적절한 setState 호출이 필요합니다.

  3. 좌표계: movement는 드래그 시작점 기준이고, delta는 이전 프레임 기준입니다.

  4. Transform과의 관계: Draggable은 내부적으로 Transform을 사용하므로 추가 Transform 적용 시 주의가 필요합니다.

  5. 드롭 타겟: 현재 Draggable은 드래그만 지원하며, 드롭 타겟 기능은 별도로 구현해야 합니다.

관련 위젯

  • Transform: 위젯 변형 및 위치 조정
  • GestureDetector: 마우스/터치 이벤트 감지
  • Container: 드래그 가능한 영역 정의
  • ZIndex: 드래그 중 레이어 순서 조정