Skip to content

Commit 9882ade

Browse files
committed
Merge pull request #55 from PolymerLabs/momentum-drawer
Momentum drawer
2 parents 95a8850 + 93bfa75 commit 9882ade

File tree

2 files changed

+407
-118
lines changed

2 files changed

+407
-118
lines changed

app-drawer/app-drawer.html

+203-66
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@
9494

9595
:host([opened]) > #contentContainer {
9696
-webkit-transform: translate3d(0, 0, 0);
97-
transform: translate3d(0, 0, 0);
97+
transform: translate3d(0, 0, 0);
9898
}
9999

100100
#scrim {
@@ -196,26 +196,27 @@
196196
},
197197

198198
listeners: {
199-
track: '_trackHandler'
199+
track: '_track',
200+
transitionend: '_transitionEnd'
200201
},
201202

202203
observers: [
203204
'resetLayout(position)',
204-
'_updateDocScroll(opened, persistent)'
205+
'_updateDocScroll(persistent)'
205206
],
206207

207-
_lastTrackDirection: 0,
208-
209208
_translateOffset: 0,
210209

210+
_trackEvents: null,
211+
212+
_isDrawerFlinging: false,
213+
211214
attached: function() {
212215
// Set the scroll direction so you can vertically scroll inside the drawer.
213216
this.setScrollDirection('y');
214217

215-
/**
216-
* Only transition the drawer shortly after it is attached (e.g. app-drawer-layout
217-
* may need to set the initial opened state which should not be transitioned).
218-
*/
218+
// Only transition the drawer shortly after it is attached (e.g. app-drawer-layout
219+
// may need to set the initial opened state which should not be transitioned).
219220
this.$.contentContainer.style.transition = 'none';
220221
this.async(function() {
221222
this.$.contentContainer.style.transition = '';
@@ -259,76 +260,212 @@
259260
},
260261

261262
_scrimTapHandler: function() {
262-
if (!this.persistent) {
263+
if (this.persistent) {
264+
return;
265+
}
266+
267+
// This debouncer is needed because of Polymer/polymer#3405.
268+
this.debounce('_scrimTapHandler', function() {
263269
this.opened = false;
270+
}, 1);
271+
},
272+
273+
_track: function(event) {
274+
if (this.persistent) {
275+
return;
276+
}
277+
278+
// Disable user selection on desktop.
279+
event.preventDefault();
280+
281+
switch (event.detail.state) {
282+
case 'start':
283+
this._trackStart(event);
284+
break;
285+
case 'track':
286+
this._trackMove(event);
287+
break;
288+
case 'end':
289+
this._trackEnd(event);
290+
break;
291+
}
292+
},
293+
294+
_trackStart: function(event) {
295+
// Disable transitions since style attributes will reflect user track events.
296+
this.$.contentContainer.style.transitionDuration = '0s';
297+
this.$.scrim.style.transitionDuration = '0s';
298+
this.$.scrim.style.visibility = 'visible';
299+
300+
var rect = this.$.contentContainer.getBoundingClientRect();
301+
if (this.position == 'left') {
302+
this._translateOffset = rect.left;
303+
} else {
304+
this._translateOffset = rect.right - window.innerWidth;
264305
}
306+
307+
this._trackEvents = [];
265308
},
266309

267-
_trackHandler: function(event) {
268-
if (!this.persistent) {
269-
switch (event.detail.state) {
270-
case 'start':
271-
// Disable transitions since styles will reflect user track events.
272-
this.$.contentContainer.style.transitionDuration = '0s';
273-
this.$.scrim.style.transitionDuration = '0s';
274-
this.$.scrim.style.visibility = 'visible';
275-
276-
if (this.opened) {
277-
this._translateOffset = 0;
278-
} else if (this.position == 'left') {
279-
this._translateOffset = -this.getWidth();
280-
} else if (this.position == 'right') {
281-
this._translateOffset = this.getWidth();
282-
}
283-
break;
284-
285-
case 'track':
286-
var translateDelta = event.detail.dx + this._translateOffset;
287-
switch (this.position) {
288-
case 'left':
289-
translateDelta = Math.min(0, translateDelta);
290-
this._translateDrawer(translateDelta);
291-
this.$.scrim.style.opacity = 1 + translateDelta / this.getWidth();
292-
break;
293-
case 'right':
294-
translateDelta = Math.max(0, translateDelta);
295-
this._translateDrawer(translateDelta);
296-
this.$.scrim.style.opacity = 1 - translateDelta / this.getWidth();
297-
break;
298-
}
299-
300-
// Keep track of the last user track event with non-zero ddx (which we
301-
// need to determine whether to keep the drawer open/closed) since the
302-
// very last track event may have zero ddx.
303-
if (event.detail.ddx !== 0) {
304-
this._lastTrackDirection = event.detail.ddx;
305-
}
306-
break;
307-
308-
case 'end':
309-
this.$.contentContainer.style.transitionDuration = '';
310-
this.$.scrim.style.transitionDuration = '';
311-
this.$.scrim.style.visibility = '';
312-
this.$.scrim.style.opacity = '';
313-
this.transform('', this.$.contentContainer);
314-
315-
// Only toggle the drawer if the user intentionally swipes in one direction.
316-
if (Math.abs(event.detail.dx) > 30) {
317-
this.opened = (this.position === 'left' && this._lastTrackDirection > 0) ||
318-
(this.position === 'right' && this._lastTrackDirection < 0);
319-
}
320-
break;
310+
_trackMove: function(event) {
311+
this._translateDrawer(event.detail.dx + this._translateOffset);
312+
this._trackEvents.push(event);
313+
},
314+
315+
_trackEnd: function(event) {
316+
// Track handler takes precedence over scrim tap handler. See Polymer/polymer#3405.
317+
this.cancelDebouncer('_scrimTapHandler');
318+
319+
// Calculate the velocity.
320+
this._velocity = this._calculateVelocity(event);
321+
322+
// No longer need the track events after calculating velocity - allow them to be GC'd.
323+
this._trackEvents = null;
324+
325+
if (Math.abs(this._velocity) > this.MIN_FLING_VELOCITY) {
326+
// If velocity is above the threshold, fling the drawer in the same direction.
327+
this._flingDrawer(event);
328+
} else {
329+
// Otherwise, toggle the opened state based on the position of the drawer.
330+
var halfWidth = this.getWidth() / 2;
331+
if (event.detail.dx < -halfWidth) {
332+
this.opened = this.position === 'right';
333+
} else if (event.detail.dx > halfWidth) {
334+
this.opened = this.position === 'left';
321335
}
336+
this._resetStyleAttributes();
322337
}
323338
},
324339

340+
_calculateVelocity: function(event) {
341+
// Find the oldest track event that is within 100ms of track end using binary search.
342+
var timeLowerBound = Date.now() - 100;
343+
var trackEvent;
344+
var min = 0;
345+
var max = this._trackEvents.length - 1;
346+
347+
while (min <= max) {
348+
// Floor of average of min and max.
349+
var mid = (min + max) >> 1;
350+
var e = this._trackEvents[mid];
351+
if (e.timeStamp >= timeLowerBound) {
352+
trackEvent = e;
353+
max = mid - 1;
354+
} else {
355+
min = mid + 1;
356+
}
357+
}
358+
359+
if (trackEvent) {
360+
var dx = event.detail.dx - trackEvent.detail.dx;
361+
var dt = (event.timeStamp - trackEvent.timeStamp) || 1;
362+
return dx / dt;
363+
}
364+
return 0;
365+
},
366+
367+
_flingDrawer: function(event) {
368+
var x = event.detail.dx + this._translateOffset;
369+
var drawerWidth = this.getWidth();
370+
var isPositionLeft = this.position === 'left';
371+
var isVelocityPositive = this._velocity > 0;
372+
var isClosingLeft = !isVelocityPositive && isPositionLeft;
373+
var isClosingRight = isVelocityPositive && !isPositionLeft;
374+
375+
// Enforce a minimum transition velocity to make the drawer feel snappy.
376+
if (isVelocityPositive) {
377+
this._velocity = Math.max(this._velocity, this.MIN_TRANSITION_VELOCITY);
378+
} else {
379+
this._velocity = Math.min(this._velocity, -this.MIN_TRANSITION_VELOCITY);
380+
}
381+
382+
// Calculate the amount of time needed to finish the transition based on the initial
383+
// slope of the timing function.
384+
var duration;
385+
if (isClosingLeft) {
386+
duration = (this.FLING_INITIAL_SLOPE * -(x + drawerWidth) / this._velocity) + 'ms';
387+
} else if (isClosingRight) {
388+
duration = (this.FLING_INITIAL_SLOPE * (drawerWidth - x) / this._velocity) + 'ms';
389+
} else {
390+
duration = (this.FLING_INITIAL_SLOPE * -x / this._velocity) + 'ms';
391+
}
392+
393+
this.$.contentContainer.style.transitionDuration = duration;
394+
this.$.contentContainer.style.transitionTimingFunction = this.FLING_TIMING_FUNCTION;
395+
this.$.scrim.style.transitionDuration = duration;
396+
this.$.scrim.style.transitionTimingFunction = this.FLING_TIMING_FUNCTION;
397+
398+
// Transform instead of toggling opened to avoid calling Polymer setters.
399+
if (isClosingLeft) {
400+
this._translateDrawer(-drawerWidth);
401+
} else if (isClosingRight) {
402+
this._translateDrawer(drawerWidth);
403+
} else {
404+
this._translateDrawer(0);
405+
}
406+
407+
this._isDrawerFlinging = true;
408+
},
409+
410+
_transitionEnd: function(event) {
411+
if (Polymer.dom(event).localTarget !== this) {
412+
return;
413+
}
414+
415+
this._updateDocScroll();
416+
417+
// Only set the opened state and reset transition timing function if this
418+
// transition end event was the end of a fling transition.
419+
if (!this._isDrawerFlinging) {
420+
return;
421+
}
422+
423+
this._isDrawerFlinging = false;
424+
425+
if (this._velocity < 0) {
426+
this.opened = this.position === 'right';
427+
} else {
428+
this.opened = this.position === 'left';
429+
}
430+
431+
this.$.contentContainer.style.transitionTimingFunction = '';
432+
this.$.scrim.style.transitionTimingFunction = '';
433+
this._resetStyleAttributes();
434+
},
435+
436+
_resetStyleAttributes: function() {
437+
this.$.contentContainer.style.transitionDuration = '';
438+
this.$.scrim.style.transitionDuration = '';
439+
this.$.scrim.style.visibility = '';
440+
this.$.scrim.style.opacity = '';
441+
this.transform('', this.$.contentContainer);
442+
},
443+
325444
_translateDrawer: function(x) {
326-
this.transform('translate3d(' + x + 'px, 0, 0)', this.$.contentContainer);
445+
var drawerWidth = this.getWidth();
446+
447+
if (this.position === 'left') {
448+
x = Math.max(-drawerWidth, Math.min(x, 0));
449+
this.$.scrim.style.opacity = 1 + x / drawerWidth;
450+
} else {
451+
x = Math.max(0, Math.min(x, drawerWidth));
452+
this.$.scrim.style.opacity = 1 - x / drawerWidth;
453+
}
454+
455+
this.translate3d(x + 'px', '0', '0', this.$.contentContainer);
327456
},
328457

329458
_updateDocScroll: function() {
330459
document.body.style.overflow = (this.persistent || !this.opened) ? '' : 'hidden';
331-
}
460+
},
461+
462+
MIN_FLING_VELOCITY: 0.2,
463+
464+
MIN_TRANSITION_VELOCITY: 1.2,
465+
466+
FLING_TIMING_FUNCTION: 'cubic-bezier(0.667, 1, 0.667, 1)',
467+
468+
FLING_INITIAL_SLOPE: 1.5
332469

333470
/**
334471
* Fired when the layout of app-drawer has changed.

0 commit comments

Comments
 (0)