UNPKG

weex-nuke

Version:

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

385 lines (354 loc) 10.8 kB
'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;