UNPKG

react-native-autoscroll-flatlist

Version:

An enhanced React Native FlatList component to provide auto-scrolling functionality

219 lines 10.2 kB
import React from "react"; import { Animated, FlatList, StyleSheet, Text, TouchableWithoutFeedback, View } from "react-native"; import { Triangle } from "./Triangle"; export class AutoScrollFlatList extends React.PureComponent { constructor(props) { super(props); this.listRef = React.createRef(); this.flatListHeight = 0; this.flatListWidth = 0; this.contentHeight = 0; this.contentWidth = 0; this.scrollTop = 0; /** * Exposing FlatList Methods To AutoScrollFlatList's Ref */ this.scrollToEnd = (params = { animated: true }) => { const offset = this.props.horizontal ? this.contentWidth - this.flatListWidth : this.contentHeight - this.flatListHeight; this.setState({ newItemCount: 0 }); this.scrollToOffset({ offset, animated: params.animated }); }; this.scrollToIndex = (params) => { this.listRef.current?.scrollToIndex(params); }; this.scrollToItem = (params) => { this.listRef.current?.scrollToItem(params); }; this.scrollToOffset = (params) => { this.listRef.current?.scrollToOffset(params); }; this.recordInteraction = () => { this.listRef.current?.recordInteraction(); }; this.flashScrollIndicators = () => { this.listRef.current?.flashScrollIndicators(); }; this.getScrollableNode = () => { return this.listRef.current?.getScrollableNode(); }; this.getNativeScrollRef = () => { return this.listRef.current?.getNativeScrollRef(); }; this.getScrollResponder = () => { return this.listRef.current?.getScrollResponder(); }; this.isAutoScrolling = () => this.state.enabledAutoScrollToEnd; /** * Private Methods */ this.getTriangleDirection = () => { const { inverted, horizontal, triangleDirection } = this.props; let direction; if (horizontal) { if (inverted) { direction = "left"; } else { direction = "right"; } } else { if (inverted) { direction = "up"; } else { direction = "down"; } } return triangleDirection ?? direction; }; this.getScrollToEndIndicatorPosition = () => { const { inverted, horizontal } = this.props; return { top: inverted && !horizontal ? 20 : undefined, bottom: inverted && !horizontal ? undefined : 20, left: inverted && horizontal ? 30 : undefined, right: inverted && horizontal ? undefined : 20, }; }; this.onLayout = (event) => { this.flatListHeight = event.nativeEvent.layout.height; this.flatListWidth = event.nativeEvent.layout.width; if (this.listRef.current && this.state.enabledAutoScrollToEnd) { this.scrollToEnd(); } // User-defined onLayout event this.props.onLayout?.(event); }; this.onContentSizeChange = (width, height) => { this.contentHeight = height; this.contentWidth = width; if (this.state.enabledAutoScrollToEnd) { this.scrollToEnd(); } // User-defined onContentSizeChange event this.props.onContentSizeChange?.(width, height); }; this.onScroll = (event) => { /** * Default behavior: if scrollTop is at the end of <Flatlist>, autoscroll will be enabled. * CAVEAT: Android has precision error here from 4 decimal places, therefore we need to use Math.floor() to make sure the calculation is correct on Android. */ const prevScrollTop = this.scrollTop; this.scrollTop = this.props.horizontal ? event.nativeEvent.contentOffset.x : event.nativeEvent.contentOffset.y; const isScrollingDown = prevScrollTop <= this.scrollTop; const scrollEnd = this.props.horizontal ? this.contentWidth - this.flatListWidth : this.contentHeight - this.flatListHeight; const isEndOfList = this.scrollTop + this.props.threshold >= Math.floor(scrollEnd); this.setState({ isEndOfList, enabledAutoScrollToEnd: this.props.autoScrollDisabled ? false : (this.state.enabledAutoScrollToEnd && isScrollingDown) || isEndOfList }, () => { // User-defined onScroll event this.props.onScroll?.(event); }); /** * Need to check if event.persist is defined before using to account for usage in react-native-web */ event.persist?.(); }; this.renderDefaultNewItemAlertComponent = (newItemCount, translateY) => { const { inverted, horizontal, newItemAlertMessage, newItemAlertContainerStyle, newItemAlertTextStyle } = this.props; const direction = this.getTriangleDirection(); const message = newItemAlertMessage ? newItemAlertMessage(newItemCount) : `${direction === "left" ? " " : ""}${newItemCount} new item${newItemCount > 1 ? "s" : ""}`; const position = inverted && !horizontal ? { bottom: translateY } : { top: translateY }; return (<Animated.View style={[styles.newItemAlert, newItemAlertContainerStyle, position]}> {direction === "left" && <Triangle size={4} direction={direction}/>} <Text style={[styles.alertMessage, newItemAlertTextStyle]}>{message}</Text> {direction !== "left" && <Triangle size={4} direction={direction}/>} </Animated.View>); }; this.renderDefaultIndicatorComponent = () => { const { indicatorContainerStyle } = this.props; return (<View style={indicatorContainerStyle ?? [styles.scrollToEndIndicator, this.getScrollToEndIndicatorPosition()]}> <Triangle direction={this.getTriangleDirection()}/> </View>); }; this.state = { enabledAutoScrollToEnd: props.autoScrollDisabled ? false : true, newItemCount: 0, alertY: new Animated.Value(0), isEndOfList: true, }; } componentDidUpdate(prevProps, prevState) { const { data, filteredDataForNewItemCount } = this.props; const { enabledAutoScrollToEnd, newItemCount, alertY } = this.state; const filteredPrevData = filteredDataForNewItemCount ? filteredDataForNewItemCount(prevProps.data ?? []) : prevProps.data ?? []; const filteredData = filteredDataForNewItemCount ? filteredDataForNewItemCount(data ?? []) : data ?? []; if (!enabledAutoScrollToEnd && filteredData.length > filteredPrevData.length) { const newCount = prevState.newItemCount + filteredData.length - filteredPrevData.length; this.setState({ newItemCount: newCount }); if (newCount === 1) { alertY.setValue(-30); Animated.timing(alertY, { toValue: 10, duration: 250, useNativeDriver: false, }).start(); } } else if (enabledAutoScrollToEnd && newItemCount) { this.setState({ newItemCount: 0 }); } } render() { /** * Need to force a refresh for the FlatList by changing the key when numColumns changes. * Ref: https://stackoverflow.com/questions/44291781/dynamically-changing-number-of-columns-in-react-native-flat-list */ const { contentContainerStyle, threshold, showScrollToEndIndicator, showNewItemAlert, newItemAlertRenderer, indicatorContainerStyle, indicatorComponent, numColumns, ...restProps } = this.props; const { enabledAutoScrollToEnd, newItemCount, alertY, isEndOfList } = this.state; return (<View style={styles.container}> <FlatList {...restProps} ref={this.listRef} key={numColumns} numColumns={numColumns} contentContainerStyle={contentContainerStyle ?? styles.contentContainer} onLayout={this.onLayout} onContentSizeChange={this.onContentSizeChange} onScroll={this.onScroll}/> {showNewItemAlert && !enabledAutoScrollToEnd && newItemCount > 0 && (<TouchableWithoutFeedback onPress={() => this.scrollToEnd()}>{newItemAlertRenderer ? newItemAlertRenderer(newItemCount, alertY) : this.renderDefaultNewItemAlertComponent(newItemCount, alertY)}</TouchableWithoutFeedback>)} {showScrollToEndIndicator && !enabledAutoScrollToEnd && !isEndOfList && <TouchableWithoutFeedback onPress={() => this.scrollToEnd()}>{indicatorComponent ?? this.renderDefaultIndicatorComponent()}</TouchableWithoutFeedback>} </View>); } } AutoScrollFlatList.displayName = "AutoScrollFlatList"; AutoScrollFlatList.defaultProps = { threshold: 0, showScrollToEndIndicator: true, showNewItemAlert: true, autoScrollDisabled: false, }; const styles = StyleSheet.create({ container: { flex: 1, overflow: "hidden", }, contentContainer: { alignItems: "stretch", paddingVertical: 8, paddingHorizontal: 8, }, scrollToEndIndicator: { position: "absolute", width: 30, height: 30, justifyContent: "center", alignItems: "center", borderWidth: 1, borderColor: "#000000", borderRadius: 5, backgroundColor: "#ffffff", }, newItemAlert: { position: "absolute", alignSelf: "center", flexDirection: "row", alignItems: "center", borderRadius: 10, borderWidth: StyleSheet.hairlineWidth, borderColor: "#000000", backgroundColor: "#ffffff", paddingVertical: 3, paddingHorizontal: 8, }, alertMessage: { marginRight: 4, }, }); //# sourceMappingURL=AutoScrollFlatList.js.map