Custom Zoomable Directive Not working On Android


#1

Greetings,

I would first like to express appreciation for such an awesome framework thus far.

template: (‘scaled’ css class sets max-width: 100% for uniform image scaling and transform-origin: ‘top left’)

<ion-content padding="true" overflow-scroll="true" >
  <div id="container" ng-style="containerStyle">
    <img id="image-scrollable" zoomable ng-src="images/{{type.typeName}}.jpg" class="scaled">
  </div>
</ion-content>

controller:

'use strict';

angular.module('myApp').controller('HerpDerperShowCtrl',
  function($scope, $stateParams, HerpDerpersFactory) {
  var id = Number($stateParams.id);
  $scope.type = HerpDerpersFactory.get(id);

  $scope.containerStyle = {};
 
});

zoomable directive:

'use strict';

angular.module('myApp').directive('zoomable', function($ionicGesture) {
  return {
    restrict: 'A',
    link: function($scope, $element, $attrs) {
      var minHeight, minWidth, maxHeight, maxWidth;

      // set max/min size after image is loaded
      $element.bind('load', function() {
        var el = $element[0];
        minHeight = el.height;
        minWidth = el.width;
        maxHeight = el.naturalHeight;
        maxWidth = el.naturalWidth;
      });

      // pinch to scale
      var handlePinch = function(e) {
        $scope.$apply(function() {
          TweenMax.set($element, { scale: e.gesture.scale });
        });
      };
      var pinchGesture = $ionicGesture.on('pinch', handlePinch, $element);

      // resize after done
      var handleTransformEnd = function() {
        //resize zoomable container
        var newHeight, newWidth;
        var dimensions = $element[0].getBoundingClientRect();

        newHeight = Math.round(dimensions.height);
        newWidth = Math.round(dimensions.width);

        newHeight = Math.min(newHeight, maxHeight);
        newWidth = Math.min(newWidth, maxWidth);

        newHeight = Math.max(newHeight, minHeight);
        newWidth = Math.max(newWidth, minWidth);

        $scope.$apply(function() {
          TweenMax.set($element, { clearProps: 'scale' });
          $scope.containerStyle.height = newHeight + 'px';
          $scope.containerStyle.width = newWidth + 'px';
        })
      };
      $ionicGesture.on('transformend', handleTransformEnd, $element);

      // cleanup
      $scope.$on('$destroy', function() {
        $ionicGesture.off(handlePinch, 'pinch', $element);
        $ionicGesture.off(handleTransformEnd, 'transformend', $element);
      });
    }
  };
});

The pinching seems to work very performant on iOS (only tested 7+) after some initial lag, but the gesture is not even being recognized on android (4.3+ and even 4.0.4 running via crosswalk).

From what I can tell, the overflow-scroll attribute only sets a css class specifying -webkit-overflow-scrolling: touch; and I am not really sure why that would suppress my gesture handler that I created.

Alternatively, I tried out the ion-scroll directive with zooming=“true”, but there are several issues with that. Namely, the zooming is done via css scale and causes the image to look terrible. Also, there is no on-scroll-complete on this directive (which I would like to have used to do a manual resize of the image so the downsampling that is done in webkit can kick in). Lastly, the direction attribute only takes either x or y and not both – which would be ideal for a container that needs to uniformly scale.

I am using ionic beta3, cordova 3.4, crosswalk 4.32.76, and the angular-ionic yeoman generator.

I would also like to add that the $destroy callback where I turn off the gesture handlers throws an error. After looking at the docs, the on and off functions have the same signatures, but when I look at the source functions, they do not – so I am not sure what is going on.

Any insight or help would be appreciated.

Thanks in advance…

Jared


#2

So I came up with a relative decent solution to my problems on Android.

here is my zoomable directive:

'use strict';

angular.module('eAlgorithm').directive('zoomable', function($timeout, $ionicGesture) {
  return {
    restrict: 'A',
    scope: true,
    link: function($scope, $element, $attrs) {
      var minHeight, minWidth, maxHeight, maxWidth;

      // set max/min size after image is loaded
      $element.bind('load', function() {
        var el = $element[0];
        minHeight = el.height;
        minWidth = el.width;
        maxHeight = el.naturalHeight;
        maxWidth = el.naturalWidth;
      });

      // pinch to scale
      var handlePinch = function(e) {
        e.gesture.srcEvent.preventDefault();
        $scope.$apply(function() {
          TweenMax.set($element, { scale: e.gesture.scale });
        });
      };
      handlePinch = ionic.animationFrameThrottle(handlePinch);
      var pinchGesture = $ionicGesture.on('pinch', handlePinch, $element);

      // resize after done
      var handleTransformEnd = function() {
        //resize zoomable container
        var newHeight, newWidth;
        var dimensions = $element[0].getBoundingClientRect();

        newHeight = Math.round(dimensions.height);
        newWidth = Math.round(dimensions.width);

        // upper bounds (dictated by naturalHeight and naturalWidth of image)
        newHeight = Math.min(newHeight, maxHeight);
        newWidth = Math.min(newWidth, maxWidth);

        // lower bounds (dictacted by screen)
        newHeight = Math.max(newHeight, minHeight);
        newWidth = Math.max(newWidth, minWidth);

        $scope.$apply(function() {
          TweenMax.set($element, { clearProps: 'scale' });
          $scope.containerStyle.height = newHeight + 'px';
          $scope.containerStyle.width = newWidth + 'px';
        });
      };
      var resizeGesture = $ionicGesture.on('transformend', handleTransformEnd, $element);

      // cleanup
      $scope.$on('$destroy', function() {
        $ionicGesture.off(pinchGesture, 'pinch', $element);
        $ionicGesture.off(resizeGesture, 'transformend', $element);
      });
    }
  };
});

and here is my directive:

<ion-scroll direction="['x', 'y']" class="padding fill-container has-header">
  <div id="container" ng-style="containerStyle">
    <img id="image-scrollable" zoomable ng-src="images/{{type.typeName}}.jpg" class="scaled">
  </div>
</ion-scroll>

note that the fill-container class is just a height and width of 100% styling.

This works on all supported versions of android and iOS in addition to android 4.0.3+ with the use of crosswalk (only tried v. 5.34.104.5)

Hope this helps anyone looking at creating pinch and zoom gestures.


#3

This is great! I would love to implement this in my project but I can’t seem to get it to work.

I have my project set up as a “tab” project with a tab for photos. These display in a thumbnail view in which you would tap to view full size in a ion-slide-box view so that even in full view mode you can slide between all the photos. I wanted to use your code to allow zooming on the photo and also be able to slide to another photo, but I can’t seem to get it to work.

Here is the code for my full-screen view with your code inserted:

<ion-view hide-nav-bar='true' hide-back-button='true'>
  <ion-content zoomable="true" overflow-scroll="true" class="detail-view photo-view">
    <ion-slide-box show-pager="false" on-slide-changed="slideChanged()">
      <ion-slide ng-repeat="photo in location.photos">
        <ion-scroll direction="['x', 'y']" class="padding fill-container photo">
          <div id="container" ng-style="containerStyle">
            <img id="image-scrollable" zoomable ng-src="{{ photo.url }}" class="scaled">
          </div>
        </ion-scroll>
      </ion-slide>
    </ion-slide-box>
  </ion-content>
</ion-view>

Is there something I’m missing here or doing wrong? And I apologize ahead of time… this is all very new to me. Thanks!


#4

I may have been a bit misleading in my last post as I didn’t show all of the code required to make it work.

I would take a look at the initial post I made and verify that you have all the pieces.

You will need to have defined a containerStyle inside of the controller for this section of the app, as that is used to resize the container after the transform is complete.

You will also need to have the zoomable directive defined somewhere (not sure how your app structure is).

A couple of things to note on what you have so far:

  • ion-content does not have a zoomable attribute
  • I have looked at the ion-scroll directive implementation extensively and noticed that the direction attribute can be specified in a simpler form:
  • direction="xy"

Additionally, I would like to add that I am currently working on a patch to the ion-scroll directive to make the built in zooming functionality work properly on Android. If Android is not a targeted platform, I would recommend using that as it is much simpler:

<ion-scroll direction="xy" zooming="true">
</ion-scroll>

#5

Thanks for the update. I think I have it sort of working. Thanks for posting your code BTW! This is a feature I wanted to add to my app and I’m glad I stumbled across your code. You should post it on GitHub or somewhere as an example.

In my app, I combined it with ion-slide, the only funky thing now is when you zoom in and then swipe around to move the image around, it changes to another ion-slide. Also, since combining your code with mine, I’m able to swipe the ion-slide to a blank slide for some strange reason. Anyway, those are my only quirks. I’d love it to work closer to the iOS Photos.app.

Here’s my modified layout that is working so far except for the quirks I described:

<ion-view hide-nav-bar='true' hide-back-button='true'>
  <ion-content overflow-scroll="true" class="detail-view photo-view">
    <ion-slide-box show-pager="false" on-slide-changed="slideChanged()">
      <ion-slide ng-repeat="photo in location.photos">
        <ion-scroll direction="xy" zooming="true">
          <div id="container" ng-style="containerStyle" class="photo">
            <img id="image-scrollable" zoomable ng-src="{{ photo.url }}" class="scaled">
          </div>
        </ion-scroll>
      </ion-slide>
    </ion-slide-box>
  </ion-content>
</ion-view>

Thanks again for sharing your code and helping me out so far!


#6

I suspect that the reason it is moving to another slide is because the gesture triggers both, the draggable and the gesture to indicate to move to the next slide. In this case, the draggable is a css class on the parent container (ion-content), and the slide gesture is the child (ion-slide-box).

An interesting thing that I see here, is that you are using both, the zooming attribute, and my zoomable directive – though that would have no bearing on the dragging to another slide. I would suggest that you pick one or the other as they both work independently.

As far as getting it to work more in line with the native iOS photos app, I’m not sure it could be done using just these directives. You would, most likely, have to extend the ion-slide directive to only move to the next slide when the edge of the container is reached.


#7

I am having problems zooming on android also. For testing purposes should it also work in the chrome emulator by holding down shift while dragging? It is not working on the device or in chrome for me.