redbadger/pride-london-app

View on GitHub
src/components/ShadowedScrollView.js

Summary

Maintainability
A
0 mins
Test Coverage
// @flow
import * as React from "react";
import { StyleSheet, ScrollView, View, Animated } from "react-native";
import LinearGradient from "react-native-linear-gradient";
import type { ViewStyleProp } from "react-native/Libraries/StyleSheet/StyleSheet";
import { blackZeroColor, blackFifteenColor } from "../constants/colors";

type Props = {
  topShadow?: boolean,
  bottomShadow?: boolean,
  children: React.Node,
  shadowOpacity?: number,
  style: ViewStyleProp
};

const shadowFadeDuration: number = 100;
const maxScrollEventThrottle: number = 16;
const shadowHeight: number = 8;

class ShadowedScrollView extends React.PureComponent<Props> {
  static defaultProps = {
    topShadow: true,
    bottomShadow: true,
    children: [],
    style: {},
    shadowOpacity: 1
  };

  // eslint-disable-next-line react/sort-comp
  topShadowOpacity: Object;
  bottomShadowOpacity: Object;
  isTopShadowPresent: boolean = false;
  isBottomShadowPresent: boolean = true;
  contentViewHeight: number;
  scrollViewHeight: number;

  fadeTopShadow = (toValue: number) =>
    Animated.timing(this.topShadowOpacity, {
      toValue,
      duration: shadowFadeDuration,
      useNativeDriver: true
    }).start();

  fadeBottomShadow = (toValue: number) =>
    Animated.timing(this.bottomShadowOpacity, {
      toValue,
      duration: shadowFadeDuration,
      useNativeDriver: true
    }).start();

  componentWillMount() {
    this.topShadowOpacity = new Animated.Value(0);
    this.bottomShadowOpacity = new Animated.Value(1);
  }

  handleScroll = (event: { nativeEvent: { contentOffset: { y: number } } }) => {
    const maxScrollOffset = this.contentViewHeight - this.scrollViewHeight - 10;
    const currentScrollOffset = event.nativeEvent.contentOffset.y;

    if (this.isTopShadowPresent && currentScrollOffset <= 0) {
      this.isTopShadowPresent = false;
      this.fadeTopShadow(0);
    }

    if (!this.isTopShadowPresent && currentScrollOffset > 0) {
      this.isTopShadowPresent = true;
      this.fadeTopShadow(1);
    }

    if (this.isBottomShadowPresent && currentScrollOffset > maxScrollOffset) {
      this.isBottomShadowPresent = false;
      this.fadeBottomShadow(0);
    }

    if (!this.isBottomShadowPresent && currentScrollOffset < maxScrollOffset) {
      this.isBottomShadowPresent = true;
      this.fadeBottomShadow(1);
    }
  };

  handleScrollViewLayout = (event: Object) => {
    const { height } = event.nativeEvent.layout;
    this.scrollViewHeight = height;
  };

  handleContentViewLayout = (event: Object) => {
    const { height } = event.nativeEvent.layout;
    this.contentViewHeight = height;
  };

  render() {
    const {
      style,
      children,
      topShadow,
      bottomShadow,
      shadowOpacity
    } = this.props;

    const topShadowOpacityStyle = {
      opacity: this.topShadowOpacity.interpolate({
        inputRange: [0, 1],
        outputRange: [0, topShadow ? 1 : 0]
      })
    };

    const bottomShadowOpacityStyle = {
      opacity: this.bottomShadowOpacity.interpolate({
        inputRange: [0, 1],
        outputRange: [0, bottomShadow ? 1 : 0]
      })
    };

    const gradientStyles = [styles.gradient, { opacity: shadowOpacity }];

    return (
      <View style={[styles.container, style]}>
        <Animated.View style={[styles.topShadow, topShadowOpacityStyle]}>
          <LinearGradient
            colors={[blackFifteenColor, blackZeroColor]}
            style={gradientStyles}
          />
        </Animated.View>
        <ScrollView
          onLayout={this.handleScrollViewLayout}
          onScroll={this.handleScroll}
          scrollEventThrottle={maxScrollEventThrottle}
        >
          <View onLayout={this.handleContentViewLayout}>{children}</View>
        </ScrollView>
        <Animated.View style={[styles.bottomShadow, bottomShadowOpacityStyle]}>
          <LinearGradient
            colors={[blackZeroColor, blackFifteenColor]}
            style={gradientStyles}
          />
        </Animated.View>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1
  },
  topShadow: {
    width: "100%",
    height: shadowHeight,
    position: "absolute",
    left: 0,
    top: 0,
    zIndex: 1
  },
  bottomShadow: {
    width: "100%",
    height: shadowHeight,
    position: "absolute",
    left: 0,
    bottom: 0
  },
  gradient: {
    width: "100%",
    height: shadowHeight
  }
});

export default ShadowedScrollView;