weex-nuke
Version:
基于 Rax 、Weex 的高性能组件体系 ~~
385 lines (354 loc) • 10.8 kB
JSX
'use strict';
/** @jsx createElement */
import { Component, createElement, findDOMNode, PropTypes } from 'rax';
import { isWeex, isWeb } from 'nuke-env';
import View from 'nuke-view';
import RefreshControl from 'nuke-refresh-control';
import styles from './styles';
const SCROLLVIEW_CLS = 'rax-scrollview';
const LOADMORE_THRESHOLD = 500;
const ON_SCROLL_THROTTLE = 50;
const FULL_WIDTH = 750;
/**
* ScrollView
* @description 可滚动容器
*/
class ScrollView extends Component {
constructor(props) {
super(props);
this.state = {
loadmoreretry: 0,
};
this.timer = null;
this.childrenAll = {};
this.scrollStartFlag = false;
this.lastScrollEventTriggerTime = 0;
this.lastScrollContentSize = 0;
this.lastScrollDistance = 0;
this.handleScroll = this.handleScroll.bind(this);
this.scrollTo = this.scrollTo.bind(this);
this.scrollToElement = this.scrollToElement.bind(this);
this.resetLoadmore = this.resetLoadmore.bind(this);
this.checkScrolling = this.checkScrolling.bind(this);
}
checkScrolling() {
const now = new Date().getTime();
if (this.lastScrollEventTriggerTime > 0 && now - this.lastScrollEventTriggerTime > 200) {
this.scrollStartFlag = false;
clearTimeout(this.timer);
this.timer = null;
this.props.onScrollEnd && this.props.onScrollEnd();
}
}
handleScroll(e) {
const { onScroll, onScrollStart, horizontal, onEndReached, onEndReachedThreshold } = this.props;
if (isWeb) {
if (!this.scrollStartFlag) {
// first time trigger onScroll
this.scrollStartFlag = true;
onScrollStart && onScrollStart(e);
}
clearTimeout(this.timer);
this.lastScrollEventTriggerTime = new Date().getTime();
if (onScroll) {
e.nativeEvent = {
contentOffset: {
x: e.target.scrollLeft,
y: e.target.scrollTop,
},
};
onScroll && onScroll(e);
}
this.timer = setTimeout(this.checkScrolling, 200);
if (onEndReached) {
if (!this.scrollerNode) {
this.scrollerNode = findDOMNode(this.refs.scroller);
this.scrollerContentNode = findDOMNode(this.refs.contentContainer);
this.scrollerNodeSize = horizontal ? this.scrollerNode.offsetWidth : this.scrollerNode.offsetHeight;
}
// NOTE:in iOS7/8 offsetHeight/Width is is inaccurate ( use scrollHeight/Width )
const scrollContentSize = horizontal ? this.scrollerNode.scrollWidth : this.scrollerNode.scrollHeight;
const scrollDistance = horizontal ? this.scrollerNode.scrollLeft : this.scrollerNode.scrollTop;
const isEndReached = scrollContentSize - scrollDistance - this.scrollerNodeSize < onEndReachedThreshold;
const isScrollToEnd = scrollDistance > this.lastScrollDistance;
const isLoadedMoreContent = scrollContentSize !== this.lastScrollContentSize;
if (isEndReached && isScrollToEnd && isLoadedMoreContent) {
this.lastScrollContentSize = scrollContentSize;
onEndReached(e);
}
this.lastScrollDistance = scrollDistance;
}
}
if (isWeex) {
e.nativeEvent = {
contentOffset: {
// HACK: weex scroll event value is opposite of web
x: -e.contentOffset.x,
y: -e.contentOffset.y,
},
};
onScroll && onScroll(e);
}
}
/**
* 兼容旧的 resetScroll
*/
resetLoadmore() {
if (isWeb) {
this.lastScrollContentSize = 0;
this.lastScrollDistance = 0;
} else {
this.refs.scroller.resetLoadmore();
}
}
scrollTo(options = {}) {
const { horizontal } = this.props;
const { x = 0, y = 0, animated = true, offset = 0 } = options;
const offsetResult = parseInt(horizontal ? x : y || offset, 10);
if (isWeex) {
const dom = require('@weex-module/dom');
const contentContainer = findDOMNode(this.refs.contentContainer);
dom.scrollToElement(contentContainer.ref, {
offset: offsetResult,
animated,
});
} else {
const pixelRatio = document.documentElement.clientWidth / FULL_WIDTH;
if (horizontal) {
findDOMNode(this.refs.scroller).scrollLeft = pixelRatio * offsetResult;
} else {
findDOMNode(this.refs.scroller).scrollTop = pixelRatio * offsetResult;
}
}
}
scrollToElement(ref, options = {}) {
const { horizontal } = this.props;
const { offset = 0, animated = true } = options;
if (isWeex) {
const dom = require('@weex-module/dom');
// const contentContainer = findDOMNode(this.refs.contentContainer);
dom.scrollToElement(findDOMNode(ref), {
offset,
animated,
});
} else {
const pixelRatio = document.documentElement.clientWidth / FULL_WIDTH;
// if (offset >= 0) {
const refDOM = findDOMNode(ref);
if (refDOM) {
if (horizontal) {
findDOMNode(this.refs.scroller).scrollLeft = refDOM.offsetLeft + pixelRatio * offset;
} else {
findDOMNode(this.refs.scroller).scrollTop = refDOM.offsetTop + pixelRatio * offset;
}
// }
}
}
}
splitChildren() {
let { children } = this.props;
if (!children) {
this.childrenAll = {
contents: null,
refreshContent: null,
};
return;
}
const contents = [];
let refreshContent;
if (!Array.isArray(children)) {
children = [children];
}
children.forEach((child) => {
if (!child) return;
if (child.type && child.type.displayName === RefreshControl.displayName) {
refreshContent = child;
} else {
contents.push(child);
}
});
this.childrenAll = {
contents,
refreshContent,
};
}
hideWebScrollBar() {
const styleNode = document.createElement('style');
styleNode.id = 'rax-scrollview-style';
document.head.appendChild(styleNode);
styleNode.innerHTML = `.${SCROLLVIEW_CLS}::-webkit-scrollbar{display: none;}`;
}
render() {
const {
style,
scrollEventThrottle,
showsHorizontalScrollIndicator,
showsVerticalScrollIndicator,
onEndReached,
onScrollStart,
onScrollEnd,
// loadmoreretry,
onScroll,
children,
horizontal,
onEndReachedThreshold,
...others
} = this.props;
let { showScrollBar } = this.props;
if (typeof showScrollBar === 'undefined') {
showScrollBar = horizontal ? showsHorizontalScrollIndicator : showsVerticalScrollIndicator;
}
const contentContainerStyle = [horizontal && styles.contentContainerHorizontal, this.props.contentContainerStyle];
// bugfix: fix scrollview flex in ios 78
if (!isWeex && !horizontal) {
contentContainerStyle.push(styles.containerWebStyle);
}
this.splitChildren();
const contentChild = this.childrenAll.contents;
const refreshContent = this.childrenAll.refreshContent;
const contentContainer = (
<View ref="contentContainer" style={contentContainerStyle}>
{contentChild}
</View>
);
const baseStyle = horizontal ? styles.horizontal : styles.vertical;
const scrollerStyle = Object.assign({}, baseStyle, isWeb ? styles.scrollerWeb : {}, this.props.style);
if (isWeex) {
const nativeProps = {
ref: 'scroller',
style: scrollerStyle,
showScrollbar: showScrollBar,
onLoadmore: onEndReached,
onScrollStart,
onScrollEnd,
onScroll: onScroll ? this.handleScroll : null,
loadmoreoffset: parseInt(onEndReachedThreshold, 10),
loadmoreretry: true,
offsetAccuracy: 20,
scrollDirection: horizontal ? 'horizontal' : 'vertical',
...others,
};
return (
<scroller {...nativeProps}>
{refreshContent}
{contentContainer}
</scroller>
);
}
/**
* trigger times throttle
*/
let handleScroll = this.handleScroll;
if (scrollEventThrottle) {
handleScroll = throttle(handleScroll, scrollEventThrottle);
}
const webProps = {
ref: 'scroller',
style: scrollerStyle,
onScroll: handleScroll,
id: 'scroller_rv',
...others,
};
if (!showScrollBar) {
this.hideWebScrollBar();
}
return (
<View {...webProps} className={SCROLLVIEW_CLS}>
{refreshContent ? (
<RefreshControl
{...refreshContent.props}
listId={webProps.id}
refreshingTime={100}
refreshing={refreshContent.props.refreshing}
/>
) : null}
{contentContainer}
</View>
);
}
}
function throttle(func, wait) {
let ctx;
let args;
let rtn;
let timeoutID;
let last = 0;
function call() {
timeoutID = 0;
last = +new Date();
rtn = func.apply(ctx, args);
ctx = null;
args = null;
}
return function throttled() {
ctx = this;
args = arguments;
const delta = new Date() - last;
if (!timeoutID) {
if (delta >= wait) call();
else timeoutID = setTimeout(call, wait - delta);
}
return rtn;
};
}
ScrollView.propTypes = {
/**
* ScrollView 样式 style of ScrollView
*/
style: PropTypes.any,
/**
* 滚动时回调 callback when scrolling
*/
onScroll: PropTypes.func,
/**
* 滚动开始时回调 callback when scrolling
*/
onScrollStart: PropTypes.func,
/**
* 滚动结束时回调 callback when scrolling
*/
onScrollEnd: PropTypes.func,
/**
* 加载到底部时回调 callback when scrolling to bottom
*/
onEndReached: PropTypes.func,
/**
* 是否横向 is horizontal
*/
horizontal: PropTypes.boolean,
/**
* 触发加载到底部回调的位移 offset of scrolling to bottom event been triggered
*/
onEndReachedThreshold: PropTypes.number,
/**
* 是否展示横向滚动条 is show horizontal scrollbar
*/
showsHorizontalScrollIndicator: PropTypes.boolean,
/**
* 是否展示纵向滚动条 is show vertical scrollbar
*/
showsVerticalScrollIndicator: PropTypes.boolean,
/**
* 内容容器样式 the style of content's wrap container
*/
contentContainerStyle: PropTypes.any,
/**
* onScroll 事件触发节流时间 the time inteval of next onScroll event being triggered
*/
scrollEventThrottle: PropTypes.number,
children: PropTypes.any,
};
ScrollView.defaultProps = {
style: {},
contentContainerStyle: {},
horizontal: false,
onScroll: null,
onScrollStart: null,
onScrollEnd: null,
onEndReached: () => {},
scrollEventThrottle: ON_SCROLL_THROTTLE,
onEndReachedThreshold: LOADMORE_THRESHOLD,
showsHorizontalScrollIndicator: true,
showsVerticalScrollIndicator: true,
};
export default ScrollView;