개요 : Step Slider가 있는데 왜 구현한거지?
만들게 된 계기
프로그램에서 예약 페이지를 개발할 때, 디자이너의 의도가 아래와 같았다. ▼
인디케이터가 5분 단위로 이동해야하며, 아래 선택된 시간만큼 인디케이터 뒷부분의 공간을 색칠해줘야한다.
여기서 가장 중요한 것은 5분 단위로 끊어서 이동해야한다는 것이다.
예약은 5분 단위로 진행되기 때문에 끊어서 이동하지 않으면 매 분, 매 초단위로 예약할 수 있다고 유저가 혼동할 수 있기 때문이다.
그렇기에 5분이라는 Step을 두어 구현을 해야하는 상황이 발생한 것이다.
근데 왜 slider를 수정하지 않은것?
Flutter에서 기본적으로 Slider 위젯을 제공해준다. ▼
기본적인 모양새는 이러하며, 속성들을 수정하여 원하는 색상과 기능을 넣을 수 있다. ▼
허나 문제는 해당 페이지에서 원하는 모습은 슬라이드 바와 인디케이터가 완전히 분리되어있어야 하며, 인디케이터 뒤로 색상을 채워야 하는 것이다.
기본 슬라이더로는 해결할 수 없는 문제이기에 아예 처음부터 만들기로 했다.
Step Indicator(스텝 인디케이터) 구현
인디케이터가 움직이는건 쉽게 구현된다.
사실 인디케이터가 좌우 방향으로 움직이는 것은 쉽게 구현이 된다.
GestureDetector
의 onHorizontalDragUpdate
메소드를 구현하면 구현할 수 있다. ▼
onHorizontalDragUpdate: (details) {
double delta = details.primaryDelta!;
reservationModalController.positionX.value += delta;
},
간단하게 코드에 대해 설명을 하면, details
매개변수는 DragUpdateDetails
의 인스턴스로, delta
, primaryDelta
, globalPosition
, localPosition
등의 필드를 가지고 있다. ▼
그 중에서 위의 코드에서 사용한 details.primaryDelta
는 위젯이 매 프레임마다 드래그 되어 움직인 변화량을 반환해준다.
매 프레임마다 어느정도를 움직였는지를 반환해준다.
그렇기에 해당 값을 위젯의 위치를 움직일 수 있게 해주는 곳에 넣어주면 위젯을 쉽게 움직이게 할 수 있다. ▼
padding: EdgeInsets.only(
left: reservationModalController.positionX.value >= 0
? reservationModalController.positionX.value : 20.w,
),
하지만 스텝을 넣는 것은 다르다
허나 단계마다 끊어지게 움직이는 것은 조금 다르게 생각을 해야했다.
단순히 X축을 자유롭게 이동하는 것과는 다르게 생각할 것들이 많았다.
구간 정하기
가장 처음에 생각해야할 것은 구간을 얼마나 효율적으로 정의하는가였다.
처음에 생각한 구간은 다음과 같았다. ▼
디자인이 Android Small을 기준으로 되어있어 세로 640, 가로 360 픽셀로 구성이 되어있다.
여기에 ScreenUtil
로 크기에 맞게 .w
와 .h
연산자를 통해 배율을 적용하기에 간단하게 360을 기준으로 구간을 정할 수 있었다.
서비스에서 2시간 이내를 시작시간 삼아 예약을 잡을 수 있고, 최대 예약은 30분이기에 현재 시각으로부터 2시간 30분 이후의 시간을 잡아야 했다.
그리고 이를 5분 단위로 쪼개면, 총 30칸이 나오게 되어 좌우 Padding을 `30.w`씩 주고, 나머지 `300.w`을 30칸으로 나누어 한 칸에 `10.w`씩으로 정했다.
이 중에서 24칸만 실제 이동범위이고, 나머지 6칸은 예약 불가능하지만 정확히 2시간 뒤에 예약을 잡을 때 보여줄 구간이기에 최대 이동거리를 `240.w`으로 잡았다.
구간 이동
구간을 정했으니 이제 구간마다 이동할 수 있게 코드를 짜야했다.
처음 코드는 아래와 같았다. ▼
if (reservationModalController.positionX.value >= 0 &&
reservationModalController.positionX.value <= indicatorMaximumPadding) {
reservationModalController.positionX.value += delta;
reservationModalController.positionX.value =
(reservationModalController.positionX.value ~/ indicatorUnit) *
indicatorUnit;
// indicatorUnit은 10.w이다.
}
`indicatorUnit`로 구간을 나누어 이동을 할 수 있게 하려 했다.
위의 코드는 현재 좌표에 이동량인 `delta`를 더해주고, 좌표가 특정 구간 내로 들어가면 `indicatorUnit`의 배수로 바꾸었다.
그럴듯해보이지만 이 코드에는 치명적인 문제가 있었는데, 이는 바로 위 함수가 프레임 단위로 실행이 된다는 것이다.
프레임 단위로 실행이 된다는 의미는 `delta` 역시도 한 프레임당 움직인 거리로 계산이 되기에, 유저가 한 프레임 동안 `indicatorUnit`인 `10.w`를 이동시키지 못하면 인디케이터는 그 자리에서 움직일 수 없게 된다. ▼
그래서 `currentIndicatorIndex`라는 인덱스 변수를 하나 추가했다. ▼
int currentIndicatorIndex =
reservationModalController.positionX.value ~/ indicatorUnit;
프레임마다 어느 구간에 속하는지를 기억해놓고, 그 구간에 맞게 곱하여 이동시키는 방식으로 진행했다. ▼
padding: EdgeInsets.only(
left: reservationModalController.indicatorIndex * indicatorUnit
...
그러나 여기에서도 문제가 있었다.
이렇게 이동시키면 실제 위치 값은 변하지 않기에 나중에 유저가 인디케이터를 눌렀을 때 허공을 누르게 될 수도 있게 된다. ▼
그렇기에 유저가 움직이는 것을 끝냈을 때, 실제 위치와 보정을 해줘야한다.
`onHorizontalDragEnd` 필드에 보정을 해주는 메소드를 넣어줬다. ▼
onHorizontalDragEnd: (details) {
reservationModalController.positionX.value =
reservationModalController.indicatorIndex * indicatorUnit;
},
이렇게 하고 몇가지 예외처리를 해주니 생각하는 대로 스텝이 생겼다!
인디케이터 뒤의 페인트
인디케이터 뒤에 선택한 시간만큼 색칠이 되는 것을 구현할 차례다. ▼
이 부분은 생각보다 쉽게 구현되었다.
`indicatorUnit`으로 단위를 정해놓았기 때문에 현재 인디케이터 위치를 받아와 `Container`로 그려주기만 하면 끝이다.
물론 위젯을 그냥 겹칠 수는 없으니 Stack을 이용했다.
아래의 코드는 Stack 내부에서 색칠된 부분을 그려주는 것이다. ▼
Row(
children: [
SizedBox(
width: additionalSpace +
reservationModalController.indicatorIndex * 10.w),
Container(
width: (reservationModalController.indicatorIndex == 24 &&
reservationModalController.duration.value != 0)
? reservationModalController.duration.value * 2.w - additionalSpace
: reservationModalController.duration.value * 2.w,
height: 15.h,
decoration: const BoxDecoration(color: AppColors.main),
),
],
)
그런데 보면 약간 이해가 안되는 부분들이 있다.
물론 다른 내부 코드랑 엮여있어 더 그럴 수 있는데, 그 중에서도 `additionalSpace` 부분이 이해가 안될 것이다.
additionalSpace를 넣은 이유는 시간 예약이 5분 단위로 진행되기 때문이다.
기간만 5분 단위인게 아니라, 시작 시간과 종료 시간도 5분 단위다.
그렇기에 유저가 7시 53분에 예약을 하려고 하면, 지금 당장 예약이 가능한게 아니라 55분 이후의 시간만 예약이 가능하다.
그래서 유저가 51분이나 52분이나 5로 나누어떨어지지 않는 시간에 예약을 할 수 없음을 보여줘야했고, 이를 보여주는 영역이 `additionalSpace`다. ▼
완성
이렇게 additionalSpace와 Controller의 시간 계산을 합치면 아래와 같이 완성이 된다. ▼
마치며
인디케이터를 만들 때는 이미지를 불러와서 사용하는게 구조적으로 별로인거 같아 CustomPaint를 사용해서 만들었다.
이렇게 하나하나 직접 만들면서 사용하는걸 보니 예전에 42서울 했을 때가 떠오른다.
그때에 비하면 이건 정말 별거 아닌 수준이지만... Flutter로는 더 큰 것, 눈에 보이고 실제로 사용하는 것을 만들기에 비슷하지 않나 생각을 한다.
이런 저런 생각을 하지만 어쨌거나 내가 해냈다는 사실이 기쁘다.
+) 처절한 사투의 흔적
'Develop > Flutter' 카테고리의 다른 글
[Flutter][Widget] Container 위젯 그리고 Container의 크기 설정 (0) | 2024.03.20 |
---|---|
[Flutter][Widget] Text 위젯 (0) | 2024.03.19 |
[Flutter][Error] Unhandled Exception: MissingPluginException (0) | 2024.02.28 |
[Flutter][Error] Firebase Realtime DB 이름 규칙 오류 (0) | 2024.02.24 |
[Flutter][Error] CocoaPod Dependency 오류 (0) | 2024.02.16 |