diff options
author | Ivan Chen <ivan@tagg.id> | 2021-05-21 20:34:30 -0400 |
---|---|---|
committer | Ivan Chen <ivan@tagg.id> | 2021-05-21 20:34:30 -0400 |
commit | 442f5608aeddb5c627183e150a8c79c9d5bd2a57 (patch) | |
tree | 4346360538d0a50407ce7d76a5e8ce6b168c52aa /src/components/common | |
parent | b4a4639f2ed05c02b9061d9febddf8339bc1fe26 (diff) | |
parent | 4849c65ff2163e1a77dcb26a12ff68840df225e7 (diff) |
Merge branch 'master' into tma853-tag-selection-screen
# Conflicts:
# src/components/common/index.ts
# src/screens/profile/CaptionScreen.tsx
Diffstat (limited to 'src/components/common')
-rw-r--r-- | src/components/common/Draggable.tsx | 364 | ||||
-rw-r--r-- | src/components/common/MomentTags.tsx | 77 | ||||
-rw-r--r-- | src/components/common/index.ts | 1 |
3 files changed, 442 insertions, 0 deletions
diff --git a/src/components/common/Draggable.tsx b/src/components/common/Draggable.tsx new file mode 100644 index 00000000..edd29b78 --- /dev/null +++ b/src/components/common/Draggable.tsx @@ -0,0 +1,364 @@ +/** + * * https://github.com/tongyy/react-native-draggable + * + */ + +import React from 'react'; +import { + Animated, + Dimensions, + GestureResponderEvent, + Image, + PanResponder, + PanResponderGestureState, + StyleProp, + StyleSheet, + Text, + TouchableOpacity, + View, + ViewStyle, +} from 'react-native'; + +function clamp(number: number, min: number, max: number) { + return Math.max(min, Math.min(number, max)); +} + +interface IProps { + /**** props that should probably be removed in favor of "children" */ + renderText?: string; + isCircle?: boolean; + renderSize?: number; + imageSource?: number; + renderColor?: string; + /**** */ + children?: React.ReactNode; + shouldReverse?: boolean; + disabled?: boolean; + debug?: boolean; + animatedViewProps?: object; + touchableOpacityProps?: object; + onDrag?: ( + e: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => void; + onShortPressRelease?: (event: GestureResponderEvent) => void; + onDragRelease?: ( + e: GestureResponderEvent, + gestureState: PanResponderGestureState, + ) => void; + onLongPress?: (event: GestureResponderEvent) => void; + onPressIn?: (event: GestureResponderEvent) => void; + onPressOut?: (event: GestureResponderEvent) => void; + onRelease?: (event: GestureResponderEvent, wasDragging: boolean) => void; + onReverse?: () => {x: number; y: number}; + x?: number; + y?: number; + // z/elevation should be removed because it doesn't sync up visually and haptically + z?: number; + minX?: number; + minY?: number; + maxX?: number; + maxY?: number; + onDragStart?: () => void; +} + +export default function Draggable(props: IProps) { + const { + renderText, + isCircle, + renderSize, + imageSource, + renderColor, + children, + shouldReverse, + disabled, + debug, + animatedViewProps, + touchableOpacityProps, + onDrag, + onShortPressRelease, + onDragRelease, + onLongPress, + onPressIn, + onPressOut, + onRelease, + x, + y, + z, + minX, + minY, + maxX, + maxY, + onDragStart, + } = props; + + // The Animated object housing our xy value so that we can spring back + const pan = React.useRef(new Animated.ValueXY()); + // Always set to xy value of pan, would like to remove + const offsetFromStart = React.useRef({x: 0, y: 0}); + // Width/Height of Draggable (renderSize is arbitrary if children are passed in) + const childSize = React.useRef({x: renderSize, y: renderSize}); + // Top/Left/Right/Bottom location on screen from start of most recent touch + const startBounds = React.useRef({top: 0, bottom: 0, left: 0, right: 0}); + // Whether we're currently dragging or not + const isDragging = React.useRef(false); + + // const [zIndex, setZIndex] = React.useState(z); + const zIndex = z; + + const getBounds = React.useCallback(() => { + const left = x + offsetFromStart.current.x; + const top = y + offsetFromStart.current.y; + return { + left, + top, + right: left + childSize.current.x, + bottom: top + childSize.current.y, + }; + }, [x, y]); + + const shouldStartDrag = React.useCallback( + (gs) => { + return !disabled && (Math.abs(gs.dx) > 2 || Math.abs(gs.dy) > 2); + }, + [disabled], + ); + + const reversePosition = React.useCallback(() => { + Animated.spring(pan.current, { + toValue: {x: 0, y: 0}, + useNativeDriver: false, + }).start(); + }, [pan]); + + const onPanResponderRelease = React.useCallback( + (e: GestureResponderEvent, gestureState: PanResponderGestureState) => { + isDragging.current = false; + if (onDragRelease) { + onDragRelease(e, gestureState); + onRelease(e, true); + } + if (!shouldReverse) { + pan.current.flattenOffset(); + } else { + reversePosition(); + } + }, + [onDragRelease, shouldReverse, onRelease, reversePosition], + ); + + const onPanResponderGrant = React.useCallback( + (_: GestureResponderEvent) => { + startBounds.current = getBounds(); + isDragging.current = true; + if (!shouldReverse) { + pan.current.setOffset(offsetFromStart.current); + pan.current.setValue({x: 0, y: 0}); + } + onDragStart(); + }, + [getBounds, shouldReverse], + ); + + const handleOnDrag = React.useCallback( + (e: GestureResponderEvent, gestureState: PanResponderGestureState) => { + const {dx, dy} = gestureState; + const {top, right, left, bottom} = startBounds.current; + const far = 999999999; + const changeX = clamp( + dx, + Number.isFinite(minX) ? minX - left : -far, + Number.isFinite(maxX) ? maxX - right : far, + ); + const changeY = clamp( + dy, + Number.isFinite(minY) ? minY - top : -far, + Number.isFinite(maxY) ? maxY - bottom : far, + ); + pan.current.setValue({x: changeX, y: changeY}); + onDrag(e, gestureState); + }, + [maxX, maxY, minX, minY, onDrag], + ); + + const panResponder = React.useMemo(() => { + return PanResponder.create({ + onMoveShouldSetPanResponder: (_, gestureState) => + shouldStartDrag(gestureState), + onMoveShouldSetPanResponderCapture: (_, gestureState) => + shouldStartDrag(gestureState), + onPanResponderGrant, + // onPanResponderStart, + onPanResponderMove: Animated.event([], { + // Typed incorrectly https://reactnative.dev/docs/panresponder + listener: handleOnDrag, + useNativeDriver: false, + }), + // onPanResponderRelease: (_) => { + // // console.log('end'); + // // setZIndex(1); + // }, + }); + }, [ + handleOnDrag, + onPanResponderGrant, + onPanResponderRelease, + shouldStartDrag, + ]); + + // TODO Figure out a way to destroy and remove offsetFromStart entirely + React.useEffect(() => { + const curPan = pan.current; // Using an instance to avoid losing the pointer before the cleanup + if (!shouldReverse) { + curPan.addListener((c) => (offsetFromStart.current = c)); + } + return () => { + // Typed incorrectly + curPan.removeAllListeners(); + }; + }, [shouldReverse]); + + const positionCss: StyleProp<ViewStyle> = React.useMemo(() => { + const Window = Dimensions.get('window'); + + return { + position: 'absolute', + top: 0, + left: 0, + width: Window.width, + height: Window.height, + elevation: zIndex, + zIndex: zIndex, + }; + }, [zIndex]); + + const dragItemCss = React.useMemo(() => { + const style: StyleProp<ViewStyle> = { + top: y, + left: x, + elevation: zIndex, + zIndex: zIndex, + }; + if (renderColor) { + style.backgroundColor = renderColor; + } + if (isCircle) { + style.borderRadius = renderSize; + } + + if (children) { + return { + ...style, + alignSelf: 'baseline', + }; + } + return { + ...style, + justifyContent: 'center', + width: renderSize, + height: renderSize, + }; + }, [children, isCircle, renderColor, renderSize, x, y, zIndex]); + + const touchableContent = React.useMemo(() => { + if (children) { + return children; + } else if (imageSource) { + return ( + <Image + style={{width: renderSize, height: renderSize}} + source={imageSource} + /> + ); + } else { + return <Text style={styles.text}>{renderText}</Text>; + } + }, [children, imageSource, renderSize, renderText]); + + const handleOnLayout = React.useCallback((event) => { + const {height, width} = event.nativeEvent.layout; + childSize.current = {x: width, y: height}; + }, []); + + // const handlePressOut = React.useCallback( + // (event: GestureResponderEvent) => { + // onPressOut(event); + // if (!isDragging.current) { + // onRelease(event, false); + // } + // }, + // [onPressOut, onRelease], + // ); + + const getDebugView = React.useCallback(() => { + const {width, height} = Dimensions.get('window'); + const far = 9999; + const constrained = minX || maxX || minY || maxY; + if (!constrained) { + return null; + } // could show other debug info here + const left = minX || -far; + const right = maxX ? width - maxX : -far; + const top = minY || -far; + const bottom = maxY ? height - maxY : -far; + return ( + <View + pointerEvents="box-none" + style={{left, right, top, bottom, ...styles.debugView}} + /> + ); + }, [maxX, maxY, minX, minY]); + + return ( + <View pointerEvents="box-none" style={positionCss}> + {debug && getDebugView()} + <Animated.View + pointerEvents="box-none" + {...animatedViewProps} + {...panResponder.panHandlers} + style={pan.current.getLayout()}> + <TouchableOpacity + {...touchableOpacityProps} + onLayout={handleOnLayout} + style={dragItemCss} + disabled={true} + onPress={onShortPressRelease} + onLongPress={onLongPress} + onPressIn={onPressIn} + onPressOut={onPressOut}> + {touchableContent} + </TouchableOpacity> + </Animated.View> + </View> + ); +} + +/***** Default props and types */ + +Draggable.defaultProps = { + renderText: '+', + renderSize: 36, + shouldReverse: false, + disabled: false, + debug: false, + onDrag: () => {}, + onShortPressRelease: () => {}, + onDragRelease: () => {}, + onLongPress: () => {}, + onPressIn: () => {}, + onPressOut: () => {}, + onRelease: () => {}, + x: 0, + y: 0, + z: 1, +}; + +const styles = StyleSheet.create({ + text: {color: '#fff', textAlign: 'center'}, + debugView: { + backgroundColor: '#ff000044', + position: 'absolute', + borderColor: '#fced0ecc', + borderWidth: 4, + }, +}); diff --git a/src/components/common/MomentTags.tsx b/src/components/common/MomentTags.tsx new file mode 100644 index 00000000..fb9ef5be --- /dev/null +++ b/src/components/common/MomentTags.tsx @@ -0,0 +1,77 @@ +import React, {MutableRefObject, useEffect, useState} from 'react'; +import {MomentTagType, ProfilePreviewType} from '../../types'; +import TaggDraggable from '../taggs/TaggDraggable'; +import Draggable from './Draggable'; + +interface MomentTagsProps { + editing: boolean; + tags: MomentTagType[]; + imageRef: MutableRefObject<null>; + deleteFromList?: (user: ProfilePreviewType) => void; +} + +const MomentTags: React.FC<MomentTagsProps> = ({ + editing, + tags, + imageRef, + deleteFromList, +}) => { + const [offset, setOffset] = useState([0, 0]); + const [curStart, setCurStart] = useState([0, 0]); + const [imageDimensions, setImageDimensions] = useState([0, 0]); + + useEffect(() => { + imageRef.current.measure((fx, fy, width, height, px, py) => { + setOffset([px, py]); + setImageDimensions([width, height]); + }); + }, []); + + if (!tags) { + return null; + } + + return editing && deleteFromList ? ( + <> + {tags.map((tag) => ( + <Draggable + x={imageDimensions[0] / 2 - curStart[0] / 2 + offset[0]} + y={offset[1] + imageDimensions[1] / 2 - curStart[1] / 2} + minX={offset[0]} + minY={offset[1]} + maxX={imageDimensions[0] + offset[0]} + maxY={imageDimensions[1] + offset[1]} + onDragStart={() => null}> + <TaggDraggable + taggedUser={tag.user} + editingView={true} + deleteFromList={() => deleteFromList(tag.user)} + setStart={setCurStart} + /> + </Draggable> + ))} + </> + ) : ( + <> + {tags.map((tag) => ( + <Draggable + x={imageDimensions[0] / 2 - curStart[0] / 2 + tag.x} + y={imageDimensions[0] / 2 - curStart[0] / 2 + tag.y} + minX={offset[0]} + minY={offset[1]} + maxX={imageDimensions[0] + offset[0]} + maxY={imageDimensions[1] + offset[1]} + onDragStart={() => null}> + <TaggDraggable + taggedUser={tag.user} + editingView={editing} + deleteFromList={() => null} + setStart={setCurStart} + /> + </Draggable> + ))} + </> + ); +}; + +export default MomentTags; diff --git a/src/components/common/index.ts b/src/components/common/index.ts index 44edbe5f..4f5c0232 100644 --- a/src/components/common/index.ts +++ b/src/components/common/index.ts @@ -28,3 +28,4 @@ export {default as TaggTypeahead} from './TaggTypeahead'; export {default as TaggUserRowCell} from './TaggUserRowCell'; export {default as LikeButton} from './LikeButton'; export {default as TaggUserSelectionCell} from './TaggUserSelectionCell'; +export {default as MomentTags} from './MomentTags'; |