From 32ecda5490ec64bf7dfe072a12e20730681d5f73 Mon Sep 17 00:00:00 2001 From: Yosua Ian Sebastian Date: Tue, 16 Apr 2019 11:21:51 +0700 Subject: [PATCH] Apply fix from #112 --- src/index.js | 856 ++++++++++++++++++++++++++------------------------- 1 file changed, 440 insertions(+), 416 deletions(-) diff --git a/src/index.js b/src/index.js index 8787b6f..2614617 100644 --- a/src/index.js +++ b/src/index.js @@ -1,442 +1,466 @@ -import React, { Component } from 'react' -import { Animated, Dimensions, View, ViewPropTypes } from 'react-native' +import React, {Component} from 'react'; +import {Animated, Dimensions, View, ViewPropTypes} from 'react-native'; -const styles = require('./styles') +const styles = require('./styles'); -import { bool, func, number, string } from 'prop-types' +import {bool, func, number, string} from 'prop-types'; -const window = Dimensions.get('window') +const window = Dimensions.get('window'); -const SCROLLVIEW_REF = 'ScrollView' +const SCROLLVIEW_REF = 'ScrollView'; -const pivotPoint = (a, b) => a - b +const pivotPoint = (a, b) => a - b; -const renderEmpty = () => +const renderEmpty = () => ; -const noRender = () => +const noRender = () => ; // Override `toJSON` of interpolated value because of // an error when serializing style on view inside inspector. // See: https://github.com/jaysoo/react-native-parallax-scroll-view/issues/23 const interpolate = (value, opts) => { - const x = value.interpolate(opts) - x.toJSON = () => x.__getValue() - return x -} + const x = value.interpolate(opts); + x.toJSON = () => x.__getValue(); + return x; +}; // Properties accepted by `ParallaxScrollView`. const IPropTypes = { - backgroundColor: string, - backgroundScrollSpeed: number, - fadeOutForeground: bool, - fadeOutBackground: bool, - contentBackgroundColor: string, - onChangeHeaderVisibility: func, - parallaxHeaderHeight: number.isRequired, - renderBackground: func, - renderContentBackground: func, - renderFixedHeader: func, - renderForeground: func, - renderScrollComponent: func, - renderStickyHeader: func, - stickyHeaderHeight: number, - contentContainerStyle: ViewPropTypes.style, - outputScaleValue: number -} + backgroundColor: string, + backgroundScrollSpeed: number, + fadeOutForeground: bool, + fadeOutBackground: bool, + contentBackgroundColor: string, + onChangeHeaderVisibility: func, + parallaxHeaderHeight: number.isRequired, + renderBackground: func, + renderContentBackground: func, + renderFixedHeader: func, + renderForeground: func, + renderScrollComponent: func, + renderStickyHeader: func, + stickyHeaderHeight: number, + contentContainerStyle: ViewPropTypes.style, + outputScaleValue: number, +}; class ParallaxScrollView extends Component { - constructor(props) { - super(props) - if (props.renderStickyHeader && !props.stickyHeaderHeight) { - console.warn( - 'Property `stickyHeaderHeight` must be set if `renderStickyHeader` is used.' - ) - } - if (props.renderParallaxHeader !== renderEmpty && !props.renderForeground) { - console.warn( - 'Property `renderParallaxHeader` is deprecated. Use `renderForeground` instead.' - ) - } - this.state = { - scrollY: new Animated.Value(0), - viewHeight: window.height, - viewWidth: window.width - } - this.scrollY = new Animated.Value(0) - this._footerComponent = { setNativeProps() { } } // Initial stub - this._footerHeight = 0 - } - - animatedEvent = Animated.event( - [{ nativeEvent: { contentOffset: { y: this.scrollY } } }], - { useNativeDriver: true } - ) - - render() { - const { - backgroundColor, - backgroundScrollSpeed, - children, - contentBackgroundColor, - fadeOutForeground, - fadeOutBackground, - parallaxHeaderHeight, - renderBackground, - renderContentBackground, - renderFixedHeader, - renderForeground, - renderParallaxHeader, - renderScrollComponent, - renderStickyHeader, - stickyHeaderHeight, - style, - contentContainerStyle, - outputScaleValue, - ...scrollViewProps - } = this.props - - const background = this._renderBackground({ - fadeOutBackground, - backgroundScrollSpeed, - backgroundColor, - parallaxHeaderHeight, - stickyHeaderHeight, - renderBackground, - outputScaleValue - }) - const foreground = this._renderForeground({ - fadeOutForeground, - parallaxHeaderHeight, - stickyHeaderHeight, - renderForeground: renderForeground || renderParallaxHeader - }) - const bodyComponent = this._wrapChildren(children, { - contentBackgroundColor, - stickyHeaderHeight, - renderContentBackground, - contentContainerStyle - }) - const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor }) - const maybeStickyHeader = this._maybeRenderStickyHeader({ - parallaxHeaderHeight, - stickyHeaderHeight, - backgroundColor, - renderFixedHeader, - renderStickyHeader - }) - const scrollElement = renderScrollComponent(scrollViewProps) - return ( - this._maybeUpdateViewDimensions(e)} - > - {background} - {React.cloneElement( - scrollElement, - { - ref: SCROLLVIEW_REF, - style: [styles.scrollView, scrollElement.props.style], - scrollEventThrottle: 1, - // Using Native Driver greatly optimizes performance - onScroll: Animated.event( - [{ nativeEvent: { contentOffset: { y: this.scrollY } } }], - { useNativeDriver: true, listener: this._onScroll.bind(this) } - ) - // onScroll: this._onScroll.bind(this) - }, - foreground, - bodyComponent, - footerSpacer - )} - {maybeStickyHeader} - - ) - } - - /* + constructor(props) { + super(props); + if (props.renderStickyHeader && !props.stickyHeaderHeight) { + console.warn( + 'Property `stickyHeaderHeight` must be set if `renderStickyHeader` is used.', + ); + } + if (props.renderParallaxHeader !== renderEmpty && !props.renderForeground) { + console.warn( + 'Property `renderParallaxHeader` is deprecated. Use `renderForeground` instead.', + ); + } + this.state = { + scrollY: new Animated.Value(0), + viewHeight: window.height, + viewWidth: window.width, + }; + this.scrollY = new Animated.Value(0); + this._footerComponent = {setNativeProps() {}}; // Initial stub + this._footerHeight = 0; + } + + animatedEvent = Animated.event( + [{nativeEvent: {contentOffset: {y: this.scrollY}}}], + {useNativeDriver: true}, + ); + + render() { + const { + backgroundColor, + backgroundScrollSpeed, + children, + contentBackgroundColor, + fadeOutForeground, + fadeOutBackground, + parallaxHeaderHeight, + renderBackground, + renderContentBackground, + renderFixedHeader, + renderForeground, + renderParallaxHeader, + renderScrollComponent, + renderStickyHeader, + stickyHeaderHeight, + style, + contentContainerStyle, + outputScaleValue, + ...scrollViewProps + } = this.props; + + const background = this._renderBackground({ + fadeOutBackground, + backgroundScrollSpeed, + backgroundColor, + parallaxHeaderHeight, + stickyHeaderHeight, + renderBackground, + outputScaleValue, + }); + const foreground = this._renderForeground({ + fadeOutForeground, + parallaxHeaderHeight, + stickyHeaderHeight, + renderForeground: renderForeground || renderParallaxHeader, + }); + const bodyComponent = this._wrapChildren(children, { + contentBackgroundColor, + stickyHeaderHeight, + renderContentBackground, + contentContainerStyle, + }); + const footerSpacer = this._renderFooterSpacer({contentBackgroundColor}); + const maybeStickyHeader = this._maybeRenderStickyHeader({ + parallaxHeaderHeight, + stickyHeaderHeight, + backgroundColor, + renderFixedHeader, + renderStickyHeader, + }); + const scrollElement = renderScrollComponent(scrollViewProps); + return ( + this._maybeUpdateViewDimensions(e)} + > + {background} + {React.cloneElement( + scrollElement, + { + ref: SCROLLVIEW_REF, + style: [styles.scrollView, scrollElement.props.style], + scrollEventThrottle: 1, + // Using Native Driver greatly optimizes performance + onScroll: Animated.event( + [{nativeEvent: {contentOffset: {y: this.scrollY}}}], + {useNativeDriver: true, listener: this._onScroll.bind(this)}, + ), + // onScroll: this._onScroll.bind(this) + }, + foreground, + bodyComponent, + footerSpacer, + )} + {maybeStickyHeader} + + ); + } + + /* * Expose `ScrollView` API so this component is composable with any component that expects a `ScrollView`. */ - getScrollResponder() { - return this.refs[SCROLLVIEW_REF]._component.getScrollResponder() - } - getScrollableNode() { - return this.getScrollResponder().getScrollableNode() - } - getInnerViewNode() { - return this.getScrollResponder().getInnerViewNode() - } - scrollTo(...args) { - this.getScrollResponder().scrollTo(...args) - } - setNativeProps(props) { - this.refs[SCROLLVIEW_REF].setNativeProps(props) - } - - /* + getScrollResponder() { + return this.refs[SCROLLVIEW_REF]._component.getScrollResponder(); + } + getScrollableNode() { + return this.getScrollResponder().getScrollableNode(); + } + getInnerViewNode() { + return this.getScrollResponder().getInnerViewNode(); + } + scrollTo(...args) { + this.getScrollResponder().scrollTo(...args); + } + setNativeProps(props) { + this.refs[SCROLLVIEW_REF].setNativeProps(props); + } + + /* * Private helpers */ - _onScroll(e) { - const { - parallaxHeaderHeight, - stickyHeaderHeight, - onChangeHeaderVisibility, - onScroll: prevOnScroll = () => { } - } = this.props - this.props.scrollEvent && this.props.scrollEvent(e) - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight) - - // This optimization wont run, since we update the animation value directly in onScroll event - // this._maybeUpdateScrollPosition(e) - - if (e.nativeEvent.contentOffset.y >= p) { - onChangeHeaderVisibility(false) - } else { - onChangeHeaderVisibility(true) - } - - prevOnScroll(e) - } - - // This optimizes the state update of current scrollY since we don't need to - // perform any updates when user has scrolled past the pivot point. - _maybeUpdateScrollPosition(e) { - const { parallaxHeaderHeight, stickyHeaderHeight } = this.props - const { scrollY } = this - const { nativeEvent: { contentOffset: { y: offsetY } } } = e - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight) - if (offsetY <= p || scrollY._value <= p) { - scrollY.setValue(offsetY) - } - } - - _maybeUpdateViewDimensions(e) { - const { nativeEvent: { layout: { width, height } } } = e - - if (width !== this.state.viewWidth || height !== this.state.viewHeight) { - this.setState({ - viewWidth: width, - viewHeight: height - }) - } - } - - _renderBackground({ - fadeOutBackground, - backgroundScrollSpeed, - backgroundColor, - parallaxHeaderHeight, - stickyHeaderHeight, - renderBackground, - outputScaleValue - }) { - const { viewWidth, viewHeight } = this.state - const { scrollY } = this - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight) - return ( - - - {renderBackground()} - - - ) - } - - _renderForeground({ - fadeOutForeground, - parallaxHeaderHeight, - stickyHeaderHeight, - renderForeground - }) { - const { scrollY } = this - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight) - return ( - - - - {renderForeground()} - - - - ) - } - - _wrapChildren( - children, - { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle, renderContentBackground } - ) { - const { viewHeight } = this.state - const containerStyles = [{ backgroundColor: contentBackgroundColor }] - - if (contentContainerStyle) containerStyles.push(contentContainerStyle) - - this.containerHeight = this.state.viewHeight; - - React.Children.forEach(children, (item) => { - if (item && Object.keys(item).length != 0) { - this.containerHeight = 0; - } - }); - - return ( - { - // Adjust the bottom height so we can scroll the parallax header all the way up. - const { nativeEvent: { layout: { height } } } = e - const footerHeight = Math.max( - 0, - viewHeight - height - stickyHeaderHeight - ) - if (this._footerHeight !== footerHeight) { - this._footerComponent.setNativeProps({ - style: { height: footerHeight } - }) - this._footerHeight = footerHeight - } - }} - > - {renderContentBackground()} - {children} - - ) - } - - _renderFooterSpacer({ contentBackgroundColor }) { - return ( - { - if (ref) { - this._footerComponent = ref; - } - }} - style={{ backgroundColor: contentBackgroundColor }} - /> - ) - } - - _maybeRenderStickyHeader({ - parallaxHeaderHeight, - stickyHeaderHeight, - backgroundColor, - renderFixedHeader, - renderStickyHeader - }) { - const { viewWidth } = this.state - const { scrollY } = this - if (renderStickyHeader || renderFixedHeader) { - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight) - return ( - - {renderStickyHeader - ? - - {renderStickyHeader()} - - - : null} - {renderFixedHeader && renderFixedHeader()} - - ) - } else { - return null - } - } + _onScroll(e) { + const { + parallaxHeaderHeight, + stickyHeaderHeight, + onChangeHeaderVisibility, + onScroll: prevOnScroll = () => {}, + } = this.props; + this.props.scrollEvent && this.props.scrollEvent(e); + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + + // This optimization wont run, since we update the animation value directly in onScroll event + // this._maybeUpdateScrollPosition(e) + + if (e.nativeEvent.contentOffset.y >= p) { + onChangeHeaderVisibility(false); + } else { + onChangeHeaderVisibility(true); + } + + prevOnScroll(e); + } + + // This optimizes the state update of current scrollY since we don't need to + // perform any updates when user has scrolled past the pivot point. + _maybeUpdateScrollPosition(e) { + const {parallaxHeaderHeight, stickyHeaderHeight} = this.props; + const {scrollY} = this; + const { + nativeEvent: { + contentOffset: {y: offsetY}, + }, + } = e; + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + if (offsetY <= p || scrollY._value <= p) { + scrollY.setValue(offsetY); + } + } + + _maybeUpdateViewDimensions(e) { + const { + nativeEvent: { + layout: {width, height}, + }, + } = e; + + if (width !== this.state.viewWidth || height !== this.state.viewHeight) { + this.setState({ + viewWidth: width, + viewHeight: height, + }); + } + } + + _renderBackground({ + fadeOutBackground, + backgroundScrollSpeed, + backgroundColor, + parallaxHeaderHeight, + stickyHeaderHeight, + renderBackground, + outputScaleValue, + }) { + const {viewWidth, viewHeight} = this.state; + const {scrollY} = this; + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + return ( + + {renderBackground()} + + ); + } + + _renderForeground({ + fadeOutForeground, + parallaxHeaderHeight, + stickyHeaderHeight, + renderForeground, + }) { + const {scrollY} = this; + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + return ( + + + + {renderForeground()} + + + + ); + } + + _wrapChildren( + children, + { + contentBackgroundColor, + stickyHeaderHeight, + contentContainerStyle, + renderContentBackground, + }, + ) { + const {viewHeight} = this.state; + const containerStyles = [{backgroundColor: contentBackgroundColor}]; + + if (contentContainerStyle) containerStyles.push(contentContainerStyle); + + this.containerHeight = this.state.viewHeight; + + React.Children.forEach(children, (item) => { + if (item && Object.keys(item).length != 0) { + this.containerHeight = 0; + } + }); + + return ( + { + // Adjust the bottom height so we can scroll the parallax header all the way up. + const { + nativeEvent: { + layout: {height}, + }, + } = e; + const footerHeight = Math.max( + 0, + viewHeight - height - stickyHeaderHeight, + ); + if (this._footerHeight !== footerHeight) { + this._footerComponent.setNativeProps({ + style: {height: footerHeight}, + }); + this._footerHeight = footerHeight; + } + }} + > + {renderContentBackground()} + {children} + + ); + } + + _renderFooterSpacer({contentBackgroundColor}) { + return ( + { + if (ref) { + this._footerComponent = ref; + } + }} + style={{backgroundColor: contentBackgroundColor}} + /> + ); + } + + _maybeRenderStickyHeader({ + parallaxHeaderHeight, + stickyHeaderHeight, + backgroundColor, + renderFixedHeader, + renderStickyHeader, + }) { + const {viewWidth} = this.state; + const {scrollY} = this; + + const translateY = interpolate(scrollY, { + inputRange: [0, stickyHeaderHeight], + outputRange: [-stickyHeaderHeight, 0], + extrapolate: 'clamp', + }); + + if (renderStickyHeader || renderFixedHeader) { + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + return ( + + {renderStickyHeader ? ( + + + {renderStickyHeader()} + + + ) : null} + {renderFixedHeader && renderFixedHeader()} + + ); + } else { + return null; + } + } } -ParallaxScrollView.propTypes = IPropTypes +ParallaxScrollView.propTypes = IPropTypes; ParallaxScrollView.defaultProps = { - backgroundScrollSpeed: 5, - backgroundColor: '#000', - contentBackgroundColor: '#fff', - fadeOutForeground: true, - onChangeHeaderVisibility: () => { }, - renderScrollComponent: props => , - renderBackground: renderEmpty, - renderContentBackground: noRender, - renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) - renderForeground: null, - stickyHeaderHeight: 0, - contentContainerStyle: null, - outputScaleValue: 5 -} - -module.exports = ParallaxScrollView + backgroundScrollSpeed: 5, + backgroundColor: '#000', + contentBackgroundColor: '#fff', + fadeOutForeground: true, + onChangeHeaderVisibility: () => {}, + renderScrollComponent: (props) => , + renderBackground: renderEmpty, + renderContentBackground: noRender, + renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) + renderForeground: null, + stickyHeaderHeight: 0, + contentContainerStyle: null, + outputScaleValue: 5, +}; + +module.exports = ParallaxScrollView;