Welcome!

Welcome to the official BlackBerry Support Community Forums.

This is your resource to discuss support topics with your peers, and learn from each other.

inside custom component

Native Development

Reply
Developer
greenmr
Posts: 882
Registered: ‎03-20-2013
My Device: Red LE Developer Z10
My Carrier: Fido

WebView - Problems and Solutions - A Tutorial

[ Edited ]

Help Index Page

WebView on Cascades is a terrific way to present HTML formatted information to users, but it has quite a few inconsistencies and bugs that complicate otherwise simple tasks. After several frustrating days debugging what should have been a simple application help feature, I thought others might benefit from my trials and tribulations.

 

I use a WebView to provide help for my app, and use a third-party help editing tool to generate the content. This tool uses templates to generate help in many different formats from one source document; Word, PDF, HTML, WinHelp, etc. One of the templates targets iPhone browsers, so I adapted that one to generate help for my native BB10 app. The template uses iPhone standard styling, which I haven't gotten around to changing yet, but I got the layout changed and functional in a WebView. As is common for help documentation the template generate three tabs, one each for "Contents", "Index", and "Search".

 

On the "Index" tab some of the keywords reference multiple topics, so I pop up a CSS modal dialog to let the user choose which one to jump to. To do this, I create a new DIV in JavaScript (JS) with this CSS styling:

  

Modal Popup

#shade-behind {
   height: 100%;
   width: 100%;
   position: absolute;
   z-index: 10000;
   background-color: black;
   opacity: 0.75
}

The absolute position and high Z index makes it float over all the other page elements and the opacity setting lets those other elements peek through. I use JS to append it to the document window.

 

Next I use JS to build the popup HTML, enclosing it in another DIV, styled like this:

 

#popup-div {
   top: 20px;
   left: 20px;
   position: absolute;
   z-index: 10001;
   background-color: black;
   padding: 2px 2px 2px 2px
}
#popup-div .cancel-button-bar {
   background: -webkit-gradient(linear,0% 0%,0% 100%,from(#cdd5df),color-stop(3%,#b0bccd),color-stop(50%,#889bb3),color-stop(51%,#8195af),color-stop(97%,#6d84a2),to(#2d3642));
   margin: 0 0 0 0;
   text-align: right;
   padding: 3px 6px;
   font-weight: bold
}
#popup-div .keyword-link {
   margin: 0 0 0 0;
   padding: 10px 15px 9px 10px;
   border-top: 1px solid;
   background-color: white
}
#popup-div .keyword-link a {
   font-size: 17px;
   text-decoration: none;
   color: black;
   font-weight: bold
}

The slightly higher Z index makes the popup float above the background shading. I use Google Chrome when laying out the CSS and testing the JavaScript since it is WebKit-based like WebView and renders HTML and CSS the same (mostly). Everything works perfectly in Chrome, but WebView's quirks caused it to behave erratically. Here is what I learned in the process of getting everything working properly for my application help system.

 

WebView: The Good

 

  • HTML5 support - 'nuff said!
  • Support for HTML5 local storage. This was critical in overcoming the problems described below.
  • Two-way JavaScript communication between WebView and the page JavaScript. Also critical to these solutions.

WebView: The Bad

 

  • Doesn't respect anchor hashes. When an URL has a "#anchor" at the end it means to load the page then scroll down to "anchor". WebView refuses, and simply loads the page without scrolling down.
  • Erratic HTML5 - some things work, some don't, even some basic functionality.
  • CSS position: fixed is broken. I initially planned to center the popup on the page with position: fixed, but this is seriously broken in WebView's implementation of WebKit. The issue is that while the elements are displayed properly and are position locked when the underlying page is scrolled, any links or "touch spots" on the fixed elements scroll up and down with the page. What this means is that if you put a <a> link on the screen with position: fixed, the link text will stay where you put it, but if you scroll the page behind it the place you have to tap to trigger the link moves up and down with the page. If the page is long enough it is possible to scroll the link trigger spot right off the screen. Also, I originally had the background shading DIV set to position: fixed too, but I noticed that as the page was scrolled behind the popup and shading, WebView would often shift the shading DIV up or down a pixel, allowing the bright background to peek through at the top or bottom of the page.
  • The DOM variable window.innerHeight always returns the full screen height, even when the bottom is obscured with an Action Bar.
  • Sometimes distorts page elements when the device orientation changes.

WebView: The Solutions/Workarounds

 

Before tackling any of these problems, let's add JavaScript to the page HTML that allows sending diagnostic messages back to the WebView. We need a function like this since JavaScript may run differently in WebView than in Chrome, and WebView has no JS debugging tools to inspect the execution. If your code works in Chrome but not in WebView you can add debug messages into your JS and send them back to the WebView with postNavigatorMessage(). This lets you trace JS code execution and check variable values as you navigate in your WebView:

 

function postNavigatorMessage(msg) {
   try {
      navigator.cascades.postMessage(msg);
   } catch(err){}
}

 The call to postMessage() is in a TRY-CATCH so your JavaScript will run outside of WebView, allowing you to debug the page's JavaScript with Chrome's debugging tool. Without the TRY-CATCH, calling this function from Chrome will crash the script and stop JS execution at the call to postMessage(), which only exists in WebView. In the WebView add code to receive the message and log it:

 

WebView {
   onMessageReceived: {
      console.log(message.data);
   }
}

Now let's fix some stuff.

 

Anchor Hashes


The first thing I fixed was the lack of support for URL anchor hashes. The trick was to respond to the onLoadingChanged signal:

 

WebView {
   onLoadingChanged: {
      var token;
      if (loadRequest.status == WebLoadStatus.Succeeded) {
         var tempStr = url.toString();
         var hashIndex = tempStr.lastIndexOf("#");
         if (hashIndex > -1) {
            // ---There is an anchor hash on the end of the URL. Force WebView to use it.
            var tempStr = tempStr.substring(hashIndex + 1);
            token = evaluateJavaScript("e = document.getElementsByName('" + tempStr + "')[0];e.scrollIntoView();");
         }
      }
   }
}

This code simply looks for a # at the end of the just-loaded URL. If it finds one it executes JavaScript that finds the anchor on the page and then scrolls down to it. All my WebViews are going to include this code from now on if I expect to encounter anchor hashes. The "token" variable captures the result of the JavaScript execution. You can eliminate it if you don't need the result.


Action Bar Height


Next I coded a workaround that lets JavaScript on the loaded page determine the usable height of the document viewport. Since window.innerHeight ignores the area blocked by the Action Bar, I created a JS function on the page HTML to allow WebView to pass in the usable height and store it in HTML5 local storage:

 

function setViewportHeight(height) {
   sessionStorage.setItem("viewportHeight", Math.floor(height / window.devicePixelRatio).toString())
}

Dividing the height argument by window.devicePixelRatio adjusts for the difference between screen pixels and CSS pixels. HTML5 local session storage keeps values around as long as the browser is active, even if pages are reloaded or the user navigates away from the page containing the JS that set the variable. If this feature wasn't supported these solutions would have been impossible. At any time JavaScript on the page can get the current value for the last stored viewport height value with:

 

eval(sessionStorage.getItem("viewportHeight")) 

 

Orientation Considerations


Since the viewport height for displaying the HTML is different in Landscape than in Portrait mode, we need a way to tell the page JavaScript when the orientation changes. Add another JS function on the page:

 

function orientationChanged(newHeight) {
   sessionStorage.setItem("viewportHeight", Math.floor(newHeight / window.devicePixelRatio).toString());
   ...
   Your additional orientation change logic here
   ...
}

Unlike setViewportHeight(), in addition to saving the new viewport height to local session storage, this function allows you perform additional JS tasks on the page when the orientation changes.

 

Since the Action Bar height is different for Landscape vs. Portrait mode (100 pixels vs. 140) we need a way to determine the current orientation and respond to orientation changes before we can tell WebView the size of the view port. This requires an attached OrientationHandler on the WebView:

 

WebView {
   attachedObjects: [
      OrientationHandler {
         id: orientationHandler
         onOrientationChanged: {
            if (orientation == UIOrientation.Landscape) {
               helpWebView.evaluateJavaScript('orientationChanged(' + (displayWidth - 100 ) + ')');
            } else {
               helpWebView.evaluateJavaScript('orientationChanged(' + (displayHeight - 140 ) + ')');
            }
         }
      }
   ]
}

Note that you will need to determine displayWidth and displayHeight (in screen pixels) for your target device. Now when the device orientation changes the JavaScript on the page will know the correct viewport height.

 

The problem now is that the viewport height is only being set when the orientation changes. We have to notify the page JS when the page first loads too so that other JavaScript in the page doesn't crash. We need to add more code to onLoadingChanged from the earlier example:

 

WebView {
   attachedObjects: [
      OrientationHandler {
         id: orientationHandler
         onOrientationChanged: {
            if (orientation == UIOrientation.Landscape) {
               helpWebView.evaluateJavaScript('orientationChanged(' + (displayWidth - 100 ) + ')');
            } else {
               helpWebView.evaluateJavaScript('orientationChanged(' + (displayHeight - 140 ) + ')');
            }
         }
      }
   ]
   onLoadingChanged: {
      var token;
      if (loadRequest.status == WebLoadStatus.Succeeded) {
         if (orientationHandler.orientation == UIOrientation.Landscape) {
            evaluateJavaScript('setViewportHeight(' + (displayWidth - 100 ) + ')');
         } else {
            evaluateJavaScript('setViewportHeight(' + (displayHeight - 140 ) + ')');
         }
         var tempStr = url.toString();
         var hashIndex = tempStr.lastIndexOf("#");
         if (hashIndex > -1) {
            // ---There is an anchor hash on the end of the URL. Force WebView to use it.
            var tempStr = tempStr.substring(hashIndex + 1);
            token = evaluateJavaScript("e = document.getElementsByName('" + tempStr + "')[0];e.scrollIntoView();");
         }
      }
   }
}

Now any JavaScript we write to handle element positioning will always know the actual viewport height, even if device orientation changes.

 

The Popup Dialog

 

Now let's get to the popup dialog location. Ideally we could have used position: fixed to ensure the dialog remained centered on the screen, and could have moved it when orientation changed by adding code in orientationChanged(). Since WebView's position:fixed bug causes undesirable side-effects, we need to find a way to keep the dialog where we want it even though we have to use position: absolute. What we have to do is move the dialog back where we want it every time the page is scrolled up or down.

 

NOTE: From here on I will be giving code examples using jQuery, but you can accomplish the same thing with standard DOM objects and notation.

 

First, we'll create a JavaScript function to vertically and horizontally center any element on the screen:

 

function centerElement(element) {
   // ---Calculate the top and left settings for the element
   var viewportTop = $(window).scrollTop();
   var viewportHeight = eval(sessionStorage.getItem("viewportHeight"));
   var elementHeight = element.height();
   var elementWidth = element.width();
   var viewportWidth = $(window).width();
   var elementLeft = ((viewportWidth - elementWidth)/2);
   var elementTop = viewportTop + ((viewportHeight - elementHeight)/2);
// ---Move the element element.css({ 'top': elementTop, 'left': elementLeft }); }

Now we need to listen for the user to scroll the page and call the centering function when they do. Note that the DIV I create to hold the popup dialog has a CSS id of "popup-div":

 

$(document).ready(function(){
   // ---Capture page scrolling
   $(window).scroll(function(){
      var popupDiv = $('#popup-div');

      // ---Only act if the popup DIV exists
      if (popupDiv.length) {
         var viewportHeight = eval(sessionStorage.getItem("viewportHeight"));

         // ---If the popup is taller than view port then let it scroll
         if (viewportHeight > popupDiv.height()) {
            centerElement(popupDiv);
         }
      }
   });
});

The last consideration is that orientation changes will change the carefully calculated location for the popup, so lets add more code to our existing JavaScript handler function. Also, when the orientation changes, WebView often messes up the layout of the popup dialog. To fix it we need to detach the DIV from the document, then reattach it, which forces WebView to redraw it. To hide this trickery from the user, we will hide the popup just BEFORE the orientation change takes effect, and show it again after we have fixed it. For this we need one more JS function:

 

function orientationChanging() {
   var popupDiv = $('#popup-div');
   if (popupDiv.length) {
      popupDiv.hide();
   }
}

...and another signal handler on the OrientationHandler:

 

OrientationHandler {
   id: orientationHandler
   onOrientationAboutToChange: {
      helpWebView.evaluateJavaScript('orientationChanging()');
   }
   onOrientationChanged: {
      if (orientation == UIOrientation.Landscape) {
         helpWebView.evaluateJavaScript('orientationChanged(' + (displayWidth - 100 ) + ')');
      } else {
         helpWebView.evaluateJavaScript('orientationChanged(' + (displayHeight - 140 ) + ')');
      }
   }
}

Now a little extra code in the JavaScript orientation changed handler:

 

function orientationChanged(newHeight) {
   sessionStorage.setItem("viewportHeight", Math.floor(newHeight / window.devicePixelRatio).toString());
   var popupDiv = $('#popup-div');
   if (popupDiv.length) {
      // ---Popup distorted going to portrait... fix it.
      popupDiv.detach();
      popupDiv.appendTo($(document.body));

      // ---WebView misreports element width after orientation change unless we bump sideways
      popupDiv.css('left', 1);

      // ---Move popup to middle of screen
      centerElement(popupDiv);

      // ---Popup was hidden in orientationChanging(). Show it again.
      popupDiv.show();
   }
}

 

UPDATE: Since originally posting this I also discovered that after an orientation change the width of the popup dialog is not reported properly to the centering function unless the popup is moved horizontally first. Because the popup gets distorted when switching back to Portrait it is reported as narrower than it really is, even though it has been redrawn by the Show() command. Bumping it horizontally while it is still invisible fixes this. Moving it vertically has no effect. I added a line to do this in the above code.

 

Conclusion

 

WebView is a powerful tool for HTML presentation in native BB10 apps, but it doesn't behave exactly like full browsers do. Some problems, like failure to respect anchor hashes, can be overcome with code to behave exactly as we want. Other issues, such as position: fixed, need workarounds to approximate our intentions. It took me about three days to solve all my WebView issues to my satisfaction. I hope that by describing my experiences and solutions other developers might have an easier time of it.

 

Cheers... Martin.



Developer of Built for BlackBerry certified multiFEED RSS/Atom feed reader and aggregator.
Please use plain text.
Developer
Innovatology
Posts: 1,280
Registered: ‎03-03-2011
My Device: Playbook, Z10, Q10, Z30 with Files & Folders and Orbit of course
My Carrier: Vodafone

Re: WebView - Problems and Solutions - A Tutorial

Wow, that's quite a write-up. Well done.

 

But did you remember to add a <meta viewport> tag to your page? If not, that may account for the positioning problems...

 

<head>
    <script>
	var meta = document.createElement("meta");
	meta.setAttribute('name','viewport');
	meta.setAttribute('content','initial-scale='+ (1/window.devicePixelRatio) + ',user-scalable=no');
	document.getElementsByTagName('head')[0].appendChild(meta);
   </script>
</head>

 

Files & Folders, the unified file & cloud manager for PlayBook and BB10 with SkyDrive, SugarSync, Box, Dropbox, Google Drive, Google Docs. Free 3-day trial! - Jon Webb - Innovatology - Utrecht, Netherlands
Please use plain text.
Developer
greenmr
Posts: 882
Registered: ‎03-20-2013
My Device: Red LE Developer Z10
My Carrier: Fido

Re: WebView - Problems and Solutions - A Tutorial


Innovatology wrote:

Wow, that's quite a write-up. Well done.

 

But did you remember to add a <meta viewport> tag to your page? If not, that may account for the positioning problems... 


Thanks for your comment. To answer your question, no, I didn't include a <meta viewport> tag since with a WebView that stuff is all set via WebSettings::viewport, which is held in the WebView::settings property.



Developer of Built for BlackBerry certified multiFEED RSS/Atom feed reader and aggregator.
Please use plain text.