mattermost-mobile/app/screens/gallery/renderers/image/transformer.tsx
2025-07-22 14:00:57 +08:00

159 lines
5 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Image, type ImageSource} from 'expo-image';
import React, {useCallback} from 'react';
import {StyleSheet} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
import Animated, {useAnimatedReaction, useAnimatedStyle} from 'react-native-reanimated';
import {SvgUri} from 'react-native-svg';
import {useTransformerSharedValues} from './context';
import useTransformerDoubleTap from './gestures/useTransformerDoubleTap';
import useTransformerPanGesture from './gestures/useTransformerPanGesture';
import useTransformerPinchGesture from './gestures/useTransformerPinchGesture';
import {useTransformerSingleTap} from './gestures/useTransformerSingleTap';
import type {GalleryPagerItem} from '@typings/screens/gallery';
const styles = StyleSheet.create({
container: {
flex: 1,
width: '100%',
},
wrapper: {
...StyleSheet.absoluteFillObject,
justifyContent: 'center',
alignItems: 'center',
},
svg: {
backgroundColor: '#FFF',
borderRadius: 8,
},
});
interface ImageTransformerProps extends Omit<GalleryPagerItem, 'index' | 'item' | 'isPagerInProgress'> {
enabled?: boolean;
isSvg: boolean;
source: ImageSource | string;
targetDimensions: { width: number; height: number };
}
const ImageTransformer = (
{
enabled = true, height, isPageActive,
onPageStateChange, source, isSvg,
targetDimensions, width, pagerPanGesture, pagerTapGesture, lightboxPanGesture,
}: ImageTransformerProps) => {
const imageSource = typeof source === 'string' ? {uri: source} : source;
const {
interactionsEnabled,
scale,
translation,
scaleTranslation,
offset,
resetSharedState,
} = useTransformerSharedValues();
const setInteractionsEnabled = useCallback((value: boolean) => {
interactionsEnabled.value = value;
}, []);
const onLoadImageSuccess = useCallback(() => {
setInteractionsEnabled(true);
}, []);
useAnimatedReaction(
() => {
if (typeof isPageActive === 'undefined') {
return true;
}
return isPageActive.value;
},
(currentActive) => {
if (!currentActive) {
resetSharedState();
}
},
);
const animatedStyles = useAnimatedStyle(() => {
const noOffset = offset.x.value === 0 && offset.y.value === 0;
const noTranslation = translation.x.value === 0 && translation.y.value === 0;
const noScaleTranslation = scaleTranslation.x.value === 0 && scaleTranslation.y.value === 0;
const isInactive = scale.value === 1 && noOffset && noTranslation && noScaleTranslation;
onPageStateChange(isInactive);
return {
transform: [
{
translateX:
scaleTranslation.x.value +
translation.x.value +
offset.x.value,
},
{
translateY:
scaleTranslation.y.value +
translation.y.value +
offset.y.value,
},
{scale: scale.value},
],
};
}, []);
const pinchGesture = useTransformerPinchGesture(true);
pinchGesture.simultaneousWithExternalGesture(pagerPanGesture, lightboxPanGesture);
const panGesture = useTransformerPanGesture(enabled);
panGesture.simultaneousWithExternalGesture(pagerPanGesture, lightboxPanGesture);
const doubleTapGesture = useTransformerDoubleTap(enabled);
doubleTapGesture.blocksExternalGesture(pagerTapGesture);
const tapGesture = useTransformerSingleTap(enabled);
tapGesture.simultaneousWithExternalGesture(pagerTapGesture);
const composedGesture = Gesture.Exclusive(
Gesture.Simultaneous(pinchGesture, panGesture),
doubleTapGesture,
tapGesture,
);
let element;
if (isSvg) {
element = (
<SvgUri
uri={imageSource.uri!}
style={styles.svg}
width={Math.min(targetDimensions.width, targetDimensions.height)}
height={Math.min(targetDimensions.width, targetDimensions.height)}
onLayout={onLoadImageSuccess}
/>
);
} else {
element = (
<Image
onLoad={onLoadImageSuccess}
source={imageSource}
style={{width, height}}
/>
);
}
return (
<GestureDetector gesture={composedGesture}>
<Animated.View style={[styles.container]}>
<Animated.View style={[styles.wrapper, animatedStyles]}>
{element}
</Animated.View>
</Animated.View>
</GestureDetector>
);
};
export default React.memo(ImageTransformer);