-
Notifications
You must be signed in to change notification settings - Fork 481
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
16 changed files
with
553 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
// Wheel.js | ||
//问题2:不支持受控,对于月份-日期不合法时有问题,如3月31日换到2月 | ||
|
||
'use strict'; | ||
|
||
import React, {Component} from "react"; | ||
import PropTypes from 'prop-types'; | ||
import {StyleSheet, View, Text, Animated, PanResponder} from 'react-native'; | ||
|
||
import Theme from 'teaset/themes/Theme'; | ||
import WheelItem from './WheelItem'; | ||
|
||
export default class Wheel extends Component { | ||
|
||
static propTypes = { | ||
...View.propTypes, | ||
items: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.number])).isRequired, | ||
itemStyle: Text.propTypes.style, | ||
holeStyle: View.propTypes.style, //height is required | ||
maskStyle: View.propTypes.style, | ||
index: PropTypes.number, | ||
defaultIndex: PropTypes.number, | ||
onChange: PropTypes.func, //(index) | ||
}; | ||
|
||
static defaultProps = { | ||
...View.defaultProps, | ||
pointerEvents: 'box-only', | ||
defaultIndex: 0, | ||
}; | ||
|
||
static Item = WheelItem; | ||
static preRenderCount = 10; | ||
|
||
constructor(props) { | ||
super(props); | ||
this.createPanResponder(); | ||
this.prevTouches = []; | ||
this.index = props.index || props.index === 0 ? props.index : props.defaultIndex; | ||
this.lastRenderIndex = this.index; | ||
this.height = 0; | ||
this.holeHeight = 0; | ||
this.hiddenOffset = 0; | ||
this.currentPosition = new Animated.Value(0); | ||
this.targetPositionValue = null; | ||
} | ||
|
||
componentWillMount() { | ||
if (!this.positionListenerId) { | ||
this.positionListenerId = this.currentPosition.addListener(e => this.handlePositionChange(e.value)); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this.positionListenerId) { | ||
this.currentPosition.removeListener(this.positionListenerId); | ||
this.positionListenerId = null; | ||
} | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (nextProps.index || nextProps.index === 0) { | ||
this.index = nextProps.index; | ||
this.currentPosition.setValue(nextProps.index * this.holeHeight); | ||
} | ||
} | ||
|
||
createPanResponder() { | ||
this.panResponder = PanResponder.create({ | ||
onStartShouldSetPanResponder: (e, gestureState) => true, | ||
onStartShouldSetPanResponderCapture: (e, gestureState) => false, | ||
onMoveShouldSetPanResponder: (e, gestureState) => true, | ||
onMoveShouldSetPanResponderCapture: (e, gestureState) => false, | ||
onPanResponderGrant: (e, gestureState) => this.onPanResponderGrant(e, gestureState), | ||
onPanResponderMove: (e, gestureState) => this.onPanResponderMove(e, gestureState), | ||
onPanResponderTerminationRequest: (e, gestureState) => true, | ||
onPanResponderRelease: (e, gestureState) => this.onPanResponderRelease(e, gestureState), | ||
onPanResponderTerminate: (e, gestureState) => null, | ||
onShouldBlockNativeResponder: (e, gestureState) => true, | ||
}); | ||
} | ||
|
||
onPanResponderGrant(e, gestureState) { | ||
this.currentPosition.stopAnimation(); | ||
this.prevTouches = e.nativeEvent.touches; | ||
this.speed = 0; | ||
} | ||
|
||
onPanResponderMove(e, gestureState) { | ||
let {touches} = e.nativeEvent; | ||
let prevTouches = this.prevTouches; | ||
this.prevTouches = touches; | ||
|
||
if (touches.length != 1 || touches[0].identifier != prevTouches[0].identifier) { | ||
return; | ||
} | ||
|
||
let dy = touches[0].pageY - prevTouches[0].pageY; | ||
let pos = this.currentPosition._value - dy; | ||
this.currentPosition.setValue(pos); | ||
|
||
let t = touches[0].timestamp - prevTouches[0].timestamp; | ||
if (t) this.speed = dy / t; | ||
} | ||
|
||
onPanResponderRelease(e, gestureState) { | ||
this.prevTouches = []; | ||
if (Math.abs(this.speed) > 0.1) this.handleSwipeScroll(); | ||
else this.handleStopScroll(); | ||
} | ||
|
||
handlePositionChange(value) { | ||
let newIndex = Math.round(value / this.holeHeight); | ||
if (newIndex != this.index && newIndex >= 0 && newIndex < this.props.items.length) { | ||
let moveCount = Math.abs(newIndex - this.lastRenderIndex); | ||
this.index = newIndex; | ||
if (moveCount > this.constructor.preRenderCount) { | ||
this.forceUpdate(); | ||
} | ||
} | ||
|
||
// let the animation stop faster | ||
if (this.targetPositionValue != null && Math.abs(this.targetPositionValue - value) <= 2) { | ||
this.targetPositionValue = null; | ||
this.currentPosition.stopAnimation(); | ||
} | ||
} | ||
|
||
handleSwipeScroll() { | ||
let {items} = this.props; | ||
|
||
let inertiaPos = this.currentPosition._value - this.speed * 300; | ||
let newIndex = Math.round(inertiaPos / this.holeHeight); | ||
if (newIndex < 0) newIndex = 0; | ||
else if (newIndex > items.length - 1) newIndex = items.length - 1; | ||
|
||
let toValue = newIndex * this.holeHeight; | ||
this.targetPositionValue = toValue; | ||
Animated.spring(this.currentPosition, { | ||
toValue: toValue, | ||
friction: 9, | ||
}).start(() => { | ||
this.currentPosition.setValue(toValue); | ||
this.props.onChange && this.props.onChange(newIndex); | ||
}); | ||
} | ||
|
||
handleStopScroll() { | ||
let toValue = this.index * this.holeHeight; | ||
this.targetPositionValue = toValue; | ||
Animated.spring(this.currentPosition, { | ||
toValue: toValue, | ||
friction: 9, | ||
}).start(() => { | ||
this.currentPosition.setValue(toValue); | ||
this.props.onChange && this.props.onChange(this.index); | ||
}); | ||
} | ||
|
||
handleLayout(height, holeHeight) { | ||
this.height = height; | ||
this.holeHeight = holeHeight; | ||
if (holeHeight) { | ||
let maskHeight = (height - holeHeight) / 2; | ||
this.hiddenOffset = Math.ceil(maskHeight / holeHeight) + this.constructor.preRenderCount; | ||
} | ||
this.forceUpdate(() => this.currentPosition.setValue(this.index * holeHeight)); | ||
} | ||
|
||
onLayout(e) { | ||
this.handleLayout(e.nativeEvent.layout.height, this.holeHeight); | ||
this.props.onLayout && this.props.onLayout(e); | ||
} | ||
|
||
onHoleLayout(e) { | ||
this.handleLayout(this.height, e.nativeEvent.layout.height); | ||
} | ||
|
||
buildProps() { | ||
let {style, items, itemStyle, holeStyle, maskStyle, ...others} = this.props; | ||
|
||
style = [{ | ||
backgroundColor: Theme.wheelColor, | ||
overflow: 'hidden', | ||
}].concat(style); | ||
itemStyle = [{ | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
fontSize: Theme.wheelFontSize, | ||
color: Theme.wheelTextColor, | ||
}].concat(itemStyle); | ||
holeStyle = [{ | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
height: Theme.wheelHoleHeight, | ||
borderColor: Theme.wheelHoleLineColor, | ||
borderTopWidth: Theme.wheelHoleLineWidth, | ||
borderBottomWidth: Theme.wheelHoleLineWidth, | ||
zIndex: 1, | ||
}].concat(holeStyle); | ||
maskStyle = [{ | ||
backgroundColor: Theme.wheelMaskColor, | ||
opacity: Theme.wheelMaskOpacity, | ||
flex: 1, | ||
zIndex: 100, | ||
}].concat(maskStyle); | ||
|
||
this.props = {style, items, itemStyle, holeStyle, maskStyle, ...others}; | ||
} | ||
|
||
renderItem(item, itemIndex) { | ||
let {itemStyle} = this.props; | ||
|
||
if (Math.abs(this.index - itemIndex) > this.hiddenOffset) return null; | ||
|
||
if (typeof item === 'string' || typeof item === 'number') { | ||
item = <Text style={itemStyle}>{item}</Text>; | ||
} | ||
|
||
return ( | ||
<this.constructor.Item | ||
itemHeight={this.holeHeight} | ||
wheelHeight={this.height} | ||
index={itemIndex} | ||
currentPosition={this.currentPosition} | ||
key={itemIndex} | ||
> | ||
{item} | ||
</this.constructor.Item> | ||
); | ||
} | ||
|
||
render() { | ||
this.buildProps(); | ||
this.lastRenderIndex = this.index; | ||
|
||
let {items, itemStyle, holeStyle, maskStyle, defaultIndex, onChange, onLayout, ...others} = this.props; | ||
|
||
return ( | ||
<View | ||
{...others} | ||
onLayout={e => this.onLayout(e)} | ||
{...this.panResponder.panHandlers} | ||
> | ||
{items.map((item, index) => this.renderItem(item, index))} | ||
<View style={maskStyle} /> | ||
<View style={holeStyle} onLayout={e => this.onHoleLayout(e)} /> | ||
<View style={maskStyle} /> | ||
</View> | ||
) | ||
} | ||
|
||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
// WheelItem.js | ||
|
||
'use strict'; | ||
|
||
import React, {Component} from "react"; | ||
import PropTypes from 'prop-types'; | ||
import {StyleSheet, View, Text, Animated} from 'react-native'; | ||
|
||
import Theme from 'teaset/themes/Theme'; | ||
|
||
export default class WheelItem extends Component { | ||
|
||
static propTypes = { | ||
...Animated.View.propTypes, | ||
index: PropTypes.number.isRequired, | ||
itemHeight: PropTypes.number.isRequired, | ||
wheelHeight: PropTypes.number.isRequired, | ||
currentPosition: PropTypes.any, //instanceOf(Animated) | ||
}; | ||
|
||
static defaultProps = { | ||
...Animated.View.defaultProps, | ||
}; | ||
|
||
constructor(props) { | ||
super(props); | ||
this.lastPosition = null; | ||
this.state = { | ||
translateY: new Animated.Value(100000), | ||
scaleX: new Animated.Value(1), | ||
scaleY: new Animated.Value(1), | ||
}; | ||
} | ||
|
||
componentWillMount() { | ||
if (!this.positionListenerId) { | ||
this.positionListenerId = this.props.currentPosition.addListener(e => { | ||
this.handlePositionChange(e.value); | ||
}); | ||
this.handlePositionChange(this.props.currentPosition._value); | ||
} | ||
} | ||
|
||
componentWillUnmount() { | ||
if (this.positionListenerId) { | ||
this.props.currentPosition.removeListener(this.positionListenerId); | ||
this.positionListenerId = null; | ||
} | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
let {itemHeight, wheelHeight, index} = this.props; | ||
if (nextProps.index != index | ||
|| nextProps.itemHeight != itemHeight | ||
|| nextProps.wheelHeight != wheelHeight) { | ||
this.handlePositionChange(nextProps.currentPosition._value, nextProps); | ||
} | ||
} | ||
|
||
calcProjection(diameter, point, width) { | ||
if (diameter == 0) return false; | ||
let radius = diameter / 2; | ||
let circumference = Math.PI * diameter; | ||
let quarter = circumference / 4; | ||
if (Math.abs(point) > quarter) return false; | ||
let alpha = point / circumference * Math.PI * 2; | ||
|
||
let pointProjection = radius * Math.sin(alpha); | ||
let distance = radius - radius * Math.sin(Math.PI / 2 - alpha); | ||
let eyesDistance = 1000; | ||
let widthProjection = width * eyesDistance / (distance + eyesDistance); | ||
|
||
return {point: pointProjection, width: widthProjection}; | ||
} | ||
|
||
handlePositionChange(value, props = null) { | ||
let {itemHeight, wheelHeight, index} = props ? props : this.props; | ||
|
||
if (!itemHeight || !wheelHeight) return; | ||
if (this.lastPosition !== null && Math.abs(this.lastPosition - value) < 1) return; | ||
|
||
let itemPosition = itemHeight * index; | ||
let halfItemHeight = itemHeight / 2; | ||
let top = itemPosition - value - halfItemHeight; | ||
let bottom = top + itemHeight; | ||
let refWidth = 100; | ||
let p1 = this.calcProjection(wheelHeight, top, refWidth); | ||
let p2 = this.calcProjection(wheelHeight, bottom, refWidth); | ||
|
||
let ty = 10000, sx = 1, sy = 1; | ||
if (p1 && p2) { | ||
let y1 = p1.point; | ||
let y2 = p2.point; | ||
ty = (y1 + y2) / 2; | ||
sy = (y2 - y1) / itemHeight; | ||
sx = (p1.width + p2.width) / 2 / refWidth; | ||
} | ||
|
||
let {translateY, scaleX, scaleY} = this.state; | ||
translateY.setValue(ty); | ||
scaleX.setValue(sx); | ||
scaleY.setValue(sy); | ||
this.lastPosition = value; | ||
} | ||
|
||
render() { | ||
let {style, itemHeight, wheelHeight, index, currentPosition, children, ...others} = this.props; | ||
let {translateY, scaleX, scaleY} = this.state; | ||
style = [{ | ||
backgroundColor: 'rgba(0, 0, 0, 0)', | ||
position: 'absolute', | ||
left: 0, | ||
right: 0, | ||
top: 0, | ||
bottom: 0, | ||
justifyContent: 'center', | ||
transform: [{scaleX}, {translateY}, {scaleY}], | ||
}].concat(style); | ||
return ( | ||
<Animated.View style={style} {...others}> | ||
{children} | ||
</Animated.View> | ||
); | ||
} | ||
|
||
} |
Oops, something went wrong.