From 33c65467fcb539b166f934016b2f7b978aa4e84f Mon Sep 17 00:00:00 2001 From: Michael Tostenson Date: Fri, 19 May 2017 08:37:39 -0700 Subject: [PATCH 1/2] Adds scrollingStickyHeader prop and disables bg scaling on Android --- README.md | 1 + src/index.js | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e335bd2..1e71ba1 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ The `ParallaxScrollView` component adds a few additional properties, as describe | `renderStickyHeader` | `func` | No | This renders an optional sticky header that will stick to the top of view when parallax header scrolls up. | | `stickyHeaderHeight` | `number` | If `renderStickyHeader` is used | If `renderStickyHeader` is set, then its height must be specified. | | `contentContainerStyle` | `object` | No | These styles will be applied to the scroll view content container which wraps all of the child views. (same as for [ScrollView](https://facebook.github.io/react-native/docs/scrollview.html#contentcontainerstyle)) | +| `scrollingStickyHeader` | `bool` | No | This causes the sticky header to scroll into view. If set to false the sticky header will only fade in. | ## Latest changes diff --git a/src/index.js b/src/index.js index 594c906..47ae7d1 100644 --- a/src/index.js +++ b/src/index.js @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { Animated, Dimensions, + Platform, ScrollView, View } from 'react-native'; @@ -42,7 +43,8 @@ const IPropTypes = { renderScrollComponent: func, renderStickyHeader: func, stickyHeaderHeight: number, - contentContainerStyle: View.propTypes.style + contentContainerStyle: View.propTypes.style, + scrollingStickyHeader: bool, }; class ParallaxScrollView extends Component { @@ -81,6 +83,7 @@ class ParallaxScrollView extends Component { stickyHeaderHeight, style, contentContainerStyle, + scrollingStickyHeader, ...scrollViewProps } = this.props; @@ -88,7 +91,7 @@ class ParallaxScrollView extends Component { const foreground = this._renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground: renderForeground || renderParallaxHeader }); const bodyComponent = this._wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle }); const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor }); - const maybeStickyHeader = this._maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader }); + const maybeStickyHeader = this._maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }); const scrollElement = renderScrollComponent(scrollViewProps); return ( @@ -203,13 +206,13 @@ class ParallaxScrollView extends Component { extrapolateRight: 'extend', extrapolateLeft: 'clamp' }) - }, { + }, Platform.OS === 'ios' ? { scale: interpolate(scrollY, { inputRange: [-viewHeight, 0], outputRange: [5, 1], extrapolate: 'clamp' }) - }] + } : null] }]}> { renderBackground() } @@ -272,7 +275,7 @@ class ParallaxScrollView extends Component { ); } - _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader }) { + _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }) { const { viewWidth, scrollY } = this.state; if (renderStickyHeader || renderFixedHeader) { const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); @@ -292,7 +295,7 @@ class ParallaxScrollView extends Component { }) }}> + } : null }> { renderStickyHeader() } @@ -329,7 +332,8 @@ ParallaxScrollView.defaultProps = { renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) renderForeground: null, stickyHeaderHeight: 0, - contentContainerStyle: null + contentContainerStyle: null, + scrollingStickyheader: true, }; module.exports = ParallaxScrollView; From ef89a9c9d0f1eb848e6a6bc622ad33b90de4af95 Mon Sep 17 00:00:00 2001 From: Michael Tostenson Date: Fri, 19 May 2017 15:25:17 -0700 Subject: [PATCH 2/2] Fix transform issue for Android --- src/index.js | 595 ++++++++++++++++++++++++++------------------------- 1 file changed, 301 insertions(+), 294 deletions(-) diff --git a/src/index.js b/src/index.js index 47ae7d1..dd1d967 100644 --- a/src/index.js +++ b/src/index.js @@ -1,10 +1,10 @@ import React, { Component } from 'react'; import { - Animated, - Dimensions, - Platform, - ScrollView, - View + Animated, + Dimensions, + Platform, + ScrollView, + View } from 'react-native'; const styles = require('./styles'); @@ -23,317 +23,324 @@ const renderEmpty = () => ; // 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, - renderFixedHeader: func, - renderForeground: func, - renderScrollComponent: func, - renderStickyHeader: func, - stickyHeaderHeight: number, - contentContainerStyle: View.propTypes.style, - scrollingStickyHeader: bool, + backgroundColor: string, + backgroundScrollSpeed: number, + fadeOutForeground: bool, + fadeOutBackground: bool, + contentBackgroundColor: string, + onChangeHeaderVisibility: func, + parallaxHeaderHeight: number.isRequired, + renderBackground: func, + renderFixedHeader: func, + renderForeground: func, + renderScrollComponent: func, + renderStickyHeader: func, + stickyHeaderHeight: number, + contentContainerStyle: View.propTypes.style, + scrollingStickyHeader: bool, }; 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._footerComponent = { setNativeProps() {} }; // Initial stub - this._footerHeight = 0; - } - - render() { - const { - backgroundColor, - backgroundScrollSpeed, - children, - contentBackgroundColor, - fadeOutForeground, - fadeOutBackground, - parallaxHeaderHeight, - renderBackground, - renderFixedHeader, - renderForeground, - renderParallaxHeader, - renderScrollComponent, - renderStickyHeader, - stickyHeaderHeight, - style, - contentContainerStyle, - scrollingStickyHeader, - ...scrollViewProps - } = this.props; - - const background = this._renderBackground({ fadeOutBackground, backgroundScrollSpeed, backgroundColor, parallaxHeaderHeight, stickyHeaderHeight, renderBackground }); - const foreground = this._renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground: renderForeground || renderParallaxHeader }); - const bodyComponent = this._wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle }); - const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor }); - const maybeStickyHeader = this._maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }); - const scrollElement = renderScrollComponent(scrollViewProps); - - return ( - this._maybeUpdateViewDimensions(e)}> - { background } - { - React.cloneElement(scrollElement, { - ref: SCROLLVIEW_REF, - style: [styles.scrollView, scrollElement.props.style], - scrollEventThrottle: 16, - 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._footerComponent = { setNativeProps() {} }; // Initial stub + this._footerHeight = 0; + } + + render() { + const { + backgroundColor, + backgroundScrollSpeed, + children, + contentBackgroundColor, + fadeOutForeground, + fadeOutBackground, + parallaxHeaderHeight, + renderBackground, + renderFixedHeader, + renderForeground, + renderParallaxHeader, + renderScrollComponent, + renderStickyHeader, + stickyHeaderHeight, + style, + contentContainerStyle, + scrollingStickyHeader, + ...scrollViewProps + } = this.props; + + const background = this._renderBackground({ fadeOutBackground, backgroundScrollSpeed, backgroundColor, parallaxHeaderHeight, stickyHeaderHeight, renderBackground }); + const foreground = this._renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground: renderForeground || renderParallaxHeader }); + const bodyComponent = this._wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle }); + const footerSpacer = this._renderFooterSpacer({ contentBackgroundColor }); + const maybeStickyHeader = this._maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }); + const scrollElement = renderScrollComponent(scrollViewProps); + + return ( + this._maybeUpdateViewDimensions(e)}> + { background } + { + React.cloneElement(scrollElement, { + ref: SCROLLVIEW_REF, + style: [styles.scrollView, scrollElement.props.style], + scrollEventThrottle: 16, + 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].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].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; - - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); - - 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.state; - 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 }) { - const { viewWidth, viewHeight, scrollY } = this.state; - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); - return ( - - - { renderBackground() } - - - ); - } - - _renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground }) { - const { scrollY } = this.state; - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); - return ( - - - - { renderForeground() } + _onScroll(e) { + const { + parallaxHeaderHeight, + stickyHeaderHeight, + onChangeHeaderVisibility, + onScroll: prevOnScroll = () => {} + } = this.props; + + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + + 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.state; + 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 }) { + const { viewWidth, viewHeight, scrollY } = this.state; + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + const transform = [{ + translateY: interpolate(scrollY, { + inputRange: [0, p], + outputRange: [0, -(p / backgroundScrollSpeed)], + extrapolateRight: 'extend', + extrapolateLeft: 'clamp' + }) + }]; + if (Platform.OS === 'ios') { + transform.push({ + scale: interpolate(scrollY, { + inputRange: [-viewHeight, 0], + outputRange: [5, 1], + extrapolate: 'clamp' + }) + }); + } + return ( + + + { renderBackground() } + + + ); + } + + _renderForeground({ fadeOutForeground, parallaxHeaderHeight, stickyHeaderHeight, renderForeground }) { + const { scrollY } = this.state; + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + return ( + + + + { renderForeground() } + + + + ); + } + + _wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle }) { + const { viewHeight } = this.state; + const containerStyles = [{backgroundColor: contentBackgroundColor}]; + + if(contentContainerStyle) + containerStyles.push(contentContainerStyle); + + 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; + } + }}> + { children } - - - ); - } - - _wrapChildren(children, { contentBackgroundColor, stickyHeaderHeight, contentContainerStyle }) { - const { viewHeight } = this.state; - const containerStyles = [{backgroundColor: contentBackgroundColor}]; - - if(contentContainerStyle) - containerStyles.push(contentContainerStyle); - - 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; - } - }}> - { children } - - ); - } - - _renderFooterSpacer({ contentBackgroundColor }) { - return ( - this._footerComponent = ref } style={{ backgroundColor: contentBackgroundColor }}/> - ); - } - - _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }) { - const { viewWidth, scrollY } = this.state; - if (renderStickyHeader || renderFixedHeader) { - const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); - return ( - - { - renderStickyHeader - ? ( - - - { renderStickyHeader() } - - - ) - : null - } - { renderFixedHeader && renderFixedHeader() } - - ); - } else { - return null; - } - } + ); + } + + _renderFooterSpacer({ contentBackgroundColor }) { + return ( + this._footerComponent = ref } style={{ backgroundColor: contentBackgroundColor }}/> + ); + } + + _maybeRenderStickyHeader({ parallaxHeaderHeight, stickyHeaderHeight, backgroundColor, renderFixedHeader, renderStickyHeader, scrollingStickyHeader }) { + const { viewWidth, scrollY } = this.state; + if (renderStickyHeader || renderFixedHeader) { + const p = pivotPoint(parallaxHeaderHeight, stickyHeaderHeight); + const stickyHeader = scrollingStickyHeader ? ( + + {this.renderStickyHeader()} + + ) : renderStickyHeader(); + return ( + + { + renderStickyHeader + ? ( + + { stickyHeader } + + ) + : null + } + { renderFixedHeader && renderFixedHeader() } + + ); + } else { + return null; + } + } } ParallaxScrollView.propTypes = IPropTypes; ParallaxScrollView.defaultProps = { - backgroundScrollSpeed: 5, - backgroundColor: '#000', - contentBackgroundColor: '#fff', - fadeOutForeground: true, - onChangeHeaderVisibility: () => {}, - renderScrollComponent: props => , - renderBackground: renderEmpty, - renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) - renderForeground: null, - stickyHeaderHeight: 0, - contentContainerStyle: null, - scrollingStickyheader: true, + backgroundScrollSpeed: 5, + backgroundColor: '#000', + contentBackgroundColor: '#fff', + fadeOutForeground: true, + onChangeHeaderVisibility: () => {}, + renderScrollComponent: props => , + renderBackground: renderEmpty, + renderParallaxHeader: renderEmpty, // Deprecated (will be removed in 0.18.0) + renderForeground: null, + stickyHeaderHeight: 0, + contentContainerStyle: null, + scrollingStickyheader: true, }; module.exports = ParallaxScrollView;