zarm-mobile
Version:
UI for react.js
384 lines (327 loc) • 9.6 kB
JSX
import React, { Component, cloneElement, Children } from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import Events from '../utils/events';
class Swipe extends Component {
constructor(props) {
super(props);
this.moveInterval = null;
this.pointStart = 0;
this.pointEnd = 0;
this.timeStart = new Date();
this.translateX = 0;
this.state = {
items: [],
activeIndex: props.activeIndex,
};
this._updateResize = this._updateResize.bind(this);
this._transitionEnd = this._transitionEnd.bind(this);
}
componentWillMount() {
this._parseItem(this.props);
this.startAutoPlay(this.props);
}
componentDidMount() {
// 监听窗口变化
Events.on(window, 'resize', this._updateResize);
Events.on(this.swipeItems, 'webkitTransitionEnd', this._transitionEnd);
Events.on(this.swipeItems, 'transitionend', this._transitionEnd);
// 设置起始位置编号
this.onJumpTo(this.props.activeIndex);
}
componentWillReceiveProps(nextProps) {
if ('children' in nextProps) {
this._parseItem(nextProps);
}
if ('activeIndex' in nextProps) {
this.onJumpTo(nextProps.activeIndex);
}
}
componentWillUnmount() {
// 自动轮播结束
this.pauseAutoPlay();
// 移除监听窗口变化
Events.off(window, 'resize', this._updateResize);
Events.off(this.swipeItems, 'webkitTransitionEnd', this._transitionEnd);
Events.off(this.swipeItems, 'transitionend', this._transitionEnd);
}
// 滑动到指定编号
onSlideTo(index) {
this._onMoveTo(index, this.props.speed);
}
// 静默跳到指定编号
onJumpTo(index) {
this._onMoveTo(index, 0);
}
// 自动轮播开始
startAutoPlay() {
this.moveInterval = (this.props.autoPlay && setInterval(() => {
let activeIndex = this.state.activeIndex;
const maxLength = this.props.children.length;
activeIndex = (['left', 'top'].indexOf(this.props.direction) > -1)
? (activeIndex + 1)
: (activeIndex - 1);
if (activeIndex > maxLength - 1) {
// 不循环暂停轮播
if (!this.props.loop) {
this.pauseAutoPlay();
return;
}
activeIndex = 0;
this.onJumpTo(-1);
} else if (activeIndex < 0) {
activeIndex = maxLength - 1;
this.onJumpTo(maxLength);
}
this.onSlideTo(activeIndex);
}, this.props.autoPlayIntervalTime));
}
// 暂停自动轮播
pauseAutoPlay() {
if (this.moveInterval) {
clearInterval(this.moveInterval);
}
}
// 处理节点(首位拼接)
_parseItem(props) {
if (props.children.length === 0) {
return;
}
// 增加头尾拼接节点
const items = [].concat(props.children);
const firstItem = items[0];
const lastItem = items[items.length - 1];
if (props.loop) {
items.push(firstItem);
items.unshift(lastItem);
}
// 节点追加后重排key
const newItems = React.Children.map(items, (element, index) => {
return cloneElement(element, {
key: index,
});
});
this.setState({
items: newItems,
});
}
// 更新窗口变化的位置偏移
_updateResize() {
this.onJumpTo(this.state.activeIndex);
}
// 移动到指定编号
_onMoveTo(index, speed) {
const dom = this.swipeItems;
if (!dom) {
return;
}
const px = (this._isDirectionX())
? -dom.offsetWidth * (index + this.props.loop)
: -dom.offsetHeight * (index + this.props.loop);
this._doTransition(px, speed);
this.translateX = px;
this.setState({
activeIndex: index,
});
}
// 执行过渡动画
_doTransition(offset, duration) {
const dom = this.swipeItems;
let x = 0;
let y = 0;
if (this._isDirectionX()) {
x = offset;
} else {
y = offset;
}
dom.style.webkitTransitionDuration = `${duration}ms`;
dom.style.transitionDuration = `${duration}ms`;
dom.style.webkitTransform = `translate3d(${x}px, ${y}px, 0)`;
dom.style.transform = `translate3d(${x}px, ${y}px, 0)`;
}
_transitionEnd() {
const activeIndex = this.state.activeIndex;
const maxLength = this.props.children.length;
if (activeIndex > maxLength - 1) {
this.onJumpTo(0);
} else if (activeIndex < 0) {
this.onJumpTo(maxLength - 1);
}
this.props.onChangeEnd(this.state.activeIndex);
}
// 触屏事件
_onTouchStart(event) {
this.pauseAutoPlay();
const pointX = this._getCurrentPoint(event);
const activeIndex = this.state.activeIndex;
const maxLength = this.props.children.length;
// 跳转到头尾
if (activeIndex <= 0) {
this.onJumpTo(0);
} else if (activeIndex >= (maxLength - 1)) {
this.onJumpTo(maxLength - 1);
}
this.pointStart = pointX;
this.timeStart = new Date();
}
_onTouchMove(event) {
event.preventDefault();
const pointX = this._getCurrentPoint(event);
const px = this.translateX + (pointX - this.pointStart);
// 设置不循环的时候
if (!this.props.loop) {
// 在首页时禁止拖动
if (this._isLastIndex() && (pointX - this.pointStart) < 0) {
return;
}
// 在尾页时禁止拖动
if (this._isFirstIndex() && (pointX - this.pointStart) > 0) {
return;
}
}
this._doTransition(px, 0);
this.pointEnd = pointX;
}
_onTouchEnd() {
const dom = this.swipeItems;
const px = (this.pointEnd !== 0)
? this.pointEnd - this.pointStart
: 0;
const timeSpan = new Date().getTime() - this.timeStart.getTime();
let activeIndex = this.state.activeIndex;
// 判断滑动临界点
// 1.滑动距离超过0,且滑动距离和父容器长度之比超过moveDistanceRatio
// 2.滑动释放时间差低于moveTimeSpan
if (
// 滑动距离超过0
px !== 0
&&
(
// 滑动距离和父容器长度之比超过moveDistanceRatio
Math.abs(px / dom.offsetWidth) >= this.props.moveDistanceRatio
||
// 滑动释放时间差低于moveTimeSpan
timeSpan <= this.props.moveTimeSpan
)
) {
activeIndex = (px > 0)
? (this.state.activeIndex - 1)
: (this.state.activeIndex + 1);
const { onChange } = this.props;
typeof onChange === 'function' && onChange(activeIndex);
}
this.onSlideTo(activeIndex);
this.pointStart = 0;
this.pointEnd = 0;
// 恢复自动轮播
this.startAutoPlay();
}
// 获取鼠标/触摸点坐标
_getCurrentPoint(event, type) {
const touch = (type === 'mouse')
? event
: event.touches[0];
const offset = (this._isDirectionX())
? touch.pageX
: touch.pageY;
return offset;
}
// 判断当前是否在最后一页
_isLastIndex() {
let result = false;
if (this.state.activeIndex >= this.props.children.length - 1) {
result = true;
}
return result;
}
// 判断当前是否在第一页
_isFirstIndex() {
let result = false;
if (this.state.activeIndex <= 0) {
result = true;
}
return result;
}
// 是否横向移动
_isDirectionX() {
return (['left', 'right'].indexOf(this.props.direction) > -1);
}
render() {
const { prefixCls, className, height, children } = this.props;
const classes = classnames({
[`${prefixCls}`]: true,
[className]: !!className,
});
const style = {
items: {},
pagination: {},
};
if (!this._isDirectionX()) {
style.items.height = height;
style.pagination.marginTop = 3;
} else {
style.items.whiteSpace = 'nowrap';
style.pagination.display = 'inline-block';
style.pagination.marginRight = 3;
}
return (
<div className={classes}>
<div
ref={(ele) => { this.swipeItems = ele; }}
className={`${prefixCls}-items`}
style={style.items}
onTouchStart={event => this._onTouchStart(event)}
onTouchMove={event => this._onTouchMove(event)}
onTouchEnd={event => this._onTouchEnd(event)}>
{ this.state.items }
</div>
<div className={`${prefixCls}-pagination`}>
<ul>
{
Children.map(children, (result, index) => {
return (
<li
role="tab"
key={`pagination-${index}`}
className={classnames({ active: index === this.state.activeIndex })}
style={style.pagination}
onClick={() => this.onSlideTo(index)}
/>
);
})
}
</ul>
</div>
</div>
);
}
}
Swipe.propTypes = {
prefixCls: PropTypes.string,
direction: PropTypes.oneOf(['left', 'right', 'top', 'bottom']),
height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
loop: PropTypes.bool,
activeIndex: PropTypes.number,
speed: PropTypes.number,
autoPlay: PropTypes.bool,
autoPlayIntervalTime: PropTypes.number,
moveDistanceRatio: PropTypes.number,
moveTimeSpan: PropTypes.number,
onChange: PropTypes.func,
onChangeEnd: PropTypes.func,
};
Swipe.defaultProps = {
prefixCls: 'za-swipe',
direction: 'left',
height: 160,
loop: false,
activeIndex: 0,
speed: 300,
autoPlay: false,
autoPlayIntervalTime: 3000,
moveDistanceRatio: 0.5,
moveTimeSpan: 300,
onChange() {},
onChangeEnd() {},
};
export default Swipe;