Hello everybody, I share this in hope that you don’t waste as much time as I did on this, it still needs one more bug to fix but more on that below.
What is this?
I have created a function that enables pinch to zoom using ionics Gesture import, for the Pan and pinch events to a div container with Vertical elements. Since I have googled this a lot a couldn’t find an answer that would let me do this without the content being an image, I found that Ionic actually uses HammerJS, to grab the touch gestures, and wraps them on the “Gesture” import as seen Here
Where can I test it?
https://github.com/p-sebastian/ionic2-pinchzoom
The Magic
There are 3 files needed
- ~/src/pages/home/home.ts
- ~/src/pages/home/home.scss
- ~/src/pages/home/home.html
home.html
The <div #zoom class="zoom">
element is the fixed container that where we’ll attach the events to, and its children are the items that will be zoomed.
home.scss
.zoom
to fill the container size, with a fixed position and the important touch-action: none
which HammerJS needs. I have added the border color red, to show where the boundaries of each children are, so that we can see it doesn’t overflow.
home.ts
/*
* @Author: Sebastian Penafiel Torres
* @Date: 2017-04-23 19:25:39
* @Last Modified by: Sebastian Penafiel Torres
* @Last Modified time: 2017-04-23 19:25:39
*/
import { Component, ViewChild, ElementRef } from '@angular/core';
import { NavController, Gesture, Content } from 'ionic-angular';
@Component({
selector: 'page-home',
templateUrl: 'home.html'
})
export class HomePage {
@ViewChild(Content) content: Content;
@ViewChild('zoom') zoom: ElementRef;
constructor(public navCtrl: NavController) {
}
ionViewDidEnter(): void {
// Page must be fully rendered, ionViewDidLoad, doesnt work for this. Because it shows clientHeight without the margin of the header
this._pinchZoom(this.zoom.nativeElement, this.content);
}
private _pinchZoom(elm: HTMLElement, content: Content): void {
const _gesture = new Gesture(elm);
// max translate x = (container_width - element absolute_width)px
// max translate y = (container_height - element absolute_height)px
let ow = 0;
let oh = 0;
for (let i = 0; i < elm.children.length; i++) {
let c = <HTMLElement>elm.children.item(i);
ow = c.offsetWidth;
oh += c.offsetHeight;
}
const original_x = content.contentWidth - ow;
const original_y = content.contentHeight - oh;
let max_x = original_x;
let max_y = original_y;
let min_x = 0;
let min_y = 0;
let x = 0;
let y = 0;
let last_x = 0;
let last_y = 0;
let scale = 1;
let base = scale;
_gesture.listen();
_gesture.on('pan', onPan);
_gesture.on('panend', onPanend);
_gesture.on('pancancel', onPanend);
// _gesture.on('tap', onTap);
_gesture.on('pinch', onPinch);
_gesture.on('pinchend', onPinchend);
_gesture.on('pinchcancel', onPinchend);
function onPan(ev) {
setCoor(ev.deltaX, ev.deltaY);
transform();
}
function onPanend() {
// remembers previous position to continue panning.
last_x = x;
last_y = y;
}
function onTap(ev) {
if (ev.tapCount === 2) {
let reset = false;
scale += .5;
if (scale > 2) {
scale = 1;
reset = true;
}
setBounds();
reset ? transform(max_x/2, max_y/2) : transform();
}
}
function onPinch(ev) {
// formula to append scale to new scale
scale = base + (ev.scale * scale - scale)/scale
setBounds();
transform();
}
function onPinchend(ev) {
if (scale > 4) {
scale = 4;
}
if (scale < 0.5) {
scale = 0.5;
}
// lets pinch know where the new base will start
base = scale;
setBounds();
transform();
}
function setBounds() {
// I am scaling the container not the elements
// since container is fixed, the container scales from the middle, while the
// content scales down and right, with the top and left of the container as boundaries
// scaled = absolute width * scale - already set width divided by 2;
let scaled_x = Math.ceil((elm.offsetWidth * scale - elm.offsetWidth) / 2);
let scaled_y = Math.ceil((elm.offsetHeight * scale - elm.offsetHeight) / 2);
// for max_x && max_y; adds the value relevant to their overflowed size
let overflow_x = Math.ceil(original_x * scale - original_x); // returns negative
let overflow_y = Math.ceil(oh * scale - oh);
max_x = original_x - scaled_x + overflow_x;
min_x = 0 + scaled_x;
// remove added height from container
max_y = original_y + scaled_y - overflow_y;
min_y = 0 + scaled_y;
setCoor(-scaled_x, scaled_y);
console.info(`x: ${x}, scaled_x: ${scaled_x}, y: ${y}, scaled_y: ${scaled_y}`)
}
function setCoor(xx: number, yy: number) {
x = Math.min(Math.max((last_x + xx), max_x), min_x);
y = Math.min(Math.max((last_y + yy), max_y), min_y);
}
// xx && yy are for resetting the position when the scale return to 1.
function transform(xx?: number, yy?: number) {
elm.style.webkitTransform = `translate3d(${xx || x}px, ${yy || y}px, 0) scale3d(${scale}, ${scale}, 1)`;
}
}
}
It takes the Content to figure out the actual size without the footer and headers getting in the way, the for loop just adds the absolute height of each children of #zoom. The original sizes equal to the viewport that the user sees minus the actual size of the content, since we override the scroll, the viewing of the content which will most likely overflow the bottom and right (because its fixed and can be any size), will be done via panning so we must know how much its overflowing.
the max sizes to pan are the original overflowed size, for now before zooming, and scale will equal to 1 at first.
base is to remember where we left off when pinched to zoom.
We initiate the event listening individually*, panend & pancancel as well as pinchend & pinchcancel are each called depending on the timing when you release the fingers so both must be added, to call their respective onEnd function.
setCoor function
sets the x & y coordinates making sure that it doesn’t go past it’s max
transform function
moves “translate”, and scales the #zoom element, the xx & yy are for resetting the position
onPinchend function
sets the base, and sets the limits of the scale
onPinch function
sets the scale depending where it left off.
setBounds function
// hard to explain.
TO-DO
The part that I am missing is that it zooms to the previous x & y coordinates, I need it to zoom to the center of the pinch.
Also to take account of negative margins the children might have.
Hope this helps somebody.