Skip to content

Commit

Permalink
add Wheel component, up to 0.4.0
Browse files Browse the repository at this point in the history
  • Loading branch information
rilyu committed Sep 8, 2017
1 parent 64b3b03 commit a67eac2
Show file tree
Hide file tree
Showing 16 changed files with 553 additions and 4 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ The document is being written, please refer to the example source code.
## AlbumView
![](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)

## Wheel
![](https://github.com/rilyu/teaset/blob/master/screenshots/14b-Wheel.png?raw=true)

## Overlay
![](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)
![](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)
Expand Down
3 changes: 2 additions & 1 deletion components/ListRow/ListRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ export default class ListRow extends Component {
//title
if (titlePlace === 'none') {
title = null;
} if (typeof title === 'string' || typeof title === 'number') {
}
if (typeof title === 'string' || typeof title === 'number') {
let textStyle = (!detail && titlePlace === 'left') ? {flexGrow: 1, flexShrink: 1} : null;
title = <Label style={[textStyle, titleStyle]} type='title' text={title} />
}
Expand Down
252 changes: 252 additions & 0 deletions components/Wheel/Wheel.js
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>
)
}

}

126 changes: 126 additions & 0 deletions components/Wheel/WheelItem.js
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>
);
}

}
Loading

0 comments on commit a67eac2

Please sign in to comment.