UNPKG

react-native-pickers

Version:

纯JS实现的React-Native 各种弹窗、日期选择控件、地址选择控件等

344 lines (316 loc) 13.9 kB
import React, { Component } from 'react'; import { View, Animated, PanResponder } from 'react-native'; import BaseComponent from './BaseComponent'; import Svg, { LinearGradient, Rect, Stop } from 'react-native-svg'; class PickerView extends BaseComponent { static defaultProps = { itemTextColor: 0x333333ff, itemSelectedColor: 0x1097D5ff, itemHeight: 40, onPickerSelected: null, selectedIndex: 0 } _previousTop = 0; lastTop = 0; constructor(props) { super(props); list = ['', ''].concat(props.list).concat(['', '']); this.colorPath = []; let length = list.length; for (let i = 0; i < length; i++) { this.colorPath.push(new Animated.Value(i == (this.props.selectedIndex + 2) ? 1 : 0)); } this.path = new Animated.Value(-props.itemHeight * this.props.selectedIndex); this.state = { list: list, selectedIndex: props.selectedIndex, }; this.maxTop = 0; this.maxBottom = -props.itemHeight * (list.length - 5); this.onStartShouldSetPanResponder = this.onStartShouldSetPanResponder.bind(this); this.onMoveShouldSetPanResponder = this.onMoveShouldSetPanResponder.bind(this); this.onPanResponderGrant = this.onPanResponderGrant.bind(this); this.onPanResponderMove = this.onPanResponderMove.bind(this); this.onPanResponderEnd = this.onPanResponderEnd.bind(this); //這裏固定在屏幕底部,所以直接寫死touch區域即可。 this.parentTopY = this.mScreenHeight - props.itemHeight * 5 - this.getSize(15); this.parentBottomY = this.mScreenHeight - this.getSize(15); } shouldComponentUpdate(nextProps, nextState) { if (nextProps) { list = ['', ''].concat(nextProps.list).concat(['', '']); listChange = JSON.stringify(list) != JSON.stringify(this.state.list); indexChange = nextProps.selectedIndex != this.state.selectedIndex; if (listChange || indexChange) { console.log('shouldComponentUpdate'); this.path.setValue(-this.props.itemHeight * nextProps.selectedIndex); if (listChange) { this.colorPath = []; let length = list.length; for (let i = 0; i < length; i++) { this.colorPath.push(new Animated.Value(i == (nextProps.selectedIndex + 2) ? 1 : 0)); } } nextState.list = list; nextState.selectedIndex = nextProps.selectedIndex; this.maxTop = 0; this.maxBottom = -this.props.itemHeight * (list.length - 5); return true; } } return false; } //用户开始触摸屏幕的时候,是否愿意成为响应者; onStartShouldSetPanResponder(evt, gestureState) { if (evt.nativeEvent.pageY < this.parentTopY || evt.nativeEvent.pageY > this.parentBottomY) { return false; } else { this.path && this.path.removeAllListeners(); this.path.stopAnimation(); this.keyDown = Date.now(); return true; } } //在每一个触摸点开始移动的时候,再询问一次是否响应触摸交互; onMoveShouldSetPanResponder(evt, gestureState) { if (evt.nativeEvent.pageY < this.parentTopY || evt.nativeEvent.pageY > this.parentBottomY) { return false; } else { this.path && this.path.removeAllListeners(); this.path.stopAnimation(); return true; } } // 开始手势操作。给用户一些视觉反馈,让他们知道发生了什么事情! onPanResponderGrant(evt, gestureState) { this.lastTop = this.path._value; } // 最近一次的移动距离为gestureState.move{X,Y} onPanResponderMove(evt, gestureState) { if (global.timer != null) { global.timer.map(item => { clearTimeout(item); }); } this._previousTop = this.lastTop + gestureState.dy; if (this._previousTop > 0) { this._previousTop = Math.min(this._previousTop, this.maxTop + this.props.itemHeight); } else { this._previousTop = Math.max(this._previousTop, this.maxBottom - this.props.itemHeight); } this.path.setValue(this._previousTop); if (this.previousTop) { this.velocity = gestureState.dy - this.previousTop; } else { this.velocity = 0; } this.previousTop = gestureState.dy; } lastEvent = null; lastTwoEvent = null; onPanResponderEnd(evt, gestureState) { let actionTime = Date.now() - this.keyDown; if (actionTime < 300 && Math.abs(gestureState.vy) < 0.1) { let clickPosition = -(parseInt((gestureState.y0 - this.parentTopY) / this.props.itemHeight) - 2); let toValue = this.path._value; let number = Math.round(toValue / this.props.itemHeight); toValue = this.props.itemHeight * number; toValue = toValue + (this.props.itemHeight * clickPosition); if (toValue > 0) { toValue = Math.min(toValue, this.maxTop); } else { toValue = Math.max(toValue, this.maxBottom); } if (isNaN(toValue)) { } else { //onSeleted Animated.timing(this.path, { toValue: toValue, duration: 200 }).start(() => { this.onSeleted(Math.abs(toValue / this.props.itemHeight - 2)); }); } } else { this.lastTop = this._previousTop; let toValue = this._previousTop + gestureState.vy * this.props.itemHeight * 2; let number = Math.round(toValue / this.props.itemHeight); toValue = this.props.itemHeight * number; if (toValue > 0) { toValue = Math.min(toValue, this.maxTop); } else { toValue = Math.max(toValue, this.maxBottom); } Animated.decay(this.path, { velocity: gestureState.vy, //通过手势设置相关速度 deceleration: 0.995, }).start(() => { if (this.path._value % this.props.itemHeight == 0) { this.path.removeListener(this.pathListener); this.pathListener = null; } else { //慣性動畫 if (this.pathListener) { this.path.removeListener(this.pathListener); this.pathListener = null; let toValue = Math.round(this.path._value / this.props.itemHeight) * this.props.itemHeight; Animated.timing(this.path, { toValue: toValue, duration: 50 }).start(() => { //onSeleted this.onSeleted(Math.abs(toValue / this.props.itemHeight - 2)); }); } } }); //當滾動超出上限或者下限時,接管慣性動畫 this.pathListener = this.path.addListener((listener) => { if (listener.value < this.maxBottom && this.pathListener) { this.path.removeListener(this.pathListener); this.pathListener = null; Animated.timing(this.path, { toValue: this.maxBottom }).start(() => { //onSeleted this.onSeleted(Math.abs(this.maxBottom / this.props.itemHeight - 2)); }); } else if (listener.value > this.maxTop - this.props.itemHeight && this.pathListener) { this.path.removeListener(this.pathListener); this.pathListener = null; Animated.timing(this.path, { toValue: this.maxTop }).start(() => { //onSeleted this.onSeleted(Math.abs(this.maxTop / this.props.itemHeight - 2)); }); } }); } } onSeleted(selectedIndex) { if (global.timer == null) { global.timer = []; } global.timer.push(setTimeout(() => { this.colorPath.map((item, index) => { if (item._value == 0 && selectedIndex == index) { item.setValue(1); } else if (item._value == 1 && selectedIndex != index) { item.setValue(0); } }) this.props.onPickerSelected && this.props.onPickerSelected(this.state.list[selectedIndex]); }, 20)); } componentWillMount(evt, gestureState) { this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: this.onStartShouldSetPanResponder, onMoveShouldSetPanResponder: this.onMoveShouldSetPanResponder, onPanResponderGrant: this.onPanResponderGrant, onPanResponderMove: this.onPanResponderMove, onPanResponderRelease: this.onPanResponderEnd, onPanResponderTerminate: this.onPanResponderEnd, }); } renderList() { return this.state.list.map((item, index) => { return this.renderItem(item, index); }); } renderItem(item, index) { return <View key={index} style={{ width: this.props.itemWidth, height: this.props.itemHeight, justifyContent: 'center', alignItems: 'center' }}> <Animated.Text style={{ color: this.colorPath[index].interpolate({ inputRange: [0, 1], outputRange: [this.props.itemTextColor, this.props.itemSelectedColor] }), fontSize: this.props.fontSize ? this.props.fontSize : this.getSize(20), backgroundColor: 'transparent', fontWeight: 'normal' }}>{item}</Animated.Text> </View > } render() { return <View style={{ width: this.props.itemWidth, height: this.props.itemHeight * 5 + this.getSize(15), backgroundColor: '#ffffff' }}> <View ref={ref => this.ref = ref} {...this._panResponder.panHandlers} style={{ overflow: 'hidden', width: this.props.itemWidth, height: this.props.itemHeight * 5 + this.getSize(15), backgroundColor: '#ffffff' }}> <Animated.View style={{ transform: [ { translateY: this.path } ] }} > {this.renderList()} </Animated.View> <View style={{ position: 'absolute', width: this.props.itemWidth, height: this.mOnePixel, top: this.props.itemHeight * 4 / 2, backgroundColor: '#E8EEF0' }} /> <View style={{ position: 'absolute', width: this.props.itemWidth, height: this.mOnePixel, top: this.props.itemHeight * 6 / 2, backgroundColor: '#E8EEF0' }} /> <Svg onStartShouldSetResponder={() => { return false; }} onResponderStart={() => { return false; }} style={{ position: 'absolute', top: 0 }} height={this.props.itemHeight * 1} width={this.props.itemWidth} > <LinearGradient id="grad" x1="0" y1={this.props.itemHeight * 1} x2={0} y2="0"> <Stop offset="0" stopColor="#ffffff" stopOpacity="0.2" /> <Stop offset="1" stopColor="#ffffff" stopOpacity="1" /> </LinearGradient> <Rect x="0" y="0" width={this.props.itemWidth} height={this.props.itemHeight * 1} fill="url(#grad)" clipPath="url(#clip)" /> </Svg> <Svg onStartShouldSetResponder={() => { return false; }} onResponderStart={() => { return false; }} style={{ position: 'absolute', bottom: this.getSize(15) }} height={this.props.itemHeight * 1} width={this.props.itemWidth} > <LinearGradient id="grad" x1="0" y1={this.props.itemHeight * 1} x2={0} y2="0"> <Stop offset="0" stopColor="#ffffff" stopOpacity="1" /> <Stop offset="1" stopColor="#ffffff" stopOpacity="0.4" /> </LinearGradient> <Rect x="0" y="0" width={this.props.itemWidth} height={this.props.itemHeight * 1} fill="url(#grad)" clipPath="url(#clip)" /> </Svg> <View style={{ width: this.mScreenWidth, height: this.getSize(15), bottom: 0, backgroundColor: '#ffffff', position: 'absolute' }} /> </View> </View> } } export default PickerView;