Advertisement
Guest User

Untitled

a guest
Apr 8th, 2020
192
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import React, { Component, PureComponent } from 'react'
  2. import {
  3.   LayoutAnimation,
  4.   YellowBox,
  5.   Animated,
  6.   FlatList,
  7.   View,
  8.   PanResponder,
  9.   Platform,
  10.   UIManager,
  11.   StatusBar,
  12.   StyleSheet,
  13. } from 'react-native'
  14.  
  15. // Measure function triggers false positives
  16. YellowBox.ignoreWarnings(['Warning: isMounted(...) is deprecated'])
  17. UIManager.setLayoutAnimationEnabledExperimental && UIManager.setLayoutAnimationEnabledExperimental(true);
  18.  
  19. const initialState = {
  20.   activeRow: -1,
  21.   showHoverComponent: false,
  22.   spacerIndex: -1,
  23.   scroll: false,
  24.   hoverComponent: null,
  25.   extraData: null,
  26. }
  27.  
  28. // Note using LayoutAnimation.easeInEaseOut() was causing blank spaces to
  29. // show up in list: https://github.com/facebook/react-native/issues/13207
  30. const layoutAnimConfig = {
  31.   duration: 300,
  32.   create: {
  33.     type: LayoutAnimation.Types.easeInEaseOut,
  34.     property: LayoutAnimation.Properties.scaleXY,
  35.   },
  36.   update: {
  37.     type: LayoutAnimation.Types.easeInEaseOut,
  38.     property: LayoutAnimation.Properties.scaleXY,
  39.   }
  40. }
  41.  
  42. class SortableFlatList extends Component {
  43.   _moveAnim = new Animated.Value(0)
  44.   _offset = new Animated.Value(0)
  45.   _hoverAnim = Animated.add(this._moveAnim, this._offset)
  46.   _spacerIndex = -1
  47.   _pixels = []
  48.   _measurements = []
  49.   _scrollOffset = 0
  50.   _containerSize
  51.   _containerOffset
  52.   _move = 0
  53.   _hasMoved = false
  54.   _refs = []
  55.   _additionalOffset = 0
  56.   _androidStatusBarOffset = 0
  57.   _releaseVal = null
  58.   _releaseAnim = null
  59.  
  60.   constructor(props) {
  61.     super(props)
  62.     this._panResponder = PanResponder.create({
  63.       onStartShouldSetPanResponderCapture: (evt, gestureState) => {
  64.         const { pageX, pageY } = evt.nativeEvent
  65.         const { horizontal } = this.props
  66.         const tappedPixel = horizontal ? pageX : pageY
  67.         const tappedRow = this._pixels[Math.floor(this._scrollOffset + tappedPixel)]
  68.         if (tappedRow === undefined) return false
  69.         this._additionalOffset = (tappedPixel + this._scrollOffset) - this._measurements[tappedRow][horizontal ? 'x' : 'y']
  70.         if (this._releaseAnim) {
  71.           return false
  72.         }
  73.         this._moveAnim.setValue(tappedPixel)
  74.         this._move = tappedPixel
  75.  
  76.         // compensate for translucent or hidden StatusBar on android
  77.         if (Platform.OS === 'android' && !horizontal) {
  78.           const isTranslucent = StatusBar._propsStack.reduce(((acc, cur) => {
  79.             return cur.translucent === undefined ? acc : cur.translucent
  80.           }), false)
  81.  
  82.           const isHidden = StatusBar._propsStack.reduce(((acc, cur) => {
  83.             return cur.hidden === null ? acc : cur.hidden.value
  84.           }), false)
  85.  
  86.           this._androidStatusBarOffset = (isTranslucent || isHidden) ? StatusBar.currentHeight : 0
  87.         }
  88.         this._offset.setValue((this._additionalOffset + this._containerOffset - this._androidStatusBarOffset) * -1)
  89.         return false
  90.       },
  91.       onMoveShouldSetPanResponder: (evt, gestureState) => {
  92.         const { activeRow } = this.state
  93.         const { horizontal } = this.props
  94.         const { moveX, moveY } = gestureState
  95.         const move = horizontal ? moveX : moveY
  96.         const shouldSet = activeRow > -1
  97.         this._moveAnim.setValue(move)
  98.         if (shouldSet) {
  99.           this.setState({ showHoverComponent: true })
  100.           // Kick off recursive row animation
  101.           this.animate()
  102.           this._hasMoved = true
  103.         }
  104.         return shouldSet;
  105.       },
  106.       onPanResponderMove: Animated.event([null, { [props.horizontal ? 'moveX' : 'moveY']: this._moveAnim }], {
  107.         listener: (evt, gestureState) => {
  108.           const { moveX, moveY } = gestureState
  109.           const { horizontal } = this.props
  110.           this._move = horizontal ? moveX : moveY
  111.         }
  112.       }),
  113.       onPanResponderTerminationRequest: ({ nativeEvent }, gestureState) => false,
  114.       onPanResponderRelease: () => {
  115.         const { activeRow, spacerIndex } = this.state
  116.         const { data, horizontal } = this.props
  117.         const activeMeasurements = this._measurements[activeRow]
  118.         const spacerMeasurements = this._measurements[spacerIndex]
  119.         const lastElementMeasurements = this._measurements[data.length - 1]
  120.         if (activeRow === -1) return
  121.         // If user flings row up and lets go in the middle of an animation measurements can error out.
  122.         // Give layout animations some time to complete and animate element into place before calling onMoveEnd
  123.  
  124.         // Spacers have different positioning depending on whether the spacer row is before or after the active row.
  125.         // This is because the active row animates to height 0, so everything after it shifts upwards, but everything before
  126.         // it shifts downward
  127.         const isAfterActive = spacerIndex > activeRow
  128.         const isLastElement = spacerIndex >= data.length
  129.         const spacerElement = isLastElement ? lastElementMeasurements : spacerMeasurements
  130.         if (!spacerElement) return
  131.         const { x, y, width, height } = spacerElement
  132.         const size = horizontal ? width : height
  133.         const offset = horizontal ? x : y
  134.         const pos = offset - this._scrollOffset + this._additionalOffset + (isLastElement ? size : 0)
  135.         const activeItemSize = horizontal ? activeMeasurements.width : activeMeasurements.height
  136.         this._releaseVal = pos - (isAfterActive ? activeItemSize : 0)
  137.         if (this._releaseAnim) this._releaseAnim.stop()
  138.         this._releaseAnim = Animated.spring(this._moveAnim, {
  139.           toValue: this._releaseVal,
  140.           stiffness: 5000,
  141.           damping: 500,
  142.           mass: 3,
  143.           useNativeDriver: true,
  144.         })
  145.  
  146.         this._releaseAnim.start(this.onReleaseAnimationEnd)
  147.       }
  148.     })
  149.     this.state = initialState
  150.   }
  151.  
  152.   onReleaseAnimationEnd = () => {
  153.     const { data, onMoveEnd } = this.props
  154.     const { activeRow, spacerIndex } = this.state
  155.     const sortedData = this.getSortedList(data, activeRow, spacerIndex)
  156.     const isAfterActive = spacerIndex > activeRow
  157.     const from = activeRow
  158.     const to = spacerIndex - (isAfterActive ? 1 : 0)
  159.     this._moveAnim.setValue(this._releaseVal)
  160.     this._spacerIndex = -1
  161.     this._hasMoved = false
  162.     this._move = 0
  163.     this._releaseAnim = null
  164.     this.setState(initialState, () => {
  165.       onMoveEnd && onMoveEnd({
  166.         row: data[activeRow],
  167.         from,
  168.         to,
  169.         data: sortedData,
  170.       })
  171.     })
  172.   }
  173.  
  174.   getSortedList = (data, activeRow, spacerIndex) => {
  175.     if (activeRow === spacerIndex) return data
  176.     const sortedData = data.reduce((acc, cur, i, arr) => {
  177.       if (i === activeRow) return acc
  178.       else if (i === spacerIndex) {
  179.         acc = [...acc, arr[activeRow], cur]
  180.       } else acc.push(cur)
  181.       return acc
  182.     }, [])
  183.     if (spacerIndex >= data.length) sortedData.push(data[activeRow])
  184.     return sortedData
  185.   }
  186.  
  187.   animate = () => {
  188.     const { activeRow } = this.state
  189.     const { scrollPercent, data, horizontal, scrollSpeed } = this.props
  190.     const scrollRatio = scrollPercent / 100
  191.     if (activeRow === -1) return
  192.     const nextSpacerIndex = this.getSpacerIndex(this._move, activeRow)
  193.     const isUndroppableIndex = this.props.undroppableLines.indexOf(nextSpacerIndex) > -1
  194.     if (!isUndroppableIndex && nextSpacerIndex > -1 && nextSpacerIndex !== this._spacerIndex) {
  195.       LayoutAnimation.configureNext(layoutAnimConfig);
  196.       this.setState({ spacerIndex: nextSpacerIndex })
  197.       this._spacerIndex = nextSpacerIndex
  198.       if (nextSpacerIndex === data.length) this._flatList.scrollToEnd()
  199.     }
  200.  
  201.     // Scroll if hovering in top or bottom of container and have set a scroll %
  202.     const isLastItem = (activeRow === data.length - 1) || nextSpacerIndex === data.length
  203.     const isFirstItem = activeRow === 0
  204.     if (this._measurements[activeRow]) {
  205.       const rowSize = this._measurements[activeRow][horizontal ? 'width' : 'height']
  206.       const hoverItemTopPosition = Math.max(0, this._move - (this._additionalOffset + this._containerOffset))
  207.       const hoverItemBottomPosition = Math.min(this._containerSize, hoverItemTopPosition + rowSize)
  208.       const fingerPosition = Math.max(0, this._move - this._containerOffset)
  209.       const shouldScrollUp = !isFirstItem && fingerPosition < (this._containerSize * scrollRatio)
  210.       const shouldScrollDown = !isLastItem && fingerPosition > (this._containerSize * (1 - scrollRatio))
  211.       if (shouldScrollUp) this.scroll(-scrollSpeed, nextSpacerIndex)
  212.       else if (shouldScrollDown) this.scroll(scrollSpeed, nextSpacerIndex)
  213.     }
  214.  
  215.     requestAnimationFrame(this.animate)
  216.   }
  217.  
  218.   scroll = (scrollAmt, spacerIndex) => {
  219.     if (spacerIndex >= this.props.data.length) return this._flatList.scrollToEnd()
  220.     if (spacerIndex === -1) return
  221.     const currentScrollOffset = this._scrollOffset
  222.     const newOffset = currentScrollOffset + scrollAmt
  223.     const offset = Math.max(0, newOffset)
  224.     this._flatList.scrollToOffset({ offset, animated: false })
  225.   }
  226.  
  227.  
  228.   getSpacerIndex = (move, activeRow) => {
  229.     const { horizontal } = this.props
  230.     if (activeRow === -1 || !this._measurements[activeRow]) return -1
  231.     // Find the row that contains the midpoint of the hovering item
  232.     const hoverItemSize = this._measurements[activeRow][horizontal ? 'width' : 'height']
  233.     const hoverItemMidpoint = move - this._additionalOffset + hoverItemSize / 2
  234.     const hoverPoint = Math.floor(hoverItemMidpoint + this._scrollOffset)
  235.     let spacerIndex = this._pixels[hoverPoint]
  236.     if (spacerIndex === undefined) {
  237.       // Fallback in case we can't find index in _pixels array
  238.       spacerIndex = this._measurements.findIndex(({ width, height, x, y }) => {
  239.         const itemOffset = horizontal ? x : y
  240.         const itemSize = horizontal ? width : height
  241.         return hoverPoint > itemOffset && hoverPoint < (itemOffset + itemSize)
  242.       })
  243.     }
  244.     // Spacer index differs according to placement. See note in onPanResponderRelease
  245.     return spacerIndex > activeRow ? spacerIndex + 1 : spacerIndex
  246.   }
  247.  
  248.   measureItem = (index) => {
  249.     const { activeRow } = this.state
  250.     const { horizontal } = this.props
  251.     // setTimeout required or else dimensions reported as 0
  252.     !!this._refs[index] && setTimeout(() => {
  253.       try {
  254.         // Using stashed ref prevents measuring an unmounted componenet, which throws an error
  255.         !!this._refs[index] && this._refs[index].measureInWindow(((x, y, width, height) => {
  256.           if ((width || height) && activeRow === -1) {
  257.             const ypos = y + this._scrollOffset
  258.             const xpos = x + this._scrollOffset
  259.             const pos = horizontal ? xpos : ypos
  260.             const size = horizontal ? width : height
  261.             const rowMeasurements = { y: ypos, x: xpos, width, height }
  262.             this._measurements[index] = rowMeasurements
  263.             for (let i = Math.floor(pos); i < pos + size; i++) {
  264.               this._pixels[i] = index
  265.             }
  266.           }
  267.         }))
  268.       } catch (e) {
  269.         console.log('## measure error -- index: ', index, activeRow, this._refs[index], e)
  270.       }
  271.     }, 100)
  272.   }
  273.  
  274.   move = (hoverComponent, index) => {
  275.     const { onMoveBegin } = this.props
  276.     if (this._releaseAnim) {
  277.       this._releaseAnim.stop()
  278.       this.onReleaseAnimationEnd()
  279.       return
  280.     }
  281.     this._refs.forEach((ref, index) => this.measureItem(ref, index))
  282.     this._spacerIndex = index
  283.     this.setState({
  284.       activeRow: index,
  285.       spacerIndex: index,
  286.       hoverComponent,
  287.     }, () => onMoveBegin && onMoveBegin(index)
  288.     )
  289.   }
  290.  
  291.   moveEnd = () => {
  292.     if (!this._hasMoved) this.setState(initialState)
  293.   }
  294.  
  295.   setRef = index => (ref) => {
  296.     if (!!ref) {
  297.       this._refs[index] = ref
  298.       this.measureItem(index)
  299.     }
  300.   }
  301.  
  302.   renderItem = ({ item, index }) => {
  303.     const { renderItem, data, horizontal } = this.props
  304.     const { activeRow, spacerIndex } = this.state
  305.     const isActiveRow = activeRow === index
  306.     const isSpacerRow = spacerIndex === index
  307.     const isLastItem = index === data.length - 1
  308.     const spacerAfterLastItem = spacerIndex >= data.length
  309.     const activeRowSize = this._measurements[activeRow] ? this._measurements[activeRow][horizontal ? 'width' : 'height'] : 0
  310.     const endPadding = (isLastItem && spacerAfterLastItem)
  311.     const spacerStyle = { [horizontal ? 'width' : 'height']: activeRowSize }
  312.  
  313.     return (
  314.       <View style={[styles.fullOpacity, { flexDirection: horizontal ? 'row' : 'column' }]} >
  315.         {isSpacerRow && <View style={spacerStyle} />}
  316.         <RowItem
  317.           horizontal={horizontal}
  318.           index={index}
  319.           isActiveRow={isActiveRow}
  320.           renderItem={renderItem}
  321.           item={item}
  322.           setRef={this.setRef}
  323.           move={this.move}
  324.           moveEnd={this.moveEnd}
  325.           extraData={this.state.extraData}
  326.         />
  327.         {endPadding && <View style={spacerStyle} />}
  328.       </View>
  329.     )
  330.   }
  331.  
  332.   renderHoverComponent = () => {
  333.     const { hoverComponent } = this.state
  334.     const { horizontal } = this.props
  335.     return !!hoverComponent && (
  336.       <Animated.View style={[
  337.         horizontal ? styles.hoverComponentHorizontal : styles.hoverComponentVertical,
  338.         { transform: [horizontal ? { translateX: this._hoverAnim } : { translateY: this._hoverAnim }] }]} >
  339.         {hoverComponent}
  340.       </Animated.View>
  341.     )
  342.   }
  343.  
  344.   measureContainer = event => {
  345.     if (this.containerView) {
  346.       const { horizontal } = this.props
  347.       this.containerView.measure((x, y, width, height, pageX, pageY) => {
  348.         this._containerOffset = horizontal ? pageX : pageY
  349.         this._containerSize = horizontal ? width : height
  350.       })
  351.     }
  352.   }
  353.  
  354.   keyExtractor = (item, index) => `sortable-flatlist-item-${index}`
  355.  
  356.   componentDidUpdate = (prevProps, prevState) => {
  357.     if (prevProps.extraData !== this.props.extraData) {
  358.       this.setState({ extraData: this.props.extraData })
  359.     }
  360.   }
  361.  
  362.   onScroll = (props) => {
  363.     const { onScroll, horizontal } = this.props
  364.     this._scrollOffset = props.nativeEvent.contentOffset[horizontal ? 'x' : 'y']
  365.     onScroll && onScroll(props)
  366.   }
  367.  
  368.   render() {
  369.     const { keyExtractor } = this.props
  370.  
  371.     return (
  372.       <View
  373.         ref={ref => {this.containerView = ref}}
  374.         onLayout={this.measureContainer}
  375.         {...this._panResponder.panHandlers}
  376.         style={styles.wrapper} // Setting { opacity: 1 } fixes Android measurement bug: https://github.com/facebook/react-native/issues/18034#issuecomment-368417691
  377.       >
  378.         <FlatList
  379.           {...this.props}
  380.           scrollEnabled={this.state.activeRow === -1}
  381.           ref={ref => this._flatList = ref}
  382.           renderItem={this.renderItem}
  383.           extraData={this.state}
  384.           keyExtractor={keyExtractor || this.keyExtractor}
  385.           onScroll={this.onScroll}
  386.           scrollEventThrottle={16}
  387.         />
  388.         {this.renderHoverComponent()}
  389.       </View>
  390.     )
  391.   }
  392. }
  393.  
  394. export default SortableFlatList
  395.  
  396. SortableFlatList.defaultProps = {
  397.   scrollPercent: 5,
  398.   scrollSpeed: 5,
  399.   contentContainerStyle: {},
  400.   undroppableLines: []
  401. }
  402.  
  403. class RowItem extends React.PureComponent {
  404.  
  405.   move = () => {
  406.     const { move, moveEnd, renderItem, item, index } = this.props
  407.     const hoverComponent = renderItem({ isActive: true, item, index, move: () => null, moveEnd })
  408.     move(hoverComponent, index)
  409.   }
  410.  
  411.   render() {
  412.     const { moveEnd, isActiveRow, horizontal, renderItem, item, index, setRef } = this.props
  413.     const component = renderItem({
  414.       isActive: false,
  415.       item,
  416.       index,
  417.       move: this.move,
  418.       moveEnd,
  419.     })
  420.     let wrapperStyle = { opacity: 1 }
  421.     if (horizontal && isActiveRow) wrapperStyle = { width: 0, opacity: 0 }
  422.     else if (!horizontal && isActiveRow) wrapperStyle = { height: 0, opacity: 0 }
  423.  
  424.     // Rendering the final row requires padding to be applied at the bottom
  425.     return (
  426.       <View ref={setRef(index)} collapsable={false} style={{ opacity: 1, flexDirection: horizontal ? 'row' : 'column' }}>
  427.         <View style={wrapperStyle}>
  428.           {component}
  429.         </View>
  430.       </View>
  431.     )
  432.   }
  433. }
  434.  
  435. const styles = StyleSheet.create({
  436.   hoverComponentVertical: {
  437.     position: 'absolute',
  438.     left: 0,
  439.     right: 0,
  440.   },
  441.   hoverComponentHorizontal: {
  442.     position: 'absolute',
  443.     bottom: 0,
  444.     top: 0,
  445.   },
  446.   wrapper: { flex: 1, opacity: 1 },
  447.   fullOpacity: { opacity: 1 }
  448. })
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement