diff options
Diffstat (limited to 'src/view/com')
-rw-r--r-- | src/view/com/lightbox/Lightbox.tsx | 3 | ||||
-rw-r--r-- | src/view/com/modals/EditProfile.tsx | 193 | ||||
-rw-r--r-- | src/view/com/modals/Modal.tsx | 19 | ||||
-rw-r--r-- | src/view/com/util/UserBanner.tsx | 2 | ||||
-rw-r--r-- | src/view/com/util/ViewSelector.tsx | 142 | ||||
-rw-r--r-- | src/view/com/util/gestures/HorzSwipe.tsx | 157 | ||||
-rw-r--r-- | src/view/com/util/gestures/SwipeAndZoom.tsx | 302 |
7 files changed, 230 insertions, 588 deletions
diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index d6cc8c254..06b48143b 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,5 +1,4 @@ import React from 'react' -import {View} from 'react-native' import {observer} from 'mobx-react-lite' import ImageView from './ImageViewing' import {useStores} from 'state/index' @@ -48,6 +47,6 @@ export const Lightbox = observer(function Lightbox() { /> ) } else { - return <View /> + return null } }) diff --git a/src/view/com/modals/EditProfile.tsx b/src/view/com/modals/EditProfile.tsx index 0feae3a80..5a1ba3638 100644 --- a/src/view/com/modals/EditProfile.tsx +++ b/src/view/com/modals/EditProfile.tsx @@ -1,13 +1,15 @@ -import React, {useState} from 'react' +import React, {useState, useCallback} from 'react' import * as Toast from '../util/Toast' import { ActivityIndicator, + KeyboardAvoidingView, + ScrollView, StyleSheet, + TextInput, TouchableOpacity, View, } from 'react-native' import LinearGradient from 'react-native-linear-gradient' -import {ScrollView, TextInput} from './util' import {Image as RNImage} from 'react-native-image-crop-picker' import {Text} from '../util/text/Text' import {ErrorMessage} from '../util/error/ErrorMessage' @@ -24,7 +26,7 @@ import {useTheme} from 'lib/ThemeContext' import {useAnalytics} from 'lib/analytics' import {cleanError, isNetworkError} from 'lib/strings/errors' -export const snapPoints = ['80%'] +export const snapPoints = ['fullscreen'] export function Component({ profileView, @@ -61,38 +63,43 @@ export function Component({ const onPressCancel = () => { store.shell.closeModal() } - const onSelectNewAvatar = async (img: RNImage | null) => { - track('EditProfile:AvatarSelected') - try { - // if img is null, user selected "remove avatar" + const onSelectNewAvatar = useCallback( + async (img: RNImage | null) => { if (!img) { setNewUserAvatar(null) setUserAvatar(null) return } - const finalImg = await compressIfNeeded(img, 1000000) - setNewUserAvatar(finalImg) - setUserAvatar(finalImg.path) - } catch (e: any) { - setError(cleanError(e)) - } - } - const onSelectNewBanner = async (img: RNImage | null) => { - if (!img) { - setNewUserBanner(null) - setUserBanner(null) - return - } - track('EditProfile:BannerSelected') - try { - const finalImg = await compressIfNeeded(img, 1000000) - setNewUserBanner(finalImg) - setUserBanner(finalImg.path) - } catch (e: any) { - setError(cleanError(e)) - } - } - const onPressSave = async () => { + track('EditProfile:AvatarSelected') + try { + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserAvatar(finalImg) + setUserAvatar(finalImg.path) + } catch (e: any) { + setError(cleanError(e)) + } + }, + [track, setNewUserAvatar, setUserAvatar, setError], + ) + const onSelectNewBanner = useCallback( + async (img: RNImage | null) => { + if (!img) { + setNewUserBanner(null) + setUserBanner(null) + return + } + track('EditProfile:BannerSelected') + try { + const finalImg = await compressIfNeeded(img, 1000000) + setNewUserBanner(finalImg) + setUserBanner(finalImg.path) + } catch (e: any) { + setError(cleanError(e)) + } + }, + [track, setNewUserBanner, setUserBanner, setError], + ) + const onPressSave = useCallback(async () => { track('EditProfile:Save') setProcessing(true) if (error) { @@ -120,11 +127,23 @@ export function Component({ } } setProcessing(false) - } + }, [ + track, + setProcessing, + setError, + error, + profileView, + onUpdate, + store, + displayName, + description, + newUserAvatar, + newUserBanner, + ]) return ( - <View style={[s.flex1, pal.view]} testID="editProfileModal"> - <ScrollView style={styles.inner}> + <KeyboardAvoidingView behavior="height"> + <ScrollView style={[pal.view]} testID="editProfileModal"> <Text style={[styles.title, pal.text]}>Edit my profile</Text> <View style={styles.photos}> <UserBanner @@ -144,65 +163,66 @@ export function Component({ <ErrorMessage message={error} /> </View> )} - <View> - <Text style={[styles.label, pal.text]}>Display Name</Text> - <TextInput - testID="editProfileDisplayNameInput" - style={[styles.textInput, pal.border, pal.text]} - placeholder="e.g. Alice Roberts" - placeholderTextColor={colors.gray4} - value={displayName} - onChangeText={v => setDisplayName(enforceLen(v, MAX_DISPLAY_NAME))} - /> - </View> - <View style={s.pb10}> - <Text style={[styles.label, pal.text]}>Description</Text> - <TextInput - testID="editProfileDescriptionInput" - style={[styles.textArea, pal.border, pal.text]} - placeholder="e.g. Artist, dog-lover, and memelord." - placeholderTextColor={colors.gray4} - keyboardAppearance={theme.colorScheme} - multiline - value={description} - onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} - /> - </View> - {isProcessing ? ( - <View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}> - <ActivityIndicator /> + <View style={styles.form}> + <View> + <Text style={[styles.label, pal.text]}>Display Name</Text> + <TextInput + testID="editProfileDisplayNameInput" + style={[styles.textInput, pal.border, pal.text]} + placeholder="e.g. Alice Roberts" + placeholderTextColor={colors.gray4} + value={displayName} + onChangeText={v => + setDisplayName(enforceLen(v, MAX_DISPLAY_NAME)) + } + /> </View> - ) : ( + <View style={s.pb10}> + <Text style={[styles.label, pal.text]}>Description</Text> + <TextInput + testID="editProfileDescriptionInput" + style={[styles.textArea, pal.border, pal.text]} + placeholder="e.g. Artist, dog-lover, and memelord." + placeholderTextColor={colors.gray4} + keyboardAppearance={theme.colorScheme} + multiline + value={description} + onChangeText={v => setDescription(enforceLen(v, MAX_DESCRIPTION))} + /> + </View> + {isProcessing ? ( + <View style={[styles.btn, s.mt10, {backgroundColor: colors.gray2}]}> + <ActivityIndicator /> + </View> + ) : ( + <TouchableOpacity + testID="editProfileSaveBtn" + style={s.mt10} + onPress={onPressSave}> + <LinearGradient + colors={[gradients.blueLight.start, gradients.blueLight.end]} + start={{x: 0, y: 0}} + end={{x: 1, y: 1}} + style={[styles.btn]}> + <Text style={[s.white, s.bold]}>Save Changes</Text> + </LinearGradient> + </TouchableOpacity> + )} <TouchableOpacity - testID="editProfileSaveBtn" - style={s.mt10} - onPress={onPressSave}> - <LinearGradient - colors={[gradients.blueLight.start, gradients.blueLight.end]} - start={{x: 0, y: 0}} - end={{x: 1, y: 1}} - style={[styles.btn]}> - <Text style={[s.white, s.bold]}>Save Changes</Text> - </LinearGradient> + testID="editProfileCancelBtn" + style={s.mt5} + onPress={onPressCancel}> + <View style={[styles.btn]}> + <Text style={[s.black, s.bold, pal.text]}>Cancel</Text> + </View> </TouchableOpacity> - )} - <TouchableOpacity - testID="editProfileCancelBtn" - style={s.mt5} - onPress={onPressCancel}> - <View style={[styles.btn]}> - <Text style={[s.black, s.bold, pal.text]}>Cancel</Text> - </View> - </TouchableOpacity> + </View> </ScrollView> - </View> + </KeyboardAvoidingView> ) } const styles = StyleSheet.create({ - inner: { - padding: 14, - }, title: { textAlign: 'center', fontWeight: 'bold', @@ -215,6 +235,9 @@ const styles = StyleSheet.create({ paddingBottom: 4, marginTop: 20, }, + form: { + paddingHorizontal: 14, + }, textInput: { borderWidth: 1, borderRadius: 6, @@ -243,7 +266,7 @@ const styles = StyleSheet.create({ avi: { position: 'absolute', top: 80, - left: 10, + left: 24, width: 84, height: 84, borderWidth: 2, diff --git a/src/view/com/modals/Modal.tsx b/src/view/com/modals/Modal.tsx index 5d034a19d..df7d7f042 100644 --- a/src/view/com/modals/Modal.tsx +++ b/src/view/com/modals/Modal.tsx @@ -1,5 +1,6 @@ import React, {useRef, useEffect} from 'react' import {StyleSheet} from 'react-native' +import {SafeAreaView} from 'react-native-safe-area-context' import {observer} from 'mobx-react-lite' import BottomSheet from '@gorhom/bottom-sheet' import {useStores} from 'state/index' @@ -92,13 +93,22 @@ export const ModalsContainer = observer(function ModalsContainer() { return null } + if (snapPoints[0] === 'fullscreen') { + return ( + <SafeAreaView style={[styles.fullscreenContainer, pal.view]}> + {element} + </SafeAreaView> + ) + } + return ( <BottomSheet ref={bottomSheetRef} snapPoints={snapPoints} index={store.shell.isModalActive ? 0 : -1} enablePanDownToClose - keyboardBehavior="fillParent" + keyboardBehavior="extend" + keyboardBlurBehavior="restore" backdropComponent={ store.shell.isModalActive ? createCustomBackdrop(onClose) : undefined } @@ -115,4 +125,11 @@ const styles = StyleSheet.create({ borderTopLeftRadius: 10, borderTopRightRadius: 10, }, + fullscreenContainer: { + position: 'absolute', + top: 0, + left: 0, + bottom: 0, + right: 0, + }, }) diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx index e58fb0ef4..fcd66ca7a 100644 --- a/src/view/com/util/UserBanner.tsx +++ b/src/view/com/util/UserBanner.tsx @@ -128,7 +128,7 @@ const styles = StyleSheet.create({ width: 24, height: 24, bottom: 8, - right: 8, + right: 24, borderRadius: 12, alignItems: 'center', justifyContent: 'center', diff --git a/src/view/com/util/ViewSelector.tsx b/src/view/com/util/ViewSelector.tsx index ba0780862..02717053d 100644 --- a/src/view/com/util/ViewSelector.tsx +++ b/src/view/com/util/ViewSelector.tsx @@ -1,12 +1,13 @@ import React, {useEffect, useState} from 'react' -import {View} from 'react-native' -import {Selector} from './Selector' -import {HorzSwipe} from './gestures/HorzSwipe' +import {Pressable, StyleSheet, View} from 'react-native' import {FlatList} from './Views' -import {useAnimatedValue} from 'lib/hooks/useAnimatedValue' import {OnScrollCb} from 'lib/hooks/useOnMainScroll' +import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' +import {Text} from './text/Text' +import {usePalette} from 'lib/hooks/usePalette' import {clamp} from 'lib/numbers' -import {s} from 'lib/styles' +import {s, colors} from 'lib/styles' +import {isAndroid} from 'platform/detection' const HEADER_ITEM = {_reactKey: '__header__'} const SELECTOR_ITEM = {_reactKey: '__selector__'} @@ -16,7 +17,6 @@ export function ViewSelector({ sections, items, refreshing, - swipeEnabled, renderHeader, renderItem, ListFooterComponent, @@ -42,19 +42,12 @@ export function ViewSelector({ onEndReached?: (info: {distanceFromEnd: number}) => void }) { const [selectedIndex, setSelectedIndex] = useState<number>(0) - const panX = useAnimatedValue(0) // events // = - const onSwipeEnd = React.useCallback( - (dx: number) => { - if (dx !== 0) { - setSelectedIndex(clamp(selectedIndex + dx, 0, sections.length)) - } - }, - [setSelectedIndex, selectedIndex, sections], - ) + const keyExtractor = React.useCallback(item => item._reactKey, []) + const onPressSelection = React.useCallback( (index: number) => setSelectedIndex(clamp(index, 0, sections.length)), [setSelectedIndex, sections], @@ -77,7 +70,6 @@ export function ViewSelector({ return ( <Selector items={sections} - panX={panX} selectedIndex={selectedIndex} onSelect={onPressSelection} /> @@ -86,7 +78,7 @@ export function ViewSelector({ return renderItem(item) } }, - [sections, panX, selectedIndex, onPressSelection, renderHeader, renderItem], + [sections, selectedIndex, onPressSelection, renderHeader, renderItem], ) const data = React.useMemo( @@ -94,28 +86,98 @@ export function ViewSelector({ [items], ) return ( - <HorzSwipe - hasPriority - panX={panX} - swipeEnabled={swipeEnabled || false} - canSwipeLeft={selectedIndex > 0} - canSwipeRight={selectedIndex < sections.length - 1} - onSwipeEnd={onSwipeEnd}> - <FlatList - data={data} - keyExtractor={item => item._reactKey} - renderItem={renderItemInternal} - ListFooterComponent={ListFooterComponent} - stickyHeaderIndices={STICKY_HEADER_INDICES} - refreshing={refreshing} - onScroll={onScroll} - onRefresh={onRefresh} - onEndReached={onEndReached} - onEndReachedThreshold={0.6} - contentContainerStyle={s.contentContainer} - removeClippedSubviews={true} - scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 - /> - </HorzSwipe> + <FlatList + data={data} + keyExtractor={keyExtractor} + renderItem={renderItemInternal} + ListFooterComponent={ListFooterComponent} + // NOTE sticky header disabled on android due to major performance issues -prf + stickyHeaderIndices={isAndroid ? undefined : STICKY_HEADER_INDICES} + refreshing={refreshing} + onScroll={onScroll} + onRefresh={onRefresh} + onEndReached={onEndReached} + onEndReachedThreshold={0.6} + contentContainerStyle={s.contentContainer} + removeClippedSubviews={true} + scrollIndicatorInsets={{right: 1}} // fixes a bug where the scroll indicator is on the middle of the screen https://github.com/bluesky-social/social-app/pull/464 + /> ) } + +export function Selector({ + selectedIndex, + items, + onSelect, +}: { + selectedIndex: number + items: string[] + onSelect?: (index: number) => void +}) { + const pal = usePalette('default') + const borderColor = useColorSchemeStyle( + {borderColor: colors.black}, + {borderColor: colors.white}, + ) + + const onPressItem = (index: number) => { + onSelect?.(index) + } + + return ( + <View style={[pal.view, styles.outer]}> + {items.map((item, i) => { + const selected = i === selectedIndex + return ( + <Pressable + testID={`selector-${i}`} + key={item} + onPress={() => onPressItem(i)}> + <View + style={[ + styles.item, + selected && styles.itemSelected, + borderColor, + ]}> + <Text + style={ + selected + ? [styles.labelSelected, pal.text] + : [styles.label, pal.textLight] + }> + {item} + </Text> + </View> + </Pressable> + ) + })} + </View> + ) +} + +const styles = StyleSheet.create({ + outer: { + flexDirection: 'row', + paddingHorizontal: 14, + }, + item: { + marginRight: 14, + paddingHorizontal: 10, + paddingTop: 8, + paddingBottom: 12, + }, + itemSelected: { + borderBottomWidth: 3, + }, + label: { + fontWeight: '600', + }, + labelSelected: { + fontWeight: '600', + }, + underline: { + position: 'absolute', + height: 4, + bottom: 0, + }, +}) diff --git a/src/view/com/util/gestures/HorzSwipe.tsx b/src/view/com/util/gestures/HorzSwipe.tsx deleted file mode 100644 index 09f6c345f..000000000 --- a/src/view/com/util/gestures/HorzSwipe.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import React, {useState} from 'react' -import { - Animated, - GestureResponderEvent, - I18nManager, - PanResponder, - PanResponderGestureState, - useWindowDimensions, - View, -} from 'react-native' -import {clamp} from 'lodash' -import {s} from 'lib/styles' - -interface Props { - panX: Animated.Value - canSwipeLeft?: boolean - canSwipeRight?: boolean - swipeEnabled?: boolean - hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture - distThresholdDivisor?: number - useNativeDriver?: boolean - onSwipeStart?: () => void - onSwipeStartDirection?: (dx: number) => void - onSwipeEnd?: (dx: number) => void - children: React.ReactNode -} - -export function HorzSwipe({ - panX, - canSwipeLeft = false, - canSwipeRight = false, - swipeEnabled = true, - hasPriority = false, - distThresholdDivisor = 1.75, - useNativeDriver = false, - onSwipeStart, - onSwipeStartDirection, - onSwipeEnd, - children, -}: Props) { - const winDim = useWindowDimensions() - const [dir, setDir] = useState<number>(0) - - const swipeVelocityThreshold = 35 - const swipeDistanceThreshold = winDim.width / distThresholdDivisor - - const isMovingHorizontally = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - return ( - Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) && - Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25) - ) - } - - const canMoveScreen = ( - event: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - if (swipeEnabled === false) { - return false - } - - const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - const willHandle = - isMovingHorizontally(event, gestureState) && - ((diffX > 0 && canSwipeLeft) || (diffX < 0 && canSwipeRight)) - return willHandle - } - - const startGesture = () => { - setDir(0) - onSwipeStart?.() - - panX.stopAnimation() - // @ts-expect-error: _value is private, but docs use it as well - panX.setOffset(panX._value) - } - - const respondToGesture = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - const diffX = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - - if ( - // swiping left - (diffX > 0 && !canSwipeLeft) || - // swiping right - (diffX < 0 && !canSwipeRight) - ) { - panX.setValue(0) - return - } - - panX.setValue(clamp(diffX / swipeDistanceThreshold, -1, 1) * -1) - - const newDir = diffX > 0 ? -1 : diffX < 0 ? 1 : 0 - if (newDir !== dir) { - setDir(newDir) - onSwipeStartDirection?.(newDir) - } - } - - const finishGesture = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - if ( - Math.abs(gestureState.dx) > Math.abs(gestureState.dy) && - Math.abs(gestureState.vx) > Math.abs(gestureState.vy) && - (Math.abs(gestureState.dx) > swipeDistanceThreshold / 4 || - Math.abs(gestureState.vx) > swipeVelocityThreshold) - ) { - const final = Math.floor( - (gestureState.dx / Math.abs(gestureState.dx)) * -1, - ) - Animated.timing(panX, { - toValue: final, - duration: 100, - useNativeDriver, - isInteraction: false, - }).start(() => { - onSwipeEnd?.(final) - panX.flattenOffset() - panX.setValue(0) - }) - } else { - onSwipeEnd?.(0) - Animated.timing(panX, { - toValue: 0, - duration: 100, - useNativeDriver, - isInteraction: false, - }).start(() => { - panX.flattenOffset() - panX.setValue(0) - }) - } - } - - const panResponder = PanResponder.create({ - onMoveShouldSetPanResponder: canMoveScreen, - onPanResponderGrant: startGesture, - onPanResponderMove: respondToGesture, - onPanResponderTerminate: finishGesture, - onPanResponderRelease: finishGesture, - onPanResponderTerminationRequest: () => !hasPriority, - }) - - return ( - <View {...panResponder.panHandlers} style={s.h100pct}> - {children} - </View> - ) -} diff --git a/src/view/com/util/gestures/SwipeAndZoom.tsx b/src/view/com/util/gestures/SwipeAndZoom.tsx deleted file mode 100644 index 75c679012..000000000 --- a/src/view/com/util/gestures/SwipeAndZoom.tsx +++ /dev/null @@ -1,302 +0,0 @@ -import React, {useState} from 'react' -import { - Animated, - GestureResponderEvent, - I18nManager, - PanResponder, - PanResponderGestureState, - useWindowDimensions, - View, -} from 'react-native' -import {clamp} from 'lodash' -import {s} from 'lib/styles' - -export enum Dir { - None, - Up, - Down, - Left, - Right, - Zoom, -} - -interface Props { - panX: Animated.Value - panY: Animated.Value - zoom: Animated.Value - canSwipeLeft?: boolean - canSwipeRight?: boolean - canSwipeUp?: boolean - canSwipeDown?: boolean - swipeEnabled?: boolean - zoomEnabled?: boolean - hasPriority?: boolean // if has priority, will not release control of the gesture to another gesture - horzDistThresholdDivisor?: number - vertDistThresholdDivisor?: number - useNativeDriver?: boolean - onSwipeStart?: () => void - onSwipeStartDirection?: (dir: Dir) => void - onSwipeEnd?: (dir: Dir) => void - children: React.ReactNode -} - -export function SwipeAndZoom({ - panX, - panY, - zoom, - canSwipeLeft = false, - canSwipeRight = false, - canSwipeUp = false, - canSwipeDown = false, - swipeEnabled = false, - zoomEnabled = false, - hasPriority = false, - horzDistThresholdDivisor = 1.75, - vertDistThresholdDivisor = 1.75, - useNativeDriver = false, - onSwipeStart, - onSwipeStartDirection, - onSwipeEnd, - children, -}: Props) { - const winDim = useWindowDimensions() - const [dir, setDir] = useState<Dir>(Dir.None) - const [initialDistance, setInitialDistance] = useState<number | undefined>( - undefined, - ) - - const swipeVelocityThreshold = 35 - const swipeHorzDistanceThreshold = winDim.width / horzDistThresholdDivisor - const swipeVertDistanceThreshold = winDim.height / vertDistThresholdDivisor - - const isMovingHorizontally = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - return ( - Math.abs(gestureState.dx) > Math.abs(gestureState.dy * 1.25) && - Math.abs(gestureState.vx) > Math.abs(gestureState.vy * 1.25) - ) - } - const isMovingVertically = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - return ( - Math.abs(gestureState.dy) > Math.abs(gestureState.dx * 1.25) && - Math.abs(gestureState.vy) > Math.abs(gestureState.vx * 1.25) - ) - } - - const canDir = (d: Dir) => { - if (d === Dir.Left) { - return canSwipeLeft - } - if (d === Dir.Right) { - return canSwipeRight - } - if (d === Dir.Up) { - return canSwipeUp - } - if (d === Dir.Down) { - return canSwipeDown - } - if (d === Dir.Zoom) { - return zoomEnabled - } - return false - } - const isHorz = (d: Dir) => d === Dir.Left || d === Dir.Right - const isVert = (d: Dir) => d === Dir.Up || d === Dir.Down - - const canMoveScreen = ( - event: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - if (zoomEnabled && gestureState.numberActiveTouches === 2) { - return true - } else if (swipeEnabled && gestureState.numberActiveTouches === 1) { - const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - const dy = gestureState.dy - const willHandle = - (isMovingHorizontally(event, gestureState) && - ((dx > 0 && canSwipeLeft) || (dx < 0 && canSwipeRight))) || - (isMovingVertically(event, gestureState) && - ((dy > 0 && canSwipeUp) || (dy < 0 && canSwipeDown))) - return willHandle - } - return false - } - - const startGesture = () => { - setDir(Dir.None) - onSwipeStart?.() - - // reset all state - panX.stopAnimation() - // @ts-expect-error: _value is private, but docs use it as well - panX.setOffset(panX._value) - panY.stopAnimation() - // @ts-expect-error: _value is private, but docs use it as well - panY.setOffset(panY._value) - zoom.stopAnimation() - // @ts-expect-error: _value is private, but docs use it as well - zoom.setOffset(zoom._value) - setInitialDistance(undefined) - } - - const respondToGesture = ( - e: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - const dx = I18nManager.isRTL ? -gestureState.dx : gestureState.dx - const dy = gestureState.dy - - let newDir = Dir.None - if (dir === Dir.None) { - // establish if the user is swiping horz or vert, or zooming - if (gestureState.numberActiveTouches === 2) { - newDir = Dir.Zoom - } else if (Math.abs(dx) > Math.abs(dy)) { - newDir = dx > 0 ? Dir.Left : Dir.Right - } else { - newDir = dy > 0 ? Dir.Up : Dir.Down - } - } else if (isHorz(dir)) { - // direction update - newDir = dx > 0 ? Dir.Left : Dir.Right - } else if (isVert(dir)) { - // direction update - newDir = dy > 0 ? Dir.Up : Dir.Down - } else { - newDir = dir - } - - if (newDir === Dir.Zoom) { - if (zoomEnabled) { - if (gestureState.numberActiveTouches === 2) { - // zoom in/out - const x0 = e.nativeEvent.touches[0].pageX - const x1 = e.nativeEvent.touches[1].pageX - const y0 = e.nativeEvent.touches[0].pageY - const y1 = e.nativeEvent.touches[1].pageY - const zoomDx = Math.abs(x0 - x1) - const zoomDy = Math.abs(y0 - y1) - const dist = Math.sqrt(zoomDx * zoomDx + zoomDy * zoomDy) / 100 - if ( - typeof initialDistance === 'undefined' || - dist - initialDistance < 0 - ) { - setInitialDistance(dist) - } else { - zoom.setValue(dist - initialDistance) - } - } else { - // pan around after zooming - panX.setValue(clamp(dx / winDim.width, -1, 1) * -1) - panY.setValue(clamp(dy / winDim.height, -1, 1) * -1) - } - } - } else if (isHorz(newDir)) { - // swipe left/right - panX.setValue( - clamp( - dx / swipeHorzDistanceThreshold, - canSwipeRight ? -1 : 0, - canSwipeLeft ? 1 : 0, - ) * -1, - ) - panY.setValue(0) - } else if (isVert(newDir)) { - // swipe up/down - panY.setValue( - clamp( - dy / swipeVertDistanceThreshold, - canSwipeDown ? -1 : 0, - canSwipeUp ? 1 : 0, - ) * -1, - ) - panX.setValue(0) - } - - if (!canDir(newDir)) { - newDir = Dir.None - } - if (newDir !== dir) { - setDir(newDir) - onSwipeStartDirection?.(newDir) - } - } - - const finishGesture = ( - _: GestureResponderEvent, - gestureState: PanResponderGestureState, - ) => { - const finish = (finalDir: Dir) => () => { - if (finalDir !== Dir.None) { - onSwipeEnd?.(finalDir) - } - setDir(Dir.None) - panX.flattenOffset() - panX.setValue(0) - panY.flattenOffset() - panY.setValue(0) - } - if ( - isHorz(dir) && - (Math.abs(gestureState.dx) > swipeHorzDistanceThreshold / 4 || - Math.abs(gestureState.vx) > swipeVelocityThreshold) - ) { - // horizontal swipe reset - Animated.timing(panX, { - toValue: dir === Dir.Left ? -1 : 1, - duration: 100, - useNativeDriver, - }).start(finish(dir)) - } else if ( - isVert(dir) && - (Math.abs(gestureState.dy) > swipeVertDistanceThreshold / 8 || - Math.abs(gestureState.vy) > swipeVelocityThreshold) - ) { - // vertical swipe reset - Animated.timing(panY, { - toValue: dir === Dir.Up ? -1 : 1, - duration: 100, - useNativeDriver, - }).start(finish(dir)) - } else { - // zoom (or no direction) reset - onSwipeEnd?.(Dir.None) - Animated.timing(panX, { - toValue: 0, - duration: 100, - useNativeDriver, - }).start() - Animated.timing(panY, { - toValue: 0, - duration: 100, - useNativeDriver, - }).start() - Animated.timing(zoom, { - toValue: 0, - duration: 100, - useNativeDriver, - }).start() - } - } - - const panResponder = PanResponder.create({ - onMoveShouldSetPanResponder: canMoveScreen, - onPanResponderGrant: startGesture, - onPanResponderMove: respondToGesture, - onPanResponderTerminate: finishGesture, - onPanResponderRelease: finishGesture, - onPanResponderTerminationRequest: () => !hasPriority, - }) - - return ( - <View {...panResponder.panHandlers} style={s.h100pct}> - {children} - </View> - ) -} |