Skip to content

Commit 92401d3

Browse files
authored
Merge pull request #35 from Riron/pinch
Pinch
2 parents 938fca8 + 31341cc commit 92401d3

10 files changed

+235
-93
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# 1.2.2
2+
3+
### Features
4+
5+
Support for pinch to zoom
6+
7+
### Bug Fixes
8+
9+
**CSS:** In iOS the status bar does not overlapps the back button anymore
10+
111
# 1.1.5
212

313
- Update for AOT support (related to PR #9)

README.md

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,6 @@ import { IonicImageViewerModule } from 'ionic-img-viewer';
3636
export class AppModule {}
3737
```
3838

39-
**For Ionic 2 beta version:**
40-
41-
Import the image viewer directive in your component.
42-
43-
```typescript
44-
import { ImageViewerDirective } from 'ionic-img-viewer';
45-
46-
47-
@Component({
48-
template: `<img [src]="url" imageViewer />`,
49-
directives: [ImageViewerDirective]
50-
})
51-
class MyComponent {
52-
53-
}
54-
```
55-
5639
## Usage
5740

5841
Add the `imageViewer` property to the pictures.
@@ -61,6 +44,15 @@ Add the `imageViewer` property to the pictures.
6144
<img src="IMAGE_URL" imageViewer />
6245
```
6346

47+
If you use thumbnails and want to display bigger images, you can use it like so :
48+
49+
```html
50+
<img src="IMAGE_URL" imageViewer="OTHER_IMAGE_URL" />
51+
```
52+
53+
However, if `OTHER_IMAGE_URL` is not preloaded, the animation might suffer. Indeed there will be no ready image to make the transition (it might blink and you'll not get the smooth transition effet while opening).
54+
So try to cache your image before the call if you use it that way.
55+
6456
# Contributing
6557

6658
See [CONTRIBUTING.md](CONTRIBUTING.md).

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ionic-img-viewer",
3-
"version": "1.2.1",
3+
"version": "1.2.2",
44
"description": "Ionic 2 component providing a Twitter inspired experience to visualize pictures.",
55
"main": "./dist/ionic-img-viewer.js",
66
"typings": "./dist/ionic-img-viewer.d.ts",
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { Renderer } from '@angular/core';
12
import { ImageViewerComponent } from './image-viewer.component';
23
import { PanGesture } from 'ionic-angular/gestures/drag-gesture';
34
import { pointerCoord } from 'ionic-angular/util/dom';
@@ -8,15 +9,15 @@ const HAMMER_THRESHOLD = 10;
89
const MAX_ATTACK_ANGLE = 45;
910
const DRAG_THRESHOLD = 70;
1011

11-
export class ImageViewerGesture extends PanGesture {
12+
export class ImageViewerTransitionGesture extends PanGesture {
1213

1314
private translationY: number;
1415
private opacity: number;
1516
private startY: number;
1617
private imageContainer: HTMLElement;
1718
private backdrop: HTMLElement;
1819

19-
constructor(platform: Platform, private component: ImageViewerComponent, domCtrl: DomController, private cb: Function) {
20+
constructor(platform: Platform, private component: ImageViewerComponent, domCtrl: DomController, private renderer: Renderer, private cb: Function) {
2021
super(platform, component.getNativeElement(), {
2122
maxAngle: MAX_ATTACK_ANGLE,
2223
threshold: HAMMER_THRESHOLD,
@@ -32,24 +33,37 @@ export class ImageViewerGesture extends PanGesture {
3233
this.listen();
3334
}
3435

36+
// As we handle both pinch and drag, we have to make sure we don't drag when we are trying to pinch
37+
isPinching(ev) {
38+
return ev.touches && ev.touches.length > 1;
39+
}
40+
3541
onDragStart(ev: any): boolean {
42+
if (this.isPinching(ev)) {
43+
this.abort(ev);
44+
}
45+
3646
let coord = pointerCoord(ev);
3747
this.startY = coord.y;
3848
return true;
3949
}
4050

41-
canStart(): boolean {
42-
return !this.component.isZoomed;
51+
canStart(ev: any): boolean {
52+
return !this.component.isZoomed && !this.isPinching(ev);
4353
}
4454

4555
onDragMove(ev: any): boolean {
56+
if (this.isPinching(ev)) {
57+
this.abort(ev);
58+
}
59+
4660
let coord = pointerCoord(ev);
4761
this.translationY = coord.y - this.startY;
4862
this.opacity = Math.max(1 - Math.abs(this.translationY) / (10 * DRAG_THRESHOLD), .5);
4963

5064
this.plt.raf(() => {
51-
this.imageContainer.style[this.plt.Css.transform] = `translateY(${this.translationY}px)`;
52-
this.backdrop.style['opacity'] = this.opacity.toString();
65+
this.renderer.setElementStyle(this.imageContainer, this.plt.Css.transform, `translateY(${this.translationY}px)`);
66+
this.renderer.setElementStyle(this.backdrop, 'opacity', this.opacity.toString());
5367
});
5468

5569
return true;

src/image-viewer-transitions.ts

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,22 @@ export function registerCustomTransitions(config: Config) {
1111
export class ImageViewerEnter extends Transition {
1212
init() {
1313

14-
let ele = this.enteringView.pageRef().nativeElement;
14+
const ele = this.enteringView.pageRef().nativeElement;
1515

16-
let fromPosition = this.enteringView.data.position;
17-
let toPosition = ele.querySelector('img').getBoundingClientRect();
18-
let flipS = fromPosition.width / toPosition.width;
19-
let flipY = fromPosition.top - toPosition.top;
20-
let flipX = fromPosition.left - toPosition.left;
16+
const fromPosition = this.enteringView.data.position;
17+
const toPosition = ele.querySelector('img').getBoundingClientRect();
18+
const flipS = fromPosition.width / toPosition.width;
19+
const flipY = fromPosition.top - toPosition.top;
20+
const flipX = fromPosition.left - toPosition.left;
2121

22-
let backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop'));
23-
let image = new Animation(this.plt, ele.querySelector('.image'));
22+
const backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop'));
23+
const image = new Animation(this.plt, ele.querySelector('.image'));
2424

2525
image.fromTo('translateY', `${flipY}px`, '0px')
2626
.fromTo('translateX', `${flipX}px`, '0px')
27-
.fromTo('scale', flipS, '1');
27+
.fromTo('scale', flipS, '1')
28+
.beforeStyles({ 'transform-origin': '0 0' })
29+
.afterClearStyles(['transform-origin']);
2830

2931
backdrop.fromTo('opacity', '0.01', '1');
3032

@@ -37,11 +39,11 @@ export class ImageViewerEnter extends Transition {
3739
const enteringNavbarEle = enteringPageEle.querySelector('ion-navbar');
3840
const enteringBackBtnEle = enteringPageEle.querySelector('.back-button');
3941

40-
let enteringNavBar = new Animation(this.plt, enteringNavbarEle);
42+
const enteringNavBar = new Animation(this.plt, enteringNavbarEle);
4143
enteringNavBar.beforeAddClass('show-navbar');
4244
this.add(enteringNavBar);
4345

44-
let enteringBackButton = new Animation(this.plt, enteringBackBtnEle);
46+
const enteringBackButton = new Animation(this.plt, enteringBackBtnEle);
4547
this.add(enteringBackButton);
4648
enteringBackButton.beforeAddClass('show-back-button');
4749
}
@@ -50,30 +52,32 @@ export class ImageViewerEnter extends Transition {
5052
export class ImageViewerLeave extends Transition {
5153
init() {
5254

53-
let ele = this.leavingView.pageRef().nativeElement;
55+
const ele = this.leavingView.pageRef().nativeElement;
5456

55-
let toPosition = this.leavingView.data.position;
56-
let fromPosition = ele.querySelector('img').getBoundingClientRect();
57+
const toPosition = this.leavingView.data.position;
58+
const fromPosition = ele.querySelector('img').getBoundingClientRect();
5759

5860
let offsetY = 0;
59-
let imageYOffset = ele.querySelector('.image').style[this.plt.Css.transform];
61+
const imageYOffset = ele.querySelector('.image').style[this.plt.Css.transform];
6062
if (imageYOffset) {
61-
let regexResult = imageYOffset.match(/translateY\((-?\d+)px\)/);
63+
const regexResult = imageYOffset.match(/translateY\((-?\d*\.?\d*)px\)/);
6264
offsetY = regexResult ? parseFloat(regexResult[1]) : offsetY;
6365
}
6466

65-
let flipS = toPosition.width / fromPosition.width;
66-
let flipY = toPosition.top - fromPosition.top + offsetY;
67-
let flipX = toPosition.left - fromPosition.left;
67+
const flipS = toPosition.width / fromPosition.width;
68+
const flipY = toPosition.top - fromPosition.top + offsetY;
69+
const flipX = toPosition.left - fromPosition.left;
6870

69-
let backdropOpacity = ele.querySelector('ion-backdrop').style['opacity'];
71+
const backdropOpacity = ele.querySelector('ion-backdrop').style['opacity'];
7072

71-
let backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop'));
72-
let image = new Animation(this.plt, ele.querySelector('.image'));
73+
const backdrop = new Animation(this.plt, ele.querySelector('ion-backdrop'));
74+
const image = new Animation(this.plt, ele.querySelector('.image'));
7375

7476
image.fromTo('translateY', `${offsetY}px`, `${flipY}px`)
7577
.fromTo('translateX', `0px`, `${flipX}px`)
76-
.fromTo('scale', '1', flipS);
78+
.fromTo('scale', '1', flipS)
79+
.beforeStyles({ 'transform-origin': '0 0' })
80+
.afterClearStyles(['transform-origin']);
7781

7882
backdrop.fromTo('opacity', backdropOpacity, '0');
7983

src/image-viewer-zoom-gesture.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { Renderer } from '@angular/core';
2+
import { Animation, DomController, Gesture, Platform } from 'ionic-angular';
3+
import { DIRECTION_HORIZONTAL, DIRECTION_VERTICAL } from 'ionic-angular/gestures/hammer';
4+
5+
import { ImageViewerComponent } from './image-viewer.component';
6+
7+
const MAX_SCALE = 3;
8+
9+
export class ImageViewerZoomGesture extends Gesture {
10+
private adjustScale = 1;
11+
private adjustDeltaX = 0;
12+
private adjustDeltaY = 0;
13+
14+
private currentScale = 1;
15+
private currentDeltaX = 0;
16+
private currentDeltaY = 0;
17+
18+
private allowedXMargin = 0;
19+
private allowedYMargin = 0;
20+
21+
constructor(private component: ImageViewerComponent, element: any, private platform: Platform, private renderer: Renderer) {
22+
super(element.nativeElement);
23+
24+
// Force both directions after super to avoid override allowing only one direction
25+
this.options({ direction: DIRECTION_HORIZONTAL | DIRECTION_VERTICAL });
26+
27+
this.listen();
28+
this.on('pinch', (e) => this.onPinch(e));
29+
this.on('pinchend', (e) => this.onPinchEnd(e));
30+
this.on('panstart', (e) => this.onPanStart(e));
31+
this.on('pan', (e) => this.onPan(e));
32+
this.on('panend', (e) => this.onPanEnd(e));
33+
this.on('doubletap', (e) => this.onDoubleTap(e));
34+
}
35+
36+
onPinch(event) {
37+
this.component.dragGesture.abort(event);
38+
39+
this.currentScale = Math.max(1, Math.min(MAX_SCALE, this.adjustScale * event.scale));
40+
41+
this.currentDeltaX = this.adjustDeltaX + (event.deltaX / this.currentScale);
42+
this.currentDeltaY = this.adjustDeltaY + (event.deltaY / this.currentScale);
43+
44+
this.setImageContainerTransform();
45+
}
46+
47+
onPinchEnd(event) {
48+
this.component.isZoomed = (this.currentScale !== 1);
49+
50+
if (!this.component.isZoomed) {
51+
52+
new Animation(this.platform, this.element)
53+
.fromTo('translateX', `${this.currentDeltaX}px`, '0')
54+
.fromTo('translateY', `${this.currentDeltaY}px`, '0')
55+
.easing('ease-in')
56+
.duration(50)
57+
.play();
58+
59+
this.currentDeltaX = 0;
60+
this.currentDeltaY = 0;
61+
}
62+
63+
// Saving the final transforms for adjustment next time the user interacts.
64+
this.adjustScale = this.currentScale;
65+
this.adjustDeltaX = this.currentDeltaX;
66+
this.adjustDeltaY = this.currentDeltaY;
67+
}
68+
69+
onPanStart(event) {
70+
if (!this.component.isZoomed) {
71+
return;
72+
}
73+
74+
const originalImageWidth = this.element.offsetWidth;
75+
const originalImageHeight = this.element.offsetHeight;
76+
77+
this.allowedXMargin = ((originalImageWidth * this.currentScale) - originalImageWidth) / 4;
78+
this.allowedYMargin = ((originalImageHeight * this.currentScale) - originalImageHeight) / 4;
79+
}
80+
81+
onPan(event) {
82+
if (!this.component.isZoomed) {
83+
return;
84+
}
85+
86+
this.currentDeltaX = this.boundAdjustement(Math.floor(this.adjustDeltaX + event.deltaX), this.allowedXMargin);
87+
this.currentDeltaY = this.boundAdjustement(Math.floor(this.adjustDeltaY + event.deltaY), this.allowedYMargin);
88+
89+
this.setImageContainerTransform();
90+
}
91+
92+
boundAdjustement(adjustement, bound) {
93+
if (adjustement > bound || adjustement < -bound) {
94+
return Math.min(bound, Math.max(adjustement, -bound));
95+
}
96+
return adjustement;
97+
}
98+
99+
onPanEnd(event) {
100+
if (!this.component.isZoomed) {
101+
return;
102+
}
103+
104+
this.adjustDeltaX = this.currentDeltaX;
105+
this.adjustDeltaY = this.currentDeltaY;
106+
}
107+
108+
onDoubleTap(event) {
109+
this.component.isZoomed = !this.component.isZoomed;
110+
if (this.component.isZoomed) {
111+
this.currentScale = 2;
112+
} else {
113+
this.currentScale = 1;
114+
115+
this.adjustDeltaX = this.currentDeltaX = 0;
116+
this.adjustDeltaY = this.currentDeltaY = 0;
117+
}
118+
119+
this.adjustScale = this.currentScale;
120+
this.setImageContainerTransform();
121+
}
122+
123+
setImageContainerTransform() {
124+
const transforms = [];
125+
transforms.push(`scale(${this.currentScale})`);
126+
transforms.push(`translate(${this.currentDeltaX}px, ${this.currentDeltaY}px)`);
127+
128+
this.renderer.setElementStyle(this.element, this.platform.Css.transform, transforms.join(' '));
129+
}
130+
}

0 commit comments

Comments
 (0)