스크롤이 멈추는 시점의 offset.y
를 이용하여, 버튼의 정확한 위치를 구하고
scrollTo 를 이용해 포커싱 동작을 구현합니다.
Snap(포커싱) 동작 구현
스크롤이 멈추는 순간 우리는 버튼의 위치를 알아낸 뒤, 현재 위치의 좌표를 이동시킬 정확한 좌표로 변환하고
해당 위치로 scrollTo 를 사용해서 이동시킬 수 있습니다.
우리는 버튼의 높이를 고정값으로 주었기 때문에, 정확한 좌표로 변환 하는것은 어렵지 않습니다.
시간 스크롤뷰를 기준으로 설명을 간단하게 해보겠습니다.
스크롤뷰의 초기 offset.y
는 0
입니다.
스크롤뷰를 버튼 높이의 1/3 만큼 이동하게 되면, offset.y
는 BUTTON_HEIGHT * 0.3
이 됩니다.
즉, 스크롤을 이동하는 만큼 offset.y
가 증가하게 됩니다.
이제 여기서 우리가 원하는 요구사항을 조금 더 구체적으로 생각해보겠습니다.
우리는 가운데 포커스된 영역에 버튼이 정확하게 위치하기를 원하고 있습니다.
우리가 원하는 가운데 포커스된 영역만을 살펴보면, 01
보다는 12
가 더 많이 차지하고 있습니다.
이럴 경우에 12
버튼이 가운데 위치하게 되도록 하려면 어떻게 하면 될까요?
단순합니다. 12
버튼이 정확하게 가운데 위치하는 offset.y
를 구해서 scorllRef.scrollTo({y: offsetY})
를 호출하면 됩니다.
12
버튼이 가운데 오려면 가장 초기의 상태, 즉 offset.y
가 0
일 때입니다.
그렇다면 01
버튼이 가운데 오려면 offset.y
는? BUTTON_HEIGHT
만큼 증가하면 됩니다.
마찬가지로 02
버튼이 가운데 오려면 BUTTON_HEIGHT
만큼 증가한, 2 * BUTTON_HEIGHT
가 되면 됩니다.
이런식으로 특정 버튼을 가운데 오게 하려면, offset.y
를 index * BUTTON_HEIGHT
에 위치하게 하면 된다는 규칙을 찾을 수 있습니다.
스크롤이 멈춘 시점의 offset.y
를 이용한다면 간단하게 반올림을 이용해서, 현재 위치의 index 를 구해서 버튼의 정확한 offset.y
좌표를 계산할 수 있습니다.
const getCenterPosition = (offsetY) => {
const buttonIndex = Math.round(offsetY / BUTTON_HEIGHT);
// 예시 코드이므로, 공백 영역은 알아서 계산..
console.log('selected item', items[buttonIndex]);
return buttonIndex * BUTTON_HEIGHT;
};
<ScrollView
ref={scrollRef}
onScrollEndDrag={event => {
const correctOffset = getCenterPosition(event.nativeEvent.contentOffset.y);
scorllRef.scrollTo({y: correctOffset});
}}
...
>
{items.map( ... )}
</ScrollView>
interpolate
를 이용하여 스크롤의 위치에 따른 포커싱 애니메이션을 구현합니다.
포커싱 애니메이션 구현
위의 과정에서 버튼의 index 를 통해서 offset.y
를 구하는 규칙을 알아냈습니다.
이를 이용하면 interpolate
를 이용하여 손쉽게 가운데 위치한 버튼에만 하이라이팅과 같은 애니메이션을 줄 수 있습니다.
const animatedValue = useRef(new Animated.Value(0)).current;
<ScrollView
onScroll={Animated.event(
[{ nativeEvent: { contentOffset: { y: animatedValue } } }],
{ useNativeDriver: false }
)}
/>
우선 Animated.event
를 사용하여, ScrollView 의 현재 스크롤 위치인 offset.y
를 Animated.Value
에 매핑시켜 줍니다.
(onScroll
이벤트가 발생될때마다 자동적으로 nativeEvent.contentOffset.y
값이 animatedValue
에 set 됩니다.)
Interpolate 란?
interpolate
는 단순하게 말해서, 증가하는 애니메이션의 값에 대해서 input/output 범위를 대칭되게 설정해서 값을 받아올 수 있습니다.
아래 코드에서animatedValue
의 값이inputRange
로 변화할때, 매칭되는outputRange
에 지정된 값을 반환합니다.
(단순 계산을 하면, 0 일때는 0, 25 일때는 0.15, 50 일때는 0.3, 75 일때는 0.65, 100 일때는 100 입니다.)const opacity = animatedValue.interpolate({ inputRange: [0, 50, 100], // animatedValue 의 inputRange 범위에 대하여 outputRange: [0, 0.3, 1], // 각 범위에 대칭되는 지점에 outputRange 값을 리턴, extrapolate: 'clamp', // 지정된 범위에 대해서만 처리 (벗어나는 input 값은 outputRange 최대/최소값으로 처리됨) }) <AnimatedButton style={{ opacity }}} />
이를 이용해서 각 버튼별로 index 를 통해서 중앙에 포커싱 되는 offset.y
좌표를 구한 뒤 가운데 기준점으로 삼고
의 기준으로 interpolate
의 input/output 범위를 설정합니다.
const buttonItemIndex = N;
const correctOffset = buttonItemIndex * BUTTON_HEIGHT;
const opacity = animatedValue.interpolate({
inputRange: [correctOffset - BUTTON_HEIGHT, correctOffset, correctOffset + BUTTON_HEIGHT],
outputRange: [0.3, 1, 0.3],
extrapolate: 'clamp',
})
이를 활용하면 opacity 이외에도 다양한 포커싱 애니메이션을 줄 수 있습니다.
전체 코드는 아래의 레포에서 확인하실 수 있습니다.
https://github.com/bang9/react-native-wheel-time-picker-example