Scroll View 만들기(1) - SingleChildScrollView
Scroll View 만들기(2) - CustomScrollView
Scroll View 만들기(3) - ListView
Scroll View 만들기(4) - GestureDetector + Stack 1편
이번 글에서는 이전 글에서 다뤘던 직접 개발하는 Scroll View 1편에 해당하는 UI 부분에 이어서 로직 부분에 대해서 작성하도록 하겠다.
상태 관리는 Get Reactive 방식을 사용하였고, 생각보다는 간단한 로직으로 개발 되어 있다.
Scroll View를 직접 개발하기 위해서 고려해야 할 부분이 있었는데, 제스쳐를 놓았을 때 얼마만큼의 거리를 이동시켜야 하는지, 속도와 애니메이션은 어떻게 처리를 해야할 지가 가장 개발하기 어렵고 고민되었던 부분이었다.
여기서는 AnimationController를 사용하여 이러한 고민을 해결하기로 하였다.
로직과 스크롤을 처리하기 위해서 Get과 AnimationController를 사용하였으며, AnimationController의 animate 기능을 사용하였다.
animate는 0부터 1사이의 값을 통해 animation이 가능하도록 처리해주는 기능으로 이 기능을 활용하여 제스처를 놓았을 때의 스크롤 애니메이션을 구현하였다.
dependencies:
get: ^4.6.5
ScrollViewGestureGetx _controller = Get.put(ScrollViewGestureGetx());
Obx(() => Positioned(
...
))
먼저 AnimationController를 사용하기 위해서는 SingleTickerState를 확장하여야 하는데, Get에서 제공하는 GetSingleTickerProviderStateMixin을 사용하면 된다.
다른 상태관리를 사용하게 되면 UI부분에서 확장하여 사용할 수 있다.
class ScrollViewGestureGetx extends GetxController
with GetSingleTickerProviderStateMixin {
...
}
AnimationController를 생성하고 GetxController가 인스턴스될 때 onInit에서 아래와 같이 vsync를 초기화 해준다.
late AnimationController scrollController;
void onInit() {
scrollController = AnimationController(vsync: this);
super.onInit();
}
스크롤 기능을 만들기 위해 사용되어야 할 변수이다. topPostion 변수가 바로 Postioned 위젯의 top에 사용되어 질 값으로 스크롤 만큼 위젯을 움직이는 값으로 사용된다.
late Animation<double> scrollAnimation;
late AnimationController scrollController;
RxDouble topPosition = (0.0).obs;
Offset dragStartOffset = const Offset(0, 0);
double dragStartValue = 0.0;
GestureDetector의 onVerticalStart에 해당하는 기능이다. DragStartDetails 객체를 받아와 dragStartOffset에 globalPostion을 넣어준다.
onVerticalStart는 사용해도 되고 사용하지 않아도 상관은 없는 기능인데, 해당 기능을 활용하면 좀 더 세세한 스크롤을 만들 수 있다.
void dragStarted(DragStartDetails details) {
dragStartOffset = details.globalPosition;
dragStartValue = topPosition.value;
}
onVerticalDragUpdate는 제스쳐가 수신될 때 지속적으로 리턴되는 기능으로 현재 제스쳐의 포지션 만큼 topPostion을 움직여 주면 되는 간단한 로직이다.
void dragUpdated({
required DragUpdateDetails details,
required double min,
required double max,
}) {
var newValue = clamping(
dragStartValue - (details.globalPosition - dragStartOffset).dy / 1.0,
min,
max);
topPosition.value = newValue;
}
double clamping<T extends num>(T number, T low, T high) =>
max(low * 1.0, min(number * 1.0, high * 1.0));
이제 가장 까다로운 부분인 제스처를 놓았을 때 처리해야 하는 기는이다. start와 update 기능의 리턴 값과 동일하게 Details 객체를 얻을 수 있는데 DragEndDetails 객체는 primaryVelocity, velocity의 변수를 제공하고 있다.
아래 로직이 내가 만든 제스쳐를 놏은 이 후 얼마만큰의 픽셀을 이동 시킬지와 속도, 애니메이션에 관한 로직이다.
AnimationController의 animate를 사용하여 addListener를 수신하게 하여 스크롤 뷰를 만들어 보았다.
void dragEnded({
required DragEndDetails details,
required double min,
required double max,
}) {
double velocity = details.primaryVelocity!;
double originalValue = topPosition.value;
void Function() currentFlingListener =
_flingListener(originalValue: originalValue, min: min, max: max);
scrollAnimation =
Tween(begin: 0.0, end: velocity / 2).animate(CurvedAnimation(
curve: Curves.fastLinearToSlowEaseIn,
parent: scrollController,
))
..addListener(currentFlingListener);
scrollController
..reset()
..forward();
}
void Function() _flingListener({
required double originalValue,
required double min,
required double max,
}) {
return () {
double newValue =
clamping(originalValue - scrollAnimation.value, min, max);
if (newValue != topPosition.value) {
topPosition.value = newValue;
}
};
}
Scroll View를 만드는 다양한 방법에 대해서 알아보았다. 사실 이번에 직접 개발한 스크롤 뷰는 그냥 개발해 보고 싶어서 한 번 만들어봤는데, Flutter 기본 스크롤 뷰처럼 부드럽고 자연스럽게는 개발하기 어렵다 보니 이쯤에서 개발을 멈추었다. 더 좋은 방법이 있으면 공유해 주세요 !!
Flutter로 개발하면서 직접 만들어서 사용할 일은 없을 것 같기도 하고 워낙에 까다로운 개발이라 개인적으로 시간될 때 추가적으로 개발해보고 개선 사항이 있다면 글을 작성하도록 하겠다.