October 29, 2010

Android bug: "Miss a drag as we are waiting for WebCore's response for touch down."

I'm using Phonegap to port an HTML5 iPad app over to the Android platform. On my development HTC Incredible phone, every time I swipe my finger far enough, my app would freeze, and the adb debugging console would give me this error: "Miss a drag as we are waiting for WebCore's response for touch down." I researched a bunch and didn't find any solutions. I did find the Java code that logs this error in the core Android WebView.java class, but it didn't give me any clues to fix it.

I searched and hacked, and removed all my touch event listening code, and it would still break. Luckily the company work for has the resources to invest in development, and we went out and got a Samsung Galaxy S phone. I set the device up, published the app, and this phone did not have the cryptic issue! It did, however, show a range of other issues with fonts and the <canvas> object, which made me sad, as it's clear that building an HTML5-based app for Android isn't as easy as I hoped. The fragmentation of the Android platform is definitely an issue if you're attempting complex UI design and interaction with HTML/Javascript. I recommend keeping your HTML5 app very simple if you're targeting multiple Android platforms. Even though all new Android devices use Webkit, there are plenty of small, ugly differences.

[UPDATE]: The following code, when removed from my project, got rid of this weird error:
document.ontouchmove = function(event) {
    event.preventDefault();
};

Another update... Check out this post, and try out the demo code to get a bit more idea about how to handle preventDefault() on touch events in the Android browser: http://code.google.com/p/android/issues/detail?id=4549. It still crashes on my HTC device, but works great on the Samsung device.

October 22, 2010

Android browser bug: -webkit-transform scaling discrepency

[UPDATE]: this only happens on my HTC Incredible device, but not my Samsung Galaxy S device. yeesh.

I'm porting an HTML5 app we built for the iPad over to Android for the upcoming Samsung tablet. With the small difference in aspect ratio, I'm scaling down the entire site to avoid rebuilding everything. It turns out, that if you scale a container with CSS like so:
-webkit-transform-origin: 0 0;
-webkit-transform : scale(0.78125);
That works fine. However, I wanted to scale it dynamically with javascript, based on the device size, like so:
var globalScale = window.innerWidth / 768;
element.style.webkitTransformOrigin = '0 0';
element.style.webkitTransform = 'scale(' + globalScale + ')';
The result looks the same, but now clicking on anything in the scaled container is completely broken. :(

September 14, 2010

Javascript: strip HTML tags from a string

Here's a super simple RegEx to use when you want to be sure all html tags are removed from a string.
theString.replace(/<.*?>/g, '');

September 10, 2010

HTML/CSS on Android: rotation CSS difference between Android 2.1 and 2.2

Well, the splinternet is getting more interesting. As many developers settle on html as the most cross-functional platform, we're faced with ever more browsers and small differences between them. One that I just found is a difference in CSS positioning and rotation between the browsers on Android 2.1 and Android 2.2.

On my current project, I have a fancy UI that has an element constantly changing rotation and position using webkit transform CSS built with javascript. On Android 2.1, it worked fine as a 1-liner:
element.style.webkitTransform = "translate3d(" + xPos + "px, " + yPos + "px, 0px) rotate(" + rotation + "deg)";
But, on Android 2.2, the rotation stopped working. It seems that you can't have the translate3d and the rotate properties all set in the style.webkitTransform property. To fix the issue, I positioned using traditional absolute coordinates with the top and left CSS properties, and then used the webkitTransform property to do the rotation. There were a ton of special browser cases in my project to handle different things. Check out my platform detection class below to see how I handled a lot of special cases in one place.
PlatformHelper = function ()
{
    this.webkit_css_enabled = false;
    this.animations_enabled = false;
    this.is_android = false;
    this.is_android21 = false;
    this.is_android22 = false;
    this.is_idevice = false;
    this.is_touchscreen = false;
    this.is_msie = false;
    this.is_msie6 = false;
    this.is_msie8 = false;
    this.is_firefox = false;
    return this;
};

PlatformHelper.prototype.init = function ()
{
    // check for webkit positioning capability
    if( navigator.userAgent.match(/iPhone/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/iPod/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/iPad/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/Chrome/i) ) this.webkit_css_enabled = true;
    else if( navigator.userAgent.match(/Safari/i) ) this.webkit_css_enabled = true;
    
    // check for certain platforms
    if( navigator.userAgent.match(/Android/i) ) this.is_android = true;
    if( navigator.userAgent.match(/Android 2.1/i) ) this.is_android21 = true;
    if( navigator.userAgent.match(/Android 2.2/i) ) this.is_android22 = true;
    if( navigator.userAgent.match(/MSIE/i) ) this.is_msie = true;
    if( navigator.userAgent.match(/MSIE 6/i) ) this.is_msie6 = true;
    if( navigator.userAgent.match(/MSIE 8/i) ) this.is_msie8 = true;
    if( navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i) ) this.is_idevice = true;
    if( navigator.userAgent.match(/Firefox/i) ) this.is_firefox = true;
    
    // special cases for touchscreens
    if( this.is_android == true || this.is_idevice == true ) this.is_touchscreen = true;
    
    // decide who sees animations
    if( this.is_msie == true ) this.animations_enabled = false;
    else this.animations_enabled = true;
};

PlatformHelper.prototype.updatePosition = function ( element, xPos, yPos, rotation )
{
    if( !this.webkit_css_enabled || this.is_android22 )
    {
        element.style.left = xPos + 'px';
        element.style.top = yPos + 'px';
        element.style.MozTransform = 'rotate(' + rotation + 'deg)';
        element.style.webkitTransform = 'rotate(' + rotation + 'deg)';
    }
    else
    {
        var new_transform = "translate3d(" + xPos + "px, " + yPos + "px, 0px) rotate(" + rotation + "deg)";
        if( element.style.webkitTransform != new_transform )    // only apply style if not already in position
         element.style.webkitTransform = new_transform;
    }
};

September 8, 2010

Javascript / CSS animated text fire effect

A coworker sent me a funny example of a text-shadow CSS fire effect. I had a little time to kill, so I took the example and created an animated version using javascript. It's not very realistic, but it is highly silly:


Copy and paste the code into an html file to try it out:
<!DOCTYPE html>
<html>
  <head>
    <title>Fire</title>

    <script type="text/javascript">
        function fireText()
        {
            var FireColorStop = function( xPos, yPos, blur, color )
            {
                this.x = xPos;
                this.y = yPos;
                this.blur = blur;
                this.color = color;
                this.oscSpeed = Math.random() * Math.abs( yPos ) / 75;
                this.oscIncrement = 0;
                this.xOffset = 0;
                this.yOffset = 0;
                this.blurOffset = 0;
            };
        
            FireColorStop.prototype.oscillate = function() 
            {
                this.oscIncrement += this.oscSpeed;
                this.xOffset = Math.sin(this.oscIncrement) * this.blur / 3;
                this.yOffset = Math.sin(this.oscIncrement) * 1;
                this.blurOsc = this.blur + 10 + Math.sin(this.oscIncrement) * 3;
            };
        
            FireColorStop.prototype.getCSS = function() 
            {
                return ( this.x + this.xOffset ) + 'px ' + ( this.y + this.yOffset ) + 'px ' + this.blurOsc + 'px ' + this.color; 
            };
            
            // create objects for each color stop for independent animation
            var fireColors = [  new FireColorStop(0,  0,  4,  '#FFFFFF'),
                                new FireColorStop(0, -5,  4,  '#FFFF33'),
                                new FireColorStop(2, -10, 6,  '#FFDD33'),
                                new FireColorStop(-2,-15, 11, '#FF8800'),
                                new FireColorStop(2, -25, 18, '#FF2200')
                                ];
        
            var fps = 1000/30;
            var text = document.getElementById('fireText');
            
            // oscillate color stops and rebuild fire css
            setInterval( function(){ 
                var shadowCSS = '';
                for( var i = 0; i < fireColors.length; i++ )
                {
                    fireColors[i].oscillate();
                
                    shadowCSS += fireColors[i].getCSS();
                    if( i < fireColors.length - 1 )
                        shadowCSS += ', ';
                }
                text.style.textShadow = shadowCSS;
            }, fps );
        }
    </script>
        
    <style>
        body, html {
            background-color:black;
        }
        #fireText {
            background-color:black;
            position:absolute;
            display:block;
            width:100%;
            height:300px;
            line-height:300px;
            color:white;
            font-family: Arial, Verdana, sans-serif;
            font-size:50px;
            font-weight:bold;
            text-align:center;
        }
    </style>
    
  </head>
  <body>
    <div id="fireText">
        Yeah Dude.
    </div>
    <script type="text/javascript">
        fireText();
    </script>
  </body>
</html>