Add swipe-click handler to media modal

Now swiping will correctly change the current media, and with a good
preview. Clicking without swiping closes the overlay.
This commit is contained in:
Tusooa Zhu 2021-08-02 19:11:59 -04:00
parent a36673a6a8
commit 29cd8fbd3b
No known key found for this signature in database
GPG Key ID: 7B467EDE43A08224
5 changed files with 94 additions and 168 deletions

View File

@ -2,10 +2,11 @@ import StillImage from '../still-image/still-image.vue'
import VideoAttachment from '../video_attachment/video_attachment.vue' import VideoAttachment from '../video_attachment/video_attachment.vue'
import Modal from '../modal/modal.vue' import Modal from '../modal/modal.vue'
import PinchZoom from '../pinch_zoom/pinch_zoom.vue' import PinchZoom from '../pinch_zoom/pinch_zoom.vue'
import fileTypeService from '../../services/file_type/file_type.service.js' import SwipeClick from '../swipe_click/swipe_click.vue'
import GestureService from '../../services/gesture_service/gesture_service' import GestureService from '../../services/gesture_service/gesture_service'
import Flash from 'src/components/flash/flash.vue' import Flash from 'src/components/flash/flash.vue'
import Vuex from 'vuex' import Vuex from 'vuex'
import fileTypeService from '../../services/file_type/file_type.service.js'
import { library } from '@fortawesome/fontawesome-svg-core' import { library } from '@fortawesome/fontawesome-svg-core'
import { import {
faChevronLeft, faChevronLeft,
@ -28,13 +29,15 @@ const MediaModal = {
StillImage, StillImage,
VideoAttachment, VideoAttachment,
PinchZoom, PinchZoom,
SwipeClick,
Modal, Modal,
Flash Flash
}, },
data () { data () {
return { return {
loading: false, loading: false,
pinchZoomOptions: {} swipeDirection: GestureService.DIRECTION_LEFT,
swipeThreshold: 50
} }
}, },
computed: { computed: {
@ -70,30 +73,19 @@ const MediaModal = {
} }
}, },
created () { created () {
this.mediaGesture = new GestureService.SwipeAndScaleGesture({ // this.mediaGesture = new GestureService.SwipeAndScaleGesture({
direction: GestureService.DIRECTION_LEFT, // callbackPositive: this.goNext,
callbackPositive: this.goNext, // callbackNegative: this.goPrev,
callbackNegative: this.goPrev, // swipePreviewCallback: this.handleSwipePreview,
swipePreviewCallback: this.handleSwipePreview, // swipeEndCallback: this.handleSwipeEnd,
swipeEndCallback: this.handleSwipeEnd, // pinchPreviewCallback: this.handlePinchPreview,
pinchPreviewCallback: this.handlePinchPreview, // pinchEndCallback: this.handlePinchEnd
pinchEndCallback: this.handlePinchEnd, // })
threshold: 50
})
}, },
methods: { methods: {
getType (media) { getType (media) {
return fileTypeService.fileType(media.mimetype) return fileTypeService.fileType(media.mimetype)
}, },
mediaTouchStart (e) {
this.mediaGesture.start(e)
},
mediaTouchMove (e) {
this.mediaGesture.move(e)
},
mediaTouchEnd (e) {
this.mediaGesture.end(e)
},
hide () { hide () {
this.$store.dispatch('closeMediaViewer') this.$store.dispatch('closeMediaViewer')
}, },
@ -105,6 +97,7 @@ const MediaModal = {
this.loading = true this.loading = true
} }
this.$store.dispatch('setCurrentMedia', newMedia) this.$store.dispatch('setCurrentMedia', newMedia)
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
} }
}, },
goNext () { goNext () {
@ -115,40 +108,25 @@ const MediaModal = {
this.loading = true this.loading = true
} }
this.$store.dispatch('setCurrentMedia', newMedia) this.$store.dispatch('setCurrentMedia', newMedia)
this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
} }
}, },
onImageLoaded () { onImageLoaded () {
this.loading = false this.loading = false
}, },
handleSwipePreview (offsets) { handleSwipePreview (offsets) {
this.$store.dispatch('swipeScaler/apply', { this.$refs.pinchZoom.setTransform({ scale: 1, x: offsets[0], y: 0 })
offsets: this.scaling > SCALING_ENABLE_MOVE_THRESHOLD ? offsets : onlyXAxis(offsets)
})
}, },
handleSwipeEnd (sign) { handleSwipeEnd (sign) {
if (this.scaling > SCALING_ENABLE_MOVE_THRESHOLD) { console.log('handleSwipeEnd:', sign)
this.$store.dispatch('swipeScaler/finish')
return
}
if (sign === 0) { if (sign === 0) {
this.$store.dispatch('swipeScaler/reset') this.$refs.pinchZoom.setTransform({ scale: 1, x: 0, y: 0 })
} else if (sign > 0) { } else if (sign > 0) {
this.goNext() this.goNext()
} else { } else {
this.goPrev() this.goPrev()
} }
}, },
handlePinchPreview (offsets, scaling) {
console.log('handle pinch preview:', offsets, scaling)
this.$store.dispatch('swipeScaler/apply', { offsets, scaling })
},
handlePinchEnd () {
if (this.scaling > SCALING_RESET_MIN) {
this.$store.dispatch('swipeScaler/finish')
} else {
this.$store.dispatch('swipeScaler/reset')
}
},
handleKeyupEvent (e) { handleKeyupEvent (e) {
if (this.showing && e.keyCode === 27) { // escape if (this.showing && e.keyCode === 27) { // escape
this.hide() this.hide()

View File

@ -4,9 +4,16 @@
class="media-modal-view" class="media-modal-view"
@backdropClicked="hide" @backdropClicked="hide"
> >
<div class="modal-image-container"> <SwipeClick
class="modal-image-container"
:direction="swipeDirection"
:threshold="swipeThreshold"
@preview-requested="handleSwipePreview"
@swipe-finished="handleSwipeEnd"
@swipeless-clicked="hide"
>
<PinchZoom <PinchZoom
options="pinchZoomOptions" ref="pinchZoom"
class="modal-image-container-inner" class="modal-image-container-inner"
selector=".modal-image" selector=".modal-image"
allow-pan-min-scale="1" allow-pan-min-scale="1"
@ -26,7 +33,7 @@
@load="onImageLoaded" @load="onImageLoaded"
> >
</PinchZoom> </PinchZoom>
</div> </SwipeClick>
<VideoAttachment <VideoAttachment
v-if="type === 'video'" v-if="type === 'video'"
class="modal-image" class="modal-image"

View File

@ -2,5 +2,10 @@ import PinchZoom from '@kazvmoe-infra/pinch-zoom-element'
export default { export default {
props: { props: {
},
methods: {
setTransform ({ scale, x, y }) {
this.$el.setTransform({ scale, x, y })
}
} }
} }

View File

@ -26,67 +26,13 @@ const mediaViewer = {
return supportedTypes.has(type) return supportedTypes.has(type)
}) })
commit('setMedia', media) commit('setMedia', media)
dispatch('swipeScaler/reset')
}, },
setCurrentMedia ({ commit, state, dispatch }, current) { setCurrentMedia ({ commit, state }, current) {
const index = state.media.indexOf(current) const index = state.media.indexOf(current)
commit('setCurrentMedia', index || 0) commit('setCurrentMedia', index || 0)
dispatch('swipeScaler/reset')
}, },
closeMediaViewer ({ commit, dispatch }) { closeMediaViewer ({ commit, dispatch }) {
commit('close') commit('close')
dispatch('swipeScaler/reset')
}
},
modules: {
swipeScaler: {
namespaced: true,
state: {
origOffsets: [0, 0],
offsets: [0, 0],
origScaling: 1,
scaling: 1
},
mutations: {
reset (state) {
state.origOffsets = [0, 0]
state.offsets = [0, 0]
state.origScaling = 1
state.scaling = 1
},
applyOffsets (state, { offsets }) {
state.offsets = state.origOffsets.map((k, n) => k + offsets[n])
},
applyScaling (state, { scaling }) {
state.scaling = state.origScaling * scaling
},
finish (state) {
state.origOffsets = [...state.offsets]
state.origScaling = state.scaling
},
revert (state) {
state.offsets = [...state.origOffsets]
state.scaling = state.origScaling
}
},
actions: {
reset ({ commit }) {
commit('reset')
},
apply ({ commit }, { offsets, scaling = 1 }) {
commit('applyOffsets', { offsets })
commit('applyScaling', { scaling })
},
finish ({ commit }) {
commit('finish')
},
revert ({ commit }) {
commit('revert')
}
}
} }
} }
} }

View File

@ -4,25 +4,27 @@ const DIRECTION_RIGHT = [1, 0]
const DIRECTION_UP = [0, -1] const DIRECTION_UP = [0, -1]
const DIRECTION_DOWN = [0, 1] const DIRECTION_DOWN = [0, 1]
const DISTANCE_MIN = 1 // const DISTANCE_MIN = 1
const isSwipeEvent = e => (e.touches.length === 1) // const isSwipeEvent = e => (e.touches.length === 1)
const isSwipeEventEnd = e => (e.changedTouches.length === 1) // const isSwipeEventEnd = e => (e.changedTouches.length === 1)
const isScaleEvent = e => (e.targetTouches.length === 2) // const isScaleEvent = e => (e.targetTouches.length === 2)
const isScaleEventEnd = e => (e.targetTouches.length === 1) // const isScaleEventEnd = e => (e.targetTouches.length === 1)
const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]] const deltaCoord = (oldCoord, newCoord) => [newCoord[0] - oldCoord[0], newCoord[1] - oldCoord[1]]
const vectorMinus = (a, b) => a.map((k, n) => k - b[n]) // const vectorMinus = (a, b) => a.map((k, n) => k - b[n])
const vectorAdd = (a, b) => a.map((k, n) => k + b[n]) // const vectorAdd = (a, b) => a.map((k, n) => k + b[n])
const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length) // const avgCoord = (coords) => [...coords].reduce(vectorAdd, [0, 0]).map(d => d / coords.length)
const touchCoord = touch => [touch.screenX, touch.screenY] const touchCoord = touch => [touch.screenX, touch.screenY]
const touchEventCoord = e => touchCoord(e.touches[0]) const touchEventCoord = e => touchCoord(e.touches[0])
const pointerEventCoord = e => [e.clientX, e.clientY]
const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1]) const vectorLength = v => Math.sqrt(v[0] * v[0] + v[1] * v[1])
const perpendicular = v => [v[1], -v[0]] const perpendicular = v => [v[1], -v[0]]
@ -78,104 +80,87 @@ const updateSwipe = (event, gesture) => {
gesture._swiping = false gesture._swiping = false
} }
class SwipeAndScaleGesture { class SwipeAndClickGesture {
// swipePreviewCallback(offsets: Array[Number]) // swipePreviewCallback(offsets: Array[Number])
// offsets: the offset vector which the underlying component should move, from the starting position // offsets: the offset vector which the underlying component should move, from the starting position
// pinchPreviewCallback(offsets: Array[Number], scaling: Number) // swipeEndCallback(sign: 0|-1|1)
// offsets: the offset vector which the underlying component should move, from the starting position
// scaling: the scaling factor we should apply to the underlying component, from the starting position
// swipeEndcallback(sign: 0|-1|1)
// sign: if the swipe does not meet the threshold, 0 // sign: if the swipe does not meet the threshold, 0
// if the swipe meets the threshold in the positive direction, 1 // if the swipe meets the threshold in the positive direction, 1
// if the swipe meets the threshold in the negative direction, -1 // if the swipe meets the threshold in the negative direction, -1
constructor ({ constructor ({
direction, direction,
// swipeStartCallback, pinchStartCallback, // swipeStartCallback
swipePreviewCallback, pinchPreviewCallback, swipePreviewCallback,
swipeEndCallback, pinchEndCallback, swipeEndCallback,
swipeCancelCallback,
swipelessClickCallback,
threshold = 30, perpendicularTolerance = 1.0 threshold = 30, perpendicularTolerance = 1.0
}) { }) {
const nop = () => { console.log('Warning: Not implemented') } const nop = () => { console.log('Warning: Not implemented') }
this.direction = direction this.direction = direction
this.swipePreviewCallback = swipePreviewCallback || nop this.swipePreviewCallback = swipePreviewCallback || nop
this.pinchPreviewCallback = pinchPreviewCallback || nop
this.swipeEndCallback = swipeEndCallback || nop this.swipeEndCallback = swipeEndCallback || nop
this.pinchEndCallback = pinchEndCallback || nop this.swipeCancelCallback = swipeCancelCallback || nop
this.swipelessClickCallback = swipelessClickCallback || nop
this.threshold = threshold this.threshold = threshold
this.perpendicularTolerance = perpendicularTolerance this.perpendicularTolerance = perpendicularTolerance
this._reset()
}
_reset () {
this._startPos = [0, 0] this._startPos = [0, 0]
this._startDistance = DISTANCE_MIN this._pointerId = -1
this._swiping = false this._swiping = false
this._swiped = false
} }
start (event) { start (event) {
console.log('start() called', event) console.log('start() called', event)
if (isSwipeEvent(event)) {
this._startPos = touchEventCoord(event) this._startPos = pointerEventCoord(event)
this._pointerId = event.pointerId
console.log('start pos:', this._startPos) console.log('start pos:', this._startPos)
this._swiping = true this._swiping = true
} else if (isScaleEvent(event)) { this._swiped = false
const coords = [...event.targetTouches].map(touchCoord)
this._startPos = avgCoord(coords)
this._startDistance = vectorLength(deltaCoord(coords[0], coords[1]))
if (this._startDistance < DISTANCE_MIN) {
this._startDistance = DISTANCE_MIN
}
this._scalePoints = [...event.targetTouches]
this._swiping = false
console.log(
'is scale event, start =', this._startPos,
'dist =', this._startDistance)
}
} }
move (event) { move (event) {
// console.log('move called', event) if (this._swiping && this._pointerId === event.pointerId) {
if (isSwipeEvent(event)) { this._swiped = true
const touch = event.changedTouches[0]
const delta = deltaCoord(this._startPos, touchCoord(touch)) const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, coord)
this.swipePreviewCallback(delta) this.swipePreviewCallback(delta)
} else if (isScaleEvent(event)) {
console.log('is scale event')
const coords = [...event.targetTouches].map(touchCoord)
const curPos = avgCoord(coords)
const curDistance = vectorLength(deltaCoord(coords[0], coords[1]))
const scaling = curDistance / this._startDistance
const posDiff = vectorMinus(curPos, this._startPos)
// const delta = vectorAdd(numProduct((1 - scaling), this._startPos), posDiff)
const delta = posDiff
// console.log(
// 'is scale event, cur =', curPos,
// 'dist =', curDistance,
// 'scale =', scaling,
// 'delta =', delta)
this.pinchPreviewCallback(delta, scaling)
} }
} }
cancel (event) {
if (!this._swiping || this._pointerId !== event.pointerId) {
return
}
this.swipeCancelCallback()
}
end (event) { end (event) {
console.log('end() called', event)
if (isScaleEventEnd(event)) {
this.pinchEndCallback()
}
if (!isSwipeEventEnd(event)) {
console.log('not swipe event')
return
}
if (!this._swiping) { if (!this._swiping) {
console.log('not swiping') console.log('not swiping')
return return
} }
this.swiping = false
console.log('is swipe event') if (this._pointerId !== event.pointerId) {
console.log('pointer id does not match')
return
}
this._swiping = false
console.log('end: is swipe event')
// movement too small // movement too small
const touch = event.changedTouches[0] const coord = pointerEventCoord(event)
const delta = deltaCoord(this._startPos, touchCoord(touch)) const delta = deltaCoord(this._startPos, coord)
this.swipePreviewCallback(delta)
const sign = (() => { const sign = (() => {
if (vectorLength(delta) < this.threshold) { if (vectorLength(delta) < this.threshold) {
@ -198,7 +183,12 @@ class SwipeAndScaleGesture {
return isPositive ? 1 : -1 return isPositive ? 1 : -1
})() })()
if (this._swiped) {
this.swipeEndCallback(sign) this.swipeEndCallback(sign)
} else {
this.swipelessClickCallback()
}
this._reset()
} }
} }
@ -210,7 +200,7 @@ const GestureService = {
swipeGesture, swipeGesture,
beginSwipe, beginSwipe,
updateSwipe, updateSwipe,
SwipeAndScaleGesture SwipeAndClickGesture
} }
export default GestureService export default GestureService