Skip to content

Commit a67eac2

Browse files
committed
add Wheel component, up to 0.4.0
1 parent 64b3b03 commit a67eac2

File tree

16 files changed

+553
-4
lines changed

16 files changed

+553
-4
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ The document is being written, please refer to the example source code.
9494
## AlbumView
9595
![](https://github.com/rilyu/teaset/blob/master/screenshots/14a-AlbumView1.png?raw=true) ![](https://github.com/rilyu/teaset/blob/master/screenshots/14a-AlbumView2.png?raw=true)
9696

97+
## Wheel
98+
![](https://github.com/rilyu/teaset/blob/master/screenshots/14b-Wheel.png?raw=true)
99+
97100
## Overlay
98101
![](https://github.com/rilyu/teaset/blob/master/screenshots/15-Overlay1.png?raw=true) ![](https://github.com/rilyu/teaset/blob/master/screenshots/15-Overlay2.png?raw=true)
99102
![](https://github.com/rilyu/teaset/blob/master/screenshots/15-Overlay3.png?raw=true) ![](https://github.com/rilyu/teaset/blob/master/screenshots/15-Overlay6.png?raw=true)

components/ListRow/ListRow.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ export default class ListRow extends Component {
8686
//title
8787
if (titlePlace === 'none') {
8888
title = null;
89-
} if (typeof title === 'string' || typeof title === 'number') {
89+
}
90+
if (typeof title === 'string' || typeof title === 'number') {
9091
let textStyle = (!detail && titlePlace === 'left') ? {flexGrow: 1, flexShrink: 1} : null;
9192
title = <Label style={[textStyle, titleStyle]} type='title' text={title} />
9293
}

components/Wheel/Wheel.js

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Wheel.js
2+
//问题2:不支持受控,对于月份-日期不合法时有问题,如3月31日换到2月
3+
4+
'use strict';
5+
6+
import React, {Component} from "react";
7+
import PropTypes from 'prop-types';
8+
import {StyleSheet, View, Text, Animated, PanResponder} from 'react-native';
9+
10+
import Theme from 'teaset/themes/Theme';
11+
import WheelItem from './WheelItem';
12+
13+
export default class Wheel extends Component {
14+
15+
static propTypes = {
16+
...View.propTypes,
17+
items: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.element, PropTypes.string, PropTypes.number])).isRequired,
18+
itemStyle: Text.propTypes.style,
19+
holeStyle: View.propTypes.style, //height is required
20+
maskStyle: View.propTypes.style,
21+
index: PropTypes.number,
22+
defaultIndex: PropTypes.number,
23+
onChange: PropTypes.func, //(index)
24+
};
25+
26+
static defaultProps = {
27+
...View.defaultProps,
28+
pointerEvents: 'box-only',
29+
defaultIndex: 0,
30+
};
31+
32+
static Item = WheelItem;
33+
static preRenderCount = 10;
34+
35+
constructor(props) {
36+
super(props);
37+
this.createPanResponder();
38+
this.prevTouches = [];
39+
this.index = props.index || props.index === 0 ? props.index : props.defaultIndex;
40+
this.lastRenderIndex = this.index;
41+
this.height = 0;
42+
this.holeHeight = 0;
43+
this.hiddenOffset = 0;
44+
this.currentPosition = new Animated.Value(0);
45+
this.targetPositionValue = null;
46+
}
47+
48+
componentWillMount() {
49+
if (!this.positionListenerId) {
50+
this.positionListenerId = this.currentPosition.addListener(e => this.handlePositionChange(e.value));
51+
}
52+
}
53+
54+
componentWillUnmount() {
55+
if (this.positionListenerId) {
56+
this.currentPosition.removeListener(this.positionListenerId);
57+
this.positionListenerId = null;
58+
}
59+
}
60+
61+
componentWillReceiveProps(nextProps) {
62+
if (nextProps.index || nextProps.index === 0) {
63+
this.index = nextProps.index;
64+
this.currentPosition.setValue(nextProps.index * this.holeHeight);
65+
}
66+
}
67+
68+
createPanResponder() {
69+
this.panResponder = PanResponder.create({
70+
onStartShouldSetPanResponder: (e, gestureState) => true,
71+
onStartShouldSetPanResponderCapture: (e, gestureState) => false,
72+
onMoveShouldSetPanResponder: (e, gestureState) => true,
73+
onMoveShouldSetPanResponderCapture: (e, gestureState) => false,
74+
onPanResponderGrant: (e, gestureState) => this.onPanResponderGrant(e, gestureState),
75+
onPanResponderMove: (e, gestureState) => this.onPanResponderMove(e, gestureState),
76+
onPanResponderTerminationRequest: (e, gestureState) => true,
77+
onPanResponderRelease: (e, gestureState) => this.onPanResponderRelease(e, gestureState),
78+
onPanResponderTerminate: (e, gestureState) => null,
79+
onShouldBlockNativeResponder: (e, gestureState) => true,
80+
});
81+
}
82+
83+
onPanResponderGrant(e, gestureState) {
84+
this.currentPosition.stopAnimation();
85+
this.prevTouches = e.nativeEvent.touches;
86+
this.speed = 0;
87+
}
88+
89+
onPanResponderMove(e, gestureState) {
90+
let {touches} = e.nativeEvent;
91+
let prevTouches = this.prevTouches;
92+
this.prevTouches = touches;
93+
94+
if (touches.length != 1 || touches[0].identifier != prevTouches[0].identifier) {
95+
return;
96+
}
97+
98+
let dy = touches[0].pageY - prevTouches[0].pageY;
99+
let pos = this.currentPosition._value - dy;
100+
this.currentPosition.setValue(pos);
101+
102+
let t = touches[0].timestamp - prevTouches[0].timestamp;
103+
if (t) this.speed = dy / t;
104+
}
105+
106+
onPanResponderRelease(e, gestureState) {
107+
this.prevTouches = [];
108+
if (Math.abs(this.speed) > 0.1) this.handleSwipeScroll();
109+
else this.handleStopScroll();
110+
}
111+
112+
handlePositionChange(value) {
113+
let newIndex = Math.round(value / this.holeHeight);
114+
if (newIndex != this.index && newIndex >= 0 && newIndex < this.props.items.length) {
115+
let moveCount = Math.abs(newIndex - this.lastRenderIndex);
116+
this.index = newIndex;
117+
if (moveCount > this.constructor.preRenderCount) {
118+
this.forceUpdate();
119+
}
120+
}
121+
122+
// let the animation stop faster
123+
if (this.targetPositionValue != null && Math.abs(this.targetPositionValue - value) <= 2) {
124+
this.targetPositionValue = null;
125+
this.currentPosition.stopAnimation();
126+
}
127+
}
128+
129+
handleSwipeScroll() {
130+
let {items} = this.props;
131+
132+
let inertiaPos = this.currentPosition._value - this.speed * 300;
133+
let newIndex = Math.round(inertiaPos / this.holeHeight);
134+
if (newIndex < 0) newIndex = 0;
135+
else if (newIndex > items.length - 1) newIndex = items.length - 1;
136+
137+
let toValue = newIndex * this.holeHeight;
138+
this.targetPositionValue = toValue;
139+
Animated.spring(this.currentPosition, {
140+
toValue: toValue,
141+
friction: 9,
142+
}).start(() => {
143+
this.currentPosition.setValue(toValue);
144+
this.props.onChange && this.props.onChange(newIndex);
145+
});
146+
}
147+
148+
handleStopScroll() {
149+
let toValue = this.index * this.holeHeight;
150+
this.targetPositionValue = toValue;
151+
Animated.spring(this.currentPosition, {
152+
toValue: toValue,
153+
friction: 9,
154+
}).start(() => {
155+
this.currentPosition.setValue(toValue);
156+
this.props.onChange && this.props.onChange(this.index);
157+
});
158+
}
159+
160+
handleLayout(height, holeHeight) {
161+
this.height = height;
162+
this.holeHeight = holeHeight;
163+
if (holeHeight) {
164+
let maskHeight = (height - holeHeight) / 2;
165+
this.hiddenOffset = Math.ceil(maskHeight / holeHeight) + this.constructor.preRenderCount;
166+
}
167+
this.forceUpdate(() => this.currentPosition.setValue(this.index * holeHeight));
168+
}
169+
170+
onLayout(e) {
171+
this.handleLayout(e.nativeEvent.layout.height, this.holeHeight);
172+
this.props.onLayout && this.props.onLayout(e);
173+
}
174+
175+
onHoleLayout(e) {
176+
this.handleLayout(this.height, e.nativeEvent.layout.height);
177+
}
178+
179+
buildProps() {
180+
let {style, items, itemStyle, holeStyle, maskStyle, ...others} = this.props;
181+
182+
style = [{
183+
backgroundColor: Theme.wheelColor,
184+
overflow: 'hidden',
185+
}].concat(style);
186+
itemStyle = [{
187+
backgroundColor: 'rgba(0, 0, 0, 0)',
188+
fontSize: Theme.wheelFontSize,
189+
color: Theme.wheelTextColor,
190+
}].concat(itemStyle);
191+
holeStyle = [{
192+
backgroundColor: 'rgba(0, 0, 0, 0)',
193+
height: Theme.wheelHoleHeight,
194+
borderColor: Theme.wheelHoleLineColor,
195+
borderTopWidth: Theme.wheelHoleLineWidth,
196+
borderBottomWidth: Theme.wheelHoleLineWidth,
197+
zIndex: 1,
198+
}].concat(holeStyle);
199+
maskStyle = [{
200+
backgroundColor: Theme.wheelMaskColor,
201+
opacity: Theme.wheelMaskOpacity,
202+
flex: 1,
203+
zIndex: 100,
204+
}].concat(maskStyle);
205+
206+
this.props = {style, items, itemStyle, holeStyle, maskStyle, ...others};
207+
}
208+
209+
renderItem(item, itemIndex) {
210+
let {itemStyle} = this.props;
211+
212+
if (Math.abs(this.index - itemIndex) > this.hiddenOffset) return null;
213+
214+
if (typeof item === 'string' || typeof item === 'number') {
215+
item = <Text style={itemStyle}>{item}</Text>;
216+
}
217+
218+
return (
219+
<this.constructor.Item
220+
itemHeight={this.holeHeight}
221+
wheelHeight={this.height}
222+
index={itemIndex}
223+
currentPosition={this.currentPosition}
224+
key={itemIndex}
225+
>
226+
{item}
227+
</this.constructor.Item>
228+
);
229+
}
230+
231+
render() {
232+
this.buildProps();
233+
this.lastRenderIndex = this.index;
234+
235+
let {items, itemStyle, holeStyle, maskStyle, defaultIndex, onChange, onLayout, ...others} = this.props;
236+
237+
return (
238+
<View
239+
{...others}
240+
onLayout={e => this.onLayout(e)}
241+
{...this.panResponder.panHandlers}
242+
>
243+
{items.map((item, index) => this.renderItem(item, index))}
244+
<View style={maskStyle} />
245+
<View style={holeStyle} onLayout={e => this.onHoleLayout(e)} />
246+
<View style={maskStyle} />
247+
</View>
248+
)
249+
}
250+
251+
}
252+

components/Wheel/WheelItem.js

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
// WheelItem.js
2+
3+
'use strict';
4+
5+
import React, {Component} from "react";
6+
import PropTypes from 'prop-types';
7+
import {StyleSheet, View, Text, Animated} from 'react-native';
8+
9+
import Theme from 'teaset/themes/Theme';
10+
11+
export default class WheelItem extends Component {
12+
13+
static propTypes = {
14+
...Animated.View.propTypes,
15+
index: PropTypes.number.isRequired,
16+
itemHeight: PropTypes.number.isRequired,
17+
wheelHeight: PropTypes.number.isRequired,
18+
currentPosition: PropTypes.any, //instanceOf(Animated)
19+
};
20+
21+
static defaultProps = {
22+
...Animated.View.defaultProps,
23+
};
24+
25+
constructor(props) {
26+
super(props);
27+
this.lastPosition = null;
28+
this.state = {
29+
translateY: new Animated.Value(100000),
30+
scaleX: new Animated.Value(1),
31+
scaleY: new Animated.Value(1),
32+
};
33+
}
34+
35+
componentWillMount() {
36+
if (!this.positionListenerId) {
37+
this.positionListenerId = this.props.currentPosition.addListener(e => {
38+
this.handlePositionChange(e.value);
39+
});
40+
this.handlePositionChange(this.props.currentPosition._value);
41+
}
42+
}
43+
44+
componentWillUnmount() {
45+
if (this.positionListenerId) {
46+
this.props.currentPosition.removeListener(this.positionListenerId);
47+
this.positionListenerId = null;
48+
}
49+
}
50+
51+
componentWillReceiveProps(nextProps) {
52+
let {itemHeight, wheelHeight, index} = this.props;
53+
if (nextProps.index != index
54+
|| nextProps.itemHeight != itemHeight
55+
|| nextProps.wheelHeight != wheelHeight) {
56+
this.handlePositionChange(nextProps.currentPosition._value, nextProps);
57+
}
58+
}
59+
60+
calcProjection(diameter, point, width) {
61+
if (diameter == 0) return false;
62+
let radius = diameter / 2;
63+
let circumference = Math.PI * diameter;
64+
let quarter = circumference / 4;
65+
if (Math.abs(point) > quarter) return false;
66+
let alpha = point / circumference * Math.PI * 2;
67+
68+
let pointProjection = radius * Math.sin(alpha);
69+
let distance = radius - radius * Math.sin(Math.PI / 2 - alpha);
70+
let eyesDistance = 1000;
71+
let widthProjection = width * eyesDistance / (distance + eyesDistance);
72+
73+
return {point: pointProjection, width: widthProjection};
74+
}
75+
76+
handlePositionChange(value, props = null) {
77+
let {itemHeight, wheelHeight, index} = props ? props : this.props;
78+
79+
if (!itemHeight || !wheelHeight) return;
80+
if (this.lastPosition !== null && Math.abs(this.lastPosition - value) < 1) return;
81+
82+
let itemPosition = itemHeight * index;
83+
let halfItemHeight = itemHeight / 2;
84+
let top = itemPosition - value - halfItemHeight;
85+
let bottom = top + itemHeight;
86+
let refWidth = 100;
87+
let p1 = this.calcProjection(wheelHeight, top, refWidth);
88+
let p2 = this.calcProjection(wheelHeight, bottom, refWidth);
89+
90+
let ty = 10000, sx = 1, sy = 1;
91+
if (p1 && p2) {
92+
let y1 = p1.point;
93+
let y2 = p2.point;
94+
ty = (y1 + y2) / 2;
95+
sy = (y2 - y1) / itemHeight;
96+
sx = (p1.width + p2.width) / 2 / refWidth;
97+
}
98+
99+
let {translateY, scaleX, scaleY} = this.state;
100+
translateY.setValue(ty);
101+
scaleX.setValue(sx);
102+
scaleY.setValue(sy);
103+
this.lastPosition = value;
104+
}
105+
106+
render() {
107+
let {style, itemHeight, wheelHeight, index, currentPosition, children, ...others} = this.props;
108+
let {translateY, scaleX, scaleY} = this.state;
109+
style = [{
110+
backgroundColor: 'rgba(0, 0, 0, 0)',
111+
position: 'absolute',
112+
left: 0,
113+
right: 0,
114+
top: 0,
115+
bottom: 0,
116+
justifyContent: 'center',
117+
transform: [{scaleX}, {translateY}, {scaleY}],
118+
}].concat(style);
119+
return (
120+
<Animated.View style={style} {...others}>
121+
{children}
122+
</Animated.View>
123+
);
124+
}
125+
126+
}

0 commit comments

Comments
 (0)