December 19, 2011

CSS: Disable text selection highlighting

When working with touch & mouse events to drag html elements, I've occasionally resorted to disabling clicking/touching on child elements by doing something like this:
elem.onmousedown = function(e){ return false; };
elem.onselectstart = function(){ return false; };
But sometimes you want to be able to interact with those child elements... I found some nice CSS over here that prevents text & element highlighting when you're dragging with a mousemove or a touchmove event:
p {
  -webkit-user-select: none;
  -khtml-user-select: none;
  -moz-user-select: none;
  -o-user-select: none;
  user-select: none;
}

December 11, 2011

Coffeescript: MapsLoader class for asynchronous loading of Google Maps API

I'm using CoffeeScript for my current project, and we needed a way to load the Google Maps API when a user hits a particular view. This static class is auto-initialized, and all you need to call is: MapsLoader.load(callbackFunction,true). If the API has already loaded, it will invoke your callback immediately. Make sure to pass the appropriate boolean if the user is on a mobile device (true), or a desktop browser (false).
class MapsLoader

  constructor: ->

  load: (successCallback,isMobileDevice) ->
    @isMobileDevice = isMobileDevice
    @successCallback = successCallback
    if @hasLoaded != true
      @loadGoogle()
    else
      @mapsLoaded()

  loadGoogle: =>
    # reference google loader callback to local method - clean up after callback
    window.loadMaps = @loadMaps 
    apiKey = "-----your-api-key-here-----"
    script = document.createElement("script")
    script.src = "https://www.google.com/jsapi?key=#{apiKey}&callback=loadMaps"
    script.type = "text/javascript"
    document.getElementsByTagName("head")[0].appendChild(script)

  loadMaps: =>
    otherParams = if @isMobileDevice then "sensor=true" else "sensor=false"
    google.load("maps", "3", {other_params: otherParams, "callback" : @mapsLoaded});

  mapsLoaded: =>
    @hasLoaded = true
    window.loadMaps = null
    if @successCallback
      @successCallback()
    @successCallback = null

@MapsLoader = new MapsLoader()

October 10, 2011

Javascript: Hide the iOS soft keyboard

I was having some trouble getting my text input field to relieve itself of focus on the iPhone, and after a little searching, I came up with a couple options. It's pretty self-explanatory. The 2nd line will de-focus all input fields, and it relies on jQuery. I found that calling blur() on the single focused textfield didn't always work. Either one of these lines should work independently, but both of them together cannot be stopped!
var hideKeyboard = function() {
 document.activeElement.blur();
 $("input").blur();
};
UPDATE: As a modern replacement for the jquery statement above, here's a simple, modern alternative:
var hideKeyboard = function() {
 document.activeElement.blur();
 var inputs = document.querySelectorAll('input');
 for(var i=0; i < inputs.length; i++) {
  inputs[i].blur();
 }
};

September 25, 2011

Given distance and friction, calculate the initial velocity to stop at the distance target

While simulating iOS scroll views in javascript, I needed to find the initial velocity to send the scroll content back into position if you drag it out of bounds. Since the in-bounds scroll view inertia was already working with a given friction, I wanted to use this to run the calculation. After some brute-force experimenting, I came up with the equation to calculate this velocity:
// set up vars
var curPos = 0;
var distance = 2000;
var friction = 0.8;
var velocity = distance / ( friction * ( 1 / ( 1 - friction ) ) );

// in timer:
setInterval(function(){
    velocity *= friction;
    curPos += velocity;
},30);
The curPos value will ease into the target distance, no matter what you set as the friction and distance. Friction must be a decimal between zero and one.

September 13, 2011

Xcode error: "The argument is invalid"

If you get this error while publishing from Xcode, you may have a rogue symlink in your project. As of Xcode 4.1, symlinks will give you the awesome, helpful error: "The argument is invalid"
Check your project for symlinks, and remove them. If you're symlinking another directory into your project, this would work in the iOS Simulator, but not on a device, so you'd probably want to come up with another strategy for including files from another project. To help with finding symlinks, run the following command from the root of your project directory. It will list any symlinks and their original location.
find ./ -type l -exec ls -l {} \;

September 1, 2011

Google Maps API: Polyline encoding/decoding issue

I'm using google's Geometry Library to encode large numbers of GPS location points into a compressed format for storage. You can add this capability to your .js by adding it to the quersystring in the javascript reference:
<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?libraries=geometry&sensor=true"></script>
The library provides a very easy way to display a path, add points realtime, store this path as an encoded & compressed string, and feed that string back into a new map when you're ready to display it later. You can see a live example here.

Encoding works great, but I ran into an issue where some of my encoded paths, generated by google.maps.geometry.encoding.encodePath() from my google.maps.Polyline object, would have major errors when using the google.maps.geometry.encoding.decodePath() method. My Polyline would have random right-angle turns that effectively ruined my path. I played around with the encoded string, trying to figure out what was causing the issue, to no avail. I found another implementation of the polyline encoding algorithm, and found an explanation of what was causing the issue.

It turns out that you need to escape the backslashes that may appear in the output string from google.maps.geometry.encoding.encodePath() (and the other library linked to above). So if you're storing the string for later, you want to do something like this:
var encodedPath = google.maps.geometry.encoding.encodePath( _poly.getPath() ).replace(/\\/g,'\\\\');
You can then feed that encoded string into a new Map's Polyline instance like so:
_poly.setPath( google.maps.geometry.encoding.decodePath( encodedPath ) );
It seems like an oversight that this double-backslash issue isn't mentioned in the Google documentation. I spent hours before trying to figure out the problem in my mobile app before coming across the fix.

Finally, after you set a decoded path as the data for a Polyline, use the following code to fit the Map to the bounds of your path:
var bounds = new google.maps.LatLngBounds();
var path = _poly.getPath();
path.forEach(function( latlng ) {
	bounds.extend( latlng );
});
_map.fitBounds( bounds );	

July 26, 2011

Javascript: Formatting latitude/longitude location between Decimal and DMS (degrees, minutes, seconds)

I'm working on a javascript UI for a mobile app that receives location data in the Decmial format. We wanted the fancy DMS format, so I found some code, rewrote it, and wrapped it up into a nice little static class for converting back and forth.
// A static class for converting between Decimal and DMS formats for a location
// ported from: http://andrew.hedges.name/experiments/convert_lat_long/
// Decimal Degrees = Degrees + minutes/60 + seconds/3600
// more info on formats here: http://www.maptools.com/UsingLatLon/Formats.html
// use: LocationFormatter.DMSToDecimal( 45, 35, 38, LocationFormatter.SOUTH );
// or:  LocationFormatter.decimalToDMS( -45.59389 );

function LocationFormatter(){
};

LocationFormatter.NORTH = 'N';
LocationFormatter.SOUTH = 'S';
LocationFormatter.EAST = 'E';
LocationFormatter.WEST = 'W';

LocationFormatter.roundToDecimal = function( inputNum, numPoints ) {
 var multiplier = Math.pow( 10, numPoints );
 return Math.round( inputNum * multiplier ) / multiplier;
};

LocationFormatter.decimalToDMS = function( location, hemisphere ){
 if( location < 0 ) location *= -1; // strip dash '-'
 
 var degrees = Math.floor( location );          // strip decimal remainer for degrees
 var minutesFromRemainder = ( location - degrees ) * 60;       // multiply the remainer by 60
 var minutes = Math.floor( minutesFromRemainder );       // get minutes from integer
 var secondsFromRemainder = ( minutesFromRemainder - minutes ) * 60;   // multiply the remainer by 60
 var seconds = LocationFormatter.roundToDecimal( secondsFromRemainder, 2 ); // get minutes by rounding to integer

 return degrees + '° ' + minutes + "' " + seconds + '" ' + hemisphere;
};

LocationFormatter.decimalLatToDMS = function( location ){
 var hemisphere = ( location < 0 ) ? LocationFormatter.SOUTH : LocationFormatter.NORTH; // south if negative
 return LocationFormatter.decimalToDMS( location, hemisphere );
};

LocationFormatter.decimalLongToDMS = function( location ){
 var hemisphere = ( location < 0 ) ? LocationFormatter.WEST : LocationFormatter.EAST;  // west if negative
 return LocationFormatter.decimalToDMS( location, hemisphere );
};

LocationFormatter.DMSToDecimal = function( degrees, minutes, seconds, hemisphere ){
 var ddVal = degrees + minutes / 60 + seconds / 3600;
 ddVal = ( hemisphere == LocationFormatter.SOUTH || hemisphere == LocationFormatter.WEST ) ? ddVal * -1 : ddVal;
 return LocationFormatter.roundToDecimal( ddVal, 5 );  
};

April 27, 2011

Webkit bug: translate3d positioning doesn't activate browser scrollbars

After some frustration in Safari and Mobile Safari, I found that by setting a -webkit-transform: translate3d() to position the y-coordinate of an html element beyond the browser window height, it will not cause the browser to activate its scrollbars. However, by switching to the web-standards css top style, this problem is alleviated. This seems like a browser bug to me.

Consider the following css, in both desktop and mobile Safari:
<html>
<head>
  <style>
    .notbroken {
      position:absolute;
      top:10000px;
    }
    .broken {
      position:absolute;
      -webkit-transform: translate3d(0px, 10000px, 0px);
    }
  </style>
</head>
<body>
  <div class="notbroken">Scrollbars, please.</div>
</body>
</html>
Both css classes position the div 10,000 pixels down the page, but if you use the .broken version, you won't get scrollbars, and you'll never be able to see the content.

April 19, 2011

Javascript: Handle touch gestures for pinch (scale) and rotation

I wrote a little class to handle pinch & rotate gestures on a web page in iOS. There's a bunch of event listener adding/removing, which can get a little messy, so this should help keep your code clean if you need to handle gestures.

Here's the class:
function GestureCallback( element, endCallback, changeCallback ) {
  this.element = element;
  this.end_callback = endCallback;
  this.change_callback = changeCallback;
  this.scale = 1;
  this.rotation = 0;
  this.init();
}

GestureCallback.prototype.init = function() {
  // scope functions for listener removal
  var self = this;
  this.gestureStart = function(e){ self.onGestureStart(e) };
  this.gestureChange = function(e){ self.onGestureChange(e) };
  this.gestureEnd = function(e){ self.onGestureEnd(e) };
  
  // really no need to check for IE stupidness, but maybe they'll support gestures someday? oy.
  if( this.element.attachEvent ) this.element.attachEvent( "touchstart", this.gestureStart ); else this.element.addEventListener( "touchstart", this.gestureStart, false );
  if( this.element.attachEvent ) this.element.attachEvent( "gesturestart", this.gestureStart ); else this.element.addEventListener( "gesturestart", this.gestureStart, false );
};

GestureCallback.prototype.onGestureStart = function ( e ) {
  if( this.element.attachEvent ) this.element.attachEvent( "gesturechange", this.gestureChange ); else this.element.addEventListener( "gesturechange", this.gestureChange, false );
  if( this.element.attachEvent ) this.element.attachEvent( "gestureend", this.gestureEnd ); else this.element.addEventListener( "gestureend", this.gestureEnd, false );
};

GestureCallback.prototype.onGestureChange = function ( e ) {
  this.scale = e.scale;
  this.rotation = e.rotation;
  if( this.change_callback ) this.change_callback( this.scale, this.rotation );
};

GestureCallback.prototype.onGestureEnd = function ( e ) {
  if( this.element.detachEvent ) this.element.detachEvent( "gesturechange", this.gestureChange ); else this.element.removeEventListener( "gesturechange", this.gestureChange, false );
  if( this.element.detachEvent ) this.element.detachEvent( "gestureend", this.gestureEnd ); else this.element.removeEventListener( "gestureend", this.gestureEnd, false );

  this.scale = e.scale;
  this.rotation = e.rotation;
  if( this.end_callback ) this.end_callback( this.scale, this.rotation );
};

GestureCallback.prototype.dispose = function() {
  if( this.element.attachEvent ) this.element.detachEvent( "touchstart", this.gestureStart ); else this.element.removeEventListener( "touchstart", this.gestureStart, false );
  if( this.element.attachEvent ) this.element.detachEvent( "gesturestart", this.gestureStart ); else this.element.removeEventListener( "gesturestart", this.gestureStart, false );
  
  this.element = null;
  this.end_callback = null;
  this.change_callback = null;
  
  this.gestureStart = null;
  this.gestureChange = null;
  this.gestureEnd = null;
};
And the instantiation:
var pinchCallback = new GestureCallback( yourElement, function( scale, rotation ){
  console.log('done: '+scale+','+rotation);
  if( scale < 1 ) {
    // leave the section, since we've pinched it closed
  }
}, function( scale, rotation ){
  console.log('changing: '+scale+','+rotation);
});
And when you're done with your gesture needs, collect your garbage:
pinchCallback.dispose();
pinchCallback = null;

April 8, 2011

Processing: MovieMaker gotchas in Eclipse

I haven't touched my Processing sketches for a while, and got lots of weird errors while trying to render a video using the built-in MovieMaker object.
Exception in thread "Animation Thread" java.lang.UnsatisfiedLinkError: quicktime.QTSession.Gestalt(I[I)S
Caused by: java.lang.UnsatisfiedLinkError: /System/Library/Java/Extensions/libQTJNative.jnilib:  no suitable image found.  Did find:  /System/Library/Java/Extensions/libQTJNative.jnilib: no matching architecture in universal wrapper
The above error was fixed by switching the JVM that Eclipse was using in the "Run..." configuration. On my Macbook, I simply switched to an alternate JVM 1.5, and it got rid of the problem:

If you get the following error, you need to delete the previous rendered movie, or use some sort of timestamp to automatically name each new movie:
The movie file already exists.  Please delete it first.
Here's a little timestamp code I wrote to help prevent file name conflicts:
_timestamp = "" + String.valueOf( p.year() ) + "-" + String.valueOf( p.month() ) + "-" + String.valueOf( p.day() ) + "-" + String.valueOf(p.hour()) + "-" + String.valueOf(p.minute()) + "-" + String.valueOf(p.second());
  
_mm = new MovieMaker( p, p.width, p.height, "output/render-"+_timestamp+".mov", _framesPerSecond, MovieMaker.ANIMATION, MovieMaker.HIGH );

A stupid user-error error message I got was due to accidentally starting 2 instances of MovieMaker. Upon calling MovieMaker.finish(), I got this error:
quicktime.std.StdQTException[QTJava:7.6.6g],-2014=invalidDuration,QT.vers:7668000

Some other issues I've run into:
* Certain codecs won't be installed on your machine. For example, I can't use MovieMaker.H264, but I can use MovieMaker.ANIMATION
* After rendering a movie, Quicktime can have trouble playing it back, especially with certain codecs. My movies look blank. A workaround is to open and export the video from Quicktime Pro in order to view it.

Finally, something I've been doing with all my Processing sketches is to add the following VM Arguments to the Run Configuration. This helps avoid running out of memory and to run in 32-bit mode (required for certain operations):
-d32
-Xmx1024M
-Xms1024M

February 21, 2011

Bash script: Harvest email addresses from a directory of files

I had to go through over a year of server logs to rescue newsletter signups that may or may not have been passed to our newsletter service, due to server updates that happened without my knowledge. I wrote a little command to scrape all the log files in my directory, and output a text file with all the addresses it found. Here we go:
# recursively(!) scrapes directories and prints out each email address to a new line
egrep -o -h -r '[a-zA-Z0-9_-\+\.]+@[a-zA-Z0-9_-\+\.]+?\.[a-zA-Z]{2,3}' *.* | sort | uniq > email_addresses.txt
Credit: I started with a command from Linux.com, and customized it until the output was correct, and nicely formatted.

jQuery fadeIn() bug on iPad: element disappears

I'm using jQuery on a new site, and a fadeIn() animation broke, but only on the iPad with iOS 3.2.2. It works fine in every other browser, including the newer iOS (4+) for iPad. To fix the problem, which in my case doesn't affect other browsers, I simply added a callback function that manually sets the width and height of the element:
newSection.fadeIn( 300 , function(){
  newSection.css( { width:320, height:480 } );
});
I tried other css properties first, but width and height are the magic properties that prevent the element from being hidden. Quick, someone tell Steve Jobs :p

February 20, 2011

Actionscript 3: Grabbing and resizing BitmapData

I found this handy little class in a Flash project that I just had to jump back into. It's an easy way to take a bitmap snapshot of any DisplayObject, then resize the image data to fit another frame. You can fill the area, letterbox/pillowbox, or just scale to new dimensions. Feel free to use this, and nevermind the warnings that show up in FDT :)
package com.cache.util 
{
 import flash.display.BitmapData;
 import flash.display.DisplayObject;
 import flash.geom.Matrix;
 import flash.geom.Point;
 import flash.geom.Rectangle;
 
 /**
  * @author justin - http://uihacker.blogspot.com/
  */
  
 public class ImageEdit 
 {
  /**
   * Return a snapshot of a DisplayObject
   */
  public static function getBitmapData( vSource:DisplayObject, vW:Number=NaN, vH:Number=NaN, vTransparent:Boolean=true, vColor:int=0xFF00FF, vMatrix:Matrix=null ):BitmapData
  {
   // set defaults.
   var vWidth:int = ( isNaN( vW )) ? vSource.width : vW;
   var vHeight:int = ( isNaN( vH )) ? vSource.height : vH;  
   
   // create BitmapData object.
   var vBmp:BitmapData = new BitmapData( vWidth, vHeight, vTransparent, vColor );
   
   // draw contents of source clip into target.
   if ( vMatrix == null ) vBmp.draw( vSource, null, null, null, null, true );
   else vBmp.draw( vSource, vMatrix, null, null, null, true ); 
   
   return vBmp;
  }
  
  /**
   * Build & return a matrix to use to scale a bitmap
   */
  public static function getScaleMatrix( scale:Number ) : Matrix
  {
   var matrix:Matrix = new Matrix();
   matrix.scale(scale, scale);
   return matrix;
  }
  
  /**
   * Pass these constants into the getResizedBitmapData() function
   */
  public static const RESIZE_SCALE:String = 'ImageEdit.resizeScale';
  public static const RESIZE_LETTERBOX:String = 'ImageEdit.resizeLetterbox';
  public static const RESIZE_CROP:String = 'ImageEdit.resizeCrop';
  
  /**
   * Return a resized BitmapData copy of the original
   */
  public static function getResizedBitmapData( sourceBitmap:BitmapData, targetWidth:Number, targetHeight:Number, resizingMethod:String = '', disposeSourceBmp:Boolean = true ) : BitmapData
  {
   // get current dimensions
   var curW:Number = sourceBitmap.width;
   var curH:Number = sourceBitmap.height;
   
   // get ratios of 2 sides
   var ratio_w:Number = targetWidth / curW;
   var ratio_h:Number = targetHeight / curH;
   var shorterRatio:Number = ( ratio_w > ratio_h ) ? ratio_h : ratio_w;
   var longerRatio:Number = ( ratio_w > ratio_h ) ? ratio_w : ratio_h;
   
   
   // apply sizing
   switch( resizingMethod )
   {
    case RESIZE_CROP :
     // get shorter ratio, so we fill the target area
     var resizedWidth:int = Math.round( curW * longerRatio );
     var resizedHeight:int = Math.round( curH * longerRatio );
     
     // create copy of, and resize the source bitmap
     var resizedSourceBmp:BitmapData = new BitmapData( resizedWidth, resizedHeight, false, 0x00000000 );
     // create scale matrix 
     var matrix:Matrix = new Matrix();
     matrix.scale( longerRatio, longerRatio );
     // take resized snapshot
     resizedSourceBmp.draw( sourceBitmap, matrix );
     
     // draw into destination bitmap, letterbox/pillowbox style
     var destBitmap:BitmapData = new BitmapData( targetWidth, targetHeight, false, 0x00000000 );
     var offset:Point = new Point( targetWidth / 2 - resizedWidth / 2, targetHeight / 2 - resizedHeight / 2 );
     destBitmap.copyPixels( resizedSourceBmp, new Rectangle( -offset.x, -offset.y, resizedSourceBmp.width, resizedSourceBmp.height ), new Point() );
     
     // clean up temp BitmapData
     resizedSourceBmp.dispose();
     if( disposeSourceBmp ) sourceBitmap.dispose();
     
     return destBitmap;
     break;
     
    case RESIZE_LETTERBOX :
     // get shorter ratio, so we fill the target area
     var resizedWidth:int = Math.round( curW * shorterRatio );
     var resizedHeight:int = Math.round( curH * shorterRatio );
     
     // create copy of, and resize the source bitmap
     var resizedSourceBmp:BitmapData = new BitmapData( resizedWidth, resizedHeight, false, 0x00000000 );
     // create scale matrix 
     var matrix:Matrix = new Matrix();
     matrix.scale( shorterRatio, shorterRatio );
     // take resized snapshot
     resizedSourceBmp.draw( sourceBitmap, matrix );
     
     // draw into destination bitmap, letterbox/pillowbox style
     var destBitmap:BitmapData = new BitmapData( targetWidth, targetHeight, false, 0x00000000 );
     var pastePoint:Point = new Point( targetWidth / 2 - resizedWidth / 2, targetHeight / 2 - resizedHeight / 2 );
     destBitmap.copyPixels( resizedSourceBmp, new Rectangle( 0, 0, resizedSourceBmp.width, resizedSourceBmp.height ), pastePoint );
     
     // clean up temp BitmapData
     resizedSourceBmp.dispose();
     if( disposeSourceBmp ) sourceBitmap.dispose();
     
     return destBitmap;
     break;
     
    case RESIZE_SCALE :
    default : 
     // create output bitmap
     var vBmp:BitmapData = new BitmapData( targetWidth, targetHeight, false, 0x00000000 );
     // create scale matrix 
     var matrix:Matrix = new Matrix();
     matrix.scale( ratio_w, ratio_h );
     // snapshot with scale & return
     vBmp.draw( sourceBitmap, matrix );
     
     // clean up temp BitmapData
     if( disposeSourceBmp ) sourceBitmap.dispose();
     
     return vBmp;
     break; 
     
   }
   return null;
  }
 }
}

February 7, 2011

Processing: Shapes disappear when switching from P3D to OPENGL renderer

I was having some issues with my 3d scene in Processing when switching to OPENGL from the P3D rendering mode. At a certain distance, my shapes were disappearing in OPENGL, but they displayed properly in P3D. My good friend (and Processing whiz) Movax gave me the suggestion to adjust my perspective(). Theses we the value that helped my situation:
perspective( 1.0f, 1.5f, 1f, 200000f );
The 200,000 value is the maximum depth that the camera will be able to see in OPENGL mode, which was large enough to view some of my distant objects that had been disappearing.

January 24, 2011

Bash shell script: Scraping and downloading image files from a ffffound RSS feed

I wrote this little script for a friend, as an exercise in bash shell scripting. This script is for OS X.

Step 1:
Install Homebrew - this is a great tool for installing common Unix tools. You should only have to open Terminal, and paste the 1-line installation script found in the link above. Something like this:
ruby -e "$(curl -fsSLk https://gist.github.com/raw/323731/install_homebrew.rb)"

Step 2:
Install wget with Homebrew: type this into Terminal and press Enter:
brew install wget

Step 3:
Save the following code into a text file called "ffffound_sssswiped.sh" and save it into your User/Pictures/ directory:
curl http://feeds.feedburner.com/ffffound/everyone | egrep -o source\ url=\"http://[^[:space:]]*.\(jpg\|png\|gif\) | egrep -o http://[^[:space:]]*.\(jpg\|png\|gif\) | xargs wget -nc -P ~/Pictures/ffffound

Step 4:
Customize! You can replace http://feeds.feedburner.com/ffffound/everyone with your own ffffound RSS feed, or anyone else's.

Step 5:
Run the script: type the following into Terminal, and hit Enter:
bash ~/Pictures/ffffound_sssswiped.sh

You should see the download progress as it scrapes the RSS feed for just the large-format image files. You can run this as often as you want, and it will skip any files you've already downloaded.

Magic!

January 19, 2011

Android + Phonegap: Scale the WebView to fit the device

I was porting an iPad app to Android for the new Samsung tablet, and I had some trouble getting my web view to scale to the size of the device screen so that I wouldn't have to resize any of my assets. Obviously this is a questionable tactic, but I was experimenting and wanted to see how it would look :)

Here's the meat of my main App.java class:
public class App extends DroidGap {
 
 // declare the original size of the iPad app
 protected float ORIG_APP_W = 768;
 protected float ORIG_APP_H = 1004;
 
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        super.loadUrl("file:///android_asset/www/index.html");
        
     // set some defaults
     this.appView.setBackgroundColor(0x000000);
     this.appView.setHorizontalScrollBarEnabled(false);
     this.appView.setHorizontalScrollbarOverlay(false);
     this.appView.setVerticalScrollBarEnabled(false);
     this.appView.setVerticalScrollbarOverlay(false);
     
     // get actual screen size
     Display display = ((WindowManager) getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
     int width = display.getWidth(); 
     int height = display.getHeight(); 
     
     // calculate target scale (only dealing with portrait orientation)
     double globalScale = Math.ceil( ( width / ORIG_APP_W ) * 100 );
     
     // make sure we're all good
     Log.v( "ORIG_APP_W", " = " + ORIG_APP_W );
     Log.v( "ORIG_APP_H", " = " + ORIG_APP_H );
     Log.v( "width", " = " + width );
     Log.v( "this.appView.getMeasuredHeight()", " = " + height );
     Log.v( "globalScale", " = " + globalScale );
     Log.v( "this.appView.getScale()", "index=" + this.appView.getScale() );
    
     // set some defaults on the web view
     this.appView.getSettings().setBuiltInZoomControls( false );
     this.appView.getSettings().setSupportZoom( false );
     this.appView.getSettings().setGeolocationEnabled( true );
     this.appView.getSettings().setLightTouchEnabled( true );
     this.appView.getSettings().setRenderPriority( RenderPriority.HIGH );
     
     // set the scale
     this.appView.setInitialScale( (int)globalScale );
   }
}
I also updated the AndroidManifest.xml file to lock the app into portrait orientation, work on tablet-sized devices, have a nice app name, and give the device access to the Internet and geolocation:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.phonegap.testapp"
      android:versionCode="1"
      android:versionName="1.0">     
     
    <application android:icon="@drawable/icon" 
        android:label="@string/app_name"
        android:debuggable="true">
        <activity android:name=".App" 
                  android:label="Test App" 
                  android:configChanges="orientation|keyboardHidden"
                  android:noHistory="true" 
                  android:stateNotNeeded="true" 
                  android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

    <!-- allows access to phonegap hardware features -->
 <uses-permission android:name="android.permission.INTERNET" />
 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
 <!--<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />-->
 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> 
 <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
 <uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />


   <supports-screens
     android:largeScreens="true"
     android:normalScreens="false"
     android:smallScreens="false"
     android:resizeable="true"
     android:anyDensity="true"
     />

</manifest> 
And finally my res/layout/main.xml file, though I'm not sure if this is different from the Phonegap default:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >    
            <WebView android:id="@+id/appView"
            android:layout_height="fill_parent"
            android:layout_width="fill_parent"
            /> 
</LinearLayout>
I hope this helps someone port their hybrid html5 iPad app to Android tablets.

Javascript: Clear a webkitTransition animation

I was animating an html element with Webkit transitions via javascript, and after the animation was done, I applied non-animated webkitTransform positioning to the same element. It animated instead of immediately displaying the new style. I came up with the following function to clear any previous Webkit animation values:
function clearAnimation( element ) {
  if( typeof element.style.webkitTransform !== 'undefined' && element.style.webkitTransform ) {   // 2nd conditional fixes bug in Chrome on windows
    element.style.webkitTransition = '';
    element.style.webkitTransform = '';
  }
}
This will prevent any overlap when switching between animatiions and instant repositioning.

January 18, 2011

Android: touchmove event bug

touchmove events in Android web browsers have a really serious bug. If you don't include the following code, the touchmove event will fire once, but not again until you're done moving your touch, which utterly kills the usefulness of the touchmove event. It's a weird one, and may very well break more advanced touch logic that works on iOS. But if you preventDefault() on the touchstart event, your touchmove will function as expected.
element.addEventListener( "touchstart", function(e){ onStart(e); }, false );
function onStart ( touchEvent ) {
  if( navigator.userAgent.match(/Android/i) ) {
    touchEvent.preventDefault();
  }
}
This bug is documented here:
http://code.google.com/p/android/issues/detail?id=5491

and is probably not entirely unrelated to my previous post about weird Android Touch event behavior:
http://uihacker.blogspot.com/2010/10/android-bug-miss-drag-as-we-are-waiting.html

CSS Grab hand cursor

I wanted a grabby hand cursor in html since we're building sites that have draggable interfaces for both desktop browsers and touchscreen devices. I found some CSS that takes care of most modern browsers, and came up with a little extra for Internet Explorer and Chrome.

Here's the CSS:
#trackbar {
  width:100%;
  height:50px;
  cursor:hand;
  cursor:grab;
  cursor:-moz-grab;
  cursor:-webkit-grab;
}

#trackbar.custom_cursor {
  cursor: url(https://mail.google.com/mail/images/2/openhand.cur), default !important;
}

#trackbar.grabbing {
  cursor:grabbing;
  cursor:-moz-grabbing;
  cursor:-webkit-grabbing;
}

#trackbar.custom_cursor.grabbing {
  cursor: url(https://mail.google.com/mail/images/2/closedhand.cur), default !important;
}
The 3 cursor attributes looked great in most modern browsers, but didn't work in IE (obviously) or Chrome. I looked at some code in Google Maps and gleaned the custom cursors, which you can pull down locally, but you have to reference the .cur files absolutely, or they won't work in IE. Awesome. So, because we'd rather use CSS, we only apply the custom cursor files if you're in IE or Chrome. Something like this:
if( navigator.userAgent.match(/MSIE/i) || navigator.userAgent.match(/Chrome/i) ) document.getElementById('trackbar').className = 'custom_cursor';
I'm not sure if there's any way around it, but it doesn't seem like IE will change cursors after you mouse down. So if you add the "grabbing" class on a mousedown event, IE will block your grabby hands :(

January 11, 2011

Actionscript: &nbsp; / HTML text issue from an old Flash 8 project

I built a project back in 2006 that's amazingly still alive and making lots of money for a client. I had a bit of code that would insert an &nbsp; into a textfield for dummy spacing in between other letters. Out of nowhere, this stopped working for the client, and all the text was space-less. This is what I had:
StringUtil.searchReplace( myStr, _dummyChar, '<span class="textDummy">&nbsp;</span>' );
This now fails to insert a space. I tried just using an actual space, but since this is an html textfield, multiple consecutive spaces don't keep making more room. So, I added a space after the &nbsp;, and magically, it works like it had in previous years:
StringUtil.searchReplace( myStr, _dummyChar, '<span class="textDummy">&nbsp;</span> ' );
Whew fun!