about summary refs log tree commit diff
path: root/src/view/com/util/ViewSelector.tsx
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-09-06 22:34:31 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-09-06 22:34:31 -0500
commit69265753bf4bb20b236401c216746fe5db43836b (patch)
tree3f2cac035a36d9ba65753491247c0fdf08aef52f /src/view/com/util/ViewSelector.tsx
parent4974f97bf3a6d9f033caf1a8984676c68afd8cd5 (diff)
downloadvoidsky-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.tsx131
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({})