Skip to content
This repository has been archived by the owner on Apr 10, 2021. It is now read-only.

Bounce At Boundaries on Swipe #14

Merged
merged 2 commits into from Feb 7, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/awesome_map/awesome-map.js
Expand Up @@ -8,7 +8,7 @@ require([
'wf-js-uicomponents/awesome_map/SwipeInterceptor',
'wf-js-common/DeviceInfo',
'wf-js-common/DOMUtil',
'wf-js-common/consoleDev',
'wf-js-common/console',
'wf-js-common/Compatibility',
'hammerjs.fakemultitouch',
'hammerjs.showtouches',
Expand Down
24 changes: 12 additions & 12 deletions examples/scroll_list/scroll-list.js
Expand Up @@ -9,7 +9,7 @@ require([
'wf-js-common/BrowserInfo',
'wf-js-common/Url',
'wf-js-common/DOMUtil',
'wf-js-common/consoleDev',
'wf-js-common/console',
'hammerjs.fakemultitouch',
'hammerjs.showtouches'
], function(
Expand Down Expand Up @@ -130,41 +130,41 @@ require([

scrollList.onContentRequested(function(sender, args) {
var itemIndex = args.itemIndex;
console.debug('content requested for', itemIndex);
console.log('content requested for', itemIndex);
createPage(args.placeholder.contentContainer, itemIndex,
args.scaleToFit, args.width, args.height);
});

scrollList.onContentRemoved(function(sender, args) {
console.debug('content removed for', args.itemIndex);
console.log('content removed for', args.itemIndex);
});

scrollList.onCurrentItemChanged(function(sender, args) {
console.debug('current item changed to', args.itemIndex);
console.log('current item changed to', args.itemIndex);
$('#page').val(args.itemIndex + 1);
updateZoomPercentage();
});

scrollList.onInteraction(function(sender, args) {
console.debug(args.event.type, args);
console.log(args.event.type, args);
});

scrollList.onInteractionStarted(function(/*sender*/) {
console.debug('interaction started');
console.log('interaction started');
});

scrollList.onInteractionFinished(function(/*sender*/) {
console.debug('interaction finished');
console.log('interaction finished');
});

scrollList.onPlaceholderRendered(function(sender, args) {
console.debug('placeholder rendered for', args.itemIndex);
console.log('placeholder rendered for', args.itemIndex);
// BEWARE: Doing big stuff here can interrupt swipe animations!
args.placeholder.contentContainer.style.backgroundColor = '#fff';
});

scrollList.onScaleChanged(function(sender, args) {
console.debug('scale changed to', args.scale);
console.log('scale changed to', args.scale);
updateZoomPercentage();
});

Expand All @@ -185,9 +185,9 @@ require([

$(function() {

$(window).on('resize', function() { console.debug('resize'); });
$(window).on('orientationchange', function() { console.debug('orientationchange'); });
$(window).on('scroll', function() { console.debug('scroll'); });
$(window).on('resize', function() { console.log('resize'); });
$(window).on('orientationchange', function() { console.log('orientationchange'); });
$(window).on('scroll', function() { console.log('scroll'); });

DOMUtil.dismissIOS7VirtualKeyboardOnOrientationChange();
DOMUtil.preventIOS7WindowScroll();
Expand Down
77 changes: 62 additions & 15 deletions src/awesome_map/BoundaryInterceptor.js
Expand Up @@ -94,7 +94,7 @@ define(function(require) {
* @param {string|{x: string, y: string}} [options.mode='stop']
* Determines how to handle boundary violations during interactions:
* 'stop' disallows dragging beyond the boundaries and
* 'slow' slows the drag effect once boundaries are violated.
* 'slow' limits drag and swipe effects beyond boundaries.
* A different effect may be applied to each boundary.
*
* @example
Expand Down Expand Up @@ -184,6 +184,7 @@ define(function(require) {
var event = args.event;
var targetState = args.targetState;
var eventType = event.type;
var originalState;

switch (eventType) {
case EventTypes.TOUCH:
Expand All @@ -202,7 +203,6 @@ define(function(require) {
else {
this._stopAtBoundaries(event, targetState, 'x');
}

if (!event.simulated && this._mode.y === Modes.SLOW) {
this._pullToBoundaries(event, targetState, 'y');
}
Expand All @@ -212,7 +212,6 @@ define(function(require) {
break;

case EventTypes.MOUSE_WHEEL:
case EventTypes.SWIPE:

this._stopAtBoundaries(event, targetState);
break;
Expand All @@ -222,6 +221,25 @@ define(function(require) {

this._snapToBoundaries(event, targetState);
break;

case EventTypes.SWIPE:

originalState = targetState.clone();
if (this._mode.x === Modes.SLOW) {
this._bounceAtBoundaries(event, targetState, 'x');
}
else {
this._stopAtBoundaries(event, targetState, 'x');
}
if (this._mode.y === Modes.SLOW) {
this._bounceAtBoundaries(event, targetState, 'y');
}
else {
this._stopAtBoundaries(event, targetState, 'y');
}
this._accelerateAnimationAtBoundaries(event, targetState, originalState);

break;
}
},

Expand Down Expand Up @@ -265,9 +283,47 @@ define(function(require) {
durationFactorY = boundedDelta / originalDelta;
}

// Apply acceleration up to the default animation duration.
// Apply acceleration. This effect should be relative to, but faster
// than other snap backs.
acceleratedDuration = targetState.duration * Math.min(durationFactorX, durationFactorY);
targetState.duration = Math.max(this._animationDuration, acceleratedDuration);
targetState.duration = Math.max(this._animationDuration / 2, acceleratedDuration);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why divided by 2?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep things relative to configured animationDuration, but to speed it up. This effect should be faster than other snap backs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good place for inline comment. We shall never remember after you move on to Netflix

},

/**
* Allow event translation to move content slightly outside of boundaries
* so that when release event occurs the content will snap back to boundaries.
* @param {InteractionEvent} event
* @param {TransformState} targetState
* @param {string} [axis]
*/
_bounceAtBoundaries: function(event, targetState, axis) {
var originalState = targetState.clone();
var viewportSize = this._awesomeMap.getViewportDimensions();

// Enforce the viewport boundaries.
var boundedPosition = this._getBoundedPosition(targetState);
var bounceDistance;
var direction;

// If the targetState ends up out of bounds, allow the translation to carry
// beyond the bounds by 10% of the relevant viewport dimension.
// On release event handling, the content will "bounce back".
// Switch the sign of the bounce distance to account for location:
// left/top has the distance added, right/bottom has the distance subtracted.
if (!axis || axis === 'x') {
if (boundedPosition.x !== originalState.translateX) {
bounceDistance = 0.1 * viewportSize.width;
direction = boundedPosition.x < originalState.translateX ? 1 : -1;
targetState.translateX = boundedPosition.x + bounceDistance * direction;
}
}
if (!axis || axis === 'y') {
if (boundedPosition.y !== originalState.translateY) {
bounceDistance = 0.1 * viewportSize.height;
direction = boundedPosition.y < originalState.translateY ? 1 : -1;
targetState.translateY = boundedPosition.y + bounceDistance * direction;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about consolidating this near-duplicate code like so

function fixTargetState (axis) {
    var translateDir = 'translate' + axis.toUpperCase();
    var side = axis === 'x' ? 'width' : 'height';
    if (boundedPosition[axis] !== originalState[translateDir]) {
        bounceDistance = 0.1 * viewportSize[side];
        direction = boundedPosition[axis] < originalState[translateDir] ? 1 : -1;
        targetState[translateDir] = boundedPosition[axis] + bounceDistance * direction;
    }
}
if (!axis) {
    fixTargetState('x');
    fixTargetState('y');
} else {
    fixTargetState(axis);
}

Looking at http://jsperf.com/bracket-vs-dot-notation-read-vs-write, it's clear that using bracket notation (not to mention the function calls) would slow things down a lot!

},

/**
Expand Down Expand Up @@ -426,10 +482,6 @@ define(function(require) {
* @private
*/
_stopAtBoundaries: function(event, targetState, axis) {
// Save off the original values.
var originalState = targetState.clone();

// Enforce the viewport boundaries.
var boundedPosition = this._getBoundedPosition(targetState);

if (!axis || axis === 'x') {
Expand All @@ -438,15 +490,10 @@ define(function(require) {
if (!axis || axis === 'y') {
targetState.translateY = boundedPosition.y;
}

// Modify the animation duration for swipe events.
if (event.type === EventTypes.SWIPE) {
this._accelerateAnimationAtBoundaries(event, targetState, originalState);
}
}
};

_.assign(BoundaryInterceptor.prototype, InterceptorMixin);

return BoundaryInterceptor;
});
});
2 changes: 1 addition & 1 deletion src/scroll_list/AwesomeMapFactory.js
Expand Up @@ -68,7 +68,7 @@ define(function(require) {
}
map.addInterceptor(new BoundaryInterceptor({
centerContent: true,
mode: 'stop'
mode: { x: 'stop', y: 'slow' }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A numerical value might be cooler than 'slow' - very minor thing.

}));

// Wire up observables.
Expand Down
11 changes: 11 additions & 0 deletions src/scroll_list/PeekInterceptor.js
Expand Up @@ -198,6 +198,17 @@ define(function(require) {
this._resetPeekState();
}
}
else {
// Modify event to impose boundary condition on item map if:
// - dragging down and top will be visible, or
// - dragging up and bottom will be visible.
if (deltaY > 0 && contentTop + deltaY >= 0) {
event.iterativeGesture.deltaY = -contentTop;
}
else if (deltaY < 0 && contentBottom + deltaY <= viewportHeight) {
event.iterativeGesture.deltaY = viewportHeight - contentBottom;
}
}

// If we have a new Y value, transform the list directly.
if (newY !== undefined) {
Expand Down
74 changes: 67 additions & 7 deletions test/awesome_map/BoundaryInterceptorSpec.js
Expand Up @@ -181,6 +181,36 @@ define(function(require) {

var contentDimensions = { width: 50, height: 100 };

function expectBounceAtViewportTopAndLeft(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'slow' });
var evt = createEvent(eventType);
var targetState = new TransformState({
translateX: viewportDimensions.width,
translateY: viewportDimensions.height
});

interceptor.register(map);
interceptor.handleTransformStarted(null, { event: evt, targetState: targetState });

expect(targetState.translateX).toBe(viewportDimensions.width - contentDimensions.width + viewportDimensions.width * 0.1);
expect(targetState.translateY).toBe(viewportDimensions.height - contentDimensions.height + viewportDimensions.height * 0.1);
}

function expectBounceAtViewportBottomAndRight(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'slow' });
var evt = createEvent(eventType);
var targetState = new TransformState({
translateX: -viewportDimensions.width * 2,
translateY: -viewportDimensions.height * 2
});

interceptor.register(map);
interceptor.handleTransformStarted(null, { event: evt, targetState: targetState });

expect(targetState.translateX).toBe(-viewportDimensions.width * 0.1);
expect(targetState.translateY).toBe(-viewportDimensions.height * 0.1);
}

function expectStopAtViewportTopAndLeft(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'stop' });
var evt = createEvent(eventType);
Expand Down Expand Up @@ -347,12 +377,12 @@ define(function(require) {

describe('during swipe', function() {

it('should stop at viewport top and left', function() {
expectStopAtViewportTopAndLeft(EventTypes.SWIPE);
it('should bounce at viewport top and left', function() {
expectBounceAtViewportTopAndLeft(EventTypes.SWIPE);
});

it('should stop at viewport bottom and right', function() {
expectStopAtViewportBottomAndRight(EventTypes.SWIPE);
it('should bounce at viewport bottom and right', function() {
expectBounceAtViewportBottomAndRight(EventTypes.SWIPE);
});
});

Expand Down Expand Up @@ -383,6 +413,36 @@ define(function(require) {

var contentDimensions = { width: 200, height: 400 };

function expectBounceAtViewportTopAndLeft(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'slow' });
var evt = createEvent(eventType);
var targetState = new TransformState({
translateX: viewportDimensions.width * 2,
translateY: viewportDimensions.height * 2
});

interceptor.register(map);
interceptor.handleTransformStarted(null, { event: evt, targetState: targetState });

expect(targetState.translateX).toBe(viewportDimensions.width * 0.1);
expect(targetState.translateY).toBe(viewportDimensions.height * 0.1);
}

function expectBounceAtViewportBottomAndRight(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'slow' });
var evt = createEvent(eventType);
var targetState = new TransformState({
translateX: -viewportDimensions.width * 2,
translateY: -viewportDimensions.height * 2
});

interceptor.register(map);
interceptor.handleTransformStarted(null, { event: evt, targetState: targetState });

expect(targetState.translateX).toBe(viewportDimensions.width - contentDimensions.width - viewportDimensions.width * 0.1);
expect(targetState.translateY).toBe(viewportDimensions.height - contentDimensions.height - viewportDimensions.height * 0.1);
}

function expectStopAtViewportTopAndLeft(eventType) {
var interceptor = new BoundaryInterceptor({ mode: 'stop' });
var evt = createEvent(eventType);
Expand Down Expand Up @@ -532,11 +592,11 @@ define(function(require) {
describe('during swipe', function() {

it('should stop at viewport top and left', function() {
expectStopAtViewportTopAndLeft(EventTypes.SWIPE);
expectBounceAtViewportTopAndLeft(EventTypes.SWIPE);
});

it('should stop at viewport bottom and right', function() {
expectStopAtViewportBottomAndRight(EventTypes.SWIPE);
expectBounceAtViewportBottomAndRight(EventTypes.SWIPE);
});
});

Expand All @@ -563,4 +623,4 @@ define(function(require) {
});
});
});
});
});
26 changes: 26 additions & 0 deletions test/scroll_list/PeekInterceptorSpec.js
Expand Up @@ -262,6 +262,19 @@ define(function(require) {
expect(result).toBe(true);
expectMostRecentTransformCallCalledWith(-100);
});

it('should modify event delta to prevent content from shifting out of bounds', function() {
var evt = createEvent(eventType, { deltaY: 20 });
var result;

listState.translateY = -200;
itemState.translateY = -10;

result = interceptor.handleInteraction(null, { event: evt });

expect(result).toBe(true);
expect(evt.iterativeGesture.deltaY).toBe(10);
});
}

describe('drag down', function() {
Expand Down Expand Up @@ -351,6 +364,19 @@ define(function(require) {
expect(result).toBe(true);
expectMostRecentTransformCallCalledWith(-100);
});

it('should modify event delta to prevent content from shifting out of bounds', function() {
var evt = createEvent(eventType, { deltaY: -20 });
var result;

listState.translateY = -200;
itemState.translateY = 10;

result = interceptor.handleInteraction(null, { event: evt });

expect(result).toBe(true);
expect(evt.iterativeGesture.deltaY).toBe(-10);
});
}

describe('drag up', function() {
Expand Down