about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-11-16 17:18:16 -0600
committerPaul Frazee <pfrazee@gmail.com>2022-11-16 17:18:16 -0600
commit361789975f0dfca46110c7024c0b4fa8568b4b6b (patch)
tree98ee09db2fc401ef4db78dcb5a21438a15d640ef
parent284c6353305b143633baaca819212966a57697ee (diff)
downloadvoidsky-361789975f0dfca46110c7024c0b4fa8568b4b6b.tar.zst
Add a fancy 'drawer' animation to the tabs selector
-rw-r--r--src/view/shell/mobile/TabsSelector.tsx254
-rw-r--r--src/view/shell/mobile/index.tsx38
2 files changed, 138 insertions, 154 deletions
diff --git a/src/view/shell/mobile/TabsSelector.tsx b/src/view/shell/mobile/TabsSelector.tsx
index a3da5fa19..1210da91f 100644
--- a/src/view/shell/mobile/TabsSelector.tsx
+++ b/src/view/shell/mobile/TabsSelector.tsx
@@ -7,8 +7,10 @@ import {
   TouchableWithoutFeedback,
   View,
 } from 'react-native'
+import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import Animated, {
   interpolate,
+  SharedValue,
   useSharedValue,
   useAnimatedStyle,
   withTiming,
@@ -24,12 +26,20 @@ import {LinkActionsModel} from '../../../state/models/shell-ui'
 const TAB_HEIGHT = 42
 
 export const TabsSelector = observer(
-  ({active, onClose}: {active: boolean; onClose: () => void}) => {
+  ({
+    active,
+    tabMenuInterp,
+    onClose,
+  }: {
+    active: boolean
+    tabMenuInterp: SharedValue<number>
+    onClose: () => void
+  }) => {
     const store = useStores()
+    const insets = useSafeAreaInsets()
     const [closingTabIndex, setClosingTabIndex] = useState<number | undefined>(
       undefined,
     )
-    const initInterp = useSharedValue<number>(0)
     const closeInterp = useSharedValue<number>(0)
     const tabsRef = useRef<ScrollView>(null)
     const tabRefs = useMemo(
@@ -40,15 +50,10 @@ export const TabsSelector = observer(
       [store.nav.tabs.length],
     )
 
-    useEffect(() => {
-      if (active) {
-        initInterp.value = withTiming(1, {duration: 150})
-      } else {
-        initInterp.value = 0
-      }
-    }, [initInterp, active])
     const wrapperAnimStyle = useAnimatedStyle(() => ({
-      bottom: interpolate(initInterp.value, [0, 1.0], [50, 75]),
+      transform: [
+        {translateY: interpolate(tabMenuInterp.value, [0, 1.0], [320, 0])},
+      ],
     }))
 
     // events
@@ -118,124 +123,116 @@ export const TabsSelector = observer(
     }
 
     return (
-      <>
-        <TouchableWithoutFeedback onPress={onClose}>
-          <View style={styles.bg} />
-        </TouchableWithoutFeedback>
-        <Animated.View style={[styles.wrapper, wrapperAnimStyle]}>
-          <View onLayout={onLayout}>
-            <View style={[s.p10, styles.section]}>
-              <View style={styles.btns}>
-                <TouchableWithoutFeedback onPress={onPressShareTab}>
-                  <View style={[styles.btn]}>
-                    <View style={styles.btnIcon}>
-                      <FontAwesomeIcon size={16} icon="share" />
-                    </View>
-                    <Text style={styles.btnText}>Share</Text>
+      <Animated.View
+        style={[
+          styles.wrapper,
+          {bottom: insets.bottom + 55},
+          wrapperAnimStyle,
+        ]}>
+        <View onLayout={onLayout}>
+          <View style={[s.p10, styles.section]}>
+            <View style={styles.btns}>
+              <TouchableWithoutFeedback onPress={onPressShareTab}>
+                <View style={[styles.btn]}>
+                  <View style={styles.btnIcon}>
+                    <FontAwesomeIcon size={16} icon="share" />
                   </View>
-                </TouchableWithoutFeedback>
-                <TouchableWithoutFeedback onPress={onPressCloneTab}>
-                  <View style={[styles.btn]}>
-                    <View style={styles.btnIcon}>
-                      <FontAwesomeIcon size={16} icon={['far', 'clone']} />
-                    </View>
-                    <Text style={styles.btnText}>Clone tab</Text>
+                  <Text style={styles.btnText}>Share</Text>
+                </View>
+              </TouchableWithoutFeedback>
+              <TouchableWithoutFeedback onPress={onPressCloneTab}>
+                <View style={[styles.btn]}>
+                  <View style={styles.btnIcon}>
+                    <FontAwesomeIcon size={16} icon={['far', 'clone']} />
                   </View>
-                </TouchableWithoutFeedback>
-                <TouchableWithoutFeedback onPress={onPressNewTab}>
-                  <View style={[styles.btn]}>
-                    <View style={styles.btnIcon}>
-                      <FontAwesomeIcon size={16} icon="plus" />
-                    </View>
-                    <Text style={styles.btnText}>New tab</Text>
+                  <Text style={styles.btnText}>Clone tab</Text>
+                </View>
+              </TouchableWithoutFeedback>
+              <TouchableWithoutFeedback onPress={onPressNewTab}>
+                <View style={[styles.btn]}>
+                  <View style={styles.btnIcon}>
+                    <FontAwesomeIcon size={16} icon="plus" />
                   </View>
-                </TouchableWithoutFeedback>
-              </View>
+                  <Text style={styles.btnText}>New tab</Text>
+                </View>
+              </TouchableWithoutFeedback>
             </View>
-            <View style={[s.p10, styles.section, styles.sectionGrayBg]}>
-              <ScrollView ref={tabsRef} style={styles.tabs}>
-                {store.nav.tabs.map((tab, tabIndex) => {
-                  const {icon} = match(tab.current.url)
-                  const isActive = tabIndex === currentTabIndex
-                  const isClosing = closingTabIndex === tabIndex
-                  return (
-                    <Swipeable
-                      key={tab.id}
-                      renderLeftActions={renderSwipeActions}
-                      renderRightActions={renderSwipeActions}
-                      leftThreshold={100}
-                      rightThreshold={100}
-                      onSwipeableWillOpen={() => onCloseTab(tabIndex)}>
+          </View>
+          <View style={[s.p10, styles.section, styles.sectionGrayBg]}>
+            <ScrollView ref={tabsRef} style={styles.tabs}>
+              {store.nav.tabs.map((tab, tabIndex) => {
+                const {icon} = match(tab.current.url)
+                const isActive = tabIndex === currentTabIndex
+                const isClosing = closingTabIndex === tabIndex
+                return (
+                  <Swipeable
+                    key={tab.id}
+                    renderLeftActions={renderSwipeActions}
+                    renderRightActions={renderSwipeActions}
+                    leftThreshold={100}
+                    rightThreshold={100}
+                    onSwipeableWillOpen={() => onCloseTab(tabIndex)}>
+                    <Animated.View
+                      style={[
+                        styles.tabOuter,
+                        isClosing ? closingTabAnimStyle : undefined,
+                      ]}>
                       <Animated.View
+                        ref={tabRefs[tabIndex]}
                         style={[
-                          styles.tabOuter,
-                          isClosing ? closingTabAnimStyle : undefined,
+                          styles.tab,
+                          styles.existing,
+                          isActive && styles.active,
                         ]}>
-                        <Animated.View
-                          ref={tabRefs[tabIndex]}
-                          style={[
-                            styles.tab,
-                            styles.existing,
-                            isActive && styles.active,
-                          ]}>
-                          <TouchableWithoutFeedback
-                            onPress={() => onPressChangeTab(tabIndex)}>
-                            <View style={styles.tabInner}>
-                              <View style={styles.tabIcon}>
-                                <FontAwesomeIcon size={20} icon={icon} />
-                              </View>
-                              <Text
-                                ellipsizeMode="tail"
-                                numberOfLines={1}
-                                suppressHighlighting={true}
-                                style={[
-                                  styles.tabText,
-                                  isActive && styles.tabTextActive,
-                                ]}>
-                                {tab.current.title || tab.current.url}
-                              </Text>
-                            </View>
-                          </TouchableWithoutFeedback>
-                          <TouchableWithoutFeedback
-                            onPress={() => onCloseTab(tabIndex)}>
-                            <View style={styles.tabClose}>
-                              <FontAwesomeIcon
-                                size={14}
-                                icon="x"
-                                style={styles.tabCloseIcon}
-                              />
+                        <TouchableWithoutFeedback
+                          onPress={() => onPressChangeTab(tabIndex)}>
+                          <View style={styles.tabInner}>
+                            <View style={styles.tabIcon}>
+                              <FontAwesomeIcon size={20} icon={icon} />
                             </View>
-                          </TouchableWithoutFeedback>
-                        </Animated.View>
+                            <Text
+                              ellipsizeMode="tail"
+                              numberOfLines={1}
+                              suppressHighlighting={true}
+                              style={[
+                                styles.tabText,
+                                isActive && styles.tabTextActive,
+                              ]}>
+                              {tab.current.title || tab.current.url}
+                            </Text>
+                          </View>
+                        </TouchableWithoutFeedback>
+                        <TouchableWithoutFeedback
+                          onPress={() => onCloseTab(tabIndex)}>
+                          <View style={styles.tabClose}>
+                            <FontAwesomeIcon
+                              size={14}
+                              icon="x"
+                              style={styles.tabCloseIcon}
+                            />
+                          </View>
+                        </TouchableWithoutFeedback>
                       </Animated.View>
-                    </Swipeable>
-                  )
-                })}
-              </ScrollView>
-            </View>
+                    </Animated.View>
+                  </Swipeable>
+                )
+              })}
+            </ScrollView>
           </View>
-        </Animated.View>
-      </>
+        </View>
+      </Animated.View>
     )
   },
 )
 
 const styles = StyleSheet.create({
-  bg: {
-    position: 'absolute',
-    top: 0,
-    right: 0,
-    bottom: 0,
-    left: 0,
-    backgroundColor: '#000',
-    opacity: 0.2,
-  },
   wrapper: {
     position: 'absolute',
-    // bottom: 75,
     width: '100%',
+    height: 320,
+    borderTopColor: colors.gray2,
+    borderTopWidth: 1,
     backgroundColor: '#fff',
-    borderRadius: 8,
     opacity: 1,
   },
   section: {
@@ -244,45 +241,6 @@ const styles = StyleSheet.create({
   },
   sectionGrayBg: {
     backgroundColor: colors.gray1,
-    borderBottomLeftRadius: 8,
-    borderBottomRightRadius: 8,
-  },
-  fatMenuItems: {
-    flexDirection: 'row',
-    marginTop: 10,
-    marginBottom: 10,
-  },
-  fatMenuItem: {
-    width: 80,
-    alignItems: 'center',
-    marginRight: 6,
-  },
-  fatMenuItemMargin: {
-    marginRight: 14,
-  },
-  fatMenuItemIconWrapper: {
-    borderRadius: 6,
-    width: 60,
-    height: 60,
-    justifyContent: 'center',
-    alignItems: 'center',
-    marginBottom: 5,
-    shadowColor: '#000',
-    shadowOpacity: 0.2,
-    shadowOffset: {width: 0, height: 2},
-    shadowRadius: 2,
-  },
-  fatMenuItemIcon: {
-    color: colors.white,
-  },
-  fatMenuImage: {
-    borderRadius: 30,
-    width: 60,
-    height: 60,
-    marginBottom: 5,
-  },
-  fatMenuItemLabel: {
-    fontSize: 13,
   },
   tabs: {
     height: 240,
diff --git a/src/view/shell/mobile/index.tsx b/src/view/shell/mobile/index.tsx
index 94407599f..83bab5e9f 100644
--- a/src/view/shell/mobile/index.tsx
+++ b/src/view/shell/mobile/index.tsx
@@ -115,6 +115,7 @@ export const MobileShell: React.FC = observer(() => {
   const scrollElRef = useRef<FlatList | undefined>()
   const winDim = useWindowDimensions()
   const swipeGestureInterp = useSharedValue<number>(0)
+  const tabMenuInterp = useSharedValue<number>(0)
   const screenRenderDesc = constructScreenRenderDesc(store.nav)
 
   const onPressHome = () => {
@@ -127,7 +128,26 @@ export const MobileShell: React.FC = observer(() => {
   const onPressSearch = () => store.nav.navigate('/search')
   const onPressMenu = () => setMainMenuActive(true)
   const onPressNotifications = () => store.nav.navigate('/notifications')
-  const onPressTabs = () => setTabsSelectorActive(true)
+  const onPressTabs = () => toggleTabsMenu(!isTabsSelectorActive)
+
+  const closeTabsSelector = () => setTabsSelectorActive(false)
+  const toggleTabsMenu = (active: boolean) => {
+    if (active) {
+      // will trigger the animation below
+      setTabsSelectorActive(true)
+    } else {
+      tabMenuInterp.value = withTiming(0, {duration: 100}, () => {
+        // hide once the animation has finished
+        runOnJS(closeTabsSelector)()
+      })
+    }
+  }
+  useEffect(() => {
+    if (isTabsSelectorActive) {
+      // trigger the animation once the tabs selector is rendering
+      tabMenuInterp.value = withTiming(1, {duration: 100})
+    }
+  }, [isTabsSelectorActive])
 
   const goBack = () => store.nav.tab.goBack()
   const swipeGesture = Gesture.Pan()
@@ -159,6 +179,9 @@ export const MobileShell: React.FC = observer(() => {
   const swipeOpacity = useAnimatedStyle(() => ({
     opacity: interpolate(swipeGestureInterp.value, [0, 1.0], [0.6, 0.0]),
   }))
+  const tabMenuTransform = useAnimatedStyle(() => ({
+    transform: [{translateY: tabMenuInterp.value * -320}],
+  }))
 
   if (!store.session.isAuthed) {
     return (
@@ -205,7 +228,9 @@ export const MobileShell: React.FC = observer(() => {
                       style={[
                         s.flex1,
                         styles.screen,
-                        current ? swipeTransform : undefined,
+                        current
+                          ? [swipeTransform, tabMenuTransform]
+                          : undefined,
                       ]}>
                       <Com
                         params={params}
@@ -220,6 +245,11 @@ export const MobileShell: React.FC = observer(() => {
           </ScreenContainer>
         </GestureDetector>
       </SafeAreaView>
+      <TabsSelector
+        active={isTabsSelectorActive}
+        tabMenuInterp={tabMenuInterp}
+        onClose={() => toggleTabsMenu(false)}
+      />
       <SafeAreaView style={styles.bottomBar}>
         <Btn icon="house" onPress={onPressHome} />
         <Btn icon="search" onPress={onPressSearch} />
@@ -236,10 +266,6 @@ export const MobileShell: React.FC = observer(() => {
         onClose={() => setMainMenuActive(false)}
       />
       <Modal />
-      <TabsSelector
-        active={isTabsSelectorActive}
-        onClose={() => setTabsSelectorActive(false)}
-      />
       <Composer
         active={store.shell.isComposerActive}
         onClose={() => store.shell.closeComposer()}