Flutter - #36. ReorderableListView

Pearl Lee·2021년 8월 4일
1

Flutter Widget

목록 보기
34/50

Flutter 일기
참고 1: https://api.flutter.dev/flutter/widgets/ReorderableDragStartListener-class.html
참고 2: https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html
참고 3: https://api.flutter.dev/flutter/material/ReorderableListView-class.html










ReorderableListView

오늘 배워볼 것은 ReorderableListView.
ListView 랑 똑같은데 사용자가 목록의 순서를 하나하나 재배열할 수 있다.
자식 요소가 너무 많아지면 사용하기 적절치 않다. 그리고 리스트의 아이템들은 모두 key 를 갖고 있어야 한다. 키가 뭘까..?

우선 아래 2가지만 넣어도 잘 동작한다.

required List<Widget> children,
required ReorderCallback onReorder,

코드로 가보자.









코드 예시로 알아보자

오늘도 공식 페이지의 예제를 가져다가 돌려보았다.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());


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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: "ReorderableListView",
      home: Scaffold(
        appBar: AppBar(title: const Text('ReorderableListView')),
        body: const MyStatefulWidget(),
      ),
    );
  }
}


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

  
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}


class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final List<int> _items = List<int>.generate(10, (int index) => index);

  
  Widget build(BuildContext context) {
    final ColorScheme colorScheme = Theme.of(context).colorScheme;
    final Color oddItemColor = colorScheme.primary.withOpacity(0.05);
    final Color evenItemColor = colorScheme.primary.withOpacity(0.15);

    return ReorderableListView(
      padding: const EdgeInsets.symmetric(horizontal: 20),
      children: <Widget>[
        for (int index = 0; index < _items.length; index++)
          ListTile(
            key: Key('$index'),
            tileColor: _items[index].isOdd ? oddItemColor : evenItemColor,
            title: Text('Item ${_items[index]}'),
            trailing: Icon(Icons.drag_handle),
          ),
      ],
      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final int item = _items.removeAt(oldIndex);
          _items.insert(newIndex, item);
        });
      },
    );
  }
}

colorScheme.primary 가 무슨 색인지 몰라서 들어가보니, 요렇게 되어있다.

return ColorScheme(
      primary: primarySwatch,
      ...
);

여기서 primarySwatch의 기본값은 파란색이다.

MaterialColor primarySwatch = Colors.blue,

일단 색은 대충 이렇고, ReorderableListView 를 파보자.





children: <Widget>[
        for (int index = 0; index < _items.length; index++)
          ListTile(
            key: Key('$index'),	//key 는 각각의 고유한 값만 가지면 되는 듯하다. 
            tileColor: _items[index].isOdd ? oddItemColor : evenItemColor,
            title: Text('Item ${_items[index]}'),
            trailing: Icon(Icons.drag_handle),
          ),
      ],
      onReorder: (int oldIndex, int newIndex) {
        setState(() {
          if (oldIndex < newIndex) {
            newIndex -= 1;
          }
          final int item = _items.removeAt(oldIndex);
          _items.insert(newIndex, item);
        });
      },

children 부분은 _items의 length 인 10만큼 ListTile 을 만드는 것으로 간단하다. onReorder 속성의 아래 두줄을 먼저 보면, 이동하려는 리스트 항목의 이동 전 위치(oldIndex)를 지우고 이동 후 위치(newIndex)자리에 다시 집어넣는 방식이다.



그런데 oldIndex < newIndex 일 때는 어째서 newIndex=-1 을 해줘야 하는가? removeAt으로 oldIndex 자리의 항목을 지워버리면, List의 길이가 하나 줄어들기 때문이다. 이 상태에서 새 위치로 그냥 이동하면 이동하려는 위치보다 한칸 더 많이 이동하게 된다. 그리고 만약 맨 마지막 위치로 이동하려고 할 때, 리스트의 범위를 벗어나게 되어 인덱스 에러가 발생한다.
반면 newIndex가 oldIndex보다 작아질 때는 상관없다.

이제 실행화면을 보자.

위처럼 longpress, 조금 오래 눌러야 이동이 활성화된다.





옆에 trailing으로 버튼 만들어놓고 왜 저거 눌러서 옮기지 않았을까?
저걸로 이동이 안된다...










빠름의 민족은 LongPress를 견딜 수 없다

ReorderableListView 안에는 buildDefaultDragHandles 라는 속성이 있다. boolean 타입인데, 기본값은 true로 설정되어 있다.

안드로이드 애뮬레이터가 아닌 Chrome 환경에서 실행을 시켜보니 trailing 속성에 지정했던 Icons.drag_handle 을 빼버려도 이 아이콘이 자동으로 생긴다. 게다가 아이콘을 누르면 longPress가 아닌 그냥 클릭으로도 이동이 가능하다. 왜일까?

buildDefaultDragHandles가 true 이면 Desktop 환경에서는 ReorderableDragStartListener 로 감싼 Icons.drag_handle 이 생성되는 것이 기본 드래그 방식이라고 한다.
하지만 모바일 환경에서는 항목 전체를 ReorderableDelayedDragStartListener로 감싼 것이 기본이다. (참고 2에 가면 있다.)




이름만 봐도 뭔가 차이가 나지 않는가? Delayed... <- Long press의 주범이다.
longpress 를 보자마자 빨리빨리 의 마음이 솟구쳤으니, ReorderableDragStartListener 를 이용해서 빨리 해보자.







코드를 다음과 같이 수정한다.

 trailing: ReorderableDragStartListener(
                index: index, child: Icon(Icons.drag_handle)),

trailing 부분의 아이콘을 ReorderableDragStartListener로 감싸준 후의 실행화면을 보자.

아이콘 부분을 가볍게 눌러서 이동할 수도 있고, 항목 자체를 오래 눌러서 이동할 수도 있다. buildDefaultDragHandles : false 로 지정하면 항목을 오래 눌러서 이동하는 것은 안된다.












빠름이 찾는다고 한참 걸렸네
오늘의 일기는 여기까지!

profile
안 되면 되게 하라

2개의 댓글

comment-user-thumbnail
2023년 9월 10일

내용이 너무 깔끔한 것 같습니다
잘 보고 갑니다!!

답글 달기
comment-user-thumbnail
2024년 6월 11일

감사합니다! LongPress에 대한 민감도를 어떻게 조정해야할지 고민이 너무 많았는데 덕분에 도움되었습니다!

답글 달기