【간단】정렬 가능한 리스트 ReorderableListView와 그 속성

ListView로 리스트를 만들 때 정렬 기능을 추가하고 싶을 때가 있지 않나요?
ReorderableListView는 사용자가 리스트 항목을 드래그 앤 드롭하여
정렬할 수 있는 Flutter의 위젯입니다.

이번에는 ReorderableListView와 설정할 수 있는 속성에 대해 소개합니다.

기본적인 ReorderableListView의 사용법

아래에 기본적인 ReorderableListView의 구현 코드를 보여드립니다.
아래 예제에서는 리스트에 포함된 항목을 드래그하여 순서를 변경할 수 있습니다.

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'ReorderableListView Example',
      home: HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  State createState() => _HomePageState();
}

class _HomePageState extends State {
  final List _items = List.generate(20, (index) => "Item $index");

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('ReorderableListView Example'),
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: const Icon(Icons.drag_handle),
          );
        },
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

ReorderableListView.builder를 사용하여 20개의 리스트 아이템을 표시하고 있습니다.

ReorderableListView의 속성

ReorderableListView에는 리스트를 커스터마이징하기 위한 많은 속성이 있습니다.
사용 가능한 속성은 ListView와 비슷하지만, 약간 다릅니다.

itemBuilder(필수)

itemBuilder는 각 항목을 생성하는 속성입니다.
받은 리스트 항목을 하나씩 처리하여 표시되는 위젯을 반환합니다.

itemCount: _items.length,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: const Icon(Icons.drag_handle),
  );
},

itemCount(필수)

itemCount는 리스트의 항목 수를 지정하는 속성입니다.
ReorderableListView에서는 itemCount 속성이 필수입니다.

itemCount: _items.length,

onReorder(필수)

리스트 항목이 재배치되었을 때 호출되는 콜백 함수입니다.
onReorder는 사용자가 드롭했을 때 실행됩니다.
다음은 기본적인 onReorder의 처리지만, 설명하기 쉽게 코드를 재배치했습니다.

onReorder: (oldIndex, newIndex) {
  setState(() {
    // 리스트에서 oldIndex의 Item을 삭제&취득합니다
    final item = _items.removeAt(oldIndex);
    // 그러면 oldIndex 이후의 Index가 1씩 밀리기 때문에,
    // oldIndex보다 newIndex가 클 경우 newIndex에서 -1 해야 합니다
    if (newIndex > oldIndex) {
      newIndex -= 1;
    }
    // 아까 삭제한 Item을 newIndex에 삽입합니다
    _items.insert(newIndex, item);
  });
},

itemExtent(옵션)

각 항목의 고정 높이(세로 길이)를 지정할 수 있습니다.
설정하면 각 항목의 높이가 통일되어 스크롤 성능이 향상됩니다. 기본값은 null입니다.

itemExtent: 30.0,

prototypeItem(옵션)

리스트의 레이아웃이 제공한 위젯에 따라 계산됩니다.
이를 통해 레이아웃 계산의 오버헤드가 감소합니다.
고정된 크기(내용)의 리스트를 사용할 때 유용합니다.

prototypeItem: ListTile(
  title: Text('Sample Item'),
),

proxyDecorator(옵션)

드래그 중인 항목의 외관을 변경할 수 있습니다.
다음은 드래그 중인 항목이 반투명해집니다.

proxyDecorator: (child, index, animation) {
  return Material(
    elevation: 6.0,
    color: Colors.transparent,
    shadowColor: Colors.black,
    child: child,
  );
},

buildDefaultDragHandles(옵션)

(PC나 스마트폰의) 기본 드래그 핸들을 사용할지 여부를 지정할 수 있습니다.
다음은 기본이 아닌 ReorderableDragStartListener로 핸들링하고 있습니다.

buildDefaultDrag

Handles: false,
itemBuilder: (context, index) {
  return ListTile(
    key: ValueKey(_items[index]),
    title: Text(_items[index]),
    trailing: ReorderableDragStartListener(
      index: index,
      child: Icon(Icons.drag_handle),
    ),
  );
},

padding(옵션)

다양한 위젯에 있는 친숙한 속성입니다. 리스트 전체에 패딩을 추가합니다.
다음과 같이 설정합니다.

padding: const EdgeInsets.only(left: 15, right: 15, top: 5, bottom: 5),

header(옵션)

리스트의 시작 부분에 표시할 위젯입니다. 리스트의 제목으로 사용할 수 있습니다.
리스트의 한 요소이므로 스크롤하면 사라집니다.
항상 표시하고 싶은 경우, 리스트 앞에 위젯을 추가하거나 SliverList를 사용할 필요가 있습니다.

header: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    '리스트 헤더',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

footer(옵션)

리스트의 끝부분에 표시할 위젯입니다. 사용법은 header와 동일합니다.

footer: Padding(
  padding: const EdgeInsets.all(16.0),
  child: Text(
    '리스트 푸터',
    style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
  ),
),

scrollDirection(옵션)

scrollDirection은 리스트의 스크롤 방향을 지정합니다.
수직 방향(Axis.vertical), 수평 방향(Axis.horizontal)으로 설정할 수 있습니다.
기본값은 수직 방향(Axis.vertical)입니다.

scrollDirection: Axis.horizontal,

reverse(옵션)

reverse는 리스트의 순서를 역순으로 할 수 있습니다.
true로 설정하면 리스트의 표시 순서가 역순이 됩니다. 기본값은 false입니다.

reverse:true,

scrollController(옵션)

프로그램에서 리스트뷰의 스크롤을 세부적으로 제어할 수 있습니다.

scrollController: _scrollController,

다음은 Top 버튼을 눌렀을 때 맨 위로 이동하는 기능을 가진 코드입니다.

class _MainState extends State {
  final List _items = List.generate(20, (index) => "Item $index");
  final ScrollController _scrollController = ScrollController();

  @override
  void initState() {
    super.initState();
    _scrollController.addListener(() {
      print("Scroll position: ${_scrollController.position.pixels}");
    });
  }

  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      duration: Duration(seconds: 1),
      curve: Curves.easeInOut,
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('맨 위로'),
        actions: [
          IconButton(
            icon: Icon(Icons.arrow_upward),
            onPressed: _scrollToTop,
          ),
        ],
      ),
      body: ReorderableListView.builder(
        itemCount: _items.length,
        itemBuilder: (context, index) {
          return ListTile(
            key: ValueKey(_items[index]),
            title: Text(_items[index]),
            trailing: ReorderableDragStartListener(
              index: index,
              child: Icon(Icons.drag_handle),
            ),
          );
        },
        scrollController: _scrollController,
        onReorder: (oldIndex, newIndex) {
          setState(() {
            if (newIndex > oldIndex) {
              newIndex -= 1;
            }
            final item = _items.removeAt(oldIndex);
            _items.insert(newIndex, item);
          });
        },
      ),
    );
  }
}

primary(옵션)

예를 들어 중첩된 스크롤뷰의 경우, 자식 리스트에 “primary: false”를 설정하면,
자식 리스트의 스크롤을 우선할 수 있습니다.
반대로 설정하지 않으면 부모 리스트의 스크롤이 우선되기 때문에,
자식 리스트의 스크롤이 부드럽게 되지 않습니다. 기본값은 true입니다.

primary: false,

physics(옵션)

스크롤의 동작을 설정할 수 있습니다.
표준 스크롤 클래스는 다음과 같습니다. 기본값은 ClampingScrollPhysics입니다.

  • BouncingScrollPhysics:
    리스트의 끝에 도달하면 스크롤이 반발
  • ClampingScrollPhysics:
    리스트의 끝에 도달하면 스크롤이 고정
  • FixedExtentScrollPhysics:
    스크롤이 일정한 높이로 위치를 정하는 움직임을 함
  • NeverScrollableScrollPhysics:
    스크롤이 불가능해짐
  • PageScrollPhysics:
    페이지뷰 같은 스크롤을 위해 수평 스크롤과 조합하면 편리할 수 있음
physics: BouncingScrollPhysics(),

shrinkWrap(옵션)

shrinkWrap는 리스트가 필요한 만큼의 크기를 가지도록 할지를 지정합니다.
true로 설정하면 리스트는 그 내용에 따라 자신의 높이를 결정합니다.
기본값은 false입니다.

shrinkWrap: true,

anchor(옵션)

리스트의 표시 위치를 설정할 수 있습니다.
설정하면 첫 번째 항목의 위에 공간이 생깁니다. 범위는 0~1입니다.
(어떤 상황에서 사용할 수 있을지 이미지가 떠오르지 않네요…)

anchor: 0.1,

cacheExtent(옵션)

스크롤뷰가 표시 영역 외에 얼마나 많은 위젯을 캐시할지를 제어할 수 있습니다.
기본적으로는 아주 적은 양만 캐시하기 때문에 스크롤 성능을 개선하기 위해
이용할 수 있습니다.

하지만 기본값도 충분히 빠르기 때문에 소량이고 크기가 작은 리스트에는 많이 필요하지 않을 수 있습니다.

dragStartBehavior(옵션)

드래그 작업의 터치 이벤트 시작 시 동작을 제어할 수 있습니다.

  • DragStartBehavior.start:
    드래그 동작은 사용자가 손가락을 움직이기 시작한 순간부터 시작됩니다.
    애니메이션이 더 부드럽습니다. 기본값은 이 설정입니다.
  • DragStartBehavior.down:
    다운 이벤트가 처음 감지된 위치에서 시작됩니다.
    드래그 동작의 반응이 조금 더 나아집니다.
dragStartBehavior: DragStartBehavior.down,

하지만 체감상으로는 둘 다 비슷했습니다. 신경 쓰이는 경우 설정을 고려하세요.

keyboardDismissBehavior(옵션)

사용자가 스크롤할 때 키보드를 닫을지 여부를 설정할 수 있습니다.

  • ScrollViewKeyboardDismissBehavior.manual:
    사용자가 스크롤해도 키보드가 자동으로 닫히지 않습니다.
    기본값은 이 설정입니다.
  • ScrollViewKeyboardDismissBehavior.onDrag:
    사용자가 스크롤하면 키보드가 자동으로 닫힙니다.
keyboardDismissBehavior: ScrollViewKeyboardDismissBehavior.onDrag,

restorationId(옵션)

리스트 상태 복원을 위한 ID를 설정할 수 있습니다.
스크롤 위치나 입력 내용 등 사용자의 세션 중 상태를 저장하고,
앱이 다시 시작되거나 재생성될 때 그 상태를 복원할 수 있습니다.
기본값은 설정되어 있지 않습니다.
자세한 내용은 Flutter 공식 (링크)을 참조하세요.

restorationId: 'reorderable_list', // 이 ID를 사용하여 복원 등의 처리를 구현합니다.

clipBehavior(옵션)

위젯의 경계를 넘어서는 부분을 어떻게 자를지를 제어하는 데 사용됩니다.
기본값은 Clip.hardEdge가 설정되어 있습니다.

  • Clip.none:
    부모의 경계를 넘어 그려지는 것을 허용합니다.
  • Clip.hardEdge:
    부모의 경계를 넘는 부분은 표시되지 않습니다.
  • Clip.antiAlias:
    부모의 경계를 넘는 부분을 안티앨리어스 처리(겹친 부분을 매끄럽게 처리)합니다.
    더 부드러운 에지가 되지만, 성능에 영향을 미칠 수 있습니다.
  • Clip.antiAliasWithSaveLayer:
    부모의 경계를 넘는 부분을 안티앨리어스 처리합니다.
    고품질 렌더링을 제공하지만 성능 비용이 높아집니다.
clipBehavior: Clip.antiAlias,

Clip.antiAliasClip.antiAliasWithSaveLayer는 텍스트 리스트에서는 크게 효과를
발휘하지 않을 수 있지만, 이미지나 도형을 사용한 리스트에서는 유효할 수 있습니다.

요약

정렬 가능한 ReorderableListView.builder를 소개했습니다.
속성을 설정함으로써 다양한 동작을 커스터마이즈할 수 있을 것 같습니다.
어떤 속성이 있는지 알아두기만 해도 구현의 폭이 넓어질 것 같습니다.

제목과 URL을 복사했습니다