src/render/browser/render/gesture.js
function touchendHandler(event) {

  if (Object.keys(gestures).length == 2) {
    var elements = []
    for (var p in gestures) {
      elements.push(gestures[p].element)
    }
    fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouchend', {
      touches: slice.call(event.touches),
      touchEvent: event
    })
  }

  for (var i = 0; i < event.changedTouches.length; i++) {
    var touch = event.changedTouches[i]
    var id = touch.identifier
    var gesture = gestures[id]

    if (!gesture) {
      continue
    }

    if (gesture.pressingHandler) {
      clearTimeout(gesture.pressingHandler)
      gesture.pressingHandler = null
    }

    if (gesture.status === 'tapping') {
      gesture.timestamp = Date.now()
      fireEvent(gesture.element, 'tap', {
        touch: touch,
        touchEvent: event
      })

      if (lastTap && gesture.timestamp - lastTap.timestamp < 300) {
        fireEvent(gesture.element, 'doubletap', {
          touch: touch,
          touchEvent: event
        })
      }

      lastTap = gesture
    }

    if (gesture.status === 'panning') {
      var now = Date.now()
      var duration = now - gesture.startTime
      var displacementX = touch.clientX - gesture.startTouch.clientX
      var displacementY = touch.clientY - gesture.startTouch.clientY

      var velocity = Math.sqrt(gesture.velocityY * gesture.velocityY
        + gesture.velocityX * gesture.velocityX)
      var isSwipe = velocity > 0.5 && (now - gesture.lastTime) < 100
      var extra = {
        duration: duration,
        isSwipe: isSwipe,
        velocityX: gesture.velocityX,
        velocityY: gesture.velocityY,
        displacementX: displacementX,
        displacementY: displacementY,
        touch: touch,
        touches: event.touches,
        changedTouches: event.changedTouches,
        touchEvent: event,
        isVertical: gesture.isVertical,
        direction: gesture.direction
      }

      fireEvent(gesture.element, 'panend', extra)
      if (isSwipe) {
        fireEvent(gesture.element, 'swipe', extra)
      }
    }

    if (gesture.status === 'pressing') {
      fireEvent(gesture.element, 'pressend', {
        touch: touch,
        touchEvent: event
      })
    }

    delete gestures[id]
  }

  if (Object.keys(gestures).length === 0) {
    docEl.removeEventListener('touchmove', touchmoveHandler, false)
    docEl.removeEventListener('touchend', touchendHandler, false)
    docEl.removeEventListener('touchcancel', touchcancelHandler, false)
  }
}
function touchendHandler(event) {

  if (Object.keys(gestures).length == 2) {
    var elements = []
    for (var p in gestures) {
      elements.push(gestures[p].element)
    }
    fireEvent(getCommonAncestor(elements[0], elements[1]), 'dualtouchend', {
      touches: slice.call(event.touches),
      touchEvent: event
    })
  }

  for (var i = 0; i < event.changedTouches.length; i++) {
    var touch = event.changedTouches[i]
    var id = touch.identifier
    var gesture = gestures[id]

    if (!gesture) {
      continue
    }

    if (gesture.pressingHandler) {
      clearTimeout(gesture.pressingHandler)
      gesture.pressingHandler = null
    }

    if (gesture.status === 'tapping') {
      gesture.timestamp = Date.now()
      fireEvent(gesture.element, 'tap', {
        touch: touch,
        touchEvent: event
      })

      if (lastTap && gesture.timestamp - lastTap.timestamp < 300) {
        fireEvent(gesture.element, 'doubletap', {
          touch: touch,
          touchEvent: event
        })
      }

      lastTap = gesture
    }

    if (gesture.status === 'panning') {
      var now = Date.now()
      var duration = now - gesture.startTime
      var displacementX = touch.clientX - gesture.startTouch.clientX
      var displacementY = touch.clientY - gesture.startTouch.clientY

      var velocity = Math.sqrt(gesture.velocityY * gesture.velocityY
        + gesture.velocityX * gesture.velocityX)
      var isSwipe = velocity > 0.5 && (now - gesture.lastTime) < 100
      var extra = {
        duration: duration,
        isSwipe: isSwipe,
        velocityX: gesture.velocityX,
        velocityY: gesture.velocityY,
        displacementX: displacementX,
        displacementY: displacementY,
        touch: touch,
        touches: event.touches,
        changedTouches: event.changedTouches,
        touchEvent: event,
        isVertical: gesture.isVertical,
        direction: gesture.direction
      }

      fireEvent(gesture.element, 'panend', extra)
      if (isSwipe) {
        fireEvent(gesture.element, 'swipe', extra)
      }
    }

    if (gesture.status === 'pressing') {
      fireEvent(gesture.element, 'pressend', {
        touch: touch,
        touchEvent: event
      })
    }

    delete gestures[id]
  }

  if (Object.keys(gestures).length === 0) {
    docEl.removeEventListener('touchmove', touchmoveHandler, false)
    docEl.removeEventListener('touchend', touchendHandler, false)
    docEl.removeEventListener('touchcancel', touchcancelHandler, false)
  }
}
function touchendHandler ( event ) { if ( Object . keys ( gestures ) . length == 2 ) { var elements = [ ] for ( var p in gestures ) { elements . push ( gestures [ p ] . element ) } fireEvent ( getCommonAncestor ( elements [ 0 ] , elements [ 1 ] ) , 'dualtouchend' , { touches : slice . call ( event . touches ) , touchEvent : event } ) } for ( var i = 0 ; i < event . changedTouches . length ; i ++ ) { var touch = event . changedTouches [ i ] var id = touch . identifier var gesture = gestures [ id ] if ( ! gesture ) { continue } if ( gesture . pressingHandler ) { clearTimeout ( gesture . pressingHandler ) gesture . pressingHandler = null } if ( gesture . status === 'tapping' ) { gesture . timestamp = Date . now ( ) fireEvent ( gesture . element , 'tap' , { touch : touch , touchEvent : event } ) if ( lastTap && gesture . timestamp - lastTap . timestamp < 300 ) { fireEvent ( gesture . element , 'doubletap' , { touch : touch , touchEvent : event } ) } lastTap = gesture } if ( gesture . status === 'panning' ) { var now = Date . now ( ) var duration = now - gesture . startTime var displacementX = touch . clientX - gesture . startTouch . clientX var displacementY = touch . clientY - gesture . startTouch . clientY var velocity = Math . sqrt ( gesture . velocityY * gesture . velocityY + gesture . velocityX * gesture . velocityX ) var isSwipe = velocity > 0.5 && ( now - gesture . lastTime ) < 100 var extra = { duration : duration , isSwipe : isSwipe , velocityX : gesture . velocityX , velocityY : gesture . velocityY , displacementX : displacementX , displacementY : displacementY , touch : touch , touches : event . touches , changedTouches : event . changedTouches , touchEvent : event , isVertical : gesture . isVertical , direction : gesture . direction } fireEvent ( gesture . element , 'panend' , extra ) if ( isSwipe ) { fireEvent ( gesture . element , 'swipe' , extra ) } } if ( gesture . status === 'pressing' ) { fireEvent ( gesture . element , 'pressend' , { touch : touch , touchEvent : event } ) } delete gestures [ id ] } if ( Object . keys ( gestures ) . length === 0 ) { docEl . removeEventListener ( 'touchmove' , touchmoveHandler , false ) docEl . removeEventListener ( 'touchend' , touchendHandler , false ) docEl . removeEventListener ( 'touchcancel' , touchcancelHandler , false ) } }
handle touchend event

1. if there are tow touch points, then trigger 'dualtouchend'如

2. traverse every touch piont：
> if tapping, then trigger 'tap'.
If there is a tap 300 milliseconds before, then it's a 'doubletap'.
> if padding, then decide to trigger 'panend' or 'swipe'
> if pressing, then trigger 'pressend'.

3. remove listeners.

@event
@param  {event} event
handle touchend event