diff --git a/native/components/full-screen-view-modal.react.js b/native/components/full-screen-view-modal.react.js --- a/native/components/full-screen-view-modal.react.js +++ b/native/components/full-screen-view-modal.react.js @@ -26,6 +26,9 @@ Easing, withDecay, cancelAnimation, + useAnimatedStyle, + interpolate, + Extrapolate, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; @@ -49,17 +52,6 @@ import type { UserProfileBottomSheetNavigationProp } from '../user-profile/user-profile-bottom-sheet-navigator.react.js'; import { clampV2 } from '../utils/animation-utils.js'; -const { - Value, - Node, - Extrapolate, - add, - sub, - multiply, - divide, - interpolateNode, -} = Animated; - const defaultTimingConfig = { duration: 250, easing: Easing.out(Easing.ease), @@ -106,127 +98,13 @@ +onCloseButtonLayout: () => void, +onMediaIconsLayout: () => void, +close: () => void, + +contentViewContainerStyle: ViewStyle, + +animatedBackdropStyle: AnimatedViewStyle, + +animatedCloseButtonStyle: AnimatedViewStyle, + +animatedMediaIconsButtonStyle: AnimatedViewStyle, }; class FullScreenViewModal extends React.PureComponent<Props> { - centerX: Value; - centerY: Value; - frameWidth: Value; - frameHeight: Value; - imageWidth: Value; - imageHeight: Value; - - scale: Node; - x: Node; - y: Node; - backdropOpacity: Node; - imageContainerOpacity: Node; - actionLinksOpacity: Node; - closeButtonOpacity: Node; - - constructor(props: Props) { - super(props); - - const { imageWidth, imageHeight } = this; - const left = sub(this.centerX, divide(imageWidth, 2)); - const top = sub(this.centerY, divide(imageHeight, 2)); - - const { initialCoordinates } = props.route.params; - const initialScale = divide(initialCoordinates.width, imageWidth); - const initialTranslateX = sub( - initialCoordinates.x + initialCoordinates.width / 2, - add(left, divide(imageWidth, 2)), - ); - const initialTranslateY = sub( - initialCoordinates.y + initialCoordinates.height / 2, - add(top, divide(imageHeight, 2)), - ); - - const { overlayContext } = props; - invariant(overlayContext, 'FullScreenViewModal should have OverlayContext'); - const navigationProgress = overlayContext.position; - invariant( - navigationProgress, - 'position should be defined in FullScreenViewModal', - ); - - // The all-important outputs - const curScale = new Value(1); - const curX = new Value(0); - const curY = new Value(0); - const curBackdropOpacity = new Value(1); - const curCloseButtonOpacity = new Value(1); - const curActionLinksOpacity = new Value(1); - - const updates: Array<Node> = []; - const updatedScale = [updates, curScale]; - const updatedCurX = [updates, curX]; - const updatedCurY = [updates, curY]; - const updatedBackdropOpacity = [updates, curBackdropOpacity]; - const updatedCloseButtonOpacity = [updates, curCloseButtonOpacity]; - const updatedActionLinksOpacity = [updates, curActionLinksOpacity]; - - const reverseNavigationProgress = sub(1, navigationProgress); - this.scale = add( - multiply(reverseNavigationProgress, initialScale), - multiply(navigationProgress, updatedScale), - ); - this.x = add( - multiply(reverseNavigationProgress, initialTranslateX), - multiply(navigationProgress, updatedCurX), - ); - this.y = add( - multiply(reverseNavigationProgress, initialTranslateY), - multiply(navigationProgress, updatedCurY), - ); - this.backdropOpacity = multiply(navigationProgress, updatedBackdropOpacity); - this.imageContainerOpacity = interpolateNode(navigationProgress, { - inputRange: [0, 0.1], - outputRange: [0, 1], - extrapolate: Extrapolate.CLAMP, - }); - const buttonOpacity = interpolateNode(updatedBackdropOpacity, { - inputRange: [0.95, 1], - outputRange: [0, 1], - extrapolate: Extrapolate.CLAMP, - }); - this.closeButtonOpacity = multiply( - navigationProgress, - buttonOpacity, - updatedCloseButtonOpacity, - ); - this.actionLinksOpacity = multiply( - navigationProgress, - buttonOpacity, - updatedActionLinksOpacity, - ); - } - - get frame(): Dimensions { - const { width, safeAreaHeight } = this.props.dimensions; - return { width, height: safeAreaHeight }; - } - - get contentViewContainerStyle(): AnimatedViewStyle { - const { height, width } = this.props.contentDimensions; - const { height: frameHeight, width: frameWidth } = this.frame; - const top = (frameHeight - height) / 2 + this.props.dimensions.topInset; - const left = (frameWidth - width) / 2; - const { verticalBounds } = this.props.route.params; - return { - height, - width, - marginTop: top - verticalBounds.y, - marginLeft: left, - opacity: this.imageContainerOpacity, - transform: [ - { translateX: this.x }, - { translateY: this.y }, - { scale: this.scale }, - ], - }; - } - get contentContainerStyle(): ViewStyle { const { verticalBounds } = this.props.route.params; const fullScreenHeight = this.props.dimensions.height; @@ -246,13 +124,6 @@ const statusBar = this.props.isActive ? ( <ConnectedStatusBar hidden /> ) : null; - const backdropStyle = { opacity: this.backdropOpacity }; - const closeButtonStyle = { - opacity: this.closeButtonOpacity, - }; - const mediaIconsButtonStyle = { - opacity: this.actionLinksOpacity, - }; let saveButton; if (saveContentCallback) { @@ -286,7 +157,10 @@ if (saveContentCallback || copyContentCallback) { mediaActionButtons = ( <Animated.View - style={[styles.mediaIconsContainer, mediaIconsButtonStyle]} + style={[ + styles.mediaIconsContainer, + this.props.animatedMediaIconsButtonStyle, + ]} > <View style={styles.mediaIconsRow} @@ -303,16 +177,21 @@ const view = ( <Animated.View style={styles.container}> {statusBar} - <Animated.View style={[styles.backdrop, backdropStyle]} /> + <Animated.View + style={[styles.backdrop, this.props.animatedBackdropStyle]} + /> <View style={this.contentContainerStyle}> - <Animated.View style={this.contentViewContainerStyle}> + <Animated.View style={this.props.contentViewContainerStyle}> {children} </Animated.View> </View> <SafeAreaView style={styles.buttonsOverlay}> <View style={styles.fill}> <Animated.View - style={[styles.closeButtonContainer, closeButtonStyle]} + style={[ + styles.closeButtonContainer, + this.props.animatedCloseButtonStyle, + ]} > <TouchableOpacity onPress={this.props.close} @@ -881,8 +760,6 @@ }, ); - // TODO: use it later - // eslint-disable-next-line no-unused-vars const curBackdropOpacity = useDerivedValue(() => { if (pinchActive.value || roundedCurScale.value > 1) { return backdropReset.value; @@ -958,6 +835,97 @@ singleTapUpdate, ]); + const navigationProgress = overlayContext.positionV2; + invariant( + navigationProgress, + 'position should be defined in FullScreenViewModal', + ); + + const { contentDimensions } = props; + const { verticalBounds, initialCoordinates } = props.route.params; + + const contentViewContainerStyle = useAnimatedStyle(() => { + const { height, width } = contentDimensions; + const { + safeAreaHeight: dimFrameHeight, + width: dimFrameWidth, + topInset, + } = dimensions; + + const left = centerX.value - imageWidth.value / 2; + const top = centerY.value - imageHeight.value / 2; + + const initialScale = initialCoordinates.width / imageWidth.value; + const initialTranslateX = + initialCoordinates.x + + initialCoordinates.width / 2 - + (left + imageWidth.value / 2); + const initialTranslateY = + initialCoordinates.y + + initialCoordinates.height / 2 - + (top + imageHeight.value / 2); + + const reverseNavigationProgress = 1 - navigationProgress.value; + const scale = + reverseNavigationProgress * initialScale + + navigationProgress.value * curScale.value; + const x = + reverseNavigationProgress * initialTranslateX + + navigationProgress.value * curX.value; + const y = + reverseNavigationProgress * initialTranslateY + + navigationProgress.value * curY.value; + + const imageContainerOpacity = interpolate( + navigationProgress.value, + [0, 0.1], + [0, 1], + Extrapolate.CLAMP, + ); + + return { + height, + width, + marginTop: (dimFrameHeight - height) / 2 + topInset - verticalBounds.y, + marginLeft: (dimFrameWidth - width) / 2, + opacity: imageContainerOpacity, + transform: [{ translateX: x }, { translateY: y }, { scale }], + }; + }, [contentDimensions, verticalBounds, initialCoordinates, dimensions]); + + const animatedBackdropStyle = useAnimatedStyle(() => ({ + opacity: navigationProgress.value * curBackdropOpacity.value, + })); + + const buttonOpacity = useDerivedValue(() => + interpolate( + curBackdropOpacity.value, + [0.95, 1], + [0, 1], + Extrapolate.CLAMP, + ), + ); + + const animatedCloseButtonStyle = useAnimatedStyle(() => { + const closeButtonOpacity = + navigationProgress.value * + buttonOpacity.value * + curCloseButtonOpacity.value; + return { + opacity: closeButtonOpacity, + }; + }); + + const animatedMediaIconsButtonStyle = useAnimatedStyle(() => { + const actionLinksOpacity = + navigationProgress.value * + buttonOpacity.value * + curActionLinksOpacity.value; + return { + opacity: actionLinksOpacity, + }; + }); + return ( <FullScreenViewModal {...props} @@ -972,6 +940,10 @@ onCloseButtonLayout={onCloseButtonLayout} onMediaIconsLayout={onMediaIconsLayout} close={close} + contentViewContainerStyle={contentViewContainerStyle} + animatedBackdropStyle={animatedBackdropStyle} + animatedCloseButtonStyle={animatedCloseButtonStyle} + animatedMediaIconsButtonStyle={animatedMediaIconsButtonStyle} /> ); });