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

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

1편에 이어서 로직 부분에 해당되는 GetController의 코드를 만들어 보자.

작성된 코드는 최대한 복잡하지 않게 작성을 하였지만, 이해가 어려우시다면 Git 클론해서 직접 실행해보면서 값을 변경해 보면 이해가 될 것이다.

자 한번 작성해보자.

Flutter

작성에 앞서 상태관리를 하여야하는 부분이 1탄에서 작성한 UI부분에 필요한 Custom TabBarView / Custom Tab Indicator 부분의 포지셔닝을 변경할 수 있는 기능이 필요할 것이고, 이 기능은 탭의 클릭시 onTap 함수와 Custom TabBarView부분에 작성된 onHorizontalUpdate / onHorizontalEnd 함수에 사용될 것이다.

추가적으로 Custom TabBarView / Custom Tab Indicator는 모두 AnimatedPositioned 위젯으로 만들어 duration 값을 변경해 주고 있기에 duration 값도 상태 관리하는 기능이 필요하다.

GetController

먼저 GetxControllera 를 상속받는 class를 만들어 준다.

class TabViewCustomGetx extends GetxController {
...
}

사용할 변수를 선언해 준다.

Stream 형태의 변화를 주어야할 값은 RxType으로 생성해주었다. 여기서 왜 RxType으로 작성하였는지는 1탄에서 짧게 설명을 하였었다.

positions List 변수에는 3개의 값을 넣어주는데 이게 TabBarView에 해당되는 위젯들의 각 left 포지션에 해당되는 값이다.
left 값이 0.0이면 위젯이 화면에 보일 것이고 Get.width는 디바이스의 오른쪽에 -Get.width는 왼쪽 부분에 각각 사라져 보일 것이다.

Custom Tab Indicator 값의 변화를 주기 위한 변수로 indicatorPosition을 선언해주었고 현재 선택되어 있는 tab 값을 관리하는 tabIndex 변수를 선언하였다.

  List<RxDouble> positions = [(0.0.obs), (Get.width).obs, (Get.width * 2).obs];
  RxInt duration = 0.obs;
  int tabIndex = 0;
  RxDouble indicatorPosition = 0.0.obs;

onTap

탭이 클릭될 때 상태 변화의 기능을 만들어 주는 부분이다.
여기는 비교적 간단하게 구성되어있다.

tabChanged 함수의 필수 값으로 클릭된 index 값을 받아와 tabIndex의 값을 변경시켜 주면 되고, tab이 클릭되어 이동될 때 animation을 주기 위해 duration 값을 0.3초로 설정하였다.

switch case 문으로 선택된 index 값에 따른 positions List 변수의 값을 각각 변경해주면 된다.

indicatorPosition 값을 보면 각 탭의 영역에 맞는 사이즈로 이동을 해주는 포지션 값이다.

 void tabChanged(int index) {
    duration.value = 300;
    tabIndex = index;
    switch (index) {
      case 0:
        positions = [(0.0).obs, (Get.width).obs, (Get.width * 2).obs];
        indicatorPosition.value = 0.0;
        break;
      case 1:
        positions = [(-Get.width).obs, (0.0).obs, (Get.width).obs];
        indicatorPosition.value = Get.width / 3;
        break;
      case 2:
        positions = [(-Get.width * 2).obs, (-Get.width).obs, (0.0).obs];
        indicatorPosition.value = Get.width - (Get.width / 3);
        break;
      default:
    }
    update();
  }

onHorizontalPanUpdate

여기 부분은 Custom TabBarView 부분의 제스쳐에 의해 값이 변경될 때 이동되는 부분인데, 여기서는 animation처리를 하면 안되기에 duration 값은 0으로 변경해 준다.

positions의 각 인덱스 값은 단순히 postions의 각 현재 값에서 DragUpdateDetails 객체에서 제공하는 dx 포지션 값을 더하여 이동해 준다.

싱크를 맞춰서 인디케이터도 함께 움직여야 하기에 아래와 같은 계산법으로 이동시켜주면 된다.

  void horizontalPanUpdated(DragUpdateDetails details) {
    duration.value = 0;
    positions[0].value = positions[0].value + details.delta.dx;
    positions[1].value = positions[1].value + details.delta.dx;
    positions[2].value = positions[2].value + details.delta.dx;
    indicatorPosition.value = indicatorPosition.value - (details.delta.dx / 3);
  }

onHorizontalPanEnd

가장 복잡한 로직 부분인데, 여기 부분 만드는데 고생을 좀 했었다 ㅠㅠ

onHorizontalPanEnd는 제스쳐를 놓았을 때 호출되는 곳인데 DragEndDetails 안에 velocity 객체 값으로 제공 받을 수 있다.

제스쳐를 놓았을 때에 맞춰 포지션을 변경해 주지 않으면 TabBarView의 포지션은 이상한 위치에 놓여져 애매한 포지션을 가지게 되므로 아래와 같은 계산 로직으로 TabBarView의 최종 포지션을 계산해 주면 된다.

제스쳐를 놓은 상태에서 animation처리를 다시 해주어야 하므로 duration 값을 다시 0.3초로 변경해 주었다.

아래 계산 로직은 복잡해 보이지만 사실 단순한 방법이다. 제스처가 놓아진 부분에서 얼마만큰의 dx 포지션이 움직이는 가에 관련된 값이 Velocity 객체에 있는 distance 값인데, 이 값을 그대로 사용하면 안되고 해당 값을 원하는 만큼 스크롤이 되게끔 축소시켜 사용하여야 한다.

여기서 계산되는 값으로 탭의 위치가 어느 포지셔닝에 가야하는 지를 정해주면 된다.

 void horizontalPanEnded(DragEndDetails details) {
    duration.value = 300;
    double _first = _returnToPanEndDouble(details.velocity, positions[0]);
    double _third = _returnToPanEndDouble(details.velocity, positions[2]);
    List<RxDouble> _returnPositions = [
      0.0.obs,
      (Get.width).obs,
      (Get.width * 2).obs
    ];
    switch (tabIndex) {
      case 0:
        if (_first < -Get.width / 2) {
          positions = [(-Get.width).obs, 0.0.obs, Get.width.obs];
          tabIndex = 1;
          indicatorPosition.value = Get.width / 3;
        } else {
          positions = [0.0.obs, (Get.width).obs, (Get.width * 2).obs];
          tabIndex = 0;
          indicatorPosition.value = 0;
        }
        break;
      case 1:
        if (_first > -Get.width / 2) {
          positions = [0.0.obs, (Get.width).obs, (Get.width * 2).obs];
          tabIndex = 0;
          indicatorPosition.value = 0;
        } else if (_third < Get.width / 2) {
          positions = [(-Get.width * 2).obs, (-Get.width).obs, 0.0.obs];
          tabIndex = 2;
          indicatorPosition.value = Get.width - (Get.width / 3);
        } else {
          positions = [(-Get.width).obs, 0.0.obs, Get.width.obs];
          tabIndex = 1;
          indicatorPosition.value = Get.width / 3;
        }

        break;
      case 2:
        if (_third < Get.width / 2) {
          positions = [(-Get.width * 2).obs, (-Get.width).obs, 0.0.obs];
          tabIndex = 2;
          indicatorPosition.value = Get.width - (Get.width / 3);
        } else {
          positions = [(-Get.width).obs, 0.0.obs, Get.width.obs];
          tabIndex = 1;
          indicatorPosition.value = Get.width / 3;
        }
        break;
      default:
        _returnPositions = _returnPositions;
    }
    update();
  }
  
  double _returnToPanEndDouble(
    Velocity velocity,
    RxDouble position,
  ) {
    double _move = (velocity.pixelsPerSecond.distance / 200) + position.value;
    return _move;
  }

Result

Git

https://github.com/boglbbogl/flutter_velog_sample/blob/main/lib/tab_view/custom/tab_view_custom_getx.dart

마무리

Tab View를 Flutter의 기본 위젯인 TabBarView와 PageView로 만들어 보았고, Custom하게 사용하기 위해 직접 만드는 방법에 대해서도 알아보았다.

기본 위젯으로도 만들 수 있는 UI 구조는 당연히 기본 위젯을 활용하는게 좋지만 기본 위젯으로 만들 수 없는 부분도 분명히 있다.
실제로 이런걸 만들어본 이유도 바로 UI 구조를 커스텀해야할 일이 실제로 있어서 직접 만들어 사용해 보았었다.

직접 만들어 실제 프로젝트에 넣은 로직은 여기서 작성한 로직보다 훨 씬 복잡하고 다양한 변수가 사용되고 있는데, 기회가 되면 공유하는 것도 좋을 것같다.

아마도 한 10편 정도의 글이 나오지 않을까 하는 생각이 든다..

힌트를 주자면 실제로 만들 때는 AnimationController도 사용해서 만들어야 하는데, 계산 로직이 많이 올라와 있지 않아서 애를 먹었었다 ㅠㅠ

수평 구조의 Tab View 외에도 수직 구조의 ScrollView도 직접 만들 수 있는데, 여기에 해당되는 글도 한 번 작성해 보도록 하겠다.

꼭 Git을 클론해서 직접 사용해 보시길 추천합니다 !

profile
Flutter Developer

0개의 댓글