본문 바로가기
flutter/Widget of the Week

Flutter[플러터] / DraggableScrollableSheet 를 사용하여 UI 크기 조절 기능 구현하기 (사이즈, 스크롤, 드래그, 드래그블스크롤브시트) (Flutter Widget of the Week)

by ch5c 2025. 6. 17.
반응형

DraggableScrollableSheet class

드래그 제스처 에 응답하여 스크롤 가능한 요소의 크기를 제한에 도달할 때까지 조절한 다음 스크롤하는 스크롤 가능한 요소의 컨테이너입니다.

https://youtu.be/Hgw819mL_78

공식 문서 코드

 


우리가 흔히 여러 프로그램들을 쓰다 보면 느낄 수 있는 공통적인 사항들이 있다. 바로 크기를 사용자가 직접 UI 의 크기를 설정할 수 있다는 건데 벌써 비주얼 스튜디오 코드나 안드로이드 스튜디오만 보더라도 툴바라든지 여러 화면의 요소들의 크기를 직접 조절할 수 있게 되어 있다. 이러한 기능을 구현려면 크기 조절에 유연한 DraggableScrollableSheet 위젯을 사용해 줄 수 있다.

DraggableScrollableSheet 위젯은 화면의 일부분으로부터 사용자가 드래그하여 확장하거나 축소할 수 있는 스크롤 가능한 시트(sheet)를 구현할 수 있도록 하는 위젯이다. 보통 BottomSheet과 비슷하지만 좀 더 유연하게 드래그와 스크롤을 결합한 UI를 구현할 수 있게 된다. 바로 한번 알아보자.

하위 속성
속성명 타입 기본값 설명
initialChildSize double 0.5 초기 높이를 부모의 높이에 대한 비율로 지정
minChildSize double 0.25 최소 높이를 부모 높이의 비율로 지정
maxChildSize double 1.0 최대 높이를 부모 높이의 비율로 지정
expand bool true 위젯이 부모 영역을 모두 차지할지 여부
snap bool false 사용자가 손을 뗐을 때 snapSizes에 지정된 크기로 자동 이동할지 여부
snapSizes List<double>? 없음 스냅할 대상 높이 목록 (비율 값)
snapAnimationDuration Duration? 없음 스냅 애니메이션의 지속 시간 지정
controller DraggableScrollableController? 없음 시트를 프로그래밍으로 제어할 수 있는 컨트롤러
shouldCloseOnMinExtent bool true 최소 크기까지 축소되었을 때 부모가 시트를 닫아야 할지 여부
builder ScrollableWidgetBuilder 필수 스크롤 가능한 내용을 생성하는 빌더 함수

 

일단 DraggableScrollableSheet 의 기본적인 사용법을 알고 가자.

기본적으로 이 위젯은 하단에서 나오는 BottomSheet 의 형태를 가지고 있다. 그리고 또한 이름에서 알 수 있듯이 기본적으로 스크롤을 지원해 주는 위젯인데 보통 안에 ListView 같은 스크롤 위젯을 넣는 편이다.

DraggableScrollableSheet(
  initialChildSize: 0.5,
  minChildSize: 0.1,
  maxChildSize: 0.8,
  builder: (context, scrollController) {
    return Container(
      decoration: BoxDecoration(
        color: Colors.white,
        borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
        boxShadow: [BoxShadow(blurRadius: 5, color: Colors.black26)],
      ),
      child: ListView.builder(
        controller: scrollController,
        itemCount: 30,
        itemBuilder: (context, index) {
          return ListTile(title: Text('아이템 $index'));
        },
      ),
    );
  },
),

일단 이 위젯에는 초기 크기와 최소/최대 크기를 지정해 줄 수 있다. 타입은 double 이지만 값을 입력받은 그대로 화면에 반영되는 게 아니라 화면의 비율을 나타내주는 값이다. 예를 들어 0.1 이면 MediaQuery.sizeOf(context).height * 0.1 이 느낌인 거다.

initialChildSize: 0.5, // 시작 크기 : 화면의 50%
minChildSize: 0.1, // 최소 크기 : 화면의 10%
maxChildSize: 0.8, // 최대 크기 : 화면의 80%

 

또한 builder: 로 빌더 함수를 받는데 여기다가 이제 넣고 싶은 거 넣어주면 되겠다.

builder: (context, scrollController) {

여기서 이렇게 인자값으로 scrollController 를 넘겨주는데 이걸 안에 작설 할 리스트뷰같은 곳의 controller 에 넣어주면 알아서 적용되게 된다.

참고로 showModalBottomSheet() 안에 넣어서 일반적인 ModalBottomSheet 의 형식으로 나타나게 할 수 또 있다.

암튼 이제 이거 크기 조절 하는 방법을 살펴보자. 공식 문서 코드를 그대로 살펴볼 것이다.

일단 가장 먼저 볼 것은 조금 재밌는 코드인데 바로 이것이다.

bool get _isOnDesktopAndWeb =>
    kIsWeb ||
        switch (defaultTargetPlatform) {
          TargetPlatform.macOS || TargetPlatform.linux || TargetPlatform.windows => true,
          TargetPlatform.android || TargetPlatform.iOS || TargetPlatform.fuchsia => false,
        };

딱 보면 감이 오지 않는가? 그렇다. 현재 플랫폼이 웹/데스크톱인지 판별하는 getter 인 것이다. 이제 이렇게 만든 건 나중에 controller 에 넣어주게 될 것이다. 

double _sheetPosition = 0.5;
final double _dragSensitivity = 600;

 

그리고 시트의 현재 높이 비율과 드래그 감도 조절 상수를 미리 선언해 놓은 모습이다. 이제 이걸로 크기 조절을 할 것이다.

class Grabber extends StatelessWidget {
  const Grabber({super.key, required this.onVerticalDragUpdate, required this.isOnDesktopAndWeb});

  final ValueChanged<DragUpdateDetails> onVerticalDragUpdate;
  final bool isOnDesktopAndWeb;

  @override
  Widget build(BuildContext context) {
    if (!isOnDesktopAndWeb) {
      return const SizedBox.shrink();
    }
    final ColorScheme colorScheme = Theme.of(context).colorScheme;

    return GestureDetector(
      onVerticalDragUpdate: onVerticalDragUpdate,
      child: Container(
        width: double.infinity,
        color: colorScheme.onSurface,
        child: Align(
          alignment: Alignment.topCenter,
          child: Container(
            margin: const EdgeInsets.symmetric(vertical: 8.0),
            width: 32.0,
            height: 4.0,
            decoration: BoxDecoration(
              color: colorScheme.surfaceContainerHighest,
              borderRadius: BorderRadius.circular(8.0),
            ),
          ),
        ),
      ),
    );
  }
}

 

시트 핸들을 드래그 가능하게 만들기 위해 위젯을 하나 만들어줬다.

final ValueChanged<DragUpdateDetails> onVerticalDragUpdate;

또한 수직 드래그 시 콜백 될 것도 하나 만들어준 모습이다.

이렇게 만든 변수를 바로 넣어주게 된다.

return GestureDetector(
  onVerticalDragUpdate: onVerticalDragUpdate,

이렇게 설정해놓으면 수직 드래그를 할 때마다 콜백이 실행되게 된다.

자 이제 바로 UI 를 만들어주면 되는데 

Grabber(
  onVerticalDragUpdate: (DragUpdateDetails details) {
    setState(() {
      _sheetPosition -= details.delta.dy / _dragSensitivity;
      if (_sheetPosition < 0.25) { // 최소값 제한
        _sheetPosition = 0.25;
      }
      if (_sheetPosition > 1.0) { // 최대값 제한
        _sheetPosition = 1.0;
      }
    });
  },
  isOnDesktopAndWeb: _isOnDesktopAndWeb,
),

이렇게 직접 제작한 Grabber 위젯을 가져와서 y 방향 드래그 변화량을 시트 비율에 반영해 주게 된다. 이제 이러면 드래그를 한 만큼 크기가 변동되게 되는 것이다.

마지막으로 Flexible 을 사용하여 남는 공간을 다 차지할 수 있도록 만들었다.

Flexible(
  child: ListView.builder(
    controller: _isOnDesktopAndWeb ? null : scrollController,
    itemCount: 25,
    itemBuilder: (BuildContext context, int index) {
      return ListTile(
        title: Text('Item $index', style: TextStyle(color: colorScheme.surface)),
      );
    },
  ),
),

이러면 크기가 변동되어도 알아서 채워지기 때문에 쉽게 작성할 수 있게 된다.

controller 에는 데스크톱/웹이면 외부에서 스크롤 제어할 수 있도록 선언해 놨다.

 

암튼 이렇게 DraggableScrollableSheet 위젯에 대해서 알아보았다. 이 위젯 자체는 막 그리 특색 있는 위젯은 아니지만 주는 기본적인 builder 함수를 가지고 있다는 점과 하위 속성들이 값을 조절하는 데에 있어서 꽤 좋아서 가끔씩은 사용되는 위젯이 아닐까 싶다. 도움이 되었길 바라며 마치겠다.

 

반응형