From f05ce1cd0d3fd0e0a48b9802a9fa3da14ec8d3bd Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Sun, 7 Jan 2018 15:18:20 +0100 Subject: [PATCH 01/11] Add missing return statement This allows onTransformGestureReleased to prevent the default behavior (impossible otherwise) --- src/Gallery.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Gallery.js b/src/Gallery.js index 4b899d7..4f197ad 100644 --- a/src/Gallery.js +++ b/src/Gallery.js @@ -232,7 +232,8 @@ export default class Gallery extends PureComponent { onViewTransformed && onViewTransformed(transform, pageId); })} onTransformGestureReleased={((transform) => { - onTransformGestureReleased && onTransformGestureReleased(transform, pageId); + // need the 'return' here because the return value is checked in ViewTransformer + return onTransformGestureReleased && onTransformGestureReleased(transform, pageId); })} ref={((ref) => { this.imageRefs.set(pageId, ref); })} key={'innerImage#' + pageId} From e62907c826f714296853abe5ede17f474256d27b Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Sun, 7 Jan 2018 15:09:37 +0100 Subject: [PATCH 02/11] =?UTF-8?q?Don=E2=80=99t=20use=20getItemLayout?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same reason as https://github.com/archriss/react-native-snap-carousel/commit/ce3bf0689b6506efddf93f5c699b9e5100515b1b --- src/libraries/ViewPager/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/libraries/ViewPager/index.js b/src/libraries/ViewPager/index.js index e44f515..9720053 100644 --- a/src/libraries/ViewPager/index.js +++ b/src/libraries/ViewPager/index.js @@ -237,6 +237,13 @@ export default class ViewPager extends PureComponent { } getItemLayout (data, index) { + // this method is called 'getItemLayout', but it is not actually used + // as the 'getItemLayout' function for the FlatList. We use it within + // the code on this page though. The reason for this is that working + // with 'getItemLayout' for FlatList is buggy. You might end up with + // unrendered / missing content. Therefore we work around it, as + // described here + // https://github.com/facebook/react-native/issues/15734#issuecomment-330616697 return { length: this.state.width + this.props.pageMargin, offset: (this.state.width + this.props.pageMargin) * index, @@ -310,8 +317,12 @@ export default class ViewPager extends PureComponent { data={pageDataArray} renderItem={this.renderRow} onLayout={this.onLayout} - getItemLayout={this.getItemLayout} - initialScrollIndex={(this.props.initialPage || undefined)} + + // use contentOffset instead of initialScrollIndex so that we don't have + // to use the buggy 'getItemLayout' prop. See + // https://github.com/facebook/react-native/issues/15734#issuecomment-330616697 and + // https://github.com/facebook/react-native/issues/14945#issuecomment-354651271 + contentOffset = {{x: this.getScrollOffsetOfPage(parseInt(this.props.initialPage)), y:0}} /> ); From 5f6213c9c44493ac9e81fbbb5a6e3bbadb02a910 Mon Sep 17 00:00:00 2001 From: Edward Akerboom Date: Sun, 7 Jan 2018 20:40:53 +0100 Subject: [PATCH 03/11] Work around a bug in FlatList More or less the same as https://github.com/archriss/react-native-snap-carousel/commit/24731a0a22a4c7baa1ae635ec0b812692badbe44 --- src/libraries/ViewPager/index.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libraries/ViewPager/index.js b/src/libraries/ViewPager/index.js index 9720053..5b6ecaa 100644 --- a/src/libraries/ViewPager/index.js +++ b/src/libraries/ViewPager/index.js @@ -108,6 +108,14 @@ export default class ViewPager extends PureComponent { const finalX = this.getScrollOffsetOfPage(page); this.scroller.startScroll(this.scroller.getCurrX(), 0, finalX - this.scroller.getCurrX(), 0, 0); + + requestAnimationFrame(() => { + // this is here to work around a bug in FlatList, as discussed here + // https://github.com/facebook/react-native/issues/1831 + // (and solved here https://github.com/facebook/react-native/commit/03ae65bc ?) + this.scrollByOffset(1); + this.scrollByOffset(-1); + }); } componentDidUpdate (prevProps) { From 821b6e9459a9f24318ebbdcfcb75e478b707b971 Mon Sep 17 00:00:00 2001 From: Michael Sageryd Date: Thu, 4 Jan 2018 10:34:25 +0100 Subject: [PATCH 04/11] Rect.equals() - Fixed: rect.bottom was never compared - Feature: now takes an argument for rounded comparison --- .prettierignore | 2 ++ src/libraries/ViewTransformer/Rect.js | 18 ++++++++++++++++-- src/libraries/ViewTransformer/index.js | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..3adbbfa --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +*.js +*.md \ No newline at end of file diff --git a/src/libraries/ViewTransformer/Rect.js b/src/libraries/ViewTransformer/Rect.js index be3abe2..8be21fb 100644 --- a/src/libraries/ViewTransformer/Rect.js +++ b/src/libraries/ViewTransformer/Rect.js @@ -41,8 +41,22 @@ export default class Rect { return new Rect(this.left, this.top, this.right, this.bottom); } - equals (rect) { - return this.left === rect.left && this.top === rect.top && this.right === rect.right && this.bottom && rect.bottom; + equals (rect, epsilon) { + if (!epsilon) { + return ( + this.left === rect.left && + this.top === rect.top && + this.right === rect.right && + this.bottom === rect.bottom + ); + } else { + return ( + Math.abs(this.left - rect.left < epsilon) && + Math.abs(this.top - rect.top < epsilon) && + Math.abs(this.right - rect.right < epsilon) && + Math.abs(this.bottom - rect.bottom < epsilon) + ); + } } isValid () { diff --git a/src/libraries/ViewTransformer/index.js b/src/libraries/ViewTransformer/index.js index d09f6ed..bcc893a 100644 --- a/src/libraries/ViewTransformer/index.js +++ b/src/libraries/ViewTransformer/index.js @@ -50,7 +50,7 @@ export default class ViewTransformer extends React.Component { pageY: 0 }; this._viewPortRect = new Rect(); // A holder to avoid new too much - + this.onLayout = this.onLayout.bind(this); this.cancelAnimation = this.cancelAnimation.bind(this); this.contentRect = this.contentRect.bind(this); @@ -352,7 +352,7 @@ export default class ViewTransformer extends React.Component { } let fromRect = this.transformedContentRect(); - if (fromRect.equals(targetRect)) { + if (fromRect.equals(targetRect, 0.01)) { return; } From 685a8324d55a65810b304e7b3f3188612e787bbf Mon Sep 17 00:00:00 2001 From: Michael Sageryd Date: Thu, 4 Jan 2018 10:51:54 +0100 Subject: [PATCH 05/11] Fixed messed upp parentesis --- src/libraries/ViewTransformer/Rect.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libraries/ViewTransformer/Rect.js b/src/libraries/ViewTransformer/Rect.js index 8be21fb..41b6d46 100644 --- a/src/libraries/ViewTransformer/Rect.js +++ b/src/libraries/ViewTransformer/Rect.js @@ -51,10 +51,10 @@ export default class Rect { ); } else { return ( - Math.abs(this.left - rect.left < epsilon) && - Math.abs(this.top - rect.top < epsilon) && - Math.abs(this.right - rect.right < epsilon) && - Math.abs(this.bottom - rect.bottom < epsilon) + Math.abs(this.left - rect.left) < epsilon && + Math.abs(this.top - rect.top) < epsilon && + Math.abs(this.right - rect.right) < epsilon && + Math.abs(this.bottom - rect.bottom) < epsilon ); } } From 1d7da87eb5f84b6f7b84d7efb3e26fbfe5a5e4da Mon Sep 17 00:00:00 2001 From: Michael Sageryd Date: Thu, 4 Jan 2018 11:35:19 +0100 Subject: [PATCH 06/11] Bundled react-native-scroller and react-native-gesture-responder --- src/Gallery.js | 2 +- .../GestureResponder/TouchDistanceMath.js | 40 ++ .../GestureResponder/TouchHistoryMath.js | 99 +++++ .../GestureResponder/createResponder.js | 316 +++++++++++++++ src/libraries/GestureResponder/index.js | 7 + src/libraries/Scroller/AnimationUtils.js | 3 + .../Scroller/ViscousFluidInterpolator.js | 29 ++ src/libraries/Scroller/index.js | 361 ++++++++++++++++++ src/libraries/ViewPager/index.js | 4 +- src/libraries/ViewTransformer/index.js | 4 +- 10 files changed, 860 insertions(+), 5 deletions(-) create mode 100644 src/libraries/GestureResponder/TouchDistanceMath.js create mode 100644 src/libraries/GestureResponder/TouchHistoryMath.js create mode 100644 src/libraries/GestureResponder/createResponder.js create mode 100644 src/libraries/GestureResponder/index.js create mode 100644 src/libraries/Scroller/AnimationUtils.js create mode 100644 src/libraries/Scroller/ViscousFluidInterpolator.js create mode 100644 src/libraries/Scroller/index.js diff --git a/src/Gallery.js b/src/Gallery.js index 4f197ad..5cc4835 100644 --- a/src/Gallery.js +++ b/src/Gallery.js @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import { View, ViewPropTypes } from 'react-native'; import PropTypes from 'prop-types'; -import { createResponder } from 'react-native-gesture-responder'; +import { createResponder } from './libraries/GestureResponder'; import TransformableImage from './libraries/TransformableImage'; import ViewPager from './libraries/ViewPager'; diff --git a/src/libraries/GestureResponder/TouchDistanceMath.js b/src/libraries/GestureResponder/TouchDistanceMath.js new file mode 100644 index 0000000..2958f05 --- /dev/null +++ b/src/libraries/GestureResponder/TouchDistanceMath.js @@ -0,0 +1,40 @@ +'use strict'; + +export function distance(touchTrackA, touchTrackB, ofCurrent) { + let xa, ya, xb, yb; + if(ofCurrent) { + xa = touchTrackA.currentPageX; + ya = touchTrackA.currentPageY; + xb = touchTrackB.currentPageX; + yb = touchTrackB.currentPageY; + } else { + xa = touchTrackA.previousPageX; + ya = touchTrackA.previousPageY; + xb = touchTrackB.previousPageX; + yb = touchTrackB.previousPageY; + } + return Math.sqrt(Math.pow(xa - xb, 2) + Math.pow(ya - yb, 2)); +} + +export function maxDistance(touchBank, ofCurrent) { + let max = 0; + for(let i = 0; i < touchBank.length - 1; i++) { + for(let j = i+1; j < touchBank.length; j++) { + let d = distance(touchBank[i], touchBank[j], ofCurrent); + if(d > max) { + max = d; + } + } + } + return max; +} + +export function pinchDistance(touchHistory, touchesChangedAfter, ofCurrent) { + let touchBank = touchHistory.touchBank; + if(touchHistory.numberActiveTouches > 1) { + let filteredTouchBank = touchBank.filter((touchTrack) => { + return touchTrack && touchTrack.currentTimeStamp >= touchesChangedAfter; + }); + return maxDistance(filteredTouchBank, ofCurrent); + } +} \ No newline at end of file diff --git a/src/libraries/GestureResponder/TouchHistoryMath.js b/src/libraries/GestureResponder/TouchHistoryMath.js new file mode 100644 index 0000000..a7c36ed --- /dev/null +++ b/src/libraries/GestureResponder/TouchHistoryMath.js @@ -0,0 +1,99 @@ +/** + * @providesModule TouchHistoryMath + */ + +'use strict'; + +var TouchHistoryMath = { + /** + * This code is optimized and not intended to look beautiful. This allows + * computing of touch centroids that have moved after `touchesChangedAfter` + * timeStamp. You can compute the current centroid involving all touches + * moves after `touchesChangedAfter`, or you can compute the previous + * centroid of all touches that were moved after `touchesChangedAfter`. + * + * @param {TouchHistoryMath} touchHistory Standard Responder touch track + * data. + * @param {number} touchesChangedAfter timeStamp after which moved touches + * are considered "actively moving" - not just "active". + * @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension. + * @param {boolean} ofCurrent Compute current centroid for actively moving + * touches vs. previous centroid of now actively moving touches. + * @return {number} value of centroid in specified dimension. + */ + centroidDimension: function (touchHistory, touchesChangedAfter, isXAxis, ofCurrent) { + var touchBank = touchHistory.touchBank; + var total = 0; + var count = 0; + + var oneTouchData = touchHistory.numberActiveTouches === 1 ? touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null; + + if (oneTouchData !== null) { + if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) { + total += ofCurrent && isXAxis ? oneTouchData.currentPageX : ofCurrent && !isXAxis ? oneTouchData.currentPageY : !ofCurrent && isXAxis ? oneTouchData.previousPageX : oneTouchData.previousPageY; + count = 1; + } + } else { + for (var i = 0; i < touchBank.length; i++) { + var touchTrack = touchBank[i]; + if (touchTrack !== null && touchTrack !== undefined && touchTrack.touchActive && touchTrack.currentTimeStamp >= touchesChangedAfter) { + var toAdd; // Yuck, program temporarily in invalid state. + if (ofCurrent && isXAxis) { + toAdd = touchTrack.currentPageX; + } else if (ofCurrent && !isXAxis) { + toAdd = touchTrack.currentPageY; + } else if (!ofCurrent && isXAxis) { + toAdd = touchTrack.previousPageX; + } else { + toAdd = touchTrack.previousPageY; + } + total += toAdd; + count++; + } + } + } + return count > 0 ? total / count : TouchHistoryMath.noCentroid; + }, + + currentCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis + true // ofCurrent + ); + }, + + previousCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis + false // ofCurrent + ); + }, + + previousCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis + false // ofCurrent + ); + }, + + currentCentroidX: function (touchHistory) { + return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter + true, // isXAxis + true // ofCurrent + ); + }, + + currentCentroidY: function (touchHistory) { + return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter + false, // isXAxis + true // ofCurrent + ); + }, + + noCentroid: -1 +}; + +module.exports = TouchHistoryMath; \ No newline at end of file diff --git a/src/libraries/GestureResponder/createResponder.js b/src/libraries/GestureResponder/createResponder.js new file mode 100644 index 0000000..adb3b84 --- /dev/null +++ b/src/libraries/GestureResponder/createResponder.js @@ -0,0 +1,316 @@ +/** + * Inspired by 'PanResponder' from Facebook. + */ + +'use strict'; + +import {InteractionManager} from 'react-native'; +import TouchHistoryMath from './TouchHistoryMath'; //copied from react/lib/TouchHistoryMath.js +import {pinchDistance} from './TouchDistanceMath'; +import TimerMixin from 'react-timer-mixin'; + +const currentCentroidXOfTouchesChangedAfter = TouchHistoryMath.currentCentroidXOfTouchesChangedAfter; +const currentCentroidYOfTouchesChangedAfter = TouchHistoryMath.currentCentroidYOfTouchesChangedAfter; +const previousCentroidXOfTouchesChangedAfter = TouchHistoryMath.previousCentroidXOfTouchesChangedAfter; +const previousCentroidYOfTouchesChangedAfter = TouchHistoryMath.previousCentroidYOfTouchesChangedAfter; +const currentCentroidX = TouchHistoryMath.currentCentroidX; +const currentCentroidY = TouchHistoryMath.currentCentroidY; + +const TAP_UP_TIME_THRESHOLD = 400; +const TAP_MOVE_THRESHOLD = 10; +const MOVE_THRESHOLD = 2; + +let DEV = false; + +function initializeGestureState(gestureState) { + gestureState.moveX = 0; + gestureState.moveY = 0; + gestureState.x0 = 0; + gestureState.y0 = 0; + gestureState.dx = 0; + gestureState.dy = 0; + gestureState.vx = 0; + gestureState.vy = 0; + gestureState.numberActiveTouches = 0; + // All `gestureState` accounts for timeStamps up until: + gestureState._accountsForMovesUpTo = 0; + + + gestureState.previousMoveX = 0; + gestureState.previousMoveY = 0; + gestureState.pinch = undefined; + gestureState.previousPinch = undefined; + gestureState.singleTapUp = false; + gestureState.doubleTapUp = false; + gestureState._singleTabFailed = false; + +} + +function updateGestureStateOnMove(gestureState, touchHistory, e) { + const movedAfter = gestureState._accountsForMovesUpTo; + const prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + const prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + const dx = x - prevX; + const dy = y - prevY; + + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + gestureState.moveX = x; + gestureState.moveY = y; + + // TODO: This must be filtered intelligently. + //const dt = touchHistory.mostRecentTimeStamp - movedAfter; + const dt = convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - movedAfter); + gestureState.vx = dx / dt; + gestureState.vy = dy / dt; + gestureState.dx += dx; + gestureState.dy += dy; + gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; + + + gestureState.previousMoveX = prevX; + gestureState.previousMoveY = prevY; + gestureState.pinch = pinchDistance(touchHistory, movedAfter, true); + gestureState.previousPinch = pinchDistance(touchHistory, movedAfter, false); +} + +function clearInteractionHandle(interactionState) { + if (interactionState.handle) { + InteractionManager.clearInteractionHandle(interactionState.handle); + interactionState.handle = null; + } +} + +/** + * Due to commit https://github.com/facebook/react-native/commit/f2c1868b56bdfc8b0d6f448733848eafed2cd440, + * Android is using nanoseconds while iOS is using milliseconds. + * @param interval + * @returns {*} + */ +function convertToMillisecIfNeeded(interval) { + if (interval > 1000000) { + return interval / 1000000; + } + return interval; +} + +function cancelSingleTapConfirm(gestureState) { + if(typeof gestureState._singleTapConfirmId !== 'undefined') { + TimerMixin.clearTimeout(gestureState._singleTapConfirmId); + gestureState._singleTapConfirmId = undefined; + } +} + +/** + * The config object contains same callbacks as the default gesture responder(https://facebook.github.io/react-native/docs/gesture-responder-system.html). + * And every callback are called with an additional argument 'gestureState', like PanResponder. + * @param config + * @returns {{}} + */ + +/** + * The config object contains same callbacks as the default gesture responder(https://facebook.github.io/react-native/docs/gesture-responder-system.html). + * And every callback are called with an additional argument 'gestureState', like PanResponder. + * @param config + * @param debug true to enable debug logs + * @returns {{}} + */ +export default function create(config) { + if(config.debug) { + DEV = true; + } + + const interactionState = { + handle: null + }; + const gestureState = { + // Useful for debugging + stateID: Math.random(), + }; + initializeGestureState(gestureState); + + const handlers = { + onStartShouldSetResponder: function (e) { + DEV && console.log('onStartShouldSetResponder...'); + cancelSingleTapConfirm(gestureState); + return config.onStartShouldSetResponder ? + config.onStartShouldSetResponder(e, gestureState) : + false; + }, + onMoveShouldSetResponder: function (e) { + DEV && console.log('onMoveShouldSetResponder...'); + + return config.onMoveShouldSetResponder && effectiveMove(config, gestureState) ? + config.onMoveShouldSetResponder(e, gestureState) : + false; + }, + onStartShouldSetResponderCapture: function (e) { + DEV && console.log('onStartShouldSetResponderCapture...'); + cancelSingleTapConfirm(gestureState); + // TODO: Actually, we should reinitialize the state any time + // touches.length increases from 0 active to > 0 active. + if (e.nativeEvent.touches.length === 1) { + initializeGestureState(gestureState); + } + gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; + return config.onStartShouldSetResponderCapture ? + config.onStartShouldSetResponderCapture(e, gestureState) : + false; + }, + + onMoveShouldSetResponderCapture: function (e) { + DEV && console.log('onMoveShouldSetResponderCapture...'); + const touchHistory = e.touchHistory; + // Responder system incorrectly dispatches should* to current responder + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return false; + } + updateGestureStateOnMove(gestureState, touchHistory, e); + return config.onMoveShouldSetResponderCapture && effectiveMove(config, gestureState) ? + config.onMoveShouldSetResponderCapture(e, gestureState) : + false; + }, + + onResponderGrant: function (e) { + DEV && console.log('onResponderGrant...'); + cancelSingleTapConfirm(gestureState); + if (!interactionState.handle) { + interactionState.handle = InteractionManager.createInteractionHandle(); + } + gestureState._grantTimestamp = e.touchHistory.mostRecentTimeStamp; + gestureState.x0 = currentCentroidX(e.touchHistory); + gestureState.y0 = currentCentroidY(e.touchHistory); + gestureState.dx = 0; + gestureState.dy = 0; + if (config.onResponderGrant) { + config.onResponderGrant(e, gestureState); + } + // TODO: t7467124 investigate if this can be removed + return config.onShouldBlockNativeResponder === undefined ? + true : + config.onShouldBlockNativeResponder(); + }, + + onResponderReject: function (e) { + DEV && console.log('onResponderReject...'); + clearInteractionHandle(interactionState); + config.onResponderReject && config.onResponderReject(e, gestureState); + }, + + onResponderRelease: function (e) { + if (gestureState.singleTapUp) { + if (gestureState._lastSingleTapUp) { + if (convertToMillisecIfNeeded(e.touchHistory.mostRecentTimeStamp - gestureState._lastReleaseTimestamp) < TAP_UP_TIME_THRESHOLD) { + gestureState.doubleTapUp = true; + } + } + gestureState._lastSingleTapUp = true; + + //schedule to confirm single tap + if (!gestureState.doubleTapUp) { + const snapshot = Object.assign({}, gestureState); + const timeoutId = TimerMixin.setTimeout(() => { + if (gestureState._singleTapConfirmId === timeoutId) { + DEV && console.log('onResponderSingleTapConfirmed...'); + config.onResponderSingleTapConfirmed && config.onResponderSingleTapConfirmed(e, snapshot); + } + }, TAP_UP_TIME_THRESHOLD); + gestureState._singleTapConfirmId = timeoutId; + } + } + gestureState._lastReleaseTimestamp = e.touchHistory.mostRecentTimeStamp; + + DEV && console.log('onResponderRelease...' + JSON.stringify(gestureState)); + clearInteractionHandle(interactionState); + config.onResponderRelease && config.onResponderRelease(e, gestureState); + initializeGestureState(gestureState); + }, + + onResponderStart: function (e) { + DEV && console.log('onResponderStart...'); + const touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + if (config.onResponderStart) { + config.onResponderStart(e, gestureState); + } + }, + + onResponderMove: function (e) { + const touchHistory = e.touchHistory; + // Guard against the dispatch of two touch moves when there are two + // simultaneously changed touches. + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return; + } + // Filter out any touch moves past the first one - we would have + // already processed multi-touch geometry during the first event. + updateGestureStateOnMove(gestureState, touchHistory, e); + + DEV && console.log('onResponderMove...' + JSON.stringify(gestureState)); + if (config.onResponderMove && effectiveMove(config, gestureState)) { + config.onResponderMove(e, gestureState); + } + }, + + onResponderEnd: function (e) { + const touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + + if (touchHistory.numberActiveTouches > 0 + || convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - gestureState._grantTimestamp) > TAP_UP_TIME_THRESHOLD + || Math.abs(gestureState.dx) >= TAP_MOVE_THRESHOLD + || Math.abs(gestureState.dy) >= TAP_MOVE_THRESHOLD + ) { + gestureState._singleTabFailed = true; + } + if (!gestureState._singleTabFailed) { + gestureState.singleTapUp = true; + } + + DEV && console.log('onResponderEnd...' + JSON.stringify(gestureState)); + clearInteractionHandle(interactionState); + config.onResponderEnd && config.onResponderEnd(e, gestureState); + }, + + onResponderTerminate: function (e) { + DEV && console.log('onResponderTerminate...'); + clearInteractionHandle(interactionState); + config.onResponderTerminate && config.onResponderTerminate(e, gestureState); + initializeGestureState(gestureState); + }, + + onResponderTerminationRequest: function (e) { + DEV && console.log('onResponderTerminationRequest...'); + return config.onResponderTerminationRequest ? + config.onResponderTerminationRequest(e.gestureState) : + true; + } + }; + return {...handlers}; +} + +/** + * On Android devices, the default gesture responder is too sensitive that a single tap(no move intended) may trigger a move event. + * We can use a moveThreshold config to avoid those unwanted move events. + * @param config + * @param gestureState + * @returns {boolean} + */ +function effectiveMove(config, gestureState) { + if (gestureState.numberActiveTouches > 1) { + // on iOS simulator, a pinch gesture(move with alt pressed) will not change gestureState.dx(always 0) + return true; + } + + let moveThreshold = MOVE_THRESHOLD; + if (typeof config.moveThreshold === 'number') { + moveThreshold = config.minMoveDistance; + } + if (Math.abs(gestureState.dx) >= moveThreshold || Math.abs(gestureState.dy) >= moveThreshold) { + return true; + } + return false; +} \ No newline at end of file diff --git a/src/libraries/GestureResponder/index.js b/src/libraries/GestureResponder/index.js new file mode 100644 index 0000000..59028a0 --- /dev/null +++ b/src/libraries/GestureResponder/index.js @@ -0,0 +1,7 @@ +'use strict'; + +import createResponder from './createResponder'; + +export { + createResponder +} \ No newline at end of file diff --git a/src/libraries/Scroller/AnimationUtils.js b/src/libraries/Scroller/AnimationUtils.js new file mode 100644 index 0000000..c9a071f --- /dev/null +++ b/src/libraries/Scroller/AnimationUtils.js @@ -0,0 +1,3 @@ +export function currentAnimationTimeMillis() { + return Date.now(); +} diff --git a/src/libraries/Scroller/ViscousFluidInterpolator.js b/src/libraries/Scroller/ViscousFluidInterpolator.js new file mode 100644 index 0000000..aea80cf --- /dev/null +++ b/src/libraries/Scroller/ViscousFluidInterpolator.js @@ -0,0 +1,29 @@ +'use strict'; + +const VISCOUS_FLUID_SCALE = 8; +const VISCOUS_FLUID_NORMALIZE = 1 / viscousFluid(1); +const VISCOUS_FLUID_OFFSET = 1 - VISCOUS_FLUID_NORMALIZE * viscousFluid(1); + +function viscousFluid(x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1) { + x -= (1 - Math.exp(-x)); + } else { + var start = 0.36787944117; // 1/e == exp(-1) + x = 1 - Math.exp(1 - x); + x = start + x * (1 - start); + } + return x; +} + +const ViscousFluidInterpolator = { + getInterpolation: function(input) { + var interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if(interpolated > 0) { + return interpolated + VISCOUS_FLUID_OFFSET; + } + return interpolated; + } +} + +export default ViscousFluidInterpolator; diff --git a/src/libraries/Scroller/index.js b/src/libraries/Scroller/index.js new file mode 100644 index 0000000..b157ebb --- /dev/null +++ b/src/libraries/Scroller/index.js @@ -0,0 +1,361 @@ +'use strict'; + +/** + * Inspired by Android Scroller + */ + +import ViscousFluidInterpolator from './ViscousFluidInterpolator'; +import {currentAnimationTimeMillis} from'./AnimationUtils'; + +/** + * The coefficient of friction applied to flings/scrolls. + * @type {number} + */ +//const SCROLL_FRICTION = 0.015; +const SCROLL_FRICTION = 0.03; + +const DEFAULT_DURATION = 250; +const SCROLL_MODE = 0; +const FLING_MODE = 1; + +const DECELERATION_RATE = Math.log(0.78) / Math.log(0.9); +const INFLEXION = 0.35; // Tension lines cross at (INFLEXION, 1) +const START_TENSION = 0.5; +const END_TENSION = 1.0; +const P1 = START_TENSION * INFLEXION; +const P2 = 1.0 - END_TENSION * (1.0 - INFLEXION); + +const NB_SAMPLES = 100; +const SPLINE_POSITION = []; +const SPLINE_TIME = []; + +const GRAVITY_EARTH = 9.80665; + +(function () { + var x_min = 0; + var y_min = 0; + for (let i = 0; i < NB_SAMPLES; i++) { + let alpha = i / NB_SAMPLES; + + let x_max = 1; + let x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0; + coef = 3.0 * x * (1.0 - x); + tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x; + + let y_max = 1.0; + let y, dy; + while (true) { + y = y_min + (y_max - y_min) / 2.0; + coef = 3.0 * y * (1.0 - y); + dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y; + } + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0; +})(); + +function signum(number) { + if (isNaN(number)) { + return NaN; + } + var sig = number; + if (number > 0) { + sig = 1; + } + else if (number < 0) { + sig = -1; + } + return sig; +} + +export default class Scroller { + /** + * + * @param flywheel specify whether or not to support progressive "flywheel" behavior in flinging. + */ + constructor(flywheel, onScrollCallback) { + this.mCurrX = 0; + this.mCurrY = 0; + this.mFinished = true; + this.mInterpolator = ViscousFluidInterpolator; + //this.mPpi = PixelRatio.get() * 160; + this.mPpi = 160; + this.mDeceleration = this.computeDeceleration(SCROLL_FRICTION); + this.mFlywheel = flywheel; + + this.mPhysicalCoeff = this.computeDeceleration(0.84); // look and feel tuning + + this.mFlingFriction = SCROLL_FRICTION; + this.onScrollCallback = onScrollCallback; + } + + computeDeceleration(friction) { + return GRAVITY_EARTH * 39.37 * this.mPpi * friction; + } + + /** + * Returns whether the scroller has finished scrolling. + * @returns {Boolean} True if the scroller has finished scrolling, false otherwise. + */ + isFinished() { + return this.mFinished; + } + + /** + * Force the finished field to a particular value. + * @param finished The new finished value. + */ + forceFinished(finished) { + this.mFinished = finished; + } + + /** + * Returns the current X offset in the scroll. + * @returns {*} The new X offset as an absolute distance from the origin. + */ + getCurrX() { + return this.mCurrX; + } + + /** + * Returns the current Y offset in the scroll. + * @returns {*} The new Y offset as an absolute distance from the origin. + */ + getCurrY() { + return this.mCurrY; + } + + getCurrVelocity() { + return this.mMode === FLING_MODE ? + this.mCurrVelocity : this.mVelocity - this.mDeceleration * this.timePassed() / 2000.0; + } + + computeScrollOffset() { + if (this.mFinished) { + this.onScrollCallback && this.onScrollCallback(0, 0, this); + return false; + } + + var timePassed = currentAnimationTimeMillis() - this.mStartTime; + + if (timePassed < this.mDuration) { + switch (this.mMode) { + case SCROLL_MODE: + let x = this.mInterpolator.getInterpolation(timePassed * this.mDurationReciprocal); + this.mCurrX = this.mStartX + Math.round(x * this.mDeltaX); + this.mCurrY = this.mStartY + Math.round(x * this.mDeltaY); + break; + case FLING_MODE: + let t = timePassed / this.mDuration; + let index = parseInt(NB_SAMPLES * t); + let distanceCoef = 1; + let velocityCoef = 0; + if (index < NB_SAMPLES) { + let t_inf = index / NB_SAMPLES; + let t_sup = (index + 1) / NB_SAMPLES; + let d_inf = SPLINE_POSITION[index]; + let d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + + this.mCurrVelocity = velocityCoef * this.mDistance / this.mDuration * 1000; + + this.mCurrX = this.mStartX + Math.round(distanceCoef * (this.mFinalX - this.mStartX)); + // Pin to mMinX <= mCurrX <= mMaxX + //this.mCurrX = Math.min(this.mCurrX, this.mMaxX); + //this.mCurrX = Math.max(this.mCurrX, this.mMinX); + + this.mCurrY = this.mStartY + Math.round(distanceCoef * (this.mFinalY - this.mStartY)); + // Pin to mMinY <= mCurrY <= mMaxY + this.mCurrY = Math.min(this.mCurrY, this.mMaxY); + this.mCurrY = Math.max(this.mCurrY, this.mMinY); + + if (this.mCurrX == this.mFinalX && this.mCurrY == this.mFinalY) { + this.mFinished = true; + } + + break; + } + } + else { + this.mCurrX = this.mFinalX; + this.mCurrY = this.mFinalY; + this.mFinished = true; + } + + var dx = this.mCurrX - this.mLastX; + var dy = this.mCurrY - this.mLastY; + + this.mLastX = this.mCurrX; + this.mLastY = this.mCurrY; + + this.onScrollCallback && this.onScrollCallback(dx, dy, this); + + if(dx === 0 && dy === 0 && this.mFinished) { + return false; + } + return true; + } + + startScroll(startX, startY, dx, dy) { + startScroll(startX, startY, dx, dy, DEFAULT_DURATION); + } + + startScroll(startX, startY, dx, dy, duration) { + this.mMode = SCROLL_MODE; + this.mFinished = false; + this.mDuration = duration; + this.mStartTime = currentAnimationTimeMillis(); + this.mStartX = startX; + this.mStartY = startY; + this.mFinalX = startX + dx; + this.mFinalY = startY + dy; + this.mDeltaX = dx; + this.mDeltaY = dy; + this.mDurationReciprocal = 1.0 / this.mDuration; + + this.mLastX = this.mStartX; + this.mLastY = this.mStartY; + + this.performAnimation(); + } + + /** + * Start scrolling based on a fling gesture. The distance travelled will + * depend on the initial velocity of the fling. + * @param startX + * @param startY + * @param velocityX Initial velocity of the fling (X) measured in dp or pt per second + * @param velocityY Initial velocity of the fling (Y) measured in dp or pt per second + * @param minX + * @param maxX + * @param minY + * @param maxY + */ + fling(startX, startY, velocityX, velocityY, + minX, maxX, minY, maxY) { + // Continue a scroll or fling in progress + if (this.mFlywheel && !this.mFinished) { + let oldVel = this.getCurrVelocity(); + + let dx = this.mFinalX - this.mStartX; + let dy = this.mFinalY - this.mStartY; + let hyp = Math.hypot(dx, dy); + + let ndx = dx / hyp; + let ndy = dy / hyp; + + let oldVelocityX = ndx * oldVel; + let oldVelocityY = ndy * oldVel; + if (signum(velocityX) === signum(oldVelocityX) && + signum(velocityY) === signum(oldVelocityY)) { + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } + + this.mMode = FLING_MODE; + this.mFinished = false; + + let velocity = Math.hypot(velocityX, velocityY); + + this.mVelocity = velocity; + this.mDuration = this.getSplineFlingDuration(velocity); + this.mStartTime = currentAnimationTimeMillis(); + this.mStartX = startX; + this.mStartY = startY; + + let coeffX = velocity == 0 ? 1.0 : velocityX / velocity; + let coeffY = velocity == 0 ? 1.0 : velocityY / velocity; + + let totalDistance = this.getSplineFlingDistance(velocity); + this.mDistance = totalDistance * signum(velocity); + + this.mMinX = minX; + this.mMaxX = maxX; + this.mMinY = minY; + this.mMaxY = maxY; + + this.mFinalX = startX + Math.round(totalDistance * coeffX); + // Pin to mMinX <= mFinalX <= mMaxX + this.mFinalX = Math.min(this.mFinalX, this.mMaxX); + this.mFinalX = Math.max(this.mFinalX, this.mMinX); + + this.mFinalY = startY + Math.round(totalDistance * coeffY); + // Pin to mMinY <= mFinalY <= mMaxY + this.mFinalY = Math.min(this.mFinalY, this.mMaxY); + this.mFinalY = Math.max(this.mFinalY, this.mMinY); + + this.mLastX = this.mStartX; + this.mLastY = this.mStartY; + + this.performAnimation(); + } + + getSplineDeceleration(velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (this.mFlingFriction * this.mPhysicalCoeff)); + } + + getSplineFlingDuration(velocity) { + var l = this.getSplineDeceleration(velocity); + var decelMinusOne = DECELERATION_RATE - 1.0; + return 1000.0 * Math.exp(l / decelMinusOne); + } + + getSplineFlingDistance(velocity) { + var l = this.getSplineDeceleration(velocity); + var decelMinusOne = DECELERATION_RATE - 1.0; + return this.mFlingFriction * this.mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + performAnimation() { + if (this.computeScrollOffset()) { + requestAnimationFrame(this.performAnimation.bind(this)); + } else { + } + } + + abortAnimation() { + this.mCurrX = this.mFinalX; + this.mCurrY = this.mFinalY; + this.mFinished = true; + } + + extendDuration(extend) { + var passed = timePassed(); + this.mDuration = passed + extend; + this.mDurationReciprocal = 1.0 / this.mDuration; + this.mFinished = false; + } + + timePassed() { + return currentAnimationTimeMillis() - this.mStartTime; + } + + setFinalX(newX) { + this.mFinalX = newX; + this.mDeltaX = this.mFinalX - this.mStartX; + this.mFinished = false; + } + + setFinalY(newY) { + this.mFinalY = newY; + this.mDeltaY = this.mFinalY - this.mStartY; + this.mFinished = false; + } + + debugInfo() { + return 'cur=' + this.mCurrX + ' ' + this.mCurrY + ', final=' + this.mFinalX + ' ' + this.mFinalY; + } +} \ No newline at end of file diff --git a/src/libraries/ViewPager/index.js b/src/libraries/ViewPager/index.js index 5b6ecaa..b786ad1 100644 --- a/src/libraries/ViewPager/index.js +++ b/src/libraries/ViewPager/index.js @@ -7,8 +7,8 @@ import { Dimensions } from 'react-native'; import PropTypes from 'prop-types'; -import Scroller from 'react-native-scroller'; -import { createResponder } from 'react-native-gesture-responder'; +import Scroller from '../Scroller'; +import { createResponder } from '../GestureResponder'; const MIN_FLING_VELOCITY = 0.5; diff --git a/src/libraries/ViewTransformer/index.js b/src/libraries/ViewTransformer/index.js index bcc893a..f919e99 100644 --- a/src/libraries/ViewTransformer/index.js +++ b/src/libraries/ViewTransformer/index.js @@ -1,8 +1,8 @@ import React from 'react'; import ReactNative, { View, Animated, Easing, NativeModules } from 'react-native'; -import Scroller from 'react-native-scroller'; +import Scroller from '../Scroller'; import PropTypes from 'prop-types'; -import { createResponder } from 'react-native-gesture-responder'; +import { createResponder } from '../GestureResponder'; import { Rect, Transform, transformedRect, availableTranslateSpace, fitCenterRect, alignedRect, getTransform } from './TransformUtils'; export default class ViewTransformer extends React.Component { From 0f8244ac5d99d6b53dc2cde21a64907664119c4d Mon Sep 17 00:00:00 2001 From: Michael Sageryd Date: Thu, 4 Jan 2018 11:59:22 +0100 Subject: [PATCH 07/11] Removed unused deps from package.json --- package.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/package.json b/package.json index ac1aa3e..e77317c 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,6 @@ "homepage": "https://github.com/archriss/react-native-image-gallery#readme", "dependencies": { "prop-types": "^15.6.0", - "react-native-gesture-responder": "0.1.1", - "react-native-scroller": "0.0.6", "react-mixin": "^3.0.5", "react-timer-mixin": "^0.13.3" }, From 0cbc19bff452b55fe939de85fd48934a8d83cdc7 Mon Sep 17 00:00:00 2001 From: Maxime Bertonnier Date: Fri, 12 Jan 2018 15:03:39 +0100 Subject: [PATCH 08/11] chore(demo): update dependencies & react-native version --- Demo/package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Demo/package.json b/Demo/package.json index d98a358..67f0c04 100644 --- a/Demo/package.json +++ b/Demo/package.json @@ -7,15 +7,15 @@ "test": "jest" }, "dependencies": { - "react": "16.0.0-alpha.12", - "react-native": "0.48.2", - "react-native-image-gallery": "2.1.3" + "react": "16.2.0", + "react-native": "0.52.0", + "react-native-image-gallery": "2.1.4" }, "devDependencies": { - "babel-jest": "21.0.2", - "babel-preset-react-native": "3.0.2", - "jest": "21.0.2", - "react-test-renderer": "16.0.0-alpha.12" + "babel-jest": "22.0.6", + "babel-preset-react-native": "4.0.0", + "jest": "22.0.6", + "react-test-renderer": "16.2.0" }, "jest": { "preset": "react-native" From e36a35e04d942479946ce44927c451b9e37972e2 Mon Sep 17 00:00:00 2001 From: Maxime Bertonnier Date: Fri, 12 Jan 2018 15:23:42 +0100 Subject: [PATCH 09/11] chore(Scroller): lint baked-in dependency --- src/libraries/Scroller/AnimationUtils.js | 2 +- .../Scroller/ViscousFluidInterpolator.js | 34 +- src/libraries/Scroller/index.js | 504 +++++++++--------- 3 files changed, 267 insertions(+), 273 deletions(-) diff --git a/src/libraries/Scroller/AnimationUtils.js b/src/libraries/Scroller/AnimationUtils.js index c9a071f..8dfd258 100644 --- a/src/libraries/Scroller/AnimationUtils.js +++ b/src/libraries/Scroller/AnimationUtils.js @@ -1,3 +1,3 @@ export function currentAnimationTimeMillis() { - return Date.now(); + return Date.now(); } diff --git a/src/libraries/Scroller/ViscousFluidInterpolator.js b/src/libraries/Scroller/ViscousFluidInterpolator.js index aea80cf..263f32b 100644 --- a/src/libraries/Scroller/ViscousFluidInterpolator.js +++ b/src/libraries/Scroller/ViscousFluidInterpolator.js @@ -4,26 +4,26 @@ const VISCOUS_FLUID_SCALE = 8; const VISCOUS_FLUID_NORMALIZE = 1 / viscousFluid(1); const VISCOUS_FLUID_OFFSET = 1 - VISCOUS_FLUID_NORMALIZE * viscousFluid(1); -function viscousFluid(x) { - x *= VISCOUS_FLUID_SCALE; - if (x < 1) { - x -= (1 - Math.exp(-x)); - } else { - var start = 0.36787944117; // 1/e == exp(-1) - x = 1 - Math.exp(1 - x); - x = start + x * (1 - start); - } - return x; +function viscousFluid (x) { + x *= VISCOUS_FLUID_SCALE; + if (x < 1) { + x -= (1 - Math.exp(-x)); + } else { + var start = 0.36787944117; // 1/e == exp(-1) + x = 1 - Math.exp(1 - x); + x = start + x * (1 - start); + } + return x; } const ViscousFluidInterpolator = { - getInterpolation: function(input) { - var interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); - if(interpolated > 0) { - return interpolated + VISCOUS_FLUID_OFFSET; + getInterpolation: function (input) { + var interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input); + if (interpolated > 0) { + return interpolated + VISCOUS_FLUID_OFFSET; + } + return interpolated; } - return interpolated; - } -} +}; export default ViscousFluidInterpolator; diff --git a/src/libraries/Scroller/index.js b/src/libraries/Scroller/index.js index b157ebb..fde05f0 100644 --- a/src/libraries/Scroller/index.js +++ b/src/libraries/Scroller/index.js @@ -5,13 +5,13 @@ */ import ViscousFluidInterpolator from './ViscousFluidInterpolator'; -import {currentAnimationTimeMillis} from'./AnimationUtils'; +import {currentAnimationTimeMillis} from './AnimationUtils'; /** * The coefficient of friction applied to flings/scrolls. * @type {number} */ -//const SCROLL_FRICTION = 0.015; +// const SCROLL_FRICTION = 0.015; const SCROLL_FRICTION = 0.03; const DEFAULT_DURATION = 250; @@ -32,50 +32,49 @@ const SPLINE_TIME = []; const GRAVITY_EARTH = 9.80665; (function () { - var x_min = 0; - var y_min = 0; - for (let i = 0; i < NB_SAMPLES; i++) { - let alpha = i / NB_SAMPLES; - - let x_max = 1; - let x, tx, coef; - while (true) { - x = x_min + (x_max - x_min) / 2.0; - coef = 3.0 * x * (1.0 - x); - tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x; - if (Math.abs(tx - alpha) < 1E-5) break; - if (tx > alpha) x_max = x; - else x_min = x; + var x_min = 0; + var y_min = 0; + for (let i = 0; i < NB_SAMPLES; i++) { + let alpha = i / NB_SAMPLES; + + let x_max = 1; + let x, tx, coef; + while (true) { + x = x_min + (x_max - x_min) / 2.0; + coef = 3.0 * x * (1.0 - x); + tx = coef * ((1.0 - x) * P1 + x * P2) + x * x * x; + if (Math.abs(tx - alpha) < 1E-5) break; + if (tx > alpha) x_max = x; + else x_min = x; + } + SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x; + + let y_max = 1.0; + let y, dy; + while (true) { + y = y_min + (y_max - y_min) / 2.0; + coef = 3.0 * y * (1.0 - y); + dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y; + if (Math.abs(dy - alpha) < 1E-5) break; + if (dy > alpha) y_max = y; + else y_min = y; + } + SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y; } - SPLINE_POSITION[i] = coef * ((1.0 - x) * START_TENSION + x) + x * x * x; - - let y_max = 1.0; - let y, dy; - while (true) { - y = y_min + (y_max - y_min) / 2.0; - coef = 3.0 * y * (1.0 - y); - dy = coef * ((1.0 - y) * START_TENSION + y) + y * y * y; - if (Math.abs(dy - alpha) < 1E-5) break; - if (dy > alpha) y_max = y; - else y_min = y; - } - SPLINE_TIME[i] = coef * ((1.0 - y) * P1 + y * P2) + y * y * y; - } - SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0; + SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0; })(); -function signum(number) { - if (isNaN(number)) { - return NaN; - } - var sig = number; - if (number > 0) { - sig = 1; - } - else if (number < 0) { - sig = -1; - } - return sig; +function signum (number) { + if (isNaN(number)) { + return NaN; + } + var sig = number; + if (number > 0) { + sig = 1; + } else if (number < 0) { + sig = -1; + } + return sig; } export default class Scroller { @@ -83,153 +82,148 @@ export default class Scroller { * * @param flywheel specify whether or not to support progressive "flywheel" behavior in flinging. */ - constructor(flywheel, onScrollCallback) { - this.mCurrX = 0; - this.mCurrY = 0; - this.mFinished = true; - this.mInterpolator = ViscousFluidInterpolator; - //this.mPpi = PixelRatio.get() * 160; - this.mPpi = 160; - this.mDeceleration = this.computeDeceleration(SCROLL_FRICTION); - this.mFlywheel = flywheel; - - this.mPhysicalCoeff = this.computeDeceleration(0.84); // look and feel tuning - - this.mFlingFriction = SCROLL_FRICTION; - this.onScrollCallback = onScrollCallback; - } - - computeDeceleration(friction) { - return GRAVITY_EARTH * 39.37 * this.mPpi * friction; - } + constructor (flywheel, onScrollCallback) { + this.mCurrX = 0; + this.mCurrY = 0; + this.mFinished = true; + this.mInterpolator = ViscousFluidInterpolator; + // this.mPpi = PixelRatio.get() * 160; + this.mPpi = 160; + this.mDeceleration = this.computeDeceleration(SCROLL_FRICTION); + this.mFlywheel = flywheel; + + this.mPhysicalCoeff = this.computeDeceleration(0.84); // look and feel tuning + + this.mFlingFriction = SCROLL_FRICTION; + this.onScrollCallback = onScrollCallback; + } + + computeDeceleration (friction) { + return GRAVITY_EARTH * 39.37 * this.mPpi * friction; + } /** * Returns whether the scroller has finished scrolling. * @returns {Boolean} True if the scroller has finished scrolling, false otherwise. */ - isFinished() { - return this.mFinished; - } + isFinished () { + return this.mFinished; + } /** * Force the finished field to a particular value. * @param finished The new finished value. */ - forceFinished(finished) { - this.mFinished = finished; - } + forceFinished (finished) { + this.mFinished = finished; + } /** * Returns the current X offset in the scroll. * @returns {*} The new X offset as an absolute distance from the origin. */ - getCurrX() { - return this.mCurrX; - } + getCurrX () { + return this.mCurrX; + } /** * Returns the current Y offset in the scroll. * @returns {*} The new Y offset as an absolute distance from the origin. */ - getCurrY() { - return this.mCurrY; - } + getCurrY () { + return this.mCurrY; + } - getCurrVelocity() { - return this.mMode === FLING_MODE ? + getCurrVelocity () { + return this.mMode === FLING_MODE ? this.mCurrVelocity : this.mVelocity - this.mDeceleration * this.timePassed() / 2000.0; - } - - computeScrollOffset() { - if (this.mFinished) { - this.onScrollCallback && this.onScrollCallback(0, 0, this); - return false; } - var timePassed = currentAnimationTimeMillis() - this.mStartTime; - - if (timePassed < this.mDuration) { - switch (this.mMode) { - case SCROLL_MODE: - let x = this.mInterpolator.getInterpolation(timePassed * this.mDurationReciprocal); - this.mCurrX = this.mStartX + Math.round(x * this.mDeltaX); - this.mCurrY = this.mStartY + Math.round(x * this.mDeltaY); - break; - case FLING_MODE: - let t = timePassed / this.mDuration; - let index = parseInt(NB_SAMPLES * t); - let distanceCoef = 1; - let velocityCoef = 0; - if (index < NB_SAMPLES) { - let t_inf = index / NB_SAMPLES; - let t_sup = (index + 1) / NB_SAMPLES; - let d_inf = SPLINE_POSITION[index]; - let d_sup = SPLINE_POSITION[index + 1]; - velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); - distanceCoef = d_inf + (t - t_inf) * velocityCoef; - } - - this.mCurrVelocity = velocityCoef * this.mDistance / this.mDuration * 1000; - - this.mCurrX = this.mStartX + Math.round(distanceCoef * (this.mFinalX - this.mStartX)); + computeScrollOffset () { + if (this.mFinished) { + this.onScrollCallback && this.onScrollCallback(0, 0, this); + return false; + } + + var timePassed = currentAnimationTimeMillis() - this.mStartTime; + + if (timePassed < this.mDuration) { + switch (this.mMode) { + case SCROLL_MODE: + let x = this.mInterpolator.getInterpolation(timePassed * this.mDurationReciprocal); + this.mCurrX = this.mStartX + Math.round(x * this.mDeltaX); + this.mCurrY = this.mStartY + Math.round(x * this.mDeltaY); + break; + case FLING_MODE: + let t = timePassed / this.mDuration; + let index = parseInt(NB_SAMPLES * t); + let distanceCoef = 1; + let velocityCoef = 0; + if (index < NB_SAMPLES) { + let t_inf = index / NB_SAMPLES; + let t_sup = (index + 1) / NB_SAMPLES; + let d_inf = SPLINE_POSITION[index]; + let d_sup = SPLINE_POSITION[index + 1]; + velocityCoef = (d_sup - d_inf) / (t_sup - t_inf); + distanceCoef = d_inf + (t - t_inf) * velocityCoef; + } + + this.mCurrVelocity = velocityCoef * this.mDistance / this.mDuration * 1000; + + this.mCurrX = this.mStartX + Math.round(distanceCoef * (this.mFinalX - this.mStartX)); // Pin to mMinX <= mCurrX <= mMaxX - //this.mCurrX = Math.min(this.mCurrX, this.mMaxX); - //this.mCurrX = Math.max(this.mCurrX, this.mMinX); + // this.mCurrX = Math.min(this.mCurrX, this.mMaxX); + // this.mCurrX = Math.max(this.mCurrX, this.mMinX); - this.mCurrY = this.mStartY + Math.round(distanceCoef * (this.mFinalY - this.mStartY)); + this.mCurrY = this.mStartY + Math.round(distanceCoef * (this.mFinalY - this.mStartY)); // Pin to mMinY <= mCurrY <= mMaxY - this.mCurrY = Math.min(this.mCurrY, this.mMaxY); - this.mCurrY = Math.max(this.mCurrY, this.mMinY); - - if (this.mCurrX == this.mFinalX && this.mCurrY == this.mFinalY) { + this.mCurrY = Math.min(this.mCurrY, this.mMaxY); + this.mCurrY = Math.max(this.mCurrY, this.mMinY); + + if (this.mCurrX == this.mFinalX && this.mCurrY == this.mFinalY) { + this.mFinished = true; + } + + break; + } + } else { + this.mCurrX = this.mFinalX; + this.mCurrY = this.mFinalY; this.mFinished = true; - } + } - break; - } - } - else { - this.mCurrX = this.mFinalX; - this.mCurrY = this.mFinalY; - this.mFinished = true; - } + var dx = this.mCurrX - this.mLastX; + var dy = this.mCurrY - this.mLastY; - var dx = this.mCurrX - this.mLastX; - var dy = this.mCurrY - this.mLastY; + this.mLastX = this.mCurrX; + this.mLastY = this.mCurrY; - this.mLastX = this.mCurrX; - this.mLastY = this.mCurrY; + this.onScrollCallback && this.onScrollCallback(dx, dy, this); - this.onScrollCallback && this.onScrollCallback(dx, dy, this); + if (dx === 0 && dy === 0 && this.mFinished) { + return false; + } + return true; + } - if(dx === 0 && dy === 0 && this.mFinished) { - return false; + startScroll (startX, startY, dx, dy, duration = DEFAULT_DURATION) { + this.mMode = SCROLL_MODE; + this.mFinished = false; + this.mDuration = duration; + this.mStartTime = currentAnimationTimeMillis(); + this.mStartX = startX; + this.mStartY = startY; + this.mFinalX = startX + dx; + this.mFinalY = startY + dy; + this.mDeltaX = dx; + this.mDeltaY = dy; + this.mDurationReciprocal = 1.0 / this.mDuration; + + this.mLastX = this.mStartX; + this.mLastY = this.mStartY; + + this.performAnimation(); } - return true; - } - - startScroll(startX, startY, dx, dy) { - startScroll(startX, startY, dx, dy, DEFAULT_DURATION); - } - - startScroll(startX, startY, dx, dy, duration) { - this.mMode = SCROLL_MODE; - this.mFinished = false; - this.mDuration = duration; - this.mStartTime = currentAnimationTimeMillis(); - this.mStartX = startX; - this.mStartY = startY; - this.mFinalX = startX + dx; - this.mFinalY = startY + dy; - this.mDeltaX = dx; - this.mDeltaY = dy; - this.mDurationReciprocal = 1.0 / this.mDuration; - - this.mLastX = this.mStartX; - this.mLastY = this.mStartY; - - this.performAnimation(); - } /** * Start scrolling based on a fling gesture. The distance travelled will @@ -243,119 +237,119 @@ export default class Scroller { * @param minY * @param maxY */ - fling(startX, startY, velocityX, velocityY, + fling (startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) { // Continue a scroll or fling in progress - if (this.mFlywheel && !this.mFinished) { - let oldVel = this.getCurrVelocity(); + if (this.mFlywheel && !this.mFinished) { + let oldVel = this.getCurrVelocity(); - let dx = this.mFinalX - this.mStartX; - let dy = this.mFinalY - this.mStartY; - let hyp = Math.hypot(dx, dy); + let dx = this.mFinalX - this.mStartX; + let dy = this.mFinalY - this.mStartY; + let hyp = Math.hypot(dx, dy); - let ndx = dx / hyp; - let ndy = dy / hyp; + let ndx = dx / hyp; + let ndy = dy / hyp; - let oldVelocityX = ndx * oldVel; - let oldVelocityY = ndy * oldVel; - if (signum(velocityX) === signum(oldVelocityX) && + let oldVelocityX = ndx * oldVel; + let oldVelocityY = ndy * oldVel; + if (signum(velocityX) === signum(oldVelocityX) && signum(velocityY) === signum(oldVelocityY)) { - velocityX += oldVelocityX; - velocityY += oldVelocityY; - } - } + velocityX += oldVelocityX; + velocityY += oldVelocityY; + } + } - this.mMode = FLING_MODE; - this.mFinished = false; + this.mMode = FLING_MODE; + this.mFinished = false; - let velocity = Math.hypot(velocityX, velocityY); + let velocity = Math.hypot(velocityX, velocityY); - this.mVelocity = velocity; - this.mDuration = this.getSplineFlingDuration(velocity); - this.mStartTime = currentAnimationTimeMillis(); - this.mStartX = startX; - this.mStartY = startY; + this.mVelocity = velocity; + this.mDuration = this.getSplineFlingDuration(velocity); + this.mStartTime = currentAnimationTimeMillis(); + this.mStartX = startX; + this.mStartY = startY; - let coeffX = velocity == 0 ? 1.0 : velocityX / velocity; - let coeffY = velocity == 0 ? 1.0 : velocityY / velocity; + let coeffX = velocity == 0 ? 1.0 : velocityX / velocity; + let coeffY = velocity == 0 ? 1.0 : velocityY / velocity; - let totalDistance = this.getSplineFlingDistance(velocity); - this.mDistance = totalDistance * signum(velocity); + let totalDistance = this.getSplineFlingDistance(velocity); + this.mDistance = totalDistance * signum(velocity); - this.mMinX = minX; - this.mMaxX = maxX; - this.mMinY = minY; - this.mMaxY = maxY; + this.mMinX = minX; + this.mMaxX = maxX; + this.mMinY = minY; + this.mMaxY = maxY; - this.mFinalX = startX + Math.round(totalDistance * coeffX); + this.mFinalX = startX + Math.round(totalDistance * coeffX); // Pin to mMinX <= mFinalX <= mMaxX - this.mFinalX = Math.min(this.mFinalX, this.mMaxX); - this.mFinalX = Math.max(this.mFinalX, this.mMinX); + this.mFinalX = Math.min(this.mFinalX, this.mMaxX); + this.mFinalX = Math.max(this.mFinalX, this.mMinX); - this.mFinalY = startY + Math.round(totalDistance * coeffY); + this.mFinalY = startY + Math.round(totalDistance * coeffY); // Pin to mMinY <= mFinalY <= mMaxY - this.mFinalY = Math.min(this.mFinalY, this.mMaxY); - this.mFinalY = Math.max(this.mFinalY, this.mMinY); - - this.mLastX = this.mStartX; - this.mLastY = this.mStartY; - - this.performAnimation(); - } - - getSplineDeceleration(velocity) { - return Math.log(INFLEXION * Math.abs(velocity) / (this.mFlingFriction * this.mPhysicalCoeff)); - } - - getSplineFlingDuration(velocity) { - var l = this.getSplineDeceleration(velocity); - var decelMinusOne = DECELERATION_RATE - 1.0; - return 1000.0 * Math.exp(l / decelMinusOne); - } - - getSplineFlingDistance(velocity) { - var l = this.getSplineDeceleration(velocity); - var decelMinusOne = DECELERATION_RATE - 1.0; - return this.mFlingFriction * this.mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); - } - - performAnimation() { - if (this.computeScrollOffset()) { - requestAnimationFrame(this.performAnimation.bind(this)); - } else { + this.mFinalY = Math.min(this.mFinalY, this.mMaxY); + this.mFinalY = Math.max(this.mFinalY, this.mMinY); + + this.mLastX = this.mStartX; + this.mLastY = this.mStartY; + + this.performAnimation(); + } + + getSplineDeceleration (velocity) { + return Math.log(INFLEXION * Math.abs(velocity) / (this.mFlingFriction * this.mPhysicalCoeff)); } - } - - abortAnimation() { - this.mCurrX = this.mFinalX; - this.mCurrY = this.mFinalY; - this.mFinished = true; - } - - extendDuration(extend) { - var passed = timePassed(); - this.mDuration = passed + extend; - this.mDurationReciprocal = 1.0 / this.mDuration; - this.mFinished = false; - } - - timePassed() { - return currentAnimationTimeMillis() - this.mStartTime; - } - - setFinalX(newX) { - this.mFinalX = newX; - this.mDeltaX = this.mFinalX - this.mStartX; - this.mFinished = false; - } - - setFinalY(newY) { - this.mFinalY = newY; - this.mDeltaY = this.mFinalY - this.mStartY; - this.mFinished = false; - } - - debugInfo() { - return 'cur=' + this.mCurrX + ' ' + this.mCurrY + ', final=' + this.mFinalX + ' ' + this.mFinalY; - } -} \ No newline at end of file + + getSplineFlingDuration (velocity) { + var l = this.getSplineDeceleration(velocity); + var decelMinusOne = DECELERATION_RATE - 1.0; + return 1000.0 * Math.exp(l / decelMinusOne); + } + + getSplineFlingDistance (velocity) { + var l = this.getSplineDeceleration(velocity); + var decelMinusOne = DECELERATION_RATE - 1.0; + return this.mFlingFriction * this.mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l); + } + + performAnimation () { + if (this.computeScrollOffset()) { + requestAnimationFrame(this.performAnimation.bind(this)); + } else { + } + } + + abortAnimation () { + this.mCurrX = this.mFinalX; + this.mCurrY = this.mFinalY; + this.mFinished = true; + } + + extendDuration (extend) { + var passed = timePassed(); + this.mDuration = passed + extend; + this.mDurationReciprocal = 1.0 / this.mDuration; + this.mFinished = false; + } + + timePassed () { + return currentAnimationTimeMillis() - this.mStartTime; + } + + setFinalX (newX) { + this.mFinalX = newX; + this.mDeltaX = this.mFinalX - this.mStartX; + this.mFinished = false; + } + + setFinalY (newY) { + this.mFinalY = newY; + this.mDeltaY = this.mFinalY - this.mStartY; + this.mFinished = false; + } + + debugInfo () { + return 'cur=' + this.mCurrX + ' ' + this.mCurrY + ', final=' + this.mFinalX + ' ' + this.mFinalY; + } +} From caf9728b4f82cad18f332a028c90539d7ae6b518 Mon Sep 17 00:00:00 2001 From: Maxime Bertonnier Date: Fri, 12 Jan 2018 15:32:55 +0100 Subject: [PATCH 10/11] chore(GestureResponder): lint baked-in dependency --- .../GestureResponder/TouchDistanceMath.js | 66 +-- .../GestureResponder/TouchHistoryMath.js | 100 ++-- .../GestureResponder/createResponder.js | 441 +++++++++--------- src/libraries/GestureResponder/index.js | 4 +- 4 files changed, 303 insertions(+), 308 deletions(-) diff --git a/src/libraries/GestureResponder/TouchDistanceMath.js b/src/libraries/GestureResponder/TouchDistanceMath.js index 2958f05..89b74c2 100644 --- a/src/libraries/GestureResponder/TouchDistanceMath.js +++ b/src/libraries/GestureResponder/TouchDistanceMath.js @@ -1,40 +1,40 @@ 'use strict'; -export function distance(touchTrackA, touchTrackB, ofCurrent) { - let xa, ya, xb, yb; - if(ofCurrent) { - xa = touchTrackA.currentPageX; - ya = touchTrackA.currentPageY; - xb = touchTrackB.currentPageX; - yb = touchTrackB.currentPageY; - } else { - xa = touchTrackA.previousPageX; - ya = touchTrackA.previousPageY; - xb = touchTrackB.previousPageX; - yb = touchTrackB.previousPageY; - } - return Math.sqrt(Math.pow(xa - xb, 2) + Math.pow(ya - yb, 2)); +export function distance (touchTrackA, touchTrackB, ofCurrent) { + let xa, ya, xb, yb; + if (ofCurrent) { + xa = touchTrackA.currentPageX; + ya = touchTrackA.currentPageY; + xb = touchTrackB.currentPageX; + yb = touchTrackB.currentPageY; + } else { + xa = touchTrackA.previousPageX; + ya = touchTrackA.previousPageY; + xb = touchTrackB.previousPageX; + yb = touchTrackB.previousPageY; + } + return Math.sqrt(Math.pow(xa - xb, 2) + Math.pow(ya - yb, 2)); } -export function maxDistance(touchBank, ofCurrent) { - let max = 0; - for(let i = 0; i < touchBank.length - 1; i++) { - for(let j = i+1; j < touchBank.length; j++) { - let d = distance(touchBank[i], touchBank[j], ofCurrent); - if(d > max) { - max = d; - } +export function maxDistance (touchBank, ofCurrent) { + let max = 0; + for (let i = 0; i < touchBank.length - 1; i++) { + for (let j = i + 1; j < touchBank.length; j++) { + let d = distance(touchBank[i], touchBank[j], ofCurrent); + if (d > max) { + max = d; + } + } } - } - return max; + return max; } -export function pinchDistance(touchHistory, touchesChangedAfter, ofCurrent) { - let touchBank = touchHistory.touchBank; - if(touchHistory.numberActiveTouches > 1) { - let filteredTouchBank = touchBank.filter((touchTrack) => { - return touchTrack && touchTrack.currentTimeStamp >= touchesChangedAfter; - }); - return maxDistance(filteredTouchBank, ofCurrent); - } -} \ No newline at end of file +export function pinchDistance (touchHistory, touchesChangedAfter, ofCurrent) { + let touchBank = touchHistory.touchBank; + if (touchHistory.numberActiveTouches > 1) { + let filteredTouchBank = touchBank.filter((touchTrack) => { + return touchTrack && touchTrack.currentTimeStamp >= touchesChangedAfter; + }); + return maxDistance(filteredTouchBank, ofCurrent); + } +} diff --git a/src/libraries/GestureResponder/TouchHistoryMath.js b/src/libraries/GestureResponder/TouchHistoryMath.js index a7c36ed..8c7e317 100644 --- a/src/libraries/GestureResponder/TouchHistoryMath.js +++ b/src/libraries/GestureResponder/TouchHistoryMath.js @@ -21,79 +21,79 @@ var TouchHistoryMath = { * touches vs. previous centroid of now actively moving touches. * @return {number} value of centroid in specified dimension. */ - centroidDimension: function (touchHistory, touchesChangedAfter, isXAxis, ofCurrent) { - var touchBank = touchHistory.touchBank; - var total = 0; - var count = 0; + centroidDimension: function (touchHistory, touchesChangedAfter, isXAxis, ofCurrent) { + var touchBank = touchHistory.touchBank; + var total = 0; + var count = 0; - var oneTouchData = touchHistory.numberActiveTouches === 1 ? touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null; + var oneTouchData = touchHistory.numberActiveTouches === 1 ? touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch] : null; - if (oneTouchData !== null) { - if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) { - total += ofCurrent && isXAxis ? oneTouchData.currentPageX : ofCurrent && !isXAxis ? oneTouchData.currentPageY : !ofCurrent && isXAxis ? oneTouchData.previousPageX : oneTouchData.previousPageY; - count = 1; - } - } else { - for (var i = 0; i < touchBank.length; i++) { - var touchTrack = touchBank[i]; - if (touchTrack !== null && touchTrack !== undefined && touchTrack.touchActive && touchTrack.currentTimeStamp >= touchesChangedAfter) { - var toAdd; // Yuck, program temporarily in invalid state. - if (ofCurrent && isXAxis) { - toAdd = touchTrack.currentPageX; - } else if (ofCurrent && !isXAxis) { - toAdd = touchTrack.currentPageY; - } else if (!ofCurrent && isXAxis) { - toAdd = touchTrack.previousPageX; - } else { - toAdd = touchTrack.previousPageY; - } - total += toAdd; - count++; + if (oneTouchData !== null) { + if (oneTouchData.touchActive && oneTouchData.currentTimeStamp > touchesChangedAfter) { + total += ofCurrent && isXAxis ? oneTouchData.currentPageX : ofCurrent && !isXAxis ? oneTouchData.currentPageY : !ofCurrent && isXAxis ? oneTouchData.previousPageX : oneTouchData.previousPageY; + count = 1; + } + } else { + for (var i = 0; i < touchBank.length; i++) { + var touchTrack = touchBank[i]; + if (touchTrack !== null && touchTrack !== undefined && touchTrack.touchActive && touchTrack.currentTimeStamp >= touchesChangedAfter) { + var toAdd; // Yuck, program temporarily in invalid state. + if (ofCurrent && isXAxis) { + toAdd = touchTrack.currentPageX; + } else if (ofCurrent && !isXAxis) { + toAdd = touchTrack.currentPageY; + } else if (!ofCurrent && isXAxis) { + toAdd = touchTrack.previousPageX; + } else { + toAdd = touchTrack.previousPageY; + } + total += toAdd; + count++; + } + } } - } - } - return count > 0 ? total / count : TouchHistoryMath.noCentroid; - }, + return count > 0 ? total / count : TouchHistoryMath.noCentroid; + }, - currentCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { - return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis + currentCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis true // ofCurrent ); - }, + }, - currentCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { - return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis + currentCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis true // ofCurrent ); - }, + }, - previousCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { - return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis + previousCentroidXOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, true, // isXAxis false // ofCurrent ); - }, + }, - previousCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { - return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis + previousCentroidYOfTouchesChangedAfter: function (touchHistory, touchesChangedAfter) { + return TouchHistoryMath.centroidDimension(touchHistory, touchesChangedAfter, false, // isXAxis false // ofCurrent ); - }, + }, - currentCentroidX: function (touchHistory) { - return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter + currentCentroidX: function (touchHistory) { + return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter true, // isXAxis true // ofCurrent ); - }, + }, - currentCentroidY: function (touchHistory) { - return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter + currentCentroidY: function (touchHistory) { + return TouchHistoryMath.centroidDimension(touchHistory, 0, // touchesChangedAfter false, // isXAxis true // ofCurrent ); - }, + }, - noCentroid: -1 + noCentroid: -1 }; -module.exports = TouchHistoryMath; \ No newline at end of file +module.exports = TouchHistoryMath; diff --git a/src/libraries/GestureResponder/createResponder.js b/src/libraries/GestureResponder/createResponder.js index adb3b84..9096690 100644 --- a/src/libraries/GestureResponder/createResponder.js +++ b/src/libraries/GestureResponder/createResponder.js @@ -5,7 +5,7 @@ 'use strict'; import {InteractionManager} from 'react-native'; -import TouchHistoryMath from './TouchHistoryMath'; //copied from react/lib/TouchHistoryMath.js +import TouchHistoryMath from './TouchHistoryMath'; // copied from react/lib/TouchHistoryMath.js import {pinchDistance} from './TouchDistanceMath'; import TimerMixin from 'react-timer-mixin'; @@ -22,64 +22,61 @@ const MOVE_THRESHOLD = 2; let DEV = false; -function initializeGestureState(gestureState) { - gestureState.moveX = 0; - gestureState.moveY = 0; - gestureState.x0 = 0; - gestureState.y0 = 0; - gestureState.dx = 0; - gestureState.dy = 0; - gestureState.vx = 0; - gestureState.vy = 0; - gestureState.numberActiveTouches = 0; +function initializeGestureState (gestureState) { + gestureState.moveX = 0; + gestureState.moveY = 0; + gestureState.x0 = 0; + gestureState.y0 = 0; + gestureState.dx = 0; + gestureState.dy = 0; + gestureState.vx = 0; + gestureState.vy = 0; + gestureState.numberActiveTouches = 0; // All `gestureState` accounts for timeStamps up until: - gestureState._accountsForMovesUpTo = 0; - - - gestureState.previousMoveX = 0; - gestureState.previousMoveY = 0; - gestureState.pinch = undefined; - gestureState.previousPinch = undefined; - gestureState.singleTapUp = false; - gestureState.doubleTapUp = false; - gestureState._singleTabFailed = false; - + gestureState._accountsForMovesUpTo = 0; + + gestureState.previousMoveX = 0; + gestureState.previousMoveY = 0; + gestureState.pinch = undefined; + gestureState.previousPinch = undefined; + gestureState.singleTapUp = false; + gestureState.doubleTapUp = false; + gestureState._singleTabFailed = false; } -function updateGestureStateOnMove(gestureState, touchHistory, e) { - const movedAfter = gestureState._accountsForMovesUpTo; - const prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); - const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); - const prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); - const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); - const dx = x - prevX; - const dy = y - prevY; +function updateGestureStateOnMove (gestureState, touchHistory, e) { + const movedAfter = gestureState._accountsForMovesUpTo; + const prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); + const prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); + const dx = x - prevX; + const dy = y - prevY; - gestureState.numberActiveTouches = touchHistory.numberActiveTouches; - gestureState.moveX = x; - gestureState.moveY = y; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + gestureState.moveX = x; + gestureState.moveY = y; // TODO: This must be filtered intelligently. - //const dt = touchHistory.mostRecentTimeStamp - movedAfter; - const dt = convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - movedAfter); - gestureState.vx = dx / dt; - gestureState.vy = dy / dt; - gestureState.dx += dx; - gestureState.dy += dy; - gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; - - - gestureState.previousMoveX = prevX; - gestureState.previousMoveY = prevY; - gestureState.pinch = pinchDistance(touchHistory, movedAfter, true); - gestureState.previousPinch = pinchDistance(touchHistory, movedAfter, false); + // const dt = touchHistory.mostRecentTimeStamp - movedAfter; + const dt = convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - movedAfter); + gestureState.vx = dx / dt; + gestureState.vy = dy / dt; + gestureState.dx += dx; + gestureState.dy += dy; + gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; + + gestureState.previousMoveX = prevX; + gestureState.previousMoveY = prevY; + gestureState.pinch = pinchDistance(touchHistory, movedAfter, true); + gestureState.previousPinch = pinchDistance(touchHistory, movedAfter, false); } -function clearInteractionHandle(interactionState) { - if (interactionState.handle) { - InteractionManager.clearInteractionHandle(interactionState.handle); - interactionState.handle = null; - } +function clearInteractionHandle (interactionState) { + if (interactionState.handle) { + InteractionManager.clearInteractionHandle(interactionState.handle); + interactionState.handle = null; + } } /** @@ -88,18 +85,18 @@ function clearInteractionHandle(interactionState) { * @param interval * @returns {*} */ -function convertToMillisecIfNeeded(interval) { - if (interval > 1000000) { - return interval / 1000000; - } - return interval; +function convertToMillisecIfNeeded (interval) { + if (interval > 1000000) { + return interval / 1000000; + } + return interval; } -function cancelSingleTapConfirm(gestureState) { - if(typeof gestureState._singleTapConfirmId !== 'undefined') { - TimerMixin.clearTimeout(gestureState._singleTapConfirmId); - gestureState._singleTapConfirmId = undefined; - } +function cancelSingleTapConfirm (gestureState) { + if (typeof gestureState._singleTapConfirmId !== 'undefined') { + TimerMixin.clearTimeout(gestureState._singleTapConfirmId); + gestureState._singleTapConfirmId = undefined; + } } /** @@ -116,180 +113,180 @@ function cancelSingleTapConfirm(gestureState) { * @param debug true to enable debug logs * @returns {{}} */ -export default function create(config) { - if(config.debug) { - DEV = true; - } - - const interactionState = { - handle: null - }; - const gestureState = { +export default function create (config) { + if (config.debug) { + DEV = true; + } + + const interactionState = { + handle: null + }; + const gestureState = { // Useful for debugging - stateID: Math.random(), - }; - initializeGestureState(gestureState); - - const handlers = { - onStartShouldSetResponder: function (e) { - DEV && console.log('onStartShouldSetResponder...'); - cancelSingleTapConfirm(gestureState); - return config.onStartShouldSetResponder ? + stateID: Math.random() + }; + initializeGestureState(gestureState); + + const handlers = { + onStartShouldSetResponder: function (e) { + DEV && console.log('onStartShouldSetResponder...'); + cancelSingleTapConfirm(gestureState); + return config.onStartShouldSetResponder ? config.onStartShouldSetResponder(e, gestureState) : false; - }, - onMoveShouldSetResponder: function (e) { - DEV && console.log('onMoveShouldSetResponder...'); + }, + onMoveShouldSetResponder: function (e) { + DEV && console.log('onMoveShouldSetResponder...'); - return config.onMoveShouldSetResponder && effectiveMove(config, gestureState) ? + return config.onMoveShouldSetResponder && effectiveMove(config, gestureState) ? config.onMoveShouldSetResponder(e, gestureState) : false; - }, - onStartShouldSetResponderCapture: function (e) { - DEV && console.log('onStartShouldSetResponderCapture...'); - cancelSingleTapConfirm(gestureState); + }, + onStartShouldSetResponderCapture: function (e) { + DEV && console.log('onStartShouldSetResponderCapture...'); + cancelSingleTapConfirm(gestureState); // TODO: Actually, we should reinitialize the state any time // touches.length increases from 0 active to > 0 active. - if (e.nativeEvent.touches.length === 1) { - initializeGestureState(gestureState); - } - gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; - return config.onStartShouldSetResponderCapture ? + if (e.nativeEvent.touches.length === 1) { + initializeGestureState(gestureState); + } + gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; + return config.onStartShouldSetResponderCapture ? config.onStartShouldSetResponderCapture(e, gestureState) : false; - }, + }, - onMoveShouldSetResponderCapture: function (e) { - DEV && console.log('onMoveShouldSetResponderCapture...'); - const touchHistory = e.touchHistory; + onMoveShouldSetResponderCapture: function (e) { + DEV && console.log('onMoveShouldSetResponderCapture...'); + const touchHistory = e.touchHistory; // Responder system incorrectly dispatches should* to current responder // Filter out any touch moves past the first one - we would have // already processed multi-touch geometry during the first event. - if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { - return false; - } - updateGestureStateOnMove(gestureState, touchHistory, e); - return config.onMoveShouldSetResponderCapture && effectiveMove(config, gestureState) ? + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return false; + } + updateGestureStateOnMove(gestureState, touchHistory, e); + return config.onMoveShouldSetResponderCapture && effectiveMove(config, gestureState) ? config.onMoveShouldSetResponderCapture(e, gestureState) : false; - }, - - onResponderGrant: function (e) { - DEV && console.log('onResponderGrant...'); - cancelSingleTapConfirm(gestureState); - if (!interactionState.handle) { - interactionState.handle = InteractionManager.createInteractionHandle(); - } - gestureState._grantTimestamp = e.touchHistory.mostRecentTimeStamp; - gestureState.x0 = currentCentroidX(e.touchHistory); - gestureState.y0 = currentCentroidY(e.touchHistory); - gestureState.dx = 0; - gestureState.dy = 0; - if (config.onResponderGrant) { - config.onResponderGrant(e, gestureState); - } + }, + + onResponderGrant: function (e) { + DEV && console.log('onResponderGrant...'); + cancelSingleTapConfirm(gestureState); + if (!interactionState.handle) { + interactionState.handle = InteractionManager.createInteractionHandle(); + } + gestureState._grantTimestamp = e.touchHistory.mostRecentTimeStamp; + gestureState.x0 = currentCentroidX(e.touchHistory); + gestureState.y0 = currentCentroidY(e.touchHistory); + gestureState.dx = 0; + gestureState.dy = 0; + if (config.onResponderGrant) { + config.onResponderGrant(e, gestureState); + } // TODO: t7467124 investigate if this can be removed - return config.onShouldBlockNativeResponder === undefined ? + return config.onShouldBlockNativeResponder === undefined ? true : config.onShouldBlockNativeResponder(); - }, - - onResponderReject: function (e) { - DEV && console.log('onResponderReject...'); - clearInteractionHandle(interactionState); - config.onResponderReject && config.onResponderReject(e, gestureState); - }, - - onResponderRelease: function (e) { - if (gestureState.singleTapUp) { - if (gestureState._lastSingleTapUp) { - if (convertToMillisecIfNeeded(e.touchHistory.mostRecentTimeStamp - gestureState._lastReleaseTimestamp) < TAP_UP_TIME_THRESHOLD) { - gestureState.doubleTapUp = true; - } - } - gestureState._lastSingleTapUp = true; - - //schedule to confirm single tap - if (!gestureState.doubleTapUp) { - const snapshot = Object.assign({}, gestureState); - const timeoutId = TimerMixin.setTimeout(() => { - if (gestureState._singleTapConfirmId === timeoutId) { - DEV && console.log('onResponderSingleTapConfirmed...'); - config.onResponderSingleTapConfirmed && config.onResponderSingleTapConfirmed(e, snapshot); + }, + + onResponderReject: function (e) { + DEV && console.log('onResponderReject...'); + clearInteractionHandle(interactionState); + config.onResponderReject && config.onResponderReject(e, gestureState); + }, + + onResponderRelease: function (e) { + if (gestureState.singleTapUp) { + if (gestureState._lastSingleTapUp) { + if (convertToMillisecIfNeeded(e.touchHistory.mostRecentTimeStamp - gestureState._lastReleaseTimestamp) < TAP_UP_TIME_THRESHOLD) { + gestureState.doubleTapUp = true; + } + } + gestureState._lastSingleTapUp = true; + + // schedule to confirm single tap + if (!gestureState.doubleTapUp) { + const snapshot = Object.assign({}, gestureState); + const timeoutId = TimerMixin.setTimeout(() => { + if (gestureState._singleTapConfirmId === timeoutId) { + DEV && console.log('onResponderSingleTapConfirmed...'); + config.onResponderSingleTapConfirmed && config.onResponderSingleTapConfirmed(e, snapshot); + } + }, TAP_UP_TIME_THRESHOLD); + gestureState._singleTapConfirmId = timeoutId; + } } - }, TAP_UP_TIME_THRESHOLD); - gestureState._singleTapConfirmId = timeoutId; - } - } - gestureState._lastReleaseTimestamp = e.touchHistory.mostRecentTimeStamp; - - DEV && console.log('onResponderRelease...' + JSON.stringify(gestureState)); - clearInteractionHandle(interactionState); - config.onResponderRelease && config.onResponderRelease(e, gestureState); - initializeGestureState(gestureState); - }, - - onResponderStart: function (e) { - DEV && console.log('onResponderStart...'); - const touchHistory = e.touchHistory; - gestureState.numberActiveTouches = touchHistory.numberActiveTouches; - if (config.onResponderStart) { - config.onResponderStart(e, gestureState); - } - }, - - onResponderMove: function (e) { - const touchHistory = e.touchHistory; + gestureState._lastReleaseTimestamp = e.touchHistory.mostRecentTimeStamp; + + DEV && console.log('onResponderRelease...' + JSON.stringify(gestureState)); + clearInteractionHandle(interactionState); + config.onResponderRelease && config.onResponderRelease(e, gestureState); + initializeGestureState(gestureState); + }, + + onResponderStart: function (e) { + DEV && console.log('onResponderStart...'); + const touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + if (config.onResponderStart) { + config.onResponderStart(e, gestureState); + } + }, + + onResponderMove: function (e) { + const touchHistory = e.touchHistory; // Guard against the dispatch of two touch moves when there are two // simultaneously changed touches. - if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { - return; - } + if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { + return; + } // Filter out any touch moves past the first one - we would have // already processed multi-touch geometry during the first event. - updateGestureStateOnMove(gestureState, touchHistory, e); - - DEV && console.log('onResponderMove...' + JSON.stringify(gestureState)); - if (config.onResponderMove && effectiveMove(config, gestureState)) { - config.onResponderMove(e, gestureState); - } - }, - - onResponderEnd: function (e) { - const touchHistory = e.touchHistory; - gestureState.numberActiveTouches = touchHistory.numberActiveTouches; - - if (touchHistory.numberActiveTouches > 0 - || convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - gestureState._grantTimestamp) > TAP_UP_TIME_THRESHOLD - || Math.abs(gestureState.dx) >= TAP_MOVE_THRESHOLD - || Math.abs(gestureState.dy) >= TAP_MOVE_THRESHOLD + updateGestureStateOnMove(gestureState, touchHistory, e); + + DEV && console.log('onResponderMove...' + JSON.stringify(gestureState)); + if (config.onResponderMove && effectiveMove(config, gestureState)) { + config.onResponderMove(e, gestureState); + } + }, + + onResponderEnd: function (e) { + const touchHistory = e.touchHistory; + gestureState.numberActiveTouches = touchHistory.numberActiveTouches; + + if (touchHistory.numberActiveTouches > 0 || + convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - gestureState._grantTimestamp) > TAP_UP_TIME_THRESHOLD || + Math.abs(gestureState.dx) >= TAP_MOVE_THRESHOLD || + Math.abs(gestureState.dy) >= TAP_MOVE_THRESHOLD ) { - gestureState._singleTabFailed = true; - } - if (!gestureState._singleTabFailed) { - gestureState.singleTapUp = true; - } - - DEV && console.log('onResponderEnd...' + JSON.stringify(gestureState)); - clearInteractionHandle(interactionState); - config.onResponderEnd && config.onResponderEnd(e, gestureState); - }, - - onResponderTerminate: function (e) { - DEV && console.log('onResponderTerminate...'); - clearInteractionHandle(interactionState); - config.onResponderTerminate && config.onResponderTerminate(e, gestureState); - initializeGestureState(gestureState); - }, - - onResponderTerminationRequest: function (e) { - DEV && console.log('onResponderTerminationRequest...'); - return config.onResponderTerminationRequest ? + gestureState._singleTabFailed = true; + } + if (!gestureState._singleTabFailed) { + gestureState.singleTapUp = true; + } + + DEV && console.log('onResponderEnd...' + JSON.stringify(gestureState)); + clearInteractionHandle(interactionState); + config.onResponderEnd && config.onResponderEnd(e, gestureState); + }, + + onResponderTerminate: function (e) { + DEV && console.log('onResponderTerminate...'); + clearInteractionHandle(interactionState); + config.onResponderTerminate && config.onResponderTerminate(e, gestureState); + initializeGestureState(gestureState); + }, + + onResponderTerminationRequest: function (e) { + DEV && console.log('onResponderTerminationRequest...'); + return config.onResponderTerminationRequest ? config.onResponderTerminationRequest(e.gestureState) : true; - } - }; - return {...handlers}; + } + }; + return {...handlers}; } /** @@ -299,18 +296,18 @@ export default function create(config) { * @param gestureState * @returns {boolean} */ -function effectiveMove(config, gestureState) { - if (gestureState.numberActiveTouches > 1) { +function effectiveMove (config, gestureState) { + if (gestureState.numberActiveTouches > 1) { // on iOS simulator, a pinch gesture(move with alt pressed) will not change gestureState.dx(always 0) - return true; - } - - let moveThreshold = MOVE_THRESHOLD; - if (typeof config.moveThreshold === 'number') { - moveThreshold = config.minMoveDistance; - } - if (Math.abs(gestureState.dx) >= moveThreshold || Math.abs(gestureState.dy) >= moveThreshold) { - return true; - } - return false; -} \ No newline at end of file + return true; + } + + let moveThreshold = MOVE_THRESHOLD; + if (typeof config.moveThreshold === 'number') { + moveThreshold = config.minMoveDistance; + } + if (Math.abs(gestureState.dx) >= moveThreshold || Math.abs(gestureState.dy) >= moveThreshold) { + return true; + } + return false; +} diff --git a/src/libraries/GestureResponder/index.js b/src/libraries/GestureResponder/index.js index 59028a0..675464a 100644 --- a/src/libraries/GestureResponder/index.js +++ b/src/libraries/GestureResponder/index.js @@ -2,6 +2,4 @@ import createResponder from './createResponder'; -export { - createResponder -} \ No newline at end of file +export { createResponder }; From 27762b97ece9c9c15f1f2595801768279a30cf4f Mon Sep 17 00:00:00 2001 From: Maxime Bertonnier Date: Fri, 12 Jan 2018 15:37:01 +0100 Subject: [PATCH 11/11] chore(package): bump to 2.1.5 --- Demo/package.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Demo/package.json b/Demo/package.json index 67f0c04..631b42e 100644 --- a/Demo/package.json +++ b/Demo/package.json @@ -9,7 +9,7 @@ "dependencies": { "react": "16.2.0", "react-native": "0.52.0", - "react-native-image-gallery": "2.1.4" + "react-native-image-gallery": "2.1.5" }, "devDependencies": { "babel-jest": "22.0.6", diff --git a/package.json b/package.json index e77317c..01b232b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-native-image-gallery", - "version": "2.1.4", + "version": "2.1.5", "description": "Pure JavaScript image gallery component for iOS and Android", "main": "src/Gallery.js", "scripts": {