Once Upon a Time...

In a Google office far, far away...

I quit.

My Web App

My Mobile App?

Native?

Cross-Compiled?

Hybrid?

My Mobile App

PhoneGap + Bootstrap + Zepto

The easy part:
making it work.


The hard part:
making it work well.

PhoneGap

Pain Points


The Docs


The Native Bridge


The Mobile Browsers

The Docs

The Name Change



PhoneGap

-> Callback

-> Cordova

-> PhoneGap?

The Repo Change


Github -> Apache JIRA

https://issues.apache.org/jira/secure/IssueNavigator.jspa?reset=true&jqlQuery=project+%3D+CB+AND+fixVersion+%3D+%221.6.0%22+AND+resolution+%3D+Unresolved+AND+component+%3D+CordovaJS+ORDER+BY+priority+DESC&mode=hide

The Native Bridge

The Camera API


Phonegap is unusable as an app platform for Android if the camera is involved.
var imageQuality = (ED.util.isAndroid() ? 50 : 70);
var destinationType = navigator.camera.DestinationType.FILE_URI;
// Check if this is a stupid Android that doesnt support canvas.toDataURL properly
if (!ED.util.supportsDataURL()) {
  destinationType   = navigator.camera.DestinationType.DATA_URL;
  targetWidth  = 400;
  targetHeight = 400;
}

/**
 * A safer decodeStream method
 * rather than the one of {@link BitmapFactory}
 * which will be easy to get OutOfMemory Exception
 * while loading a big image file.
 */
protected Bitmap safeDecodeStream(Uri uri, int width, int height) {
}
☞ gist: PhoneGap Camera.java
String hackUri = uri.toString() + "?orientation=" + exif.orientation;
this.success(new PluginResult(PluginResult.Status.OK, hackUri), this.callbackId);

var degree = 0;
var canvasWidth = imageWidth, canvasHeight = imageHeight;
var canvasX = 0, canvasY = 0;

var orientation = ED.util.getUrlParam('orientation', imageUri);
if (orientation == 6) degree = 90;
if (orientation == 3) degree = 180;
if (orientation == 8) degree = 270;

switch(degree){
  case 90:
    canvasWidth = imageHeight;
    canvasHeight = imageWidth;
    canvasY = imageHeight * (-1);
    break;
   case 180:
    canvasX = imageWidth * (-1);
    canvasY = imageHeight * (-1);
    break;
   case 270:
    canvasWidth = imageHeight;
    canvasHeight = imageWidth;
    canvasX = imageWidth * (-1);
    break;
}
canvas.setAttribute('width', canvasWidth);
canvas.setAttribute('height', canvasHeight);
if (degree > 0) {
  canvasContext.rotate(degree * Math.PI / 180);
}
canvasContext.drawImage($img[0], canvasX, canvasY);
☞ gist: Rotating photos with PhoneGap

The Mobile Browsers

(AKA $!?%! Android)

Fixed Positions


function setupFixedFix() {
    if (isFixedBroken()) {
      $('.container-fluid').css({'position': 'static'});
      $('.actionsbar, .navbar').css({'position': 'absolute'});
      fixFixed();
      $(window).bind('scroll', fixFixed);
      $('.mobile-page').bind('touchend', fixFixed);
    }
}

function isFixedBroken(){
    // Feature detection didnt seem to work so we check the user agent instead
    return (ED.util.isIOS() && navigator.userAgent.match(/[5-9]_[0-9]/) === null);
}

function fixFixed() {
    if (isFixedBroken()) {
      $('.actionsbar').css({'top': (window.pageYOffset) + 'px'});
      $('.navbar').css({'top': (window.pageYOffset + window.innerHeight - 30) + 'px'});
    }
}

Touch Events


ED.util.addTouchOrClickHandler($('.mobile-log-a'), openPageLink);
				    
ED.util.addClickHandler($('.mobile-footer-a'), openPageLink);
ED.util.addClickHandler($('.mobile-stream-a'), openStreamLink);
function addTouchOrClickHandler(dom, callback, logThis) {
    if (useTouchEvents()) {
      dom.each(function() {
        $(this).unbind('tap', callback);
        $(this).bind('tap', callback);

        $(this).bind('touchstart', function(e) {
          e.preventDefault();
          var item = e.currentTarget;
          if (ISTOUCHING) return;
          item.moved = false;
          ISTOUCHING = true;
          item.startX = e.touches[0].pageX;
          item.startY = e.touches[0].pageY;
          $(item).addClass('active');
        });

        $(this).bind('touchmove', function(e) {
          var item = e.currentTarget;
          if (Math.abs(e.touches[0].pageX - item.startX) > 10 ||
              Math.abs(e.touches[0].pageY - item.startY) > 10) {
            item.moved = true;
            $(item).removeClass('active');
          }
        });

        $(this).bind('touchend', function(e) {
          var item = e.currentTarget;
          ISTOUCHING = false;
          if(!item.moved) {
            $(item).trigger('tap');
          }
          setTimeout(function() {
            $(item).removeClass('active');
          }, 300);
          delete item.moved;
        });

      });
    } else {
      dom.unbind('click', callback).bind('click', callback);
    }
  }
				    
def click_button(self, selector):
        if self.driver.name == 'iPhone':
            self.driver.execute_script('$("%s").trigger("tap")' % (selector))
        else:
            try:
                self.get_el(selector).click()

Scrolling


function resetScroll(top) {
    top = top || 0;
    $(document).scrollTop(top);
    window.setTimeout(function() {
      $(document).scrollTop(top);
    }, 10);
  }

function show() {
    ED.util.resetScroll();
    ED.util.makeAutoResize($(DOM.notesInput));
    ED.util.putCursorAtEnd($(DOM.notesInput));
}

Scrolling+Fixed+Touch

☞ Device Bugs: #1

Keyboards

<input type="number" step="0.01">

☞ Triggering Numeric Keyboards

Hiding the Keyboard

function hideKeyboard() {
    $('input, textarea').blur();

    if (ED.util.isIOS()) {
      document.activeElement.blur();
    }
    if (ED.util.isAndroid()) {
      var field = $('<input type="text">');
      $('#mobile-logs-page').append(field);

      setTimeout(function() {
          field.focus();
          setTimeout(function() {
              field.hide();
          }, 50);
      }, 50);
    }
  }

FOCUS!

Offline Detection

var MSG_LOG_OFFLINE = 'We can\'t connect to the network now, so your log may be out of date.';
runIfOnline(function() {
    fetchNewData();
    }, MSG_LOG_OFFLINE);
}
function runIfOnline(onlineFunc, message, offlineFunc) {

    message = message || 'Sorry, we can\'t connect to the network right now.';
    if (isOnline()) {
      try {
        onlineFunc();
      }  catch(e) {
        logError(e);
      }
    } else {
      alert(message);
      if (offlineFunc) {
        try {
          offlineFunc();
        } catch(ee) {
          logError(ee);
        }
      }
    }
}
function isOnline() {
    // Presume online unless PhoneGap or HTML5 tell us otherwise.
    if (navigator.network && navigator.network.connection.type == Connection.NONE && 0) {
      ED.util.log('Found Connection.NONE');
      return false;
    }
    if (navigator.onLine === false) {
      ED.util.log('Found navigator.onLine is false');
      return false;
    }
    return true;
}

Loading Performance

☞ Measuring PhoneGap Loading Performance

<html>
 <head>  
 <script>
  var timedEvents = [];
  function timeEvent(name) {
    timedEvents.push({'name': name || 'unnamed', time: Date.now()});
  }

  function showTimedEvents() {
    var timeText = '';
    var timeHtml = '<table>';
    for (var i = 0; i < timedEvents.length; i++) {
      var timedEvent = timedEvents[i];
      timeText += timedEvent.name + ': ' + timedEvent.time;
      var diff = '';
      if (i > 0) {
        diff = (timedEvent.time - timedEvents[i-1].time);
        timeText += ' (' + diff + 'ms elapsed)';
      }
      timeHtml += '<tr><td>' + timedEvent.name + '<td>' + timedEvent.time + '<td>' + diff;
      timeText += '\n';
    }
    console.log(timeText);
    document.body.innerHTML += timeHtml;
  }
  </script>
  <script>timeEvent('Before CSS');</script>
  <link rel="stylesheet" href="css/all-phonegap-min.css?v=10201550"/>
  <script>timeEvent('After CSS');</script>
 </head>
 <body>
  <div>HTML here (more than this)</div>
<script>timeEvent('After HTML');</script>
<script src="js/libs/jquery-1.6.2.min.js"></script>
<script>timeEvent('After jQuery script tag');</script>
<script src="js/all-phonegap-min.js?v=10201550"></script>
<script>timeEvent('After other script tag');</script>
<script>

$(document).ready(function() {
  timeEvent('After document ready');
  myCustomMobileFunction();
  timeEvent('After ready JS called');
  showTimedEvents();
});
</script>
</body>
</html>

jQuery -> Zepto

Shaved: 22%

☞ Porting from jQuery to Zepto

onload -> deviceready

Shaved: 67%

document.addEventListener("DOMContentLoaded", function() {
    ED.mobile.setupMobile();
  });
document.addEventListener('deviceready', function() {
  ED.mobile.setupPhonegap();
}, false);
  

function setupMobile() {
    // Sets up the page CSS
    // Binds events to DOM
}
                    
function setupPhonegap() {
    // Shows splash page
    // Fetches data
    // Binds PhoneGap events
}
☞ Get to deviceready faster on Android

Rendering Performance

☞ Working Around Android Webkit
.android {
  .modal {
    @include box-shadow(none);
    @include background-clip(border-box);
    @include border-radius(0px);
    border: 1px solid black;
  }
}
$(window).on('scroll', $.throttle(500, loadVisibleImages));
☞ Delayed Image Loading on Long Pages
$textAreas.autoResizer({resizeOnChange: !isAndroid()});
window.setTimeout(function() {
    ED.shared.setupStream();
    ED.shared.setupStreamTimer();
}, 2000);

window.setTimeout(function() {
    ED.shared.maybeFetchStream();
}, 100);
    
window.setTimeout(function() {
    fetchNewData();
}, 1000); 
☞ Zakas: Responsive Interfaces

The future is bright!


PhoneGap is improving


Community and resources are growing


:%s/AndroidBrowser/Chrome


phonegap-pain-points.appspot.com