RN开发图标拖动效果实现

项目介绍

目前,很多的软件在功能模块都会展示很多的功能按钮,并且支持根据自己的使用习惯进行排列。

在Git hub上找到一个可以实现功能图标自由拖动的开源代码,主要的结构为container为各种模式的图标拖动界面,data为测试数据,widget为主要的依赖组件源代码。

先放上源码链接:GitHub源码
image-20210406143636497

主要功能代码:

图标拖动主要依赖以下两个组件,

AutoDragSortableView. js
import React, {Component} from 'react'
import {Animated, Dimensions, Easing, PanResponder, StyleSheet, TouchableOpacity, View, ScrollView,Platform} from 'react-native'
const PropTypes = require('prop-types')
const {width,height} = Dimensions.get('window')
const defaultZIndex = 8
const touchZIndex = 99
export default class AutoDragSortableView extends Component{
    constructor(props) {
        super(props)
        this.sortRefs = new Map()
        const itemWidth = props.childrenWidth+props.marginChildrenLeft+props.marginChildrenRight
        const itemHeight = props.childrenHeight + props.marginChildrenTop + props.marginChildrenBottom

        // this.reComplexDataSource(true,props) // react < 16.3
        // react > 16.3 Fiber
        const rowNum = parseInt(props.parentWidth / itemWidth);
        const dataSource = props.dataSource.map((item, index) => {
            const newData = {}
            const left = (index % rowNum) * itemWidth
            const top = parseInt((index / rowNum)) * itemHeight
            newData.data = item
            newData.originIndex = index
            newData.originLeft = left
            newData.originTop = top
            newData.position = new Animated.ValueXY({
                x: parseInt(left + 0.5),
                y: parseInt(top + 0.5),
            })
            newData.scaleValue = new Animated.Value(1)
            return newData
        });
        this.state = {
            dataSource: dataSource,
            curPropsDataSource: props.dataSource,
            height: Math.ceil(dataSource.length / rowNum) * itemHeight,
            itemWidth,
            itemHeight,
        };
        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => {
                this.isMovePanResponder = false
                return false
            },
            onMoveShouldSetPanResponder: (evt, gestureState) => this.isMovePanResponder,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.isMovePanResponder,
            onPanResponderGrant: (evt, gestureState) => {},
            onPanResponderMove: (evt, gestureState) => this.moveTouch(evt, gestureState),
            onPanResponderRelease: (evt, gestureState) => this.endTouch(evt),
            onPanResponderTerminationRequest: (evt, gestureState) => false,
            onShouldBlockNativeResponder: (evt, gestureState) => false,
        })
    }
    // react > 16.3 Fiber
    static getDerivedStateFromProps(nextprops, prevState) {
        const itemWidth = nextprops.childrenWidth + nextprops.marginChildrenLeft + nextprops.marginChildrenRight
        const itemHeight = nextprops.childrenHeight + nextprops.marginChildrenTop + nextprops.marginChildrenBottom
        if (nextprops.dataSource != prevState.curPropsDataSource || itemWidth !== prevState.itemWidth || itemHeight !== prevState.itemHeight) {
            
            const rowNum = parseInt(nextprops.parentWidth / itemWidth);
            const dataSource = nextprops.dataSource.map((item, index) => {
                const newData = {};
                const left = index % rowNum * itemWidth;
                const top = parseInt(index / rowNum) * itemHeight;
                newData.data = item;
                newData.originIndex = index;
                newData.originLeft = left;
                newData.originTop = top;
                newData.position = new Animated.ValueXY({
                    x: parseInt(left + 0.5),
                    y: parseInt(top + 0.5),
                });
                newData.scaleValue = new Animated.Value(1);
                return newData;
            });
            return {
                dataSource: dataSource,
                curPropsDataSource: nextprops.dataSource,
                height: Math.ceil(dataSource.length / rowNum) * itemHeight,
                itemWidth,
                itemHeight,
            }
        }
        return null;
    }
    componentDidMount() {
        this.initTag()
        this.autoMeasureHeight()
    }
    componentDidUpdate() {
        this.autoMeasureHeight()
    }
    // Compatible with different systems and paging loading
    autoMeasureHeight = () => {
        if (!this.isHasMeasure) {
            setTimeout(()=>{
                this.scrollTo(1, false)
                this.scrollTo(0, false)
            }, 30)
        }
    }
    // Initialization tag
    initTag = () => {
        this.clearAutoInterval();
        this.autoObj = {
            curDy: 0,
            scrollDx: 0,
            scrollDy: 0,
            hasScrollDy: null,
            forceScrollStatus: 0, // 0: NONE 1: DOWN 2: ONLY_DOWN -1: UP -2: ONLY_UP
        }
    }
    // Unified processing
    dealtScrollStatus = () => {
        const scrollData = this.curScrollData;
        if (scrollData == null || scrollData.offsetY == null) return;
        const { totalHeight, windowHeight, offsetY } = scrollData;
        if (totalHeight <= windowHeight + offsetY) {
            this.autoObj.forceScrollStatus = -2;
        } else if (offsetY <= 0) {
            this.autoObj.forceScrollStatus = 2;
        }
    }
    // Handle automatic slide timer
    clearAutoInterval = () => {
        if (this.autoInterval) {
            clearInterval(this.autoInterval);
            this.autoInterval = null;
        }
    }
    startAutoScroll = () => {
        if (this.autoInterval != null) {
            return;
        }
        // Start automatic swipe
        this.autoInterval = setInterval(() => {
            if (this.autoObj.forceScrollStatus === 0 ||
                this.autoObj.forceScrollStatus === 2 ||
                this.autoObj.forceScrollStatus === -2) {
                this.clearAutoInterval();
                return;
            }
            // Anti-shake 1.x1
            if (!this.curScrollData.hasScroll) {
                return;
            }
            if (this.autoObj.forceScrollStatus === 1) {
                this.autoObj.scrollDy = this.autoObj.scrollDy + this.props.autoThrottle;
            } else if (this.autoObj.forceScrollStatus === -1){
                this.autoObj.scrollDy = this.autoObj.scrollDy - this.props.autoThrottle;
            }
            this.scrollTo(this.autoObj.scrollDy, false);
            this.dealtScrollStatus();
            // Android slide time 30ms-50ms, iOS close to 0ms, optimize Android jitter
            if (Platform.OS === 'android') {
                setTimeout(()=>{ 
                    if (this.isHasMove) this.moveTouch(null,{dx: this.autoObj.scrollDx, dy: this.autoObj.curDy + this.autoObj.scrollDy})
                },1)
            } else {
                this.moveTouch(null,{dx: this.autoObj.scrollDx, dy: this.autoObj.curDy + this.autoObj.scrollDy})
            }
            
        }, this.props.autoThrottleDuration)
    }
    startTouch(touchIndex) {
        //Prevent drag
        const fixedItems = this.props.fixedItems;
        if (fixedItems.length > 0 && fixedItems.includes(touchIndex)){
            return;
        }
        this.isHasMove = false
        this.isHasMeasure = true

        if (!this.props.sortable) return

        const key = this._getKey(touchIndex);
        if (this.sortRefs.has(key)) {
            // Initialization data
            if (this.isStartupAuto()) {
                this.autoObj.scrollDy = this.autoObj.hasScrollDy = this.curScrollData.offsetY;
            }

            this.setState({
                scrollEnabled: false
            })
            if (this.props.onDragStart) {
                this.props.onDragStart(touchIndex) 
            }
            Animated.timing(
                this.state.dataSource[touchIndex].scaleValue,
                {
                    toValue: this.props.maxScale,
                    duration: this.props.scaleDuration,
                    useNativeDriver: false,
                }
            ).start(()=>{
                this.touchCurItem = {
                    ref: this.sortRefs.get(key),
                    index: touchIndex,
                    originLeft: this.state.dataSource[touchIndex].originLeft,
                    originTop: this.state.dataSource[touchIndex].originTop,
                    moveToIndex: touchIndex,
                }
                this.isMovePanResponder = true
            })
        }
    }

    moveTouch (nativeEvent,gestureState) {
        this.isHasMove = true;
        if (this.touchCurItem) {
            let {dx, dy, vy} = gestureState;
            const itemWidth = this.state.itemWidth;
            const itemHeight = this.state.itemHeight;
            const rowNum = parseInt(this.props.parentWidth/itemWidth);
            const maxWidth = this.props.parentWidth-itemWidth;
            const maxHeight = itemHeight*Math.ceil(this.state.dataSource.length/rowNum) - itemHeight;
            // Is it free to drag
            if (!this.props.isDragFreely) {
                // Maximum or minimum after out of bounds
                if (this.touchCurItem.originLeft + dx < 0) {
                    dx = -this.touchCurItem.originLeft
                } else if (this.touchCurItem.originLeft + dx > maxWidth) {
                    dx = maxWidth - this.touchCurItem.originLeft
                }
                if (!this.isStartupAuto()) {
                    if (this.touchCurItem.originTop + dy < 0) {
                        dy = -this.touchCurItem.originTop
                    } else if (this.touchCurItem.originTop + dy > maxHeight) {
                        dy = maxHeight - this.touchCurItem.originTop
                    }
                }
            }
            if (this.isStartupAuto()) {
                const curDis = this.touchCurItem.originTop + dy - this.autoObj.hasScrollDy;
                if (nativeEvent != null) {
                    const tempStatus = this.autoObj.forceScrollStatus;
                    // Automatic sliding
                    const minDownDiss = curDis + this.props.childrenHeight * (1 + (this.props.maxScale - 1) / 2) + this.props.marginChildrenTop + this.props.headerViewHeight;
                    const maxUpDiss = curDis + this.props.marginChildrenTop + this.props.headerViewHeight;
                    if ((tempStatus === 0 || tempStatus === 2) && vy > 0.01 && minDownDiss > this.curScrollData.windowHeight) {
                        this.autoObj.curDy = dy;
                        this.autoObj.forceScrollStatus = 1;
                        this.startAutoScroll();
                    } else if ((tempStatus === 0 || tempStatus === -2) && -vy > 0.01 && maxUpDiss < 0) {
                        this.autoObj.curDy = dy;
                        this.autoObj.forceScrollStatus = -1;
                        this.startAutoScroll();
                    }
                }
                // Determine whether to change steering
                if (vy != null) {
                    // Slide down 1、2
                    if (this.autoObj.forceScrollStatus >= 1 && -vy > 0.01) {
                        this.autoObj.forceScrollStatus = 0;
                        // Slide up -1、-2
                    } else if (this.autoObj.forceScrollStatus <= -1 && vy > 0.01) {
                        this.autoObj.forceScrollStatus = 0;
                    }
                }
                // Remember the X axis
                this.autoObj.scrollDx = dx;
                // Correction data 1
                dy = dy - this.autoObj.hasScrollDy;
                if (nativeEvent != null) {
                    // Correction data 2
                    dy = dy + this.autoObj.scrollDy; 
                    // Prevent fingers from sliding when sliding automatically
                    if (this.autoObj.forceScrollStatus === 1 || this.autoObj.forceScrollStatus === -1) {
                        return;
                    }                    
                }
            }
            const left = this.touchCurItem.originLeft + dx;
            const top = this.touchCurItem.originTop + dy;
            this.touchCurItem.ref.setNativeProps({
                style: {
                    zIndex: touchZIndex,
                }
            })
            this.state.dataSource[this.touchCurItem.index].position.setValue({
                x: left,
                y: top,
            })
            let moveToIndex = 0
            let moveXNum = dx/itemWidth
            let moveYNum = dy/itemHeight
            if (moveXNum > 0) {
                moveXNum = parseInt(moveXNum+0.5)
            } else if (moveXNum < 0) {
                moveXNum = parseInt(moveXNum-0.5)
            }
            if (moveYNum > 0) {
                moveYNum = parseInt(moveYNum+0.5)
            } else if (moveYNum < 0) {
                moveYNum = parseInt(moveYNum-0.5)
            }
            moveToIndex = this.touchCurItem.index+moveXNum+moveYNum*rowNum
            if (moveToIndex > this.state.dataSource.length-1) {
                moveToIndex = this.state.dataSource.length-1
            } else if (moveToIndex < 0) {
                moveToIndex = 0;
            }
            if (this.props.onDragging) {
                this.props.onDragging(gestureState, left, top, moveToIndex)
            }
            if (this.touchCurItem.moveToIndex != moveToIndex ) {
                const fixedItems = this.props.fixedItems;
                if (fixedItems.length > 0 && fixedItems.includes(moveToIndex)) return;
                this.touchCurItem.moveToIndex = moveToIndex
                this.state.dataSource.forEach((item,index)=>{

                    let nextItem = null
                    if (index > this.touchCurItem.index && index <= moveToIndex) {
                        nextItem = this.state.dataSource[index-1]

                    } else if (index >= moveToIndex && index < this.touchCurItem.index) {
                        nextItem = this.state.dataSource[index+1]

                    } else if (index != this.touchCurItem.index &&
                        (item.position.x._value != item.originLeft ||
                            item.position.y._value != item.originTop)) {
                        nextItem = this.state.dataSource[index]

                    } else if ((this.touchCurItem.index-moveToIndex > 0 && moveToIndex == index+1) ||
                        (this.touchCurItem.index-moveToIndex < 0 && moveToIndex == index-1)) {
                        nextItem = this.state.dataSource[index]
                    }
                    if (nextItem != null) {
                        Animated.timing(
                            item.position,
                            {
                                toValue: {x: parseInt(nextItem.originLeft+0.5),y: parseInt(nextItem.originTop+0.5)},
                                duration: this.props.slideDuration,
                                easing: Easing.out(Easing.quad),
                                useNativeDriver: false,
                            }
                        ).start()
                    }
                })
            }
        }
    }
    endTouch (nativeEvent) {
        this.isHasMove = false;
        this.initTag()
        //clear
        if (this.touchCurItem) {
            this.setState({
                scrollEnabled: true
            })
            if (this.props.onDragEnd) {
                this.props.onDragEnd(this.touchCurItem.index,this.touchCurItem.moveToIndex)
            }
            //this.state.dataSource[this.touchCurItem.index].scaleValue.setValue(1)
            Animated.timing(
                this.state.dataSource[this.touchCurItem.index].scaleValue,
                {
                    toValue: 1,
                    duration: this.props.scaleDuration,
                    useNativeDriver: false,
                }
            ).start(()=>{
                if (this.touchCurItem) {
                    this.touchCurItem.ref.setNativeProps({
                        style: {
                            zIndex: defaultZIndex,
                        }
                    })
                    this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex)
                    this.touchCurItem = null
                }
            })
        }
    }
    onPressOut () {
        this.isScaleRecovery = setTimeout(()=> {
            if (this.isMovePanResponder && !this.isHasMove) {
                this.endTouch()
            }
        },220)
    }
    changePosition(startIndex,endIndex) {

        if (startIndex == endIndex) {
            const curItem = this.state.dataSource[startIndex]
            if (curItem != null) {
                curItem.position.setValue({
                    x: parseInt(curItem.originLeft + 0.5),
                    y: parseInt(curItem.originTop + 0.5),
                })
            }
            return;
        }
        let isCommon = true
        if (startIndex > endIndex) {
            isCommon = false
            let tempIndex = startIndex
            startIndex = endIndex
            endIndex = tempIndex
        }
        const newDataSource = [...this.state.dataSource].map((item,index)=>{
            let newIndex = null
            if (isCommon) {
                if (endIndex > index && index >= startIndex) {
                    newIndex = index+1
                } else if (endIndex == index) {
                    newIndex = startIndex
                }
            } else {
                if (endIndex >= index && index > startIndex) {
                    newIndex = index-1
                } else if (startIndex == index) {
                    newIndex = endIndex
                }
            }
            if (newIndex != null) {
                const newItem = {...this.state.dataSource[newIndex]}
                newItem.originLeft = item.originLeft
                newItem.originTop = item.originTop
                newItem.position = new Animated.ValueXY({
                    x: parseInt(item.originLeft+0.5),
                    y: parseInt(item.originTop+0.5),
                })
                item = newItem
            }
            return item
        })
        this.setState({
            dataSource: newDataSource
        },()=>{
            if (this.props.onDataChange) {
                this.props.onDataChange(this.getOriginalData())
            }
            // Prevent RN from drawing the beginning and end
            const startItem = this.state.dataSource[startIndex]
            this.state.dataSource[startIndex].position.setValue({
                x: parseInt(startItem.originLeft+0.5),
                y: parseInt(startItem.originTop+0.5),
            })
            const endItem = this.state.dataSource[endIndex]
            this.state.dataSource[endIndex].position.setValue({
                x: parseInt(endItem.originLeft+0.5),
                y: parseInt(endItem.originTop+0.5),
            })
        })
    }
    reComplexDataSource(isInit,props) {
        const itemWidth = this.state.itemWidth;
        const itemHeight = this.state.itemHeight;
        const rowNum = parseInt(props.parentWidth/itemWidth);
        const dataSource = props.dataSource.map((item,index)=>{
            const newData = {}
            const left = (index%rowNum)*itemWidth
            const top = parseInt((index/rowNum))*itemHeight
            newData.data = item
            newData.originIndex = index
            newData.originLeft = left
            newData.originTop = top
            newData.position = new Animated.ValueXY({
                x: parseInt(left+0.5),
                y: parseInt(top+0.5),
            })
            newData.scaleValue = new Animated.Value(1)
            return newData
        })
        if (isInit) {
            this.state = {
                scrollEnabled: true,
                dataSource: dataSource,
                height: Math.ceil(dataSource.length/rowNum)*itemHeight
            }
        } else {
            this.setState({
                dataSource: dataSource,
                height: Math.ceil(dataSource.length/rowNum)*itemHeight
            })
        }
    }
    getOriginalData () {
        return this.state.dataSource.map((item,index)=> item.data)
    }

    isStartupAuto = () => {
        if (this.curScrollData == null) {
            return false;
        }
        return true;
    }

    scrollTo = (height, animated = true) => {
        // Prevent iOS from sliding when elastically sliding negative numbers
        if (this.curScrollData) {
            if (this.autoObj.forceScrollStatus < 0 && this.curScrollData.offsetY <= 0) {
                this.autoObj.scrollDy = 0; // Correcting data system deviations
                return;
            } else if (this.autoObj.forceScrollStatus > 0 && this.curScrollData.windowHeight + this.curScrollData.offsetY >= this.curScrollData.totalHeight) {
                this.autoObj.scrollDy = this.curScrollData.offsetY; //Correcting data system deviations
                return;
            }
            //Barrel effect, the slowest is 1.x1
            this.curScrollData.hasScroll = false;
        }
        this.scrollRef && this.scrollRef.scrollTo({x: 0, y: height, animated});
    }

    onScrollListener = (event) => {
        const nativeEvent = event.nativeEvent
        this.curScrollData = {
            totalHeight: nativeEvent.contentSize.height,
            windowHeight: nativeEvent.layoutMeasurement.height,
            offsetY: nativeEvent.contentOffset.y,
            hasScroll: true,
        }
        if (nativeEvent.contentOffset.y !== 0) this.isHasMeasure = true
        if (this.props.onScrollListener) this.props.onScrollListener(event);
    }

    render() {
        return (
            <ScrollView
                bounces={false}
                scrollEventThrottle={1}
                scrollIndicatorInsets={this.props.scrollIndicatorInsets}
                ref={(scrollRef)=> {
                    if (this.props.onScrollRef) this.props.onScrollRef(scrollRef)
                    this.scrollRef = scrollRef
                    return this.scrollRef
                }}
                scrollEnabled = {this.state.scrollEnabled}
                onScroll={this.onScrollListener}
                style={styles.container}>  
                {this.props.renderHeaderView ? this.props.renderHeaderView : null} 
                <View
                    //ref={(ref)=>this.sortParentRef=ref}
                    style={[styles.swipe,{
                        width: this.props.parentWidth,
                        height: this.state.height,
                    }]}
                    //onLayout={()=> {}}
                    >
                    {this._renderItemView()}
                </View>
                {this.props.renderBottomView ? this.props.renderBottomView : null}
            </ScrollView>
        )
    }

    _getKey = (index) => {
        const item = this.state.dataSource[index];
        return this.props.keyExtractor ? this.props.keyExtractor(item.data, index) : item.originIndex;
    }

    _renderItemView = () => {
        const {maxScale, minOpacity} = this.props
        const inputRange = maxScale >= 1 ? [1, maxScale] : [maxScale, 1]
        const outputRange = maxScale >= 1 ? [1, minOpacity] : [minOpacity, 1]
        return this.state.dataSource.map((item,index)=>{
            const transformObj = {}
            transformObj[this.props.scaleStatus] = item.scaleValue
            const key = this.props.keyExtractor ? this.props.keyExtractor(item.data,index) : item.originIndex
            return (
                <Animated.View
                    key={key}
                    ref={(ref) => this.sortRefs.set(key,ref)}
                    {...this._panResponder.panHandlers}
                    style={[styles.item,{
                        marginTop: this.props.marginChildrenTop,
                        marginBottom: this.props.marginChildrenBottom,
                        marginLeft: this.props.marginChildrenLeft,
                        marginRight: this.props.marginChildrenRight,
                        left: item.position.x,
                        top: item.position.y,
                        opacity: item.scaleValue.interpolate({inputRange,outputRange}),
                        transform: [transformObj]
                    }]}>
                    <TouchableOpacity
                        activeOpacity = {1}
                        delayLongPress={this.props.delayLongPress}
                        onPressOut={()=> this.onPressOut()}
                        onLongPress={()=>this.startTouch(index)}
                        onPress={()=>{
                            if (this.props.onClickItem) {
                                this.isHasMeasure = true
                                this.props.onClickItem(this.getOriginalData(),item.data,index)
                            }
                        }}>
                        {this.props.renderItem(item.data,index)}
                    </TouchableOpacity>
                </Animated.View>
            )
        })
    }

    componentWillUnmount() {
        if (this.isScaleRecovery) clearTimeout(this.isScaleRecovery)
        this.clearAutoInterval()
    }

}

AutoDragSortableView.propTypes = {
    dataSource: PropTypes.array.isRequired,
    parentWidth: PropTypes.number,
    childrenHeight: PropTypes.number.isRequired,
    childrenWidth: PropTypes.number.isRequired,

    marginChildrenTop: PropTypes.number,
    marginChildrenBottom: PropTypes.number,
    marginChildrenLeft: PropTypes.number,
    marginChildrenRight: PropTypes.number,

    sortable: PropTypes.bool,

    onClickItem: PropTypes.func,
    onDragStart: PropTypes.func,
    onDragEnd: PropTypes.func,
    onDataChange: PropTypes.func,
    renderItem: PropTypes.func.isRequired,
    scaleStatus: PropTypes.oneOf(['scale','scaleX','scaleY']),
    fixedItems: PropTypes.array,
    keyExtractor: PropTypes.func,
    delayLongPress: PropTypes.number,
    isDragFreely: PropTypes.bool,
    onDragging: PropTypes.func,
    maxScale: PropTypes.number,
    minOpacity: PropTypes.number,
    scaleDuration: PropTypes.number,
    slideDuration: PropTypes.number,
    autoThrottle: PropTypes.number,
    autoThrottleDuration: PropTypes.number,
    renderHeaderView: PropTypes.element,
    scrollIndicatorInsets: PropTypes.shape({
      top: PropTypes.number,
      left: PropTypes.number,
      bottom: PropTypes.number,
      right: PropTypes.number,
    }),
    headerViewHeight: PropTypes.number,
    renderBottomView: PropTypes.element,
    bottomViewHeight: PropTypes.number,
    onScrollListener: PropTypes.func,
    onScrollRef: PropTypes.func
}

AutoDragSortableView.defaultProps = {
    marginChildrenTop: 0,
    marginChildrenBottom: 0,
    marginChildrenLeft: 0,
    marginChildrenRight: 0,
    parentWidth: width,
    sortable: true,
    scaleStatus: 'scale',
    fixedItems: [],
    isDragFreely: false,
    maxScale: 1.1,
    minOpacity: 0.8,
    scaleDuration: 100,
    slideDuration: 300,
    autoThrottle: 2,
    autoThrottleDuration: 10,
    scrollIndicatorInsets: {
      top: 0,
      left: 0,
      bottom: 0,
      right: 1,
    },
    headerViewHeight: 0,
    bottomViewHeight: 0,
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
    },
    swipe: {
        flexWrap: 'wrap',
        flexDirection: 'row',
    },
    item: {
        position: 'absolute',
        zIndex: defaultZIndex,
    },
})

DragSortableView.js
import React, {Component} from 'react'
import {Animated, Dimensions, Easing, PanResponder, StyleSheet, TouchableOpacity, View} from 'react-native'

const PropTypes = require('prop-types')
const {width} = Dimensions.get('window')

const defaultZIndex = 8
const touchZIndex = 99

export default class DragSortableView extends Component{

    constructor(props) {
        super(props)

        this.sortRefs = new Map()

        const itemWidth = props.childrenWidth+props.marginChildrenLeft+props.marginChildrenRight
        const itemHeight = props.childrenHeight+props.marginChildrenTop+props.marginChildrenBottom

        // this.reComplexDataSource(true,props) // react < 16.3
        // react > 16.3 Fiber
        const rowNum = parseInt(props.parentWidth/itemWidth);
        const dataSource = props.dataSource.map((item,index)=>{
            const newData = {}
            const left = (index%rowNum)*itemWidth
            const top = parseInt((index/rowNum))*itemHeight

            newData.data = item
            newData.originIndex = index
            newData.originLeft = left
            newData.originTop = top
            newData.position = new Animated.ValueXY({
                x: parseInt(left+0.5),
                y: parseInt(top+0.5),
            })
            newData.scaleValue = new Animated.Value(1)
            return newData
        });
        this.state = {
            dataSource: dataSource,
            curPropsDataSource: props.dataSource,
            height: Math.ceil(dataSource.length / rowNum) * itemHeight,
            itemWidth,
            itemHeight,
        };

        this._panResponder = PanResponder.create({
            onStartShouldSetPanResponder: (evt, gestureState) => true,
            onStartShouldSetPanResponderCapture: (evt, gestureState) => {
                this.isMovePanResponder = false
                return false
            },
            onMoveShouldSetPanResponder: (evt, gestureState) => this.isMovePanResponder,
            onMoveShouldSetPanResponderCapture: (evt, gestureState) => this.isMovePanResponder,

            onPanResponderGrant: (evt, gestureState) => {},
            onPanResponderMove: (evt, gestureState) => this.moveTouch(evt,gestureState),
            onPanResponderRelease: (evt, gestureState) => this.endTouch(evt),

            onPanResponderTerminationRequest: (evt, gestureState) => false,
            onShouldBlockNativeResponder: (evt, gestureState) => false,
        })
    }

    // react < 16.3
    // componentWillReceiveProps(nextProps) {
    //     if (this.props.dataSource != nextProps.dataSource) {
    //         this.reComplexDataSource(false,nextProps)
    //     }
    // }

    // react > 16.3 Fiber
    static getDerivedStateFromProps(nextprops, prevState) {
        const itemWidth = nextprops.childrenWidth + nextprops.marginChildrenLeft + nextprops.marginChildrenRight
        const itemHeight = nextprops.childrenHeight + nextprops.marginChildrenTop + nextprops.marginChildrenBottom
        if (nextprops.dataSource != prevState.curPropsDataSource || itemWidth !== prevState.itemWidth || itemHeight !== prevState.itemHeight) {
            
            const rowNum = parseInt(nextprops.parentWidth / itemWidth);
            const dataSource = nextprops.dataSource.map((item, index) => {
                const newData = {};
                const left = index % rowNum * itemWidth;
                const top = parseInt(index / rowNum) * itemHeight;

                newData.data = item;
                newData.originIndex = index;
                newData.originLeft = left;
                newData.originTop = top;
                newData.position = new Animated.ValueXY({
                    x: parseInt(left + 0.5),
                    y: parseInt(top + 0.5),
                });
                newData.scaleValue = new Animated.Value(1);
                return newData;
            });
            return {
                dataSource: dataSource,
                curPropsDataSource: nextprops.dataSource,
                height: Math.ceil(dataSource.length / rowNum) * itemHeight,
                itemWidth,
                itemHeight,
            }
        }
        return null;
    }

    startTouch(touchIndex) {

        //防止拖动
        const fixedItems = this.props.fixedItems;
        if (fixedItems.length > 0 && fixedItems.includes(touchIndex)){
            return;
        }

        this.isHasMove = false

        if (!this.props.sortable) return

        const key = this._getKey(touchIndex);
        if (this.sortRefs.has(key)) {
            if (this.props.onDragStart) {
                this.props.onDragStart(touchIndex)
            }
            Animated.timing(
                this.state.dataSource[touchIndex].scaleValue,
                {
                    toValue: this.props.maxScale,
                    duration: this.props.scaleDuration,
                    useNativeDriver: false,
                }
            ).start(()=>{
                this.touchCurItem = {
                    ref: this.sortRefs.get(key),
                    index: touchIndex,
                    originLeft: this.state.dataSource[touchIndex].originLeft,
                    originTop: this.state.dataSource[touchIndex].originTop,
                    moveToIndex: touchIndex,
                }
                this.isMovePanResponder = true
            })
        }
    }



    moveTouch (nativeEvent,gestureState) {

        this.isHasMove = true

        //if (this.isScaleRecovery) clearTimeout(this.isScaleRecovery)

        if (this.touchCurItem) {

            let dx = gestureState.dx
            let dy = gestureState.dy
            const itemWidth = this.state.itemWidth;
            const itemHeight = this.state.itemHeight;

            const rowNum = parseInt(this.props.parentWidth/itemWidth);
            const maxWidth = this.props.parentWidth-itemWidth
            const maxHeight = itemHeight*Math.ceil(this.state.dataSource.length/rowNum) - itemHeight

            // Is it free to drag
            if (!this.props.isDragFreely) {
                // Maximum or minimum after out of bounds
                if (this.touchCurItem.originLeft + dx < 0) {
                    dx = -this.touchCurItem.originLeft
                } else if (this.touchCurItem.originLeft + dx > maxWidth) {
                    dx = maxWidth - this.touchCurItem.originLeft
                }
                if (this.touchCurItem.originTop + dy < 0) {
                    dy = -this.touchCurItem.originTop
                } else if (this.touchCurItem.originTop + dy > maxHeight) {
                    dy = maxHeight - this.touchCurItem.originTop
                }
            }

            let left = this.touchCurItem.originLeft + dx
            let top = this.touchCurItem.originTop + dy

            this.touchCurItem.ref.setNativeProps({
                style: {
                    zIndex: touchZIndex,
                }
            })

            this.state.dataSource[this.touchCurItem.index].position.setValue({
                x: left,
                y: top,
            })

            let moveToIndex = 0
            let moveXNum = dx/itemWidth
            let moveYNum = dy/itemHeight
            if (moveXNum > 0) {
                moveXNum = parseInt(moveXNum+0.5)
            } else if (moveXNum < 0) {
                moveXNum = parseInt(moveXNum-0.5)
            }
            if (moveYNum > 0) {
                moveYNum = parseInt(moveYNum+0.5)
            } else if (moveYNum < 0) {
                moveYNum = parseInt(moveYNum-0.5)
            }

            moveToIndex = this.touchCurItem.index+moveXNum+moveYNum*rowNum

            if (moveToIndex > this.state.dataSource.length-1) {
                moveToIndex = this.state.dataSource.length-1
            } else if (moveToIndex < 0) {
                moveToIndex = 0;
            }

            if (this.props.onDragging) {
                this.props.onDragging(gestureState, left, top, moveToIndex)
            }

            if (this.touchCurItem.moveToIndex != moveToIndex ) {
                const fixedItems = this.props.fixedItems;
                if (fixedItems.length > 0 && fixedItems.includes(moveToIndex)) return;
                this.touchCurItem.moveToIndex = moveToIndex
                this.state.dataSource.forEach((item,index)=>{

                    let nextItem = null
                    if (index > this.touchCurItem.index && index <= moveToIndex) {
                        nextItem = this.state.dataSource[index-1]

                    } else if (index >= moveToIndex && index < this.touchCurItem.index) {
                        nextItem = this.state.dataSource[index+1]

                    } else if (index != this.touchCurItem.index &&
                        (item.position.x._value != item.originLeft ||
                            item.position.y._value != item.originTop)) {
                        nextItem = this.state.dataSource[index]

                    } else if ((this.touchCurItem.index-moveToIndex > 0 && moveToIndex == index+1) ||
                        (this.touchCurItem.index-moveToIndex < 0 && moveToIndex == index-1)) {
                        nextItem = this.state.dataSource[index]
                    }

                    if (nextItem != null) {
                        Animated.timing(
                            item.position,
                            {
                                toValue: {x: parseInt(nextItem.originLeft+0.5),y: parseInt(nextItem.originTop+0.5)},
                                duration: this.props.slideDuration,
                                easing: Easing.out(Easing.quad),
                                useNativeDriver: false,
                            }
                        ).start()
                    }

                })
            }

        }
    }

    endTouch (nativeEvent) {

        //clear
        if (this.touchCurItem) {
            if (this.props.onDragEnd) {
                this.props.onDragEnd(this.touchCurItem.index,this.touchCurItem.moveToIndex)
            }
            //this.state.dataSource[this.touchCurItem.index].scaleValue.setValue(1)
            Animated.timing(
                this.state.dataSource[this.touchCurItem.index].scaleValue,
                {
                    toValue: 1,
                    duration: this.props.scaleDuration,
                    useNativeDriver: false,
                }
            ).start(()=>{
                this.touchCurItem.ref.setNativeProps({
                    style: {
                        zIndex: defaultZIndex,
                    }
                })
                this.changePosition(this.touchCurItem.index,this.touchCurItem.moveToIndex)
                this.touchCurItem = null
            })
            
        }
    }

    onPressOut () {
        this.isScaleRecovery = setTimeout(()=> {
            if (this.isMovePanResponder && !this.isHasMove) {
                this.endTouch()
            }
        },220)
    }

    changePosition(startIndex,endIndex) {

        if (startIndex == endIndex) {
            const curItem = this.state.dataSource[startIndex]
            if (curItem != null) {
                curItem.position.setValue({
                    x: parseInt(curItem.originLeft + 0.5),
                    y: parseInt(curItem.originTop + 0.5),
                })
            }
            return;
        }

        let isCommon = true
        if (startIndex > endIndex) {
            isCommon = false
            let tempIndex = startIndex
            startIndex = endIndex
            endIndex = tempIndex
        }

        const newDataSource = [...this.state.dataSource].map((item,index)=>{
            let newIndex = null
            if (isCommon) {
                if (endIndex > index && index >= startIndex) {
                    newIndex = index+1
                } else if (endIndex == index) {
                    newIndex = startIndex
                }
            } else {
                if (endIndex >= index && index > startIndex) {
                    newIndex = index-1
                } else if (startIndex == index) {
                    newIndex = endIndex
                }
            }

            if (newIndex != null) {
                const newItem = {...this.state.dataSource[newIndex]}
                newItem.originLeft = item.originLeft
                newItem.originTop = item.originTop
                newItem.position = new Animated.ValueXY({
                    x: parseInt(item.originLeft+0.5),
                    y: parseInt(item.originTop+0.5),
                })
                item = newItem
            }

            return item
        })

        this.setState({
            dataSource: newDataSource
        },()=>{
            if (this.props.onDataChange) {
                this.props.onDataChange(this.getOriginalData())
            }
            // Prevent RN from drawing the beginning and end
            const startItem = this.state.dataSource[startIndex]
            this.state.dataSource[startIndex].position.setValue({
                x: parseInt(startItem.originLeft+0.5),
                y: parseInt(startItem.originTop+0.5),
            })
            const endItem = this.state.dataSource[endIndex]
            this.state.dataSource[endIndex].position.setValue({
                x: parseInt(endItem.originLeft+0.5),
                y: parseInt(endItem.originTop+0.5),
            })
        })

    }

    reComplexDataSource(isInit,props) {
        const itemWidth = this.state.itemWidth;
        const itemHeight = this.state.itemHeight;
        const rowNum = parseInt(props.parentWidth/itemWidth);
        const dataSource = props.dataSource.map((item,index)=>{
            const newData = {}
            const left = (index%rowNum)*itemWidth
            const top = parseInt((index/rowNum))*itemHeight

            newData.data = item
            newData.originIndex = index
            newData.originLeft = left
            newData.originTop = top
            newData.position = new Animated.ValueXY({
                x: parseInt(left+0.5),
                y: parseInt(top+0.5),
            })
            newData.scaleValue = new Animated.Value(1)
            return newData
        })

        if (isInit) {
            this.state = {
                dataSource: dataSource,
                height: Math.ceil(dataSource.length/rowNum)*itemHeight
            }
        } else {
            this.setState({
                dataSource: dataSource,
                height: Math.ceil(dataSource.length/rowNum)*itemHeight
            })
        }

    }

    getOriginalData () {
        return this.state.dataSource.map((item,index)=> item.data)
    }

    render() {
        return (
            <View
                //ref={(ref)=>this.sortParentRef=ref}
                style={[styles.container,{
                    width: this.props.parentWidth,
                    height: this.state.height,
                }]}
                //onLayout={()=> {}}
                >
                {this._renderItemView()}
            </View>
        )
    }

    _getKey = (index) => {
        const item = this.state.dataSource[index];
        return this.props.keyExtractor ? this.props.keyExtractor(item.data, index) : item.originIndex;
    }

    _renderItemView = () => {
        const {maxScale, minOpacity} = this.props
        const inputRange = maxScale >= 1 ? [1, maxScale] : [maxScale, 1]
        const outputRange = maxScale >= 1 ? [1, minOpacity] : [minOpacity, 1]
        return this.state.dataSource.map((item,index)=>{
            const transformObj = {}
            transformObj[this.props.scaleStatus] = item.scaleValue
            const key = this._getKey(index);
            return (
                <Animated.View
                    key={key}
                    ref={(ref) => this.sortRefs.set(key,ref)}
                    {...this._panResponder.panHandlers}
                    style={[styles.item,{
                        marginTop: this.props.marginChildrenTop,
                        marginBottom: this.props.marginChildrenBottom,
                        marginLeft: this.props.marginChildrenLeft,
                        marginRight: this.props.marginChildrenRight,
                        left: item.position.x,
                        top: item.position.y,
                        opacity: item.scaleValue.interpolate({inputRange,outputRange}),
                        transform: [transformObj]
                    }]}>
                    <TouchableOpacity
                        activeOpacity = {1}
                        delayLongPress={this.props.delayLongPress}
                        onPressOut={()=> this.onPressOut()}
                        onLongPress={()=>this.startTouch(index)}
                        onPress={()=>{
                            if (this.props.onClickItem) {
                                this.props.onClickItem(this.getOriginalData(),item.data,index)
                            }
                        }}>
                        {this.props.renderItem(item.data,index)}
                    </TouchableOpacity>
                </Animated.View>
            )
        })
    }

    componentWillUnmount() {
        if (this.isScaleRecovery) clearTimeout(this.isScaleRecovery)
    }

}

DragSortableView.propTypes = {
    dataSource: PropTypes.array.isRequired,
    parentWidth: PropTypes.number,
    childrenHeight: PropTypes.number.isRequired,
    childrenWidth: PropTypes.number.isRequired,

    marginChildrenTop: PropTypes.number,
    marginChildrenBottom: PropTypes.number,
    marginChildrenLeft: PropTypes.number,
    marginChildrenRight: PropTypes.number,

    sortable: PropTypes.bool,

    onClickItem: PropTypes.func,
    onDragStart: PropTypes.func,
    onDragEnd: PropTypes.func,
    onDataChange: PropTypes.func,
    renderItem: PropTypes.func.isRequired,
    scaleStatus: PropTypes.oneOf(['scale','scaleX','scaleY']),
    fixedItems: PropTypes.array,
    keyExtractor: PropTypes.func,
    delayLongPress: PropTypes.number,
    isDragFreely: PropTypes.bool,
    onDragging: PropTypes.func,
    maxScale: PropTypes.number,
    minOpacity: PropTypes.number,
    scaleDuration: PropTypes.number,
    slideDuration: PropTypes.number
}

DragSortableView.defaultProps = {
    marginChildrenTop: 0,
    marginChildrenBottom: 0,
    marginChildrenLeft: 0,
    marginChildrenRight: 0,
    parentWidth: width,
    sortable: true,
    scaleStatus: 'scale',
    fixedItems: [],
    isDragFreely: false,
    maxScale: 1.1,
    minOpacity: 0.8,
    scaleDuration: 100,
    slideDuration: 300,
}

const styles = StyleSheet.create({
    container: {
        flexWrap: 'wrap',
        flexDirection: 'row',
    },
    item: {
        position: 'absolute',
        zIndex: defaultZIndex,
    },
})

组件使用

使用时,在页面js代码内,使用该组件并配置相关数据信息即可。

界面展示

image-20210406151023727

组件使用代码展示

引入组件

import DragSortableView from '../widget/DragSortableView'

组件调用

image-20210406150710356

数据加载部分代码
image-20210406150800358
点击操作

点击编辑时,再次点击标签会将此标签从当前列表中删除。

image-20210406151056510

image-20210406150918236

从下方推荐频道里点击标签会添加到我的频道。

image-20210406153335948

点击时经过的代码操作:

image-20210406153413103

整体页面效果

图标拖动

整体页面代码
import React, { Component } from 'react'
import {
    View,
    Text,
    StyleSheet,
    Dimensions,
    TouchableOpacity,
    Image,
    ScrollView,
    SafeAreaView,
} from 'react-native'
import DragSortableView from '../widget/DragSortableView'
const marginwidth = 10;
const deviceWidth = Dimensions.get('window').width
const childrenWidth = (deviceWidth - marginwidth * 2) / 5;
const childrenHeight = deviceWidth / 10;
const itemWidth = 72
const itemHeight = 36
const sortWidth = deviceWidth
const items = [
    { text: '关注', isSelect: true },
    { text: '推荐', isSelect: true },
    { text: '热点', isSelect: true },
    { text: 'NBA', isSelect: true },
    { text: '体育', isSelect: true },
    { text: '动漫', isSelect: true },
    { text: '精品课', isSelect: true },
    { text: '科技', isSelect: true },
    { text: '股票', isSelect: true },
    { text: '军事', isSelect: true },
    { text: '科学', isSelect: true },
    { text: '娱乐', isSelect: true },
    { text: '健康', isSelect: true },
    { text: '微头条', isSelect: true },
    { text: '财经', isSelect: true },
    { text: '手机', isSelect: true },
    { text: '历史', isSelect: true },
    { text: '新时代', isSelect: true },
    { text: '国风', isSelect: true },
    { text: '小视频', isSelect: true },
    { text: '值点', isSelect: true },
    { text: '问答', isSelect: true },
    { text: '小说', isSelect: true },
    { text: '音频', isSelect: false },
    { text: '深圳', isSelect: false },
    { text: '视频', isSelect: false },
    { text: '时尚', isSelect: false },
    { text: '美食', isSelect: false },
    { text: '养生', isSelect: false },
    { text: '电影', isSelect: false },
    { text: '宠物', isSelect: false },
    { text: '家具', isSelect: false },
    { text: '情感', isSelect: false },
    { text: '文化', isSelect: false },
    { text: '精选', isSelect: false },
    { text: '图片', isSelect: false },
    { text: '正能量', isSelect: false },
    { text: '冬奥', isSelect: false },
    { text: '收藏', isSelect: false },
    { text: '公益', isSelect: false },
    { text: '彩票', isSelect: false },
]

export default class CommonSortPage extends Component {

    constructor(props) {
        super(props)

        this.state = {
            scrollEnabled: true,
            isEditState: false,
            selectedItems: items.filter((item, index) => item.isSelect),
            unselectedItems: items.filter((item, index) => !item.isSelect)
        }
    }

    render() {
        return (
            <SafeAreaView>{/* 1 */}
                <ScrollView
                    scrollEnabled={this.state.scrollEnabled}
                    style={styles.container}>{/* 2 */}
                    <View style={styles.hurdle}>{/* 2-1 */}
                        <Text style={styles.hurdle_title}>{'我的频道'}</Text>
                        <TouchableOpacity style={styles.hurdle_edit} onPress={this.onEditClick}>
                            <Text style={styles.hurdle_edit_text}>{this.state.isEditState ? '完成' : '编辑'}</Text>
                        </TouchableOpacity>
                    </View>
                    {/* 2-2 */}
                    <DragSortableView
                        dataSource={this.state.selectedItems}
                        parentWidth={sortWidth}
                        childrenWidth={childrenWidth}
                        childrenHeight={childrenHeight}
                        marginChildrenTop={10}
                        onDragStart={this.onSelectedDragStart}
                        onDragEnd={this.onSelectedDragEnd}
                        onDataChange={(data) => { this.setState({ selectedItems: data }) }}
                        keyExtractor={(item, index) => item.text} // FlatList作用一样,优化
                        onClickItem={this.onSelectedClickItem}
                        renderItem={this.renderSelectedItemView} />

                    <View style={[styles.hurdle, { justifyContent: 'flex-start', marginTop: 40 }]}>{/* 2-1 */}
                        <Text style={styles.hurdle_title}>{'推荐频道'}</Text>
                    </View>

                    {/* 2-2 */}
                    <DragSortableView
                        dataSource={this.state.unselectedItems}
                        parentWidth={sortWidth}
                        sortable={false}
                        childrenWidth={childrenWidth}
                        childrenHeight={childrenHeight}
                        marginChildrenTop={10}
                        onDataChange={(data) => { this.setState({ unselectedItems: data }) }}
                        keyExtractor={(item, index) => item.text} // FlatList作用一样,优化
                        onClickItem={this.onUnSelectedClickItem}
                        renderItem={this.renderUnSelectedItemView} />
                </ScrollView>
            </SafeAreaView>
        )
    }

    renderSelectedItemView = (item, index) => {
        const clearIcon = this.state.isEditState ?
            <Image style={styles.selected_item_icon} source={require('../data/img/clear.png')} /> : undefined
        return (
            <View style={styles.selected_container}>{/* 2-2 */}
                <View style={styles.selected_item}>
                    <Text style={styles.selected_item_text}>{item.text}</Text>
                </View>
                {clearIcon}
            </View>
        )
    }

    renderUnSelectedItemView = (item, index) => {
        return (
            <View style={styles.selected_container}>
                <View style={styles.unselected_item}>
                    <Image style={styles.unselected_item_icon} source={require('../data/img/add.png')} />
                    <Text style={styles.selected_item_text}>{item.text}</Text>
                </View>
            </View>
        )
    }

    onSelectedDragEnd = () => this.setState({ scrollEnabled: true })

    onSelectedDragStart = () => {
        if (!this.state.isEditState) {
            this.setState({
                isEditState: true,
                scrollEnabled: false
            })
        } else {
            this.setState({
                scrollEnabled: false
            })
        }
    }

    onSelectedClickItem = (data, item, index) => {
        // delete, data 是最新的数据
        if (this.state.isEditState) {
            this.setState({
                selectedItems: [...data].filter((wItem, windex) => windex !== index),
                unselectedItems: [item, ...this.state.unselectedItems]
            })
        }
    }

    onUnSelectedClickItem = (data, item, index) => {
        this.setState({
            selectedItems: [...this.state.selectedItems, item],
            unselectedItems: [...data].filter((wItem, windex) => windex !== index)
        })
    }

    onEditClick = () => {
        this.setState({ isEditState: !this.state.isEditState })
    }

}

const styles = StyleSheet.create({
    container: {
        // backgroundColor: '#fff',
        marginRight: marginwidth,
        marginLeft: marginwidth
    },
    hurdle: {
        flexDirection: 'row',
        alignItems: 'center',
        justifyContent: 'space-between',
        marginTop: 20,
        // backgroundColor: '#154'
    },
    hurdle_title: {
        color: '#333',
        fontSize: 25,
        marginLeft: 20
    },
    hurdle_edit: {
        height: 24,
        paddingLeft: 10,
        paddingRight: 10,
        justifyContent: 'center',
        borderWidth: 1,
        borderColor: '#ff6548',
        marginRight: 15,
        borderRadius: 12
    },
    hurdle_edit_text: {
        color: '#ff6548',
        fontSize: 16
    },
    selected_container: {
        width: childrenWidth,
        height: childrenHeight,
        alignItems: 'center',
        justifyContent: 'center',
        // backgroundColor: '#fff',
    },
    selected_item: {
        width: 72,
        height: 36,
        backgroundColor: '#a1a3a6',
        borderRadius: 2,
        alignItems: 'center',
        justifyContent: 'center'
    },
    selected_item_text: {
        fontSize: 16,
        color: '#444'
    },
    selected_item_icon: {
        width: 16,
        height: 16,
        resizeMode: 'contain',
        position: 'absolute',
        top: (childrenHeight - itemHeight - 25) / 2 + 15 * 0.25, //下移点
        left: (childrenWidth + itemWidth - 10) / 2 - 15 * 0.25 //右移点,也可以换个布局
    },
    unselected_item: {
        width: 72,
        height: 36,
        backgroundColor: '#a1a3a6',
        borderRadius: 2,
        alignItems: 'center',
        justifyContent: 'center',
        flexDirection: 'row'
    },
    unselected_item_icon: {
        width: 14,
        height: 14,
        resizeMode: 'contain',
        // marginLeft: 2
    }
})

DragSortableView.js详解

代码内封装了一套组件移动的转换逻辑,使用了PanResponder,Animated,Dimensions等主要知识点。组件运行首先获取设置的props信息,用来初始化标签显示。然后封装了startTouch,moveTouch、endTouch,changePosition四个主要函数。

PanResponder

PanResponder类可以将多点触摸操作协调成一个手势。它使得一个单点触摸可以接受更多的触摸操作,也可以用于识别简单的多点触摸手势。

默认情况下PanResponder会通过InteractionManager来阻止长时间运行的 JS 事件打断当前的手势活动。

它提供了一个对触摸响应系统响应器的可预测的包装。对于每一个处理函数,它在原生事件之外提供了一个新的gestureState对象:

onPanResponderMove: (event, gestureState) => {}

原生事件是指由以下字段组成的合成触摸事件:

  • nativeEvent
    
    • changedTouches - 在上一次事件之后,所有发生变化的触摸事件的数组集合(即上一次事件后,所有移动过的触摸点)
    • identifier - 触摸点的 ID
    • locationX - 触摸点相对于父元素的横坐标
    • locationY - 触摸点相对于父元素的纵坐标
    • pageX - 触摸点相对于根元素的横坐标
    • pageY - 触摸点相对于根元素的纵坐标
    • target - 触摸点所在的元素 ID
    • timestamp - 触摸事件的时间戳,可用于移动速度的计算
    • touches - 当前屏幕上的所有触摸点的集合

一个gestureState对象有如下的字段:

  • stateID - 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。
  • moveX - 最近一次移动时的屏幕横坐标
  • moveY - 最近一次移动时的屏幕纵坐标
  • x0 - 当响应器产生时的屏幕坐标
  • y0 - 当响应器产生时的屏幕坐标
  • dx - 从触摸操作开始时的累计横向路程
  • dy - 从触摸操作开始时的累计纵向路程
  • vx - 当前的横向移动速度
  • vy - 当前的纵向移动速度
  • numberActiveTouches - 当前在屏幕上的有效触摸点的数量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值