项目介绍
目前,很多的软件在功能模块都会展示很多的功能按钮,并且支持根据自己的使用习惯进行排列。
在Git hub上找到一个可以实现功能图标自由拖动的开源代码,主要的结构为container为各种模式的图标拖动界面,data为测试数据,widget为主要的依赖组件源代码。
先放上源码链接:GitHub源码
主要功能代码:
图标拖动主要依赖以下两个组件,
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代码内,使用该组件并配置相关数据信息即可。
界面展示
组件使用代码展示
引入组件
import DragSortableView from '../widget/DragSortableView'
组件调用
数据加载部分代码

点击操作
点击编辑时,再次点击标签会将此标签从当前列表中删除。
从下方推荐频道里点击标签会添加到我的频道。
点击时经过的代码操作:
整体页面效果
整体页面代码
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
- 触摸点的 IDlocationX
- 触摸点相对于父元素的横坐标locationY
- 触摸点相对于父元素的纵坐标pageX
- 触摸点相对于根元素的横坐标pageY
- 触摸点相对于根元素的纵坐标target
- 触摸点所在的元素 IDtimestamp
- 触摸事件的时间戳,可用于移动速度的计算touches
- 当前屏幕上的所有触摸点的集合
一个gestureState
对象有如下的字段:
stateID
- 触摸状态的 ID。在屏幕上有至少一个触摸点的情况下,这个 ID 会一直有效。moveX
- 最近一次移动时的屏幕横坐标moveY
- 最近一次移动时的屏幕纵坐标x0
- 当响应器产生时的屏幕坐标y0
- 当响应器产生时的屏幕坐标dx
- 从触摸操作开始时的累计横向路程dy
- 从触摸操作开始时的累计纵向路程vx
- 当前的横向移动速度vy
- 当前的纵向移动速度numberActiveTouches
- 当前在屏幕上的有效触摸点的数量