// Parallax effect inspired by https://github.com/oblador/react-native-parallax/ import React, { Component } from 'react'; import { View, ImageProps, Image, Animated, Easing, ActivityIndicator, findNodeHandle, StyleProp, ViewStyle, FlatList, ScrollView, ImageStyle, NativeSyntheticEvent, ImageLoadEventData, ImageErrorEventData } from 'react-native'; import PropTypes from 'prop-types'; import styles from './ParallaxImage.style'; import type Carousel from '../carousel/Carousel'; type ParallaxImageAllProps = { carouselRef: ScrollView | FlatList, // passed from itemHeight: number, // passed from itemWidth: number, // passed from scrollPosition: Animated.Value, // passed from sliderHeight: number, // passed from sliderWidth: number, // passed from vertical: boolean, // passed from containerStyle: StyleProp, /** * On screen dimensions of the image */ dimensions: { width: number, height: number }, /** * Duration of fade in when object is loaded. Default of 500 */ fadeDuration: number, /** * Speed of parallax effect. A higher value appears more 'zoomed in' */ parallaxFactor: number, /** * Whether to display a loading spinner */ showSpinner: boolean, /** * Color of the loading spinner if displayed */ spinnerColor: string, AnimatedImageComponent: typeof Animated.Image | Animated.AnimatedComponent } & ImageProps; type ParallaxImageProps = Omit & Partial>; type ParallaxImageState = { /** * 1 -> loading; 2 -> loaded // 3 -> transition finished; 4 -> error */ status: 1 | 2 | 3 | 4, animOpacity: Animated.Value, offset: number, width: number, height: number } export default class ParallaxImage extends Component { _container: View | null = null; _mounted = false; static defaultProps = { containerStyle: {}, fadeDuration: 500, parallaxFactor: 0.3, showSpinner: true, spinnerColor: 'rgba(0, 0, 0, 0.4)', AnimatedImageComponent: Animated.Image } constructor(props: ParallaxImageProps) { super(props); this.state = { offset: 0, width: 0, height: 0, status: 1, // 1 -> loading; 2 -> loaded // 3 -> transition finished; 4 -> error animOpacity: new Animated.Value(0) }; this._onLoad = this._onLoad.bind(this); this._onError = this._onError.bind(this); this._measureLayout = this._measureLayout.bind(this); } setNativeProps(nativeProps: object) { this._container!.setNativeProps(nativeProps); } componentDidMount() { this._mounted = true; setTimeout(() => { this._measureLayout(); }, 0); } componentWillUnmount() { this._mounted = false; } _measureLayout() { if (this._container) { const { dimensions, vertical, carouselRef, sliderWidth, sliderHeight, itemWidth, itemHeight } = this.props; if (carouselRef) { this._container.measureLayout( findNodeHandle(carouselRef)!, (x, y, width, height) => { const offset = vertical ? y - ((sliderHeight - itemHeight) / 2) : x - ((sliderWidth - itemWidth) / 2); this.setState({ offset: offset, width: dimensions && dimensions.width ? dimensions.width : Math.ceil(width), height: dimensions && dimensions.height ? dimensions.height : Math.ceil(height) }); }, () => { // lol do nothing when fail } ); } } } _onLoad(event: NativeSyntheticEvent) { const { animOpacity } = this.state; const { fadeDuration, onLoad } = this.props; if (!this._mounted) { return; } this.setState({ status: 2 }); if (onLoad) { onLoad(event); } Animated.timing(animOpacity, { toValue: 1, duration: fadeDuration, easing: Easing.out(Easing.quad), isInteraction: false, useNativeDriver: true }).start(() => { this.setState({ status: 3 }); }); } // If arg is missing from method signature, it just won't be called _onError(event: NativeSyntheticEvent) { const { onError } = this.props; this.setState({ status: 4 }); if (onError) { onError(event); } } get image() { const { status, animOpacity, offset, width, height } = this.state; const { scrollPosition, dimensions, vertical, sliderWidth, sliderHeight, parallaxFactor, style, AnimatedImageComponent, ...other } = this.props as ParallaxImageAllProps; const parallaxPadding = (vertical ? height : width) * parallaxFactor; const requiredStyles = { position: 'relative' } as ImageStyle; const dynamicStyles = { width: vertical ? width : width + parallaxPadding * 2, height: vertical ? height + parallaxPadding * 2 : height, opacity: animOpacity, transform: scrollPosition ? [ { translateX: !vertical ? scrollPosition.interpolate({ inputRange: [offset - sliderWidth, offset + sliderWidth], outputRange: [-parallaxPadding, parallaxPadding], extrapolate: 'clamp' }) : 0 }, { translateY: vertical ? scrollPosition.interpolate({ inputRange: [offset - sliderHeight, offset + sliderHeight], outputRange: [-parallaxPadding, parallaxPadding], extrapolate: 'clamp' }) : 0 } ] : [] }; return ( ); } get spinner() { const { status } = this.state; const { showSpinner, spinnerColor } = this.props; return status === 1 && showSpinner ? ( ) : false; } render() { const { containerStyle } = this.props; return ( { this._container = c; }} pointerEvents={'none'} style={[containerStyle, styles.container]} onLayout={this._measureLayout} > {this.image} {this.spinner} ); } }