[Flutter] Tab View 만들기(2) - PageView

Tyger·2023년 1월 23일
0

Flutter

목록 보기
9/64
post-custom-banner

Tab View 만들기(2) - PageView

Tab View 만들기(1) - Tabbar
Tab View 만들기(3) - Custom 1탄
Tab View 만들기(4) - Custom 2탄

이전 글에 이어서 이번에는 Tab View를 PageView를 활용하여 만드는 방법에 대해서 소개하도록 하겠다.

여기서는 PageView로 만들기 때문에 TabBar도 직접 커스텀해서 만들어 볼 예정이다.
사실 TabBarView 위젯을 보면 PageView로 개발이 되어있기도 하고 TabBarView, PageView 작동 방식이 거의 비슷하다고 볼 수 있다.

PageView와 직접 개발한 TabBar의 상태를 관리하기 위해서 상태 관리는 Provider를 사용하도록 하겠다.
요새 Bloc을 잘 안써서 웬만하면 Bloc을 사용하려고 하는데 간단하게 작성 하다보니 계속 Provider만 사용하는 것 같다..ㅠ

Flutter

구조를 보면 상단에 사용 할 Provider를 등록하고 Scaffold의 body안을 Column위젯으로 만들어서 TabBar와 PageView를 넣어서 개발을 하였다.
전체적인 코드를 가져오지는 않았기에 아래 공유된 Git repository에서 코드를 복사해서 사용해보면 좋을 것 같다.

UI

TabBar에 해당되는 위젯을 만들어 Wrap형태로 수평으로 배치될 수 있도록 3개의 커스텀된 위젯을 배치하였다.

해당 TabBar는 Stack으로 덮어 상단 부분에 TabBar가 들어가고 하단에 인디케이터가 배치될 수 있도록 만들어주었다.

  SizedBox(
                  height: 53,
                  child: Stack(
                    children: [
                      Wrap(
                        children: [
                          _tabBar(
                            index: 0,
                            currentIndex: state.tabIndex,
                            onTap: (i) => state.tabChanged(i),
                            context: context,
                            title: 'List',
                          ),
                          _tabBar(
                            index: 1,
                            currentIndex: state.tabIndex,
                            onTap: (i) => state.tabChanged(i),
                            context: context,
                            title: 'Grid',
                          ),
                          _tabBar(
                            index: 2,
                            currentIndex: state.tabIndex,
                            onTap: (i) => state.tabChanged(i),
                            context: context,
                            title: 'Box',
                          ),
                        ],
                      ),
                      AnimatedPositioned(
                        duration: const Duration(milliseconds: 300),
                        bottom: 0,
                        left: state.tabIndicatorPosition,
                        child: Container(
                          width: MediaQuery.of(context).size.width / 3,
                          height: 3,
                          color: Colors.white,
                        ),
                      ),
                    ],
                  ),
                ),

커스텀한 TabBar UI를 만드는 부분인데, 현재의 tab을 알려주는 currentIndex 변수와 각 TabBar의 값을 주기 위해서 넣어준 index값으로 UI를 변경하여 선택된 Tab의 색상과 사이즈를 다르게 처리하고 있다.

  GestureDetector _tabBar({
    required BuildContext context,
    required String title,
    required int index,
    required int currentIndex,
    required Function(int) onTap,
  }) {
    return GestureDetector(
      onTap: () => onTap(index),
      child: Container(
        color: Colors.transparent,
        width: MediaQuery.of(context).size.width / 3,
        height: 50,
        child: Center(
            child: Text(
          title,
          style: TextStyle(
            color: currentIndex == index
                ? Colors.white
                : const Color.fromRGBO(215, 215, 215, 1),
            fontWeight: FontWeight.bold,
            fontSize: currentIndex == index ? 20 : 18,
          ),
        )),
      ),
    );
  }

PageView도 TabBarView와 동일하게 위젯의 사이즈 값을 넣어주어야 하므로 Expanded 확장 위젯으로 남은 영역을 모두 차지할 수 있도록 해주었다.

여기서 눈여겨 봐야할 점이 PageView의 controller를 사용하여야 한다는 점이다.

controller 없이도 스와이프로 페이지를 넘기거나 하는데는 문제가 없지만 페이지가 변경되거나 커스텀으로 만든 탭이 클릭될 때 같이 싱크를 맞춰 변경해주어야 하므로 controller를 사용해야 한다.

Expanded(
	child: PageView(
    	controller: state.pageController,
        onPageChanged: (i) => state.tabChanged(i),
        children: [
        ...
        ]
    )
)

PageView에 들어가는 위젯들이다. 여기서도 위젯의 key 값에 PageStorageKey(uniqueKey)를 넣어주지 않으면 페이지 이동시 스크롤 포지션이 초기화 된다.

                    ListView.builder(
                        key: const PageStorageKey("LIST_VIEW"),
                        itemCount: 1000,
                        itemBuilder: (context, index) {
                          return Container(
                            padding: const EdgeInsets.symmetric(vertical: 12),
                            width: MediaQuery.of(context).size.width,
                            child: Center(
                              child: Text(
                                "List View $index",
                                style: TextStyle(
                                    fontSize: 16,
                                    color: Colors.accents[index % 15],
                                    fontWeight: FontWeight.bold),
                              ),
                            ),
                          );
                        }),
                    GridView.builder(
                        key: const PageStorageKey("GRID_VIEW"),
                        itemCount: 1000,
                        gridDelegate:
                            const SliverGridDelegateWithFixedCrossAxisCount(
                          crossAxisCount: 3,
                          crossAxisSpacing: 12,
                          mainAxisSpacing: 12,
                        ),
                        itemBuilder: ((context, index) {
                          List<int> _number = [
                            Random().nextInt(255),
                            Random().nextInt(255),
                            Random().nextInt(255)
                          ];
                          return Container(
                            color: Color.fromRGBO(
                                _number[0], _number[1], _number[2], 1),
                            child: Center(
                                child: Text(
                              "Grid View $index",
                              style: const TextStyle(
                                  fontSize: 16,
                                  color: Colors.white,
                                  fontWeight: FontWeight.bold),
                            )),
                          );
                        })),
                    Container(
                      width: 10,
                      color: const Color.fromRGBO(91, 91, 91, 1),
                      child: const Center(
                        child: Text(
                          'Box',
                          style: TextStyle(
                              color: Colors.white,
                              fontSize: 56,
                              fontWeight: FontWeight.bold),
                        ),
                      ),
                    ),

Provider

PageView로 Tab View를 구현할 상태 값이다.
PageView를 제어할 pageController가 필요하고 현재 탭을 알려줄 tabIndex 값과 탭 인디케이터의 포지션을 이동하기 위한 tabIndicatorPosition 변수를 선언해 준다.

추가로 탭 인디케이터 포지션 값을 계산하기 위해 device의 가로 폭을 알아야 하므로 sizeWidth 변수도 선언해 준다.

  PageController pageController = PageController(initialPage: 0);
  int tabIndex = 0;
  double _sizeWidth = 0.0;
  double tabIndicatorPosition = 0.0;

해당 Provider를 생성할 때 디바이스 가로 사이즈를 가져오기 위한 함수를 만들어 준다.

void started(double width) {
    _sizeWidth = width;
  }

tabChanged 로직이 Tab View를 구현하는 로직이다. 정수형 index 값을 필수 파라미터로 받아와 현재 이벤트가 발생한 index 값으로 아래와 같이 인디케이터 포지션을 이동시켜 주면 된다.

받아온 index 값으로 PageView도 함께 넘겨주기 위해 .jumpToPage(index) 기능을 이용하여 선택된 index 값으로 이동시켜 주면 끝이난다.

  void tabChanged(int index) {
    tabIndex = index;
    switch (index) {
      case 0:
        tabIndicatorPosition = 0.0;
        break;
      case 1:
        tabIndicatorPosition = _sizeWidth / 3;
        break;
      case 2:
        tabIndicatorPosition = _sizeWidth - (_sizeWidth / 3);
        break;
      default:
    }
    pageController.jumpToPage(index);

    notifyListeners();
  }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/tree/main/lib/tab_view/pageview

마무리

지난번에 이어서 Tab View를 생성해 보았느데, TabBarView에 비해서 PageView를 사용하게 되면 확실히 개발이 복잡해 진다. 하지만 이렇게 조금씩 커스텀을 하면서 사용하는 연습을 해야 다양한 UI 구조를 개발하게 되더라도 어려움 없이 개발을 완성시킬 수 있다.

공유한 Git repository에 소스 코드를 가져와서 실행해 보면 이해가 훨씬 빠르게 될 것이다.

이번에는 커스텀해서 만든 TabBar와 PageView로 구현을 해보았는데, 다음 글에서는 훨씬 더 복잡한 구조로 Tab View를 만드는 방법에 대해서 다루게 될 것이다. PageView는 기본적으로 횡 스크롤로 페이지 간의 이동을 도와주는 위젯이라 그나마 이번에 다룬 내용이 쉬운 편이지만 다음 글부터는 TabBarView와 PageView를 사용하지 않고 직접 이러한 위젯을 만드는 방법에 대해서 작성해 보도록 하겠다.

profile
Flutter Developer
post-custom-banner

1개의 댓글

comment-user-thumbnail
2024년 3월 12일

개발 공부하는데 많은 도움 됐습니다. 좋은 글 써주셔서 감사합니다~

답글 달기