From 798f38db342cec73db31354bb025612edd76d3c8 Mon Sep 17 00:00:00 2001 From: Alan Orozco Date: Tue, 19 Feb 2019 18:12:11 -0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8Flick=20to=20dismiss=20docked=20video?= =?UTF-8?q?=20(#20906)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Tracks dragging velocity. * On drag end, video is dismissed if velocity is over a certain threshold by either: * sliding out, if the component area is out of view * or bouncing into position, if the component area is in view Videos docked to slot elements cannot be dragged, so this feature won't work in this case. An upcoming PR will introduce dragging for slot elements so they can be flicked-to-dismiss. --- .../0.1/amp-video-docking.js | 127 ++++++++++++++++-- .../amp-video-docking/amp-video-docking.md | 1 + 2 files changed, 119 insertions(+), 9 deletions(-) diff --git a/extensions/amp-video-docking/0.1/amp-video-docking.js b/extensions/amp-video-docking/0.1/amp-video-docking.js index 35302af94432..024f6797c8fc 100644 --- a/extensions/amp-video-docking/0.1/amp-video-docking.js +++ b/extensions/amp-video-docking/0.1/amp-video-docking.js @@ -347,6 +347,12 @@ export class VideoDocking { /** @private {boolean} */ this.isDragging_ = false; + /** @private {number} */ + this.previousDragOffsetX_ = 0; + + /** @private {number} */ + this.dragVelocityX_ = 0; + /** @private {!Array} */ this.observed_ = []; @@ -1320,6 +1326,9 @@ export class VideoDocking { const {centerX} = this.getCenter_(offsetX, offsetY); const offsetRelativeX = this.calculateRelativeX_(centerX); + this.dragVelocityX_ = offsetX - this.previousDragOffsetX_; + this.previousDragOffsetX_ = offsetX; + this.placeAt_(video, x + offsetX, y + offsetY, scale, step, transitionDurationMs, offsetRelativeX); } @@ -1471,7 +1480,100 @@ export class VideoDocking { this.getControls_().enable(); - this.snap_(offset.x, offset.y); + if (Math.abs(this.dragVelocityX_) < 40) { + this.snap_(offset.x, offset.y); + } else { + this.flickToDismiss_(this.previousDragOffsetX_, + Math.sign(this.dragVelocityX_)); + } + + this.dragVelocityX_ = 0; + this.previousDragOffsetX_ = 0; + } + + /** + * @param {number} offsetX + * @param {number} direction -1 or 1 + * @private + */ + flickToDismiss_(offsetX, direction) { + devAssert(Math.abs(direction) == 1); + + const video = this.getDockedVideo_(); + + video.pause(); + + if (this.isVisible_(video.element, 0.2)) { + this.bounceToDismiss_(video, offsetX, direction); + return; + } + + const step = 1; + const {target} = devAssert(this.currentlyDocked_); + + const {x, y, width} = this.getTargetArea_(video, target); + const {scale} = this.getDims_(video, target, step); + + const currentX = x + offsetX; + const nextX = direction == 1 ? + this.getRightEdge_() : + this.getLeftEdge_() - width; + + const transitionDurationMs = + this.calculateDismissalTransitionDurationMs_(nextX - currentX); + + this.reconcileUndocked_(); + + // Show immediately due to Chrome freeze bug when out-of-view. + video.showControls(); + + this.placeAt_(video, nextX, y, scale, /* step */ 0, transitionDurationMs) + .then(() => { + this.resetOnUndock_(video); + }); + } + + /** + * @param {!../../../src/video-interface.VideoOrBaseElementDef} video + * @param {number} offsetX + * @param {number} direction -1 or 1 + * @private + */ + bounceToDismiss_(video, offsetX, direction) { + devAssert(Math.abs(direction) == 1); + + const step = 1; + const {target} = devAssert(this.currentlyDocked_); + + const {x, y, width} = this.getTargetArea_(video, target); + const {scale} = this.getDims_(video, target, step); + + const areaWidth = this.getAreaWidth_(); + + const currentX = x + offsetX; + const nextX = direction == 1 ? + calculateRightJustifiedX(areaWidth, width, /* margin */ 0, step) : + calculateLeftJustifiedX(areaWidth, width, /* margin */ 0, step); + + const transitionDurationMs = + this.calculateDismissalTransitionDurationMs_(nextX - currentX); + + this.reconcileUndocked_(); + + this.placeAt_(video, nextX, y, scale, /* step */ 0, transitionDurationMs) + .then(() => { + this.undock_(video, /* reconciled */ true); + video.showControls(); + }); + } + + /** + * @param {number} deltaX + * @return {number} + * @private + */ + calculateDismissalTransitionDurationMs_(deltaX) { + return Math.min(300, Math.abs(deltaX) / 2); } /** @@ -1637,16 +1739,14 @@ export class VideoDocking { /** * @param {!../../../src/video-interface.VideoOrBaseElementDef} video + * @param {boolean=} opt_reconciled * @return {!Promise} * @private */ - undock_(video) { + undock_(video, opt_reconciled) { dev().info(TAG, 'undock', {video}); - this.getControls_().disable(); - const {element} = video; - const isMostlyInView = this.isVisible_(element, REVERT_TO_INLINE_RATIO); if (!isMostlyInView) { @@ -1658,10 +1758,9 @@ export class VideoDocking { video.showControls(); } - // Prevents ghosting - this.getControls_().hide(/* respectSticky */ false, /* immediately */ true); - - this.trigger_(Actions.UNDOCK); + if (!opt_reconciled) { + this.reconcileUndocked_(); + } const step = 0; @@ -1685,6 +1784,16 @@ export class VideoDocking { }); } + /** @private */ + reconcileUndocked_() { + this.getControls_().disable(); + + // Prevents ghosting + this.getControls_().hide(/* respectSticky */ false, /* immediately */ true); + + this.trigger_(Actions.UNDOCK); + } + /** * @param {!../../../src/video-interface.VideoOrBaseElementDef} video diff --git a/extensions/amp-video-docking/amp-video-docking.md b/extensions/amp-video-docking/amp-video-docking.md index b2ca7012415d..711559a57a39 100644 --- a/extensions/amp-video-docking/amp-video-docking.md +++ b/extensions/amp-video-docking/amp-video-docking.md @@ -55,6 +55,7 @@ component's visual area. If the user scrolls back, the video reverts to its orig - The video can be docked to a default corner or to a custom fixed position. - The video can be dragged and repositioned by the user on a different corner. +- The video can be flicked to be dismissed from its docked position. - Multiple videos on the same page can be docked, but only one at a time will be docked and fixed. ### Support