UNPKG

weex-nuke

Version:

基于 Rax 、Weex 的高性能组件体系 ~~

568 lines (455 loc) 12.6 kB
'use strict'; /** @jsx createElement */ import { Component, createElement, PropTypes, render } from 'rax'; import View from 'nuke-view'; import Text from 'nuke-text'; import Item from './item'; import ThemeProvider from 'nuke-theme-provider'; import stylesProvider from '../styles'; const { connectStyle } = ThemeProvider; import { Animate, easeInOutQuad } from './animate'; const noop = () => { }; const VISIBLE_ITEM_COUNT = 5; const MAX_TOUCH_PATH_LENGTH = 20; const VELOCITY_DETERMINE_PERIOD = 100; const MILLISECONDS = 1000; const MIN_VELOCITY = 50; const VELOCITY_DECREASE_RATE = 0.9; const BOUNCE_RESISTANCE = 0.5; const BOUNCE_RESISTANCE_DISTANCE = 200; // px function getComputedStyleHeight(el) { if (typeof window === 'undefined') { return 0; } return parseFloat(window.getComputedStyle(el).height); } class PickerColumn extends Component { static propTypes = { value: PropTypes.any, dataSource: PropTypes.array, onChange: PropTypes.func, labelMap: PropTypes.func, valueMap: PropTypes.func, }; static defaultProps = { selectedKey: null, dataSource: [], onChange: noop, labelMap, valueMap, }; static contextTypes = { onUpdate: PropTypes.func, onChange: PropTypes.func, __picker__: PropTypes.bool, selectedValue: PropTypes.any, }; static childContextTypes = { __column__: PropTypes.bool, selectedValue: PropTypes.any, }; constructor(props, context) { super(props); let value; if (context.__picker__) { value = context.selectedValue[props.index]; } else if ('value' in props) { value = props.value; } else { value = props.selectedKey; } this.state = { value, }; this.bound = null; this.isTracking = false; this.isDragging = false; this.currentScrollTop = 0; this.touchPath = []; // use to calculate move velocity ['onChange', 'swipeStart', 'swipeMove', 'swipeEnd'].forEach((m) => { this[m] = this[m].bind(this); }); } getChildContext() { return { __column__: true, selectedValue: this.state.value, }; } componentWillReceiveProps(nextProps, nextContext) { if (nextContext.__picker__) { this.setState({ value: nextContext.selectedValue[nextProps.index], }); } else if ('value' in nextProps) { this.setState({ value: nextProps.value, }); } } componentWillUpdate(nextProps, nextState) { if ( nextProps.dataSource !== this.props.dataSource || JSON.stringify(nextProps.dataSource) !== JSON.stringify(this.props.dataSource) ) { this.changed = true; } } componentDidMount() { this.componentDidUpdate(); } componentDidUpdate() { // calculate scroll content bound // inMatrix arg for config platform if (!this.bound || this.changed) { this.calculateBound(); } // set scroll offset according to default value this.setScroll(this.state.value); this.changed = false; } calculateBound(props) { props = props || this.props; const { indicator } = this.refs; const { children, dataSource } = props; let top; let bottom; let itemHeight; let length; const halfOfVisibleCount = Math.floor(VISIBLE_ITEM_COUNT / 2); itemHeight = getComputedStyleHeight(indicator); length = dataSource.length; bottom = itemHeight * halfOfVisibleCount; top = -itemHeight * (length - (VISIBLE_ITEM_COUNT - halfOfVisibleCount)); this.bound = { top, bottom, itemHeight, }; } setScroll(value) { const { itemHeight, bottom } = this.bound; const { valueMap, dataSource, children, keyMap } = this.props; let index = 0; let _children; let item; if (typeof value === 'undefined') { value = dataSource[0].key; } for (let i = 0, l = dataSource.length; i < l; i++) { if (keyMap(dataSource[i]).toString() === value.toString()) { index = i; item = dataSource[i]; break; } } if (this.context.__picker__) { this.context.onUpdate(item, this.props.index); } this.updateOffset(bottom - itemHeight * index); } startAnimate(stepFunc, callback, duration, easeFunc) { this.stopAnimate(); return (this.animateId = Animate.start( stepFunc, callback, duration, easeFunc )); } stopAnimate() { if (this.animateId) { Animate.stop(this.animateId); this.animateId = null; } } isInBound(scrollTop) { const { top, bottom } = this.bound; if (scrollTop === undefined) { scrollTop = this.currentScrollTop; } return !(scrollTop > bottom || scrollTop < top); } getBouncedDiff(scrollTop) { const { top, bottom } = this.bound; if (!this.isInBound(scrollTop)) { if (scrollTop > bottom) { return scrollTop - bottom; } if (scrollTop < top) { return scrollTop - top; } } return 0; } setTransformStyle(offset) { const { content } = this.refs; if (content) { const style = `translate(0, ${offset}px)`; // debugger; //bug fix: avoid translate3d, bug in android 4.3 content.style.WebkitTransform = style; content.style.transform = style; } } setOffset(top) { this.currentScrollTop = top; this.setTransformStyle(top); } updateOffset(top, animation, callback, easeFunc) { let lastScrollTop; let diff; if (animation) { (lastScrollTop = this.currentScrollTop), (diff = top - lastScrollTop); this.startAnimate( (percent) => { this.setOffset(lastScrollTop + diff * percent); }, () => { this.setOffset(top); callback && callback(); }, 200, easeFunc ); } else { this.setOffset(top); callback && callback(); } } restrictScrollBound(scrollTop) { const { top, bottom } = this.bound; const { max, min } = Math; return min(max(top, scrollTop), bottom); } // when touch move end or animation end, // adjust scrollTop value to stop by nearest picker item stopBy() { const { abs } = Math; // debugger; const current = this.currentScrollTop; const itemHeight = this.bound.itemHeight; const remainder = current % itemHeight; const multiple = parseInt(current / itemHeight, 10); let stopBy = current; if (abs(remainder) >= itemHeight / 2) { // reserve the sign that represent scroll direction stopBy = remainder / abs(remainder) * (abs(multiple) + 1) * itemHeight; } else { stopBy = multiple * itemHeight; } stopBy = this.restrictScrollBound(stopBy); this.updateOffset( stopBy, false, () => { this.onChange(); }, easeInOutQuad ); } swipeStart(e) { e.preventDefault(); const pageY = e.touches ? e.touches[0].pageY : e.clientY; this.isTracking = true; this.isDragging = false; this.touchPageY = pageY; this.lastScrollTop = this.currentScrollTop; this.touchPath = [ { time: Date.now(), pageY, }, ]; this.stopAnimate(); } swipeMove(e) { e.preventDefault(); if (!this.isTracking) { return; } this.isDragging = true; const { abs, max, min } = Math; const initPageY = this.touchPageY; const currentPageY = e.touches ? e.touches[0].pageY : e.clientY; let diff = currentPageY - initPageY; let currentScrollTop = this.lastScrollTop + diff; // bounce if (!this.isInBound(currentScrollTop)) { let bound; let bounce; // bounced diff diff = this.getBouncedDiff(currentScrollTop); bound = currentScrollTop - diff; // apply bounce resistance diff = min( max(-BOUNCE_RESISTANCE_DISTANCE, diff), BOUNCE_RESISTANCE_DISTANCE ); bounce = diff * (1 - abs(diff) / (BOUNCE_RESISTANCE_DISTANCE * 2)); currentScrollTop = bound + bounce; } this.updateOffset(currentScrollTop); // record move path this.touchPath.push({ time: Date.now(), pageY: currentPageY, }); // restrict length if (this.touchPath.length > MAX_TOUCH_PATH_LENGTH) { this.touchPath = this.touchPath.slice(MAX_TOUCH_PATH_LENGTH / 2); } } swipeEnd(e) { e.preventDefault(); // Ignore event when tracking is not enabled (event might be outside of element) if (!this.isDragging) { return; } const { abs } = Math; // velocity calculation const lastTouchTime = Date.now(); const startPos = this.touchPath.length - 1; let endPos = startPos; let timeDiff = 0; let velocity = 0; for ( let i = startPos - 1; i >= 0 && timeDiff < VELOCITY_DETERMINE_PERIOD; i-- ) { const position = this.touchPath[i]; timeDiff = lastTouchTime - position.time; endPos = i; } if (startPos !== endPos) { const firstPageY = this.touchPath[endPos].pageY; const lastPageY = this.touchPath[startPos].pageY; velocity = (lastPageY - firstPageY) / timeDiff * MILLISECONDS; } if (abs(velocity) > MIN_VELOCITY) { const animateId = this.startAnimate((percent, timeDiff, timeFrame) => { const currentScrollTop = this.currentScrollTop; velocity *= VELOCITY_DECREASE_RATE; // bounce if (!this.isInBound(currentScrollTop)) { velocity *= BOUNCE_RESISTANCE; } const diff = velocity * timeFrame / MILLISECONDS; this.setOffset(currentScrollTop + diff); if (abs(velocity) < MIN_VELOCITY) { Animate.stop(animateId); this.stopBy(); } }); } else { this.stopBy(); } this.isTracking = false; this.isDragging = false; } setValue(value) { if (!('value' in this.props)) { this.setState({ value, }); } } getValue() { return this.state.value; } onChange(e) { const scrollTop = this.currentScrollTop; const { bottom, itemHeight } = this.bound; const itemIndex = Math.round((bottom - scrollTop) / itemHeight); const { index, children, dataSource, valueMap, keyMap } = this.props; let value; let item; if (children) { item = this.props.children[itemIndex]; value = item.props.value; } else { item = dataSource[itemIndex]; value = keyMap(item); } if (value === this.state.value) { return; } if (this.context.__picker__) { this.context.onChange(value, item, index); } else { this.setValue(value); this.props.onChange(value, item); } } render() { const { value } = this.state; const styles = this.props.themeStyle; const { dataSource, labelMap, valueMap, className, style = {}, prefix = this.defaultPrefix, } = this.props; let { children } = this.props; if (!children) { children = dataSource.map((item, index) => ( <Item key={keyMap(item)} value={valueMap(item)}> {valueMap(item)} </Item> )); } return ( <div x="column-item" style={[styles['picker-column'], style]} onMouseDown={this.swipeStart} onMouseMove={this.swipeMove} onMouseUp={this.swipeEnd} onMouseLeave={this.swipeEnd} onTouchStart={this.swipeStart} onTouchMove={this.swipeMove} onTouchEnd={this.swipeEnd} onTouchCancel={this.swipeEnd} > <div ref="content" x="column-item-scroll" style={styles['picker-column-scroll']} > {children} </div> <div x="column-item-mask" style={styles['picker-column-mask']} /> <div x="column-item-indicator" ref="indicator" style={styles['picker-column-indicator']} /> </div> ); } } PickerColumn.displayName = 'Picker'; const StyledPickerColumn = connectStyle(stylesProvider)(PickerColumn); export default StyledPickerColumn; // for label map export function labelMap(item) { if (typeof item === 'object') { return item.label; } return item; } // for value map export function valueMap(item) { if (typeof item === 'object') { return item.value; } return item; } // for key map export function keyMap(item) { if (typeof item === 'object') { return item.key; } return item; }