Merge pull request #6656 from chenjiahan/image_preview_scale_0630

fix(ImagePreview): incorrect position after scale
This commit is contained in:
neverland 2020-06-30 17:16:21 +08:00 committed by GitHub
parent e93bec677e
commit 1ee44f772a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 139 additions and 83 deletions

View File

@ -11,8 +11,6 @@ import Image from '../image';
import Loading from '../loading'; import Loading from '../loading';
import SwipeItem from '../swipe-item'; import SwipeItem from '../swipe-item';
const DOUBLE_CLICK_INTERVAL = 250;
function getDistance(touches) { function getDistance(touches) {
return Math.sqrt( return Math.sqrt(
(touches[0].clientX - touches[1].clientX) ** 2 + (touches[0].clientX - touches[1].clientX) ** 2 +
@ -40,6 +38,8 @@ export default {
moveY: 0, moveY: 0,
moving: false, moving: false,
zooming: false, zooming: false,
displayWidth: 0,
displayHeight: 0,
}; };
}, },
@ -58,34 +58,33 @@ export default {
return style; return style;
}, },
maxMoveX() {
if (this.displayWidth) {
return Math.max(
0,
(this.scale * this.displayWidth - this.windowWidth) / 2
);
}
return 0;
},
maxMoveY() {
if (this.displayHeight) {
return Math.max(
0,
(this.scale * this.displayHeight - this.windowHeight) / 2
);
}
return 0;
},
},
mounted() {
this.bindTouchEvent(this.$el);
}, },
methods: { methods: {
startMove() {
this.setMaxMove();
this.moving = true;
this.startMoveX = this.moveX;
this.startMoveY = this.moveY;
},
setMaxMove() {
const {
scale,
windowWidth,
windowHeight,
displayWidth,
displayHeight,
} = this;
if (this.displayWidth && this.displayHeight) {
this.maxMoveX = Math.max(0, (displayWidth * scale - windowWidth) / 2);
this.maxMoveY = Math.max(0, (displayHeight * scale - windowHeight) / 2);
} else {
this.maxMoveX = 0;
this.maxMoveY = 0;
}
},
resetScale() { resetScale() {
this.setScale(1); this.setScale(1);
this.moveX = 0; this.moveX = 0;
@ -115,10 +114,15 @@ export default {
this.touchStart(event); this.touchStart(event);
this.touchStartTime = new Date(); this.touchStartTime = new Date();
if (touches.length === 1 && this.scale !== 1) { this.startMoveX = this.moveX;
this.startMove(); this.startMoveY = this.moveY;
} else if (touches.length === 2 && !offsetX) {
this.startZoom(event); this.moving = touches.length === 1 && this.scale !== 1;
this.zooming = touches.length === 2 && !offsetX;
if (this.zooming) {
this.startScale = this.scale;
this.startDistance = getDistance(event.touches);
} }
}, },
@ -147,9 +151,12 @@ export default {
}, },
onTouchEnd(event) { onTouchEnd(event) {
let stopPropagation = false;
/* istanbul ignore else */ /* istanbul ignore else */
if (this.moving || this.zooming) { if (this.moving || this.zooming) {
let stopPropagation = true;
stopPropagation = true;
if ( if (
this.moving && this.moving &&
@ -160,8 +167,13 @@ export default {
} }
if (!event.touches.length) { if (!event.touches.length) {
if (this.zooming) {
this.moveX = range(this.moveX, -this.maxMoveX, this.maxMoveX);
this.moveY = range(this.moveY, -this.maxMoveY, this.maxMoveY);
this.zooming = false;
}
this.moving = false; this.moving = false;
this.zooming = false;
this.startMoveX = 0; this.startMoveX = 0;
this.startMoveY = 0; this.startMoveY = 0;
this.startScale = 1; this.startScale = 1;
@ -170,38 +182,36 @@ export default {
this.resetScale(); this.resetScale();
} }
} }
if (stopPropagation) {
preventDefault(event, true);
}
} }
const deltaTime = new Date() - this.touchStartTime; preventDefault(event, stopPropagation);
const { offsetX = 0, offsetY = 0 } = this;
// prevent long tap to close component
if (deltaTime < DOUBLE_CLICK_INTERVAL && offsetX < 10 && offsetY < 10) {
if (!this.doubleClickTimer) {
this.doubleClickTimer = setTimeout(() => {
this.$emit('close');
this.doubleClickTimer = null;
}, DOUBLE_CLICK_INTERVAL);
} else {
clearTimeout(this.doubleClickTimer);
this.doubleClickTimer = null;
this.toggleScale();
}
}
this.checkTap();
this.resetTouchStatus(); this.resetTouchStatus();
}, },
startZoom(event) { checkTap() {
this.moving = false; const { offsetX = 0, offsetY = 0 } = this;
this.zooming = true; const deltaTime = new Date() - this.touchStartTime;
this.startScale = this.scale; const TAP_TIME = 250;
this.startDistance = getDistance(event.touches); const TAP_OFFSET = 10;
if (
offsetX < TAP_OFFSET &&
offsetY < TAP_OFFSET &&
deltaTime < TAP_TIME
) {
if (this.doubleTapTimer) {
clearTimeout(this.doubleTapTimer);
this.doubleTapTimer = null;
this.toggleScale();
} else {
this.doubleTapTimer = setTimeout(() => {
this.$emit('close');
this.doubleTapTimer = null;
}, TAP_TIME);
}
}
}, },
onLoad(event) { onLoad(event) {
@ -226,12 +236,7 @@ export default {
}; };
return ( return (
<SwipeItem <SwipeItem>
nativeOnTouchstart={this.onTouchStart}
nativeOnTouchmove={this.onTouchMove}
nativeOnTouchend={this.onTouchEnd}
nativeOnTouchcancel={this.onTouchEnd}
>
<Image <Image
src={this.src} src={this.src}
fit="contain" fit="contain"

View File

@ -60,10 +60,12 @@ exports[`set show-index prop to false 1`] = `
</div> </div>
`; `;
exports[`zoom 1`] = ` exports[`zoom in and drag image to move 1`] = `
<div class="van-image van-image-preview__image" style="transition-duration: 0s; transform: scale(2, 2) translate(0px, 0px);"><img src="https://img.yzcdn.cn/1.png" class="van-image__img" style="object-fit: contain;"> <div class="van-image van-image-preview__image" style="transition-duration: 0s; transform: scale(2, 2) translate(0px, 0px);"><img src="https://img.yzcdn.cn/1.png" class="van-image__img" style="object-fit: contain;">
<div class="van-image__loading"> <div class="van-image__loading">
<div class="van-loading van-loading--spinner"><span class="van-loading__spinner van-loading__spinner--spinner"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span></div> <div class="van-loading van-loading--spinner"><span class="van-loading__spinner van-loading__spinner--spinner"><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i></span></div>
</div> </div>
</div> </div>
`; `;
exports[`zoom in and drag image to move 2`] = `<div class="van-image van-image-preview__image" style="transition-duration: 0s; transform: scale(2, 2) translate(25px, 25px);"><img src="https://img.yzcdn.cn/1.png" class="van-image__img" style="object-fit: contain;"></div>`;

View File

@ -1,14 +1,27 @@
import Vue from 'vue'; import Vue from 'vue';
import ImagePreview from '..'; import ImagePreview from '..';
import ImagePreviewVue from '../ImagePreview'; import ImagePreviewVue from '../ImagePreview';
import { mount, trigger, triggerDrag, later } from '../../../test'; import { mount, trigger, triggerDrag, later, mockGetBoundingClientRect } from '../../../test';
function triggerZoom(el, x, y) { function triggerTwoFingerTouchmove(el, x, y) {
trigger(el, 'touchstart', 0, 0, { x, y });
trigger(el, 'touchmove', -x / 4, -y / 4, { x, y });
trigger(el, 'touchmove', -x / 3, -y / 3, { x, y });
trigger(el, 'touchmove', -x / 2, -y / 2, { x, y });
trigger(el, 'touchmove', -x, -y, { x, y }); trigger(el, 'touchmove', -x, -y, { x, y });
}
function triggerZoom(el, x, y, direction = 'in') {
trigger(el, 'touchstart', 0, 0, { x, y });
if (direction === 'in') {
triggerTwoFingerTouchmove(el, x / 4, y / 4);
triggerTwoFingerTouchmove(el, x / 3, y / 3);
triggerTwoFingerTouchmove(el, x / 2, y / 2);
triggerTwoFingerTouchmove(el, x, y);
} else if (direction === 'out') {
triggerTwoFingerTouchmove(el, x, y);
triggerTwoFingerTouchmove(el, x / 2, y / 2);
triggerTwoFingerTouchmove(el, x / 3, y / 3);
triggerTwoFingerTouchmove(el, x / 4, y / 4);
}
trigger(el, 'touchend', 0, 0, { touchList: [] }); trigger(el, 'touchend', 0, 0, { touchList: [] });
} }
@ -172,9 +185,7 @@ test('onChange option', async (done) => {
}); });
test('onScale option', async (done) => { test('onScale option', async (done) => {
const { getBoundingClientRect } = Element.prototype; const restore = mockGetBoundingClientRect({ width: 100 });
Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 100 }));
const instance = ImagePreview({ const instance = ImagePreview({
images, images,
startPosition: 0, startPosition: 0,
@ -188,7 +199,7 @@ test('onScale option', async (done) => {
await later(); await later();
const image = instance.$el.querySelector('img'); const image = instance.$el.querySelector('img');
triggerZoom(image, 300, 300); triggerZoom(image, 300, 300);
Element.prototype.getBoundingClientRect = getBoundingClientRect; restore();
}); });
test('register component', () => { test('register component', () => {
@ -196,21 +207,59 @@ test('register component', () => {
expect(Vue.component(ImagePreviewVue.name)).toBeTruthy(); expect(Vue.component(ImagePreviewVue.name)).toBeTruthy();
}); });
test('zoom', async () => { test('zoom in and drag image to move', async () => {
const { getBoundingClientRect } = Element.prototype; const restore = mockGetBoundingClientRect({ width: 100 });
Element.prototype.getBoundingClientRect = jest.fn(() => ({ width: 100 })); const originWindowWidth = window.innerWidth;
const originWindowHeight = window.innerHeight;
window.innerWidth = 100;
window.innerHeight = 100;
const wrapper = mount(ImagePreviewVue, { const wrapper = mount(ImagePreviewVue, {
propsData: { images, value: true }, propsData: { images, value: true },
}); });
await later(); await later();
const image = wrapper.find('.van-image'); const image = wrapper.find('img');
triggerZoom(image, 300, 300); triggerZoom(image, 300, 300);
triggerDrag(image, 300, 300);
expect(image).toMatchSnapshot(); // mock image size
Element.prototype.getBoundingClientRect = getBoundingClientRect; ['naturalWidth', 'naturalHeight'].forEach((key) => {
Object.defineProperty(image.element, key, { value: 300 });
});
// drag image before load
triggerDrag(image, 300, 300);
expect(wrapper.find('.van-image')).toMatchSnapshot();
// drag image after load
image.trigger('load');
triggerDrag(image, 300, 300);
expect(wrapper.find('.van-image')).toMatchSnapshot();
window.innerWidth = originWindowWidth;
window.innerHeight = originWindowHeight;
restore();
});
test('zoom out', async () => {
const restore = mockGetBoundingClientRect({ width: 100 });
const onScale = jest.fn();
const wrapper = mount(ImagePreviewVue, {
propsData: { images, value: true },
listeners: {
scale: onScale,
},
});
await later();
const image = wrapper.find('.van-image');
triggerZoom(image, 300, 300, 'out');
expect(onScale).toHaveBeenLastCalledWith({ index: 0, scale: 1 });
restore();
}); });
test('set show-index prop to false', () => { test('set show-index prop to false', () => {