diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-09-06 22:34:31 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-09-06 22:34:31 -0500 |
commit | 69265753bf4bb20b236401c216746fe5db43836b (patch) | |
tree | 3f2cac035a36d9ba65753491247c0fdf08aef52f /src/view/com/util/ViewSelector.tsx | |
parent | 4974f97bf3a6d9f033caf1a8984676c68afd8cd5 (diff) | |
download | voidsky-69265753bf4bb20b236401c216746fe5db43836b.tar.zst |
Refactor profile to use new ViewSelector element which is reusable and now supports swipe gestures
Diffstat (limited to 'src/view/com/util/ViewSelector.tsx')
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx new file mode 100644 index 000000000..5dab54de7 --- /dev/null +++ b/src/view/com/util/ViewSelector.tsx @@ -0,0 +1,131 @@ +import React, {useEffect, useState, useMemo} from 'react' +import {FlatList, StyleSheet, View} from 'react-native' +import {GestureDetector, Gesture} from 'react-native-gesture-handler' +import {useSharedValue, withTiming, runOnJS} from 'react-native-reanimated' +import {Selector} from './Selector' + +const HEADER_ITEM = {_reactKey: '__header__'} +const SELECTOR_ITEM = {_reactKey: '__selector__'} +const STICKY_HEADER_INDICES = [1] +const SWIPE_GESTURE_MAX_DISTANCE = 200 +const SWIPE_GESTURE_HIT_SLOP = {left: -20, top: 0, right: 0, bottom: 0} // we ignore the left 20 pixels to avoid conflicts with the page-nav gesture + +export function ViewSelector({ + sections, + items, + refreshing, + renderHeader, + renderItem, + onSelectView, + onRefresh, + onEndReached, +}: { + sections: string[] + items: any[] + refreshing?: boolean + renderHeader?: () => JSX.Element + renderItem: (item: any) => JSX.Element + onSelectView?: (viewIndex: number) => void + onRefresh?: () => void + onEndReached?: (info: {distanceFromEnd: number}) => void +}) { + const [selectedIndex, setSelectedIndex] = useState<number>(0) + const swipeGestureInterp = useSharedValue<number>(0) + + // events + // = + + const onPressSelection = (index: number) => setSelectedIndex(index) + useEffect(() => { + onSelectView?.(selectedIndex) + }, [selectedIndex]) + + // gestures + // = + + const swipeGesture = useMemo( + () => + Gesture.Pan() + .hitSlop(SWIPE_GESTURE_HIT_SLOP) + .onUpdate(e => { + // calculate [-1, 1] range for the gesture + const clamped = Math.min(e.translationX, SWIPE_GESTURE_MAX_DISTANCE) + const reversed = clamped * -1 + const scaled = reversed / SWIPE_GESTURE_MAX_DISTANCE + swipeGestureInterp.value = scaled + }) + .onEnd(e => { + if (swipeGestureInterp.value >= 0.5) { + // swiped to next + if (selectedIndex < sections.length - 1) { + // interp to the next item's position... + swipeGestureInterp.value = withTiming(1, {duration: 100}, () => { + // ...then update the index, which triggers the useEffect() below [1] + runOnJS(setSelectedIndex)(selectedIndex + 1) + }) + } else { + swipeGestureInterp.value = withTiming(0, {duration: 100}) + } + } else if (swipeGestureInterp.value <= -0.5) { + // swiped to prev + if (selectedIndex > 0) { + // interp to the prev item's position... + swipeGestureInterp.value = withTiming(-1, {duration: 100}, () => { + // ...then update the index, which triggers the useEffect() below [1] + runOnJS(setSelectedIndex)(selectedIndex - 1) + }) + } else { + swipeGestureInterp.value = withTiming(0, {duration: 100}) + } + } else { + swipeGestureInterp.value = withTiming(0, {duration: 100}) + } + }), + [swipeGestureInterp, selectedIndex, sections.length], + ) + useEffect(() => { + // [1] completes the swipe gesture animation by resetting the interp value + // this has to be done as an effect so that it occurs *after* the selectedIndex has been updated + swipeGestureInterp.value = 0 + }, [swipeGestureInterp, selectedIndex]) + + // rendering + // = + + const renderItemInternal = ({item}: {item: any}) => { + if (item === HEADER_ITEM) { + if (renderHeader) { + return renderHeader() + } + return <View /> + } else if (item === SELECTOR_ITEM) { + return ( + <Selector + items={sections} + selectedIndex={selectedIndex} + swipeGestureInterp={swipeGestureInterp} + onSelect={onPressSelection} + /> + ) + } else { + return renderItem(item) + } + } + + const data = [HEADER_ITEM, SELECTOR_ITEM, ...items] + return ( + <GestureDetector gesture={swipeGesture}> + <FlatList + data={data} + keyExtractor={item => item._reactKey} + renderItem={renderItemInternal} + stickyHeaderIndices={STICKY_HEADER_INDICES} + refreshing={refreshing} + onRefresh={onRefresh} + onEndReached={onEndReached} + /> + </GestureDetector> + ) +} + +const styles = StyleSheet.create({}) |