[Flutter] Vertical scroll tabbar 을 만들어보자

2023. 6. 5. 19:48IT/Flutter

SMALL

 

오늘 만들어볼 ui는 스크롤이 인덱스 아이템을 넘어갈때마다 tabbar 아이템이 자동으로 변경되는 ui이다.

 

필요한 것은 다음과 같다.

 

1. 각 인덱스 별 아이템의 높이를 동적으로 구한다.

2. 스크롤 된 offset 이 인덱스 별 아이템의 높이를 벗어날때마다 tabbar 의 아이템을 변경해준다.

3. tabbar 의 아이템을 tap 하면 해당 인덱스의 아이템으로 스크롤시켜준다.

 

해당 ui를 구현하는데 있어서 가장 까다로웠던것이 두가지가 있었는데 그중 첫번째가 아이템의 높이를 동적으로 구해주는 것이었다.

다행히도 Globalkey 를 이용하여 Widget의 Renderbox 에 접근해 크기를 구할 수 있었다.

 

final ancestor = context.findRenderObject(); // 부모 위젯의 renderObject

for (var element in widget.children) {
  assert(element.key != null && element.key is GlobalKey); // 위젯의 context 에 접근하기 위해서 globalKey 가 반드시 필요하다

  // 해당 위젯의 부모로부터의 위치를 구한다
  final res = ((element.key as GlobalKey)
          .currentContext
          ?.findRenderObject() as RenderBox)//RenderBox 에 접근
      .localToGlobal(Offset.zero, ancestor: ancestor);// 부모위젯의 크기로부터 상대적인 위치를 구하기 위함
}

 

단, 해당 연산 과정은 Frame 을 구성하기전에는 수행할 수 없는데 Frame이 구성되지않으면 위젯의 크기또한 아직 구성되기 전이라 동적으로 크기를 계산할 수 없기 때문이다.

 

Flutter에서는 Frame 구성이 종료되었음을 Future  Widgetsbinding.instance.endOfFrame 메서드로 제공해주고 있기때문에 해당 해당 메서드를 이용하여 Frame 구성이 끝난후 위 연산이 실행되도록 하였다.

WidgetsBinding.instance.endOfFrame.then((value) {
  final ancestor = context.findRenderObject();

  for (var element in widget.children) {
    assert(element.key != null && element.key is GlobalKey);

    final res = ((element.key as GlobalKey)
            .currentContext
            ?.findRenderObject() as RenderBox)
        .localToGlobal(Offset.zero, ancestor: ancestor);
  }

});

 

두번째로 스크롤된 offset이 인덱스 아이템 별 높이를 벗어날 때 마다 tabbar 아이템을 변경해주는것은, ScrollController 와 TabController 을 이용하여 쉽게 구현할 수 있다.

 

먼저 아이템 별로 높이갚을 이전에 구했으니 해당 값들을 이용해서 현재 스크롤된 값이 어느위치인지 파악한 후 tabbar 아이템을 변경해주면 된다.

 

scrollController.addListener(() {
   /// indexList - children 의 Offset값을 가지고있는 리스트

  for (var index = indexList.length - 1; index >= 0; index--) {
    if (scrollController.offset >= indexList[index]) { 
// 인덱스의 마지막부터 검사를 시작하여 현재 스크롤 offset이 더 크거나 같다면 tabbar 의 아이템을 변경한다
      tabController?.animateTo(index);
      return;
    }
  }
});

 

마지막으로 tabbar의 아이템을 tap해주면 해당 아이템의 인덱스위치로 스크롤 해주는 기능을 구현해본다.

여기서 앞서말했던 구현하기 까다로웠던 작업이 발생했다.

 

처음 생각은 tab 아이템 클릭 이벤트에서 ScrollController 을 위치를 아이템 인덱스로 옮겨주면 해결될 것이라고 생각했었다.

그러나, 기존에 선언해놓았었던 ScrollController 의 리스너 때문에 tabbar 아이템 인덱스가 정상적으로 옮겨지지 않고 스크롤 됨에 따라 이동되어 부자연스러운 행동이 나타나게 되었다.

이를 해결하기 위해서 tabbar 아이템을 클릭했을경우에는 mutex 값을 하나 만들고 mutex가 활성화된 상태일때는 스크롤 리스너가 동작하지 않도록 했다. 이후 스크롤 애니메이션이 동작하는 시간만큼 디바운스 처리를 하여 해결하였다.

 

//탭 아이템의 onTap 함수
onTap: (index) {
  mutex = true;
  scrollController.animateTo(indexList[index],
      duration: const Duration(milliseconds: 500),
      curve: Curves.easeIn);
},

// 탭 컨트롤러의 리스너
 tabController?.addListener(() {
      if (tabController?.animation?.isCompleted ?? true) {
        timer?.cancel();
        timer = Timer(const Duration(milliseconds: 500), () { //500ms 는 스크롤러 애니메이션 시간
          mutex = false;
        });
      }
    });
    
 // 스크롤 컨트롤러의 리스너
 scrollController.addListener(() {
      if (mutex) return; // mutex 가 true 라면 아래의 로직을 실행하지 않음

      for (var index = indexList.length - 1; index >= 0; index--) {
        if (scrollController.offset >= indexList[index]) {
          tabController?.animateTo(index);
          return;
        }
      }
    });

 

github : https://github.com/sejun2/vertical_scroll_tabbar

 

GitHub - sejun2/vertical_scroll_tabbar

Contribute to sejun2/vertical_scroll_tabbar development by creating an account on GitHub.

github.com

pub.dev: https://pub.dev/packages/vertical_scroll_tabbar

LIST