Ruby's Louvre

每天学习一点点算法

导航

React/anu实现Touchable

在RN中有一个叫Touchable 的组件,这里我们重演如何实现它。

Touchable存在的意义是屏蔽click的问题。移动端与手机的click 在一些浏览器是有差异,比如说著名的300ms延迟。

Touchable的实现要点是将事件通过包装,然后绑定在它的下一级元素节点上。

而一级元素节点可以通过this.props.children[0]取到。为了解决兼容问题,我们通常用React.Children.only(this.props.children)来取这个节点。

而事件的传递则通过React.cloneElement(child, newPropsWithEvents)实现。

最后是事件包装,在移动端下,点击事件是通过4个事件实现的:ontouchstart, ontouchmove, ontouchend, ontouchcancel

为了模块化的需要,我们将事件包装这块拆出来,叫gesture.js

/**
 * touchable手势处理,解决Scroller内部的手势冲突
 * 在滚动时不会触发active
 * 在active之后发生滚动会取消active状态
 */
import ReactDOM from 'react-dom';
const TAP_SLOP = 5;
export const TAP_DELAY = 50;
/**
 * @param endPoint
 * @param startPoint
 * @returns {number}
 * 求两个点之间的距离
 */
function getDistance(endPoint, startPoint) {
    return Math.sqrt(Math.pow(endPoint.pageX - startPoint.pageX, 2) + Math.pow(endPoint.pageY - startPoint.pageY, 2));
}

/**
 * @param endPoint
 * @param startPoint
 * @returns {boolean}
 *如果移动的距离太远了,应该是认为其他事件,而不是Tap事件
 */
function onTouchMoveShouldCancelTap(endPoint, startPoint) {
    return getDistance(endPoint, startPoint) > TAP_SLOP;
}

/**
 * @param evt
 * @returns {touch/null}
 * 获取触点
 */
function getTouchPoint(evt) {
    return evt.touches.length ? { pageX: evt.touches[0].pageX, pageY: evt.touches[0].pageY } : null;
}

/**
 * @param domNode
 * @param activeClass
 * 移除item的activeClass
 */
function removeActiveClass(domNode, activeClass) {
    if (domNode && activeClass) {
        domNode.className = domNode.className.replace(` ${activeClass}`, '');
    }
}

/**
 * @param scroller
 * @returns {boolean}
 * 判断组件是否在滚动
 */
function isScrolling(scroller) {
    return scroller ? scroller.isScrolling : false;
}

function isAnySwipeMenuOpen(swipeMenuList) {
    return swipeMenuList ? swipeMenuList.openIndex !== -1 : false;
}

// touchStart的位置,是否需要放弃Tap触发,Tap周期(start,move,end)是否已经结束
let startPoint,
    shouldAbortTap;
let captured = null;

export default function ({
    component,
    scroller,
    swipeMenuList,
    activeClass,
    onTap,
    onTouchStart,
    disabled
}) {
    const gestureObj = {
        onTouchStart(evt) {
            const domNode = ReactDOM.findDOMNode(component);
            removeActiveClass(domNode, activeClass);
            // 如果组件正在滚动,直接放弃Tap触发
            shouldAbortTap = isScrolling(scroller) || isAnySwipeMenuOpen(swipeMenuList);
            startPoint = getTouchPoint(evt);
            onTouchStart(evt);
            if (!captured) {
                captured = domNode;
            }
            // TAP_DELAY之后再次判断是否要触发Tap,如果这段时间内出现了大的位移,if后面的逻辑就不会执行
            setTimeout(() => {
                const className = activeClass;
                if (!shouldAbortTap && className && captured === domNode && !disabled) {
                    domNode.className += ` ${className}`;
                }
            }, TAP_DELAY);
        },
        onTouchMove(evt) {
            const domNode = ReactDOM.findDOMNode(component);
            const currentPoint = getTouchPoint(evt);
            // 根据touchmove的距离判断是否要放弃tap
            if (onTouchMoveShouldCancelTap(currentPoint, startPoint)) {
                shouldAbortTap = true;
                captured = null;
                removeActiveClass(domNode, activeClass);
            }
        },
        onTouchEnd(evt) {
            const target = evt.target;
            const domNode = ReactDOM.findDOMNode(component);
            // 如果需要触发tap,在TAP_DELAY之后触发onTap回调
            if (!shouldAbortTap && captured === domNode) {
                setTimeout(() => {
                    if (!disabled) {
                        onTap(target);
                    }
                    removeActiveClass(domNode, activeClass);
                    captured = null;
                }, TAP_DELAY + 10);
            } else if (shouldAbortTap) {
                captured = null;
            }
        },
        onTouchCancel() {
            const domNode = ReactDOM.findDOMNode(component);
            removeActiveClass(domNode, activeClass);
        }
    };

    return gestureObj;
}

Touchable.js的源码如下

import { Component, PropTypes,cloneElement } from 'react';
import gesture from './gesture';

export class Touchable extends Component {

    static propTypes = {
        /**
         * @property touchClass
         * @type String
         * @default null
         * @description 触摸Touchable时附加的className,可以用来实现Native常见的触摸反馈功能(例如给触摸区域添加深色背景或者改变透明度等等)。
         */
        touchClass: PropTypes.string,
        /**
         * @property onTap
         * @type Function
         * @default null
         * @param {DOMElement} target tap事件的target
         * @description 给Touchable绑定的onTap事件。
         */
        onTap: PropTypes.func,
        /**
         * @property disabled
         * @type Bool
         * @default false
         * @description Touchable是否处于可点击状态,如果设为true,那么onTap事件回调和触摸反馈效果都不可用。
         * @version 3.0.7
         */
        disabled: PropTypes.bool,
        /**
         * @skip 给List定制的属性
         */
        onTouchStart: PropTypes.func,
        /**
         * @skip 内部使用标志
         */
        internalUse: PropTypes.bool,
        children: PropTypes.object
    };

    static defaultProps = {
        onTouchStart: () => {
        },
        touchClass: null,
        onTap: () => {
        },
        internalUse: false,
        disabled: false
    };

    static contextTypes = {
        scroller: PropTypes.object,
        swipeMenuList: PropTypes.object
    };

    render() {
        if (process.env.NODE_ENV !== 'production') {
            if (this.props.touchClass == null && !this.props.internalUse) {
                console.error('yo-touchable: Touchable组件没有设置touchClass, 出于用户体验考虑, 应该尽量给触摸区域添加触摸反馈。');
            }
        }

        const onlyChild = React.Children.only(this.props.children);
        const gestureObj = gesture({
            component: this,
            scroller: this.context.scroller,
            swipeMenuList: this.context.swipeMenuList,
            activeClass: this.props.touchClass,
            onTap: this.props.onTap,
            onTouchStart: this.props.onTouchStart,
            disabled: this.props.disabled
        });
        const { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel } = gestureObj;

        return cloneElement(onlyChild, { onTouchStart, onTouchMove, onTouchEnd, onTouchCancel });
    }
}

Touchable就是将用户传人它的属性提出来,复制到第一个子节点的props上。这个过程我们用cloneElement实现。

使用

<Touchable onTap={(e)=>{ console.log(e)}}

posted on 2017-06-17 13:28  司徒正美  阅读(1188)  评论(1编辑  收藏  举报