import React, { useState } from 'react'; import { StyleSheet, ImageStyle, LayoutChangeEvent } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue, withSpring, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useHeaderHeight } from '@react-navigation/stack'; const CHAT_HEADS = [ { imageUrl: 'https://avatars0.githubusercontent.com/u/379606?v=4&s=460' }, { imageUrl: 'https://avatars3.githubusercontent.com/u/90494?v=4&s=460' }, { imageUrl: 'https://avatars3.githubusercontent.com/u/726445?v=4&s=460' }, { imageUrl: 'https://avatars.githubusercontent.com/u/15989228?v=4&s=460' }, ]; interface AnimatedOffset { x: Animated.SharedValue; y: Animated.SharedValue; } interface FollowingChatHeadProps { imageUri: string; offset: AnimatedOffset; offsetToFollow: AnimatedOffset; style?: ImageStyle; } function FollowingChatHead({ imageUri, style, offset, offsetToFollow, }: FollowingChatHeadProps) { useDerivedValue(() => { offset.x.value = withSpring(offsetToFollow.x.value); offset.y.value = withSpring(offsetToFollow.y.value); }, []); const animatedStyle = useAnimatedStyle(() => { return { transform: [ { translateX: offset.x.value }, { translateY: offset.y.value }, ], }; }); return ( ); } function useOffsetAnimatedValue() { return { x: useSharedValue(0), y: useSharedValue(0), }; } function clampToValues({ value, bottom, top, }: { value: number; bottom: number; top: number; }) { 'worklet'; return Math.max(bottom, Math.min(value, top)); } const Example = () => { const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const panOffset = useOffsetAnimatedValue(); const mainChatHeadPosition = useOffsetAnimatedValue(); const chatHeadsOffsets = CHAT_HEADS.map(useOffsetAnimatedValue); const headerHeight = useHeaderHeight(); const onLayout = ({ nativeEvent }: LayoutChangeEvent) => { const { width, height } = nativeEvent.layout; setDimensions({ width, height }); }; const panHandler = Gesture.Pan() .onUpdate(({ translationX, translationY }) => { 'worklet'; panOffset.x.value = mainChatHeadPosition.x.value + translationX; panOffset.y.value = mainChatHeadPosition.y.value + translationY; }) .onEnd(({ absoluteX, absoluteY, velocityX, velocityY }) => { 'worklet'; const { height, width } = dimensions; const velocityDragX = clampToValues({ value: velocityX * 0.05, bottom: -100, top: 100, }); const velocityDragY = clampToValues({ value: velocityY * 0.05, bottom: -100, top: 100, }); const distFromTop = absoluteY + velocityDragY - headerHeight; const distFromBottom = height + velocityDragY - absoluteY; const distFromLeft = absoluteX + velocityDragX; const distFromRight = width - absoluteX + velocityDragX; const minDist = Math.min( distFromTop, distFromBottom, distFromLeft, distFromRight ); // drag to the edge switch (minDist) { case distFromTop: { panOffset.y.value = withSpring(-IMAGE_SIZE / 2); panOffset.x.value = withSpring(panOffset.x.value + velocityDragX); mainChatHeadPosition.y.value = -IMAGE_SIZE / 2; mainChatHeadPosition.x.value = panOffset.x.value; break; } case distFromBottom: { panOffset.y.value = withSpring(height - IMAGE_SIZE / 2); panOffset.x.value = withSpring(panOffset.x.value + velocityDragX); mainChatHeadPosition.y.value = height - IMAGE_SIZE / 2; mainChatHeadPosition.x.value = panOffset.x.value; break; } case distFromLeft: { panOffset.x.value = withSpring(-IMAGE_SIZE / 2); panOffset.y.value = withSpring(panOffset.y.value + velocityDragY); mainChatHeadPosition.x.value = -IMAGE_SIZE / 2; mainChatHeadPosition.y.value = panOffset.y.value; break; } case distFromRight: { panOffset.x.value = withSpring(width - IMAGE_SIZE / 2); panOffset.y.value = withSpring(panOffset.y.value + velocityDragY); mainChatHeadPosition.x.value = width - IMAGE_SIZE / 2; mainChatHeadPosition.y.value = panOffset.y.value; break; } } }); const headsComponents = CHAT_HEADS.map(({ imageUrl }, idx) => { const headOffset = chatHeadsOffsets[idx]; if (idx === 0) { return ( ); } return ( ); }); return ( {/* we want ChatHead with gesture on top */} {headsComponents.reverse()} ); }; export default Example; const IMAGE_SIZE = 80; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#F5FCFF', }, box: { position: 'absolute', width: IMAGE_SIZE, height: IMAGE_SIZE, borderColor: '#F5FCFF', backgroundColor: 'plum', borderRadius: IMAGE_SIZE / 2, }, });