스크롤 인디케이터 직접 만들기

스크롤 인디케이터 - Horizontal
무한 스크롤 만들기 - Vertical
무한 스크롤 만들기 - Horizontal
무한 스크롤 만들기 - PageView

이번 글에서는 스크롤 바를 직접 개발하는 방법에 대해서 알아보도록 하겠다.

보통 스크롤 바는 Flutter 에서 지원하는 meterial widget에 있는 ScrollBar() 위젯을 사용하면 손쉽게 기능을 추가할 수 있다.

하지만 ScrollBar 위젯은 List/Grid View에만 사용할 수 있고 만일 커스텀이라도 하고 싶다면 그냥 만들어서 사용하는게 더 편하다고 생각이 된다.

저는 웹뷰 구조안에 ScrollBar를 넣고 싶어서 직접 만들게 되었는데, 이전에도 Horizontal List View 인디케이터는 개발을 진행한 적이 있어서 그냥 직접 만들기로 하고 Vertical 구조에 맞는 스크롤 인디케이터 개발을 진행하였다.

이번 글에서는 Vertical 구조에 맞는 스크롤 바를 만들고 다음 글에서는 Horizontal에 맞는 스크롤 바에 대하여 작성할 예정이다.

상태관리 Provider를 사용하여 예제를 작성하였다.

Flutter

UI

먼저 Stack 구조를 사용하여 스택 안에 리스트에 인덱스를 증가시키는 무한 스크롤을 만들고 스크롤 바를 넣어줄 것이다.
아래는 무한스크롤이 가능한 간단한 예제이고, 무한 스크롤이 진행될 때 로딩바를 구현하기 위해 isMoreLoading이라는 변수를 생성하였다.

SingleChildScrollView(
                  controller: value.scrollController,
                  child: Column(
                    children: [
                      ...List.generate(
                          value.indexSample.length,
                          (index) => SizedBox(
                              width: size.width,
                              height: 60,
                              child: Padding(
                                padding:
                                    EdgeInsets.only(left: size.width / 2.5),
                                child: Text(
                                  value.indexSample[index],
                                  style: TextStyle(
                                    fontSize: 20,
                                    fontWeight: FontWeight.bold,
                                    color: value.color[index],
                                  ),
                                ),
                              ))),
                      if (value.isMoreLoading) ...[
                        const SizedBox(
                          height: 50,
                          child: Padding(
                            padding: EdgeInsets.all(8.0),
                            child: CircularProgressIndicator(
                              color: Colors.deepOrange,
                            ),
                          ),
                        ),
                      ],
                      const SizedBox(height: 40),
                    ],
                  ),
                ),

아래 코드가 스크롤 바를 보여줄 UI 코드이다.
스택 구조안에 Positioned 위젯을 사용하여 스크롤 바의 움직임을 provider의 notifyListeners()로 상태를 변경시킬 것이다.

Positioned(
	right: 8,
	top: value.verticalPosition,
	child: Container(
			width: 5,
			height: value.verticalHeight,
			decoration: BoxDecoration(
			borderRadius: BorderRadius.circular(8),
			color: const Color.fromRGBO(175, 175, 175, 1),
			),
		),
	),

스크롤이 변화하는 값을 수신 받기 위해 NotificationListener 위젯으로 UI를 감싸게 되면 스크롤 픽셀 값을 얻을 수 있다.

NotificationListener<ScrollUpdateNotification>(
          onNotification: (ScrollUpdateNotification notification) {
            value.listener();
            return false;
          },
          child: Scaffold(

Provider

Provider에서 사용할 상태 값을 선언해준다.

  List<String> indexSample = [];
  List<Color> color = [];
  ScrollController scrollController = ScrollController();
  double deviceHeight = 0.0;
  bool isMoreLoading = false;
  double verticalPosition = 0.0;
  double verticalHeight = 0.0;

해당 함수는 페이지 진입시 Provider 생성과 함께 상태 값을 변경해주기 위해 사용되는 함수이다.
indexSample 변수에 15개의 아이템을 넣어주고, 15개의 변수에 함께 사용할 color 값도 생성해준다.

 void started({
    required BuildContext context,
  }) {
    deviceHeight = MediaQuery.of(context).size.height;
    indexSample = List.generate(15, (index) => 'Index $index');
    color = List.generate(15, (index) => Colors.accents[index]);
  }

무한 스크롤을 만들기 위해 아이템을 더 불러오는 함수이다.
isMoreLoading이라는 함수가 UI 로딩 바에 사용하기도 하고 해당 함수가 종료될 때까지 한 번만 실행되기 하기 위해서 사용하는 변수이다.
1초의 딜레이를 주고 함수를 실행시키는 것은 로딩 바를 보여주기 위해 딜레이를 사용하는 것이다.
해당 함수의 아래에 호출되는 _verticalIndicatorPosition() 함수가 리스트의 높이에 맞게 스크롤 바 높이 및 스크롤 포지션을 제어하기 위해 사용하는 함수이다.

void _addItem() {
    if (!isMoreLoading) {
      isMoreLoading = true;
      notifyListeners();
      Future.delayed(const Duration(milliseconds: 1000), () {
        int _last = int.parse(indexSample.last.replaceAll("Index ", ""));
        indexSample.addAll(
            [...List.generate(15, (index) => 'Index ${index + (_last + 1)}')]);
        color.addAll(List.generate(15, (index) => Colors.accents[index]));
        Future.delayed(const Duration(milliseconds: 100), () {
          _verticalIndicatorPosition();
        });
        isMoreLoading = false;
        notifyListeners();
      });
    }
  }

아래 함수의 verticalHeight은 스크롤의 높이 값이고, verticalPosition은 스크롤의 포지션 값을 계산한 결과 값이다.

150이라는 값은 리스트의 가장 아래로 부터 여유 공간을 만들기 위해서 해당 값만큼의 높이 값에서 제거해주었다.

void _verticalIndicatorPosition() {
    verticalHeight =
        (deviceHeight / scrollController.position.maxScrollExtent) *
            deviceHeight;
    double _currentPixels = scrollController.position.pixels;
    double _mainContainer = (scrollController.position.maxScrollExtent) /
        (deviceHeight - verticalHeight - 150);
    verticalPosition = _currentPixels / _mainContainer;
    notifyListeners();
  }

NotificationListener 위젯에서 스크롤 포지션을 수신받기 위해 생성한 함수이다.

  void listener() {
    scrollController.addListener(() {
      double _pixels = scrollController.position.pixels;
      if (_pixels > scrollController.position.maxScrollExtent * 0.8) {
        _addItem();
      }
      _verticalIndicatorPosition();
    });
  }

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/scroll/vertical_indicator

Result

profile
Flutter Developer

0개의 댓글