Going Mobile with ADF – Running ADF Faces on Mobile Phones and Tablets

Introduction

With over 90% of internet traffic now coming from mobile devices, there is a huge pressure on companies to make their customer-facing applications suitable for rendering on smart phones and tablets. A-team is involved with a number of customers who are looking for ways to adapt their existing Oracle ADF applications and make them “mobile-enabled” . This article is the second in a series of articles that describe what we learned from the experiences with these customers both from a technical and a non-technical perspective. The first article “Understanding the Options” provides insight in the technology choices you need to make and the implications of those choices. It also contains a lot of links with additional information. This second article provides tips and techniques to optimize rendering of ADF Faces applications on mobile devices.

Main Article

We will take an existing ADF application (version 12.1.2 or 11.1.1.7) that runs fine on desktop browsers. We then run the same application both on an iPad and an Android smart phone. We will discuss the changes we need to make to run the same application with the same web pages successfully on a desktop browser, tablet and smart phone. Please refer to the first article in this series for a discussion on the desirability of this “adaptive design” approach. In this article, we assume you have valid reasons to choose this approach, and are looking for technical guidance in getting the job done.

 Testing the Sample Application on Mobile Devices

The application is built using the UIShell approach: the application consists of a single .jsf page (or .jspx page when using JDeveloper 11.1.1.x) and uses a dynamic ADF region to switch the page content based on the menu tab that is selected. For details on this best-practice implementation see the article Core ADF11: UIShell with Menu Driving a Dynamic Region. We use the Skyros skin introduced in JDeveloper 11.1.1.7 as this skin minimizes the usage of images in favor of CSS3 shadows and gradients, making it perform better than older skins, in particular on mobile devices using a small bandwidth network. If you want to use another skin in your desktop browser, you can make the skin dynamic as well, by using an EL expression in trinidad-config.xml.

Here are two screen shots to show you how the sample application looks when run within a desktop browser.

EmpTableDesktop

EmpEditDesktop

Testing on iPad

If we run the same application on an iPad, the employees table page looks like this

EmpTableIpad

As you can see the vertical and horizontal scrollbars are gone, and you can swipe to the left to see the remaining columns.Also note that the stretching layout has been replaced with a flowing layout. The number of records shown depends on the table fetch size. This becomes more clear when we switch the orientation to portrait, while there is room for more records, only 25 records are shown. This implies we need to make the fetchsize dynamic based on the form factors of the mobile device. The pagination controls to scroll through the table rows are not visible, this is because the autoHeightRows property is not yet set on the table. This property must be set to get proper pagination controls as we will see later on. Note that in JDeveloper 11.1.1.7 the behavior is slightly different: you will see pagination controls when the autoHeightRows property is not set, and the paging size is taken from the iterator binding range size as defined in the page definition.

When we navigate to the edit employee page, it looks pretty much the same as in the desktop browser.

EmpEditIpad

The calendar popup you get on the date field is nice but looks different than the standard iOS date picker. If you tap into the email field, a default keyboard pops up without the @ character. Likewise, it would be nice when the numeric keyboard shows up when tapping in a number field like Salary or CommissionPct. These small annoyances with email, number and date fields can be solved by using HTML5 input types as we will discuss later on.

Testing on Smart Phone

When we run the same application on an Android phone (HTC Desire X), this is what we get

EmpTablePhonePortraitUnscaled

It is hardly readable,if we switch to landscape orientation, it is slightly better, but still unusable

EmpTablePhoneLandscapeUnscaled

Before the user can even read what is on the screen he needs to reverse-pinch with two fingers to enlarge the font. We will explain how to fix this initial rendering in the next section.

Setting the Initial Scaling

By default, mobile web browsers will try to fit the whole page within the available width, which means the browser will zoom out until the page fits. To fix this we need to add the viewport meta tag. See articles Don’t Forget the Viewport Meta Tag and Using the viewport meta tag to control layout on mobile browsers for more information.

To add this meta tag in our sample application, we add the following code snippet to our UIShell page:

<f:view >
  <af:document title="ADF Faces Mobile Demo" id="d1">
    <f:facet name="metaContainer">
      <af:group id="g1">
        <meta name="viewport" content="width=device-width, initial-scale=1"/> 
      </af:group>
    </f:facet>

If your application consists of multiple .jspx or .jsf pages, you obviously need to add the meta tag to every page. Now, let’s refresh the application in our smart phone browser. Below you can see the result for both portrait and landscape orientation.

EmpTablePhonePortrait EmpEditPhone

EmpTablePhoneLandscape

EmpEditPhoneLandscape

Not surprisingly,  even if the application is scaled properly, you can see that this page layout is not going to work on such a small screen. A lot of precious screen real estate is taken by the page header, footer and menu, and the table and form layouts are too wide forcing the user to swipe to the left all the time. The “View” and “Detach” options (shown in dutch in the screen shot) of the panelCollection surrounding the table are also quite useless on a mobile phone and they consume some valuable space as well. In section “Adapting the Layout to Enhance Mobile Rendering” we will discuss a technique that allows you to radically change the page layouts and make it suitable for display on smart phones without making changes to individual pages. But first we need to collect as much information as possible about the browser agent used on the mobile device, a prerequisite for determining how the layout should be adapted.

Obtaining Browser Agent Info

The ADF Faces API provides various methods to get information about the browser agent. Two agent properties are useful in the context of this article, all other documented properties either return the same value for all devices, or return the string “unknown”. The table below shows the two properties and their values for the three test devices used for this article.

Property Name Expression Desktop Value iPad Value HTC Value
 Platform Name  #{requestContext.agent.platformName}  windows iPhone android
 Touch Screen #{requestContext.agent.capabilities['touchScreen']}  none multiple single

To get the same information in Java, you can use code like this:

RequestContext context = RequestContext.getCurrentInstance();
Agent agent = context.getAgent();
String platformName = agent.getPlatformName();
String touchScreen = (String) agent.getCapabilities().get("touchScreen");

While this information helps, it does not tell us which kind of device is used. Both an iPad and an iPhone will return “iphone” as platform name, and both will return “multiple” as value for the touchScreen property. Since there are so may different sizes for mobile phones and tablets, we really need the actual browser window width and height to be able to adapt the layout properly. This can be done using the combination of JavaScript, and an ADF Faces clientListener and serverListener, that together ensure the browser dimensions are stored in a session-scoped “AgentInfo” bean.

This is the code that needs to be added to the UIShell page:

<af:resource type="javascript">
  function setWindowSize(){
  var h = document.documentElement.clientHeight, w = document.documentElement.clientWidth;
  comp = AdfPage.PAGE.findComponentByAbsoluteId('d1');
  AdfCustomEvent.queue(comp, "setScreenSize",{'screenWidth':w,'screenHeight':h}, true);}
</af:resource>
<af:clientListener method="setWindowSize" type="load"/>
<af:serverListener type="setScreenSize"  method="#{agentInfo.setWindowSize}"/>

Note that in a real application, you would store this JavaScript method in a separate library. The serverListener ensures that the setWindowSize method in the AgentInfoBean is called which has has the following signature:

public void setWindowSize(ClientEvent clientEvent)
{
  if (getScreenWidth()==0)
  {
    Map<String, Object> map = clientEvent.getParameters();
    Double width = (Double) map.get("screenWidth");
    Double height = (Double) map.get("screenHeight");
    setScreenHeight(height);
    setScreenWidth(width);      
    JsfUtils.redirectToSelf();
  }
}

Note the call to the redirect-to-self utility method at the end, this is needed because the above code is executed AFTER the page is rendered, which means the screen size info cannot be used on initial page rendering. Now, to prevent this costly redirect-to-self you can have the end user start the application with just specifying the web context root. In the index.html that will be automatically launched, you can forward to the UIShell page, passing in the screen width and height as request parameters. Here is an example index.html page using this technique:

<!DOCTYPE HTML>
<html>
  <head>
    <script>
        var height = document.documentElement.clientHeight; // window.innerHeight does not work on IE8 and FF ;
        var width = document.documentElement.clientWidth; //window.innerWidth does not work on IE8 and FF ;
        window.location.href = "faces/UIShell?screenHeight=" + height + "&screenWidth=" + width;
    </script>
  </head>
  <body></body>
</html>

In the AgentInfoBean a method annotated with the PostConstruct will then read the request parameters and store them in member variables inside the bean:

@PostConstruct
/**
 * Check if request contains screenWidth, screenHeight params. If so, store
 * the values in corresponding properties
 */
public void init()
{
  String width = (String) JsfUtils.getRequestParam(SCREEN_WIDTH_KEY);
  String height = (String) JsfUtils.getRequestParam(SCREEN_HEIGHT_KEY);
  if (width!=null)
  {
    screenWidth = new Double(width);
  }
  if (height!=null)
  {
    screenHeight = new Double(height);
  }    
}

With this code in place, the user gets “rewarded” with faster initial loading when he accesses the application through index.html. The setWindowSize method should stay as a fallback scenario when the end user launched the application using a URL that included the UIShell target.

With the two agent properties and screen width and height available, we can add a number of convenience methods to our AgentInfoBean that we can use in EL expressions to dynamically adapt the layout based on the mobile device:

public boolean isIOS()
{
  return "iphone".equals(getAgent().getPlatformName());
}

public boolean isAndroid()
{
  return "android".equals(getAgent().getPlatformName());
}

public boolean isLandscape()
{
  return getScreenWidth() > getScreenHeight();
}

public boolean isTouchScreen()
{
  return !"none".equals(getAgent().getCapabilities().get("touchScreen"));
}

/**
 * Return true when device has touch screen and either screen width or height is < 400px.
 * We check either screen width or height depending on the device orientation.
 * 400px is pretty arbitrary number, change it as desired
 */
public boolean isSmartPhone()
{ 
 double size = isLandscape() ? getScreenHeight() : getScreenWidth(); 
 return isTouchScreen() && getScreenWidth()!=0 
 && (size <=400);
}

public boolean isTablet()
{
 return isTouchScreen() && !isSmartPhone();
}

Note that when the user changes the device orientation while using the ADF Faces application, this does not affect the values returned by the above methods. If you want to support a change in device orientation while running the application, additional JavaScript is needed to detect such an orientation change and call some resize method on the AgentInfoBean with the new width and height values.

If you want to detect whether the iOS device is having a Retina screen you can extend the above code and also pass in the value of window.devicePixelRatio. When this value is 2, it is a Retina device. See this article on devicePixelratio for more information.

Adapting the Layout to Enhance Mobile Rendering

With the information about device orientation and screen width and height available, you can now start to determine how you want to adapt the layout based on the viewport size of the mobile device. As you probably want to support a wider range of mobile phones and tablets, than you have at your disposal for actual testing, this overview of viewport sizes for mobiles and tablets might come in handy.

Now, let’s see how we can adapt the layout of our application to make it more usable for smart phones and small and (small) tablets.

Using Dynamic Page Templates

The first thing we want to do is use a different page template when rendering on smaller devices. We will use the isSmartPhone convenience method from the AgentInfoBean to dynamically set the page template:

<af:pageTemplate viewId="#{agentInfo.smartPhone ? '/common/pageTemplates/PhonePageTemplate.jspx' : '/common/pageTemplates/TabsMenuPageTemplate.jspx'}" id="pt">
  <f:facet name="pageContent">
    <af:region value="#{bindings.mainRegion.regionModel}" id="mr"/>
  </f:facet>
  <f:attribute name="menuModel" value="#{menuModel}"/>
</af:pageTemplate>

The PhonePageTemplate.jspx is a basic page template with a simple page header that holds the application title, and display the menu navigation pane as a drop down list instead of tabs:

<af:panelStretchLayout id="pt_pgl1" topHeight="32px">
  <f:facet name="top">
    <af:panelGridLayout id="pt_gPbl" styleClass="AFBrandingBar">
      <af:gridRow id="pt_rh1" height="auto" marginTop="4px"
                  marginBottom="4px">
        <af:gridCell id="pt_bt" width="auto" valign="middle"
                     marginStart="4px">
          <af:outputText value="#{attrs.brandingTitle}"
                         styleClass="AFBrandingBarTitle" id="pt_ot1"/>
        </af:gridCell>
        <af:gridCell id="pt_flexSpaceHead" width="100%"/>
        <af:gridCell id="mgie" width="auto" valign="middle"
                     marginStart="4px">
          <af:navigationPane id="Menu1" var="menuItem"
                             partialTriggers="Item1"
                             value="#{attrs.menuModel}" hint="choice">
            <f:facet name="nodeStamp">
              <af:commandNavigationItem id="Item1"
                                        textAndAccessKey="#{menuItem.label}"
                                        actionListener="#{pageFlowScope.pendingChangesBean.handle}"
                                        action="#{menuItem.doAction}"
                                        rendered="#{menuItem.rendered}"/>
            </f:facet>
          </af:navigationPane>
        </af:gridCell>
      </af:gridRow>
    </af:panelGridLayout>
  </f:facet>
  <f:facet name="center">
    <af:facetRef facetName="pageContent"/>
  </f:facet>
</af:panelStretchLayout>

You might wonder why we still use a panelStretchLayout in this template as stretch layouts are automatically converted to flowing layouts on touch devices. Well, this automatic conversion is exactly the reason we need to preserve the stretching in the page template, so ADF Faces will correctly compute the fixed table height based on the available vertical space. If we would enclose the pageContent facetRef in a panelGroupLayout, and then navigate to the employees table page, about 8 rows would be visible, while there is room for about 15 rows.

The application now looks like this on a HTC phone:

EmpTablePhoneWrongFetchSize EmpEditPhoneTemplate

While the phone-specific page template is a huge improvement in terms of efficient real estate management, the display of the table and form layout is still far from optimal. In the next sub-sections we will address these display issues. We will first introduce a generic technique that allows us to change the properties of UIComponent elements in the JSF component tree at runtime, and then we will see how we can use that technique to optimize various UI components for mobile rendering.

Traversing and Changing the UIComponent Tree at Runtime

To traverse and change the UI Component tree at runtime, we will use a JSF phase listener in combination with the VisitTree Callback API. In JSF 1.2 (JDeveloper 11.1.1.x) the VisitTree API was included in the Trinidad library, which is used as foundation for ADF Faces RC. In JSF 2.0 and beyond (JDeveloper 11.1.2.x and 12c) the VisitTree implementation in trinidad has become part of the JSF standard. So, depending on the JDeveloper version you are using, your imports will change, but the code can largely stay the same, as you can see in the two samples applications you can download at the bottom of this post.

We start with a Java class that implements the VisitCallback interface:

public class MobileRenderingVisitCallback implements VisitCallback {

    public VisitContext createVisitContext() {
        return VisitContext.createVisitContext(FacesContext.getCurrentInstance());
    }

    public VisitResult visit(VisitContext context, UIComponent target) {
        AgentInfoBean agentInfo = AgentInfoBean.getInstance();
        if (!agentInfo.isTouchScreen()) {
            // No touch device, do nothing and stop tree traversal
            return VisitResult.COMPLETE;
        }
        // TODO: some mobile rendering enhancements
        return VisitResult.ACCEPT;
    }
}

For more information on using the VisitTree API see the article Efficient component tree traversal in JSF. Next, we will create a JSF phase listener that uses this class just before render response phase:

public class MobilePhaseListener implements PhaseListener {
    public MobilePhaseListener() {
        super();
    }

    public void afterPhase(PhaseEvent phaseEvent) {
    }

    public void beforePhase(PhaseEvent phaseEvent) {
        if (phaseEvent.getPhaseId() == PhaseId.RENDER_RESPONSE) {
            MobileRenderingVisitCallback cb =
                new MobileRenderingVisitCallback();
            UIXComponent.visitTree(cb.createVisitContext(),
                                   JsfUtils.getViewRoot(), cb);
        }
    }

    public PhaseId getPhaseId() {
        return PhaseId.ANY_PHASE;
    }
}

And finally, we register this class as a managed bean in adfc-config.xml:

<managed-bean id="__18">
  <managed-bean-name id="__19">MobilePhaseListener</managed-bean-name>
  <managed-bean-class id="__21">oracle.ateam.demo.ui.application.MobilePhaseListener</managed-bean-class>
  <managed-bean-scope id="__20">request</managed-bean-scope>
</managed-bean>

and reference the bean in the beforePhase property of the f:view element of the UIShell page:

<f:view beforePhase="#{MobilePhaseListener.beforePhase}">
  <af:document title="ADF Faces Mobile Demo" id="d1">

You might be tempted to register the JSF phase listener in faces-config.xml as this is faster and does not require a managed bean definition. However that will not work in this case because you cannot access the UI component tree on initial page loading when the phase listener is defined in faces-config.xml!

With this generic code in place, we can now start adding code to our MobileRenderingVisitCallback class to fix various rendering issues.

Changing the PanelFormLayout to One Column

To avoid horizontal swiping in multi-column form layouts, we need to set the maxColumns property of the panelFormLayout components to 1 in all our pages and page fragments. With the above generic UI component tree traversal in place, this is pretty straightforward. We add the following method to the MobileRenderingVisitCallback class

private void processPanelFormLayout(UIComponent target) {
    if (target instanceof RichPanelFormLayout) {
        RichPanelFormLayout pfl = (RichPanelFormLayout)target;
        pfl.setMaxColumns(1);
    }
}

and call this method from the visit method

public VisitResult visit(VisitContext context, UIComponent target) {
    AgentInfoBean agentInfo = AgentInfoBean.getInstance();
    if (!agentInfo.isTouchScreen()) {
        return VisitResult.COMPLETE;
    }
    if (agentInfo.isSmartPhone()) {
        processPanelFormLayout(target);            
    }
    return VisitResult.ACCEPT;
}

With this code in place, the form layouts start to look much better on smart phones, as you can see below.

OneColumn

You could further extend this sample code, for example to shorten the width of large input fields, and turn it into multi-row input fields.

Fixing Table Pagination

In JDeveloper 11.1.1.7 the pagination size is controlled by the table fetch size property, in JDeveloper 12.1.2. it is controlled by the autoHeightRows property. If this property is not set you will not see pagination controls. You should make sure that the number of rows visible on the screen matches the value of the fetchSize and autoHeightRows properties. If the pagination size is larger than the number of rows visible ,it can cause a confusing user experience. For example, when fetchSize/autoHeightRows is set to 25, and only 18 rows are visible, then navigating to the next “table page” will show rows 26 to 44. Rows 19 to 25 remained invisible. The end user could have visited these rows by using a swipe-up action, but this is counter-intuitive as pagination controls are added to navigate through the rows. 

Similar to changing the number of columns in a panelFormLayout, we can change the table fetch size and autoHeightRows property in the MobileRenderingVisitCallback class:

public VisitResult visit(VisitContext context, UIComponent target) {
    AgentInfoBean agentInfo = AgentInfoBean.getInstance();
    if (!agentInfo.isTouchScreen()) {
        return VisitResult.COMPLETE;
    }
    if (agentInfo.isSmartPhone()) {
        processPanelFormLayout(target);
    }
    processTable(target, agentInfo);
    return VisitResult.ACCEPT;
}

The processTable method looks like this:

private void processTable(UIComponent target, AgentInfoBean agentInfo) {
    if (target instanceof RichTable) {
        RichTable table = (RichTable) target;
        // set fetch size and autoHeightRows so we get proper pagination controls
        int tabletRows = agentInfo.isLandscape() ? 21 : 35;
        int rows = agentInfo.isSmartPhone() ? 16 : tabletRows;
        table.setFetchSize(rows);
        table.setAutoHeightRows(rows);
    }
}

With this code in place, the pagination controls on the employees table show up nicely on the iPad:

TablePaginationIpad

The tricky thing here is to determine the number of rows that will be visible. Based on your test devices you should come up with a formula that computes the number of rows based on the screen height, and other elements on the page that consume real estate. If this doesn’t work out, you need to specify the autoHeightRows property for eacht af:table directly in the page source.

Also note that with table pagination turned on, you typically do not want to fill the last page with rows, and just show the last remaining rows. This can be done by going to the view object general tab, tuning section, and uncheck the checkbox as shown below

FillLastPage

 Hiding the Surrounding PanelCollection

Many of the standard features in a panelCollection are less useful on a mobile phone. For example, the ability to show/hide columns, attach/detach the table, or freeze columns isn’t something we expect a mobile phone user to do. So, we can save some real estate and display more rows in the table by hiding the panelCollection chrome. This is easily done by adding the following code inside the processTable method:

if (agentInfo.isSmartPhone() && table.getParent() instanceof RichPanelCollection) {
    RichPanelCollection pc =
        (RichPanelCollection)table.getParent();
    HashSet<String> featuresOff = new HashSet<String>();
    featuresOff.add("viewMenu");
    featuresOff.add("formatMenu");
    featuresOff.add("wrap");
    featuresOff.add("detach");
    featuresOff.add("freeze");
    featuresOff.add("statusBar");
    pc.setFeaturesOff(featuresOff);
}

With this code in place, we can increase the fetch size to 19, resulting in a page like this:

EmpTableNoPC

Handling Wide Tables

Most tables initially designed for desktop browsers will be wider than the available width on mobile phones. In the table shown above, a user can swipe to the left with two fingers simultaneously to see the remaining columns. While this works it is not an intuitive touch gesture. There are a couple of ways to address this issue of “overflow” columns:

  • SImply hide the additional columns, and show that information in a detail screen after the user selected a row.
  • Use the concept of “detail disclosure” by defining a detailStamp facetof the af:table component and move the remaining items inside a panelFormLayout in the detailStamp facet.
  • Replace the table component with a listView component, which is more suited for mobile devices and tablets.

The first two solutions can easily be implemented in the MobileRenderingVisitCallback class. Here is example code for implementing the detail disclosure:

private void moveTableItemsToDetailStamp(RichTable table) {
    if (table.getFacets().get("detailStamp") != null) {
        return;
    }
    RichPanelGroupLayout form = new RichPanelGroupLayout();
    RichSpacer spacer = new RichSpacer();
    spacer.setWidth("5px");
    spacer.setHeight("5px");
    form.getFacets().put("separator", spacer);
    table.getFacets().put("detailStamp", form);
    List<UIComponent> children = table.getChildren();
    int counter = 0;
    for (UIComponent kid : children) {
        if (kid instanceof RichColumn) {
            counter++;
            RichColumn column = (RichColumn)kid;
            if (counter > 3) {
                column.getChildren().get(0).setParent(form);
                form.getChildren().add(column.getChildren().get(0));
                column.setRendered(false);
            }
        }
    }
}

With this code in place, the employees table has an additional detail disclosure icon at the beginning that can be used to expand the row:

DetailDisclosure

If you want to have a more structured layout of the items in detail disclosure area, you can change the code to use a panelFormLayout and get the item label values from the column header.

While these improvements make the table component somewhat usable on a smart phone, you might want to consider replacing the table component with a listView (javadoc and demo) component which provides a more native look and feel.

Using HTML5 Input Types

A number of new input types have been added to HTML 5. Using these input types has the following advantages:

  • Device-native rendering of the input element. For example, a date input type will render with iOS or Android native date picker
  • Context-sensitive virtual keyboard. For example, an email input type will show the @-character, a number input type will only show numeric values on the virtual keyboard
  • Declarative validation. For example, a number input type will raise an error when entering a non-numeric value

HTML5 input types can be used in ADF Faces by specifying the usage property on the af:inputText component. The property help and code insight in the JDeveloper page visual editor will only show Auto, text or search as allowable values, but you can enter the new HTML5 input types as well. To prevent the red underlining of the property, you can enclose the literal input type value inside an EL expression.

Here is an example of using the HTML5 date type in your page:

<af:inputText label="#{bindings.HireDate.hints.label}" required="#{bindings.HireDate.hints.mandatory}"
      usage="#{'date'}" value="#{bindings.HireDate.inputValue}" id="it9">
  <af:convertDateTime pattern="yyyy-MM-dd" id="cdt22"  type="date" />
</af:inputText>

Note the pattern used to convert the date, this format is the ‘wire’ format for the date input type as prescribed by the HTML5 standard. If you don’t add an af:convertDateTime tag with this wire date format as value for the pattern attribute, then the field will not show any data.

The way the date input field will be rendered is browser-specific. On the desktop, only Google Chrome currently displays a nice calendar widget to pick a date using the user’s locale, Internet Explorer and Firefox display a simple input field, using the wire format as display date format. Chrome on android devices, and Safari on iOS nicely display a native date picker. Since most desktop browsers do not show a calendar widget, you probably want to stick with the standard ADF Faces calendar that you get for free when using an af:inputDate component on desktop browsers. So, to leverage the native date picker on iOS and android, you need to conditionally render either the af:inputDate component or the af:inputText component with usage property set to ‘date’. Here is a code snippet that implements this approach:

<af:inputDate value="#{bindings.HireDate.inputValue}" label="#{bindings.HireDate.hints.label}"
              required="#{bindings.HireDate.hints.mandatory}" columns="#{bindings.HireDate.hints.displayWidth}"
              shortDesc="#{bindings.HireDate.hints.tooltip}" rendered="#{!agentInfo.touchScreen}" id="id1">
  <f:validator binding="#{bindings.HireDate.validator}"/>
  <af:convertDateTime pattern="#{bindings.HireDate.format}"/>
</af:inputDate>
<af:inputText label="#{bindings.HireDate.hints.label}" required="#{bindings.HireDate.hints.mandatory}"
      value="#{bindings.HireDate.inputValue}" usage="#{'date'}" rendered="#{agentInfo.touchScreen}"   id="it9">
  <f:validator binding="#{bindings.HireDate.validator}"/>
  <af:convertDateTime pattern="yyyy-MM-dd" id="cdt22" type="date"/>
</af:inputText>

To avoid the tedious work of adding this af:inputText to each and every page with a date item, we can add the HTML5 date support in a generic fashion using the JSF phase listener and VisitTree API as explained in the section ‘Traversing and Changing the UIComponent Tree at Runtime’. Here is a sample method that can be added to the MobileRenderingVisitCallback class to achieve this:

private void processDate(UIComponent target) {
    if (target instanceof RichInputDate) {
        RichInputDate date = (RichInputDate) target;
        int index = date.getParent().getChildren().indexOf(date);
        if (!date.isRendered()) {
            // html5 date already added or not needed
            return;
        }
        RichInputText html5Date = new RichInputText();
        html5Date.setUsage("date");
        html5Date.setValueExpression("rendered", date.getValueExpression("rendered"));
        html5Date.setValueExpression("required", date.getValueExpression("required"));
        html5Date.setValueExpression("disabled", date.getValueExpression("disabled"));
        html5Date.setValueExpression("readOnly", date.getValueExpression("readOnly"));
        html5Date.setValueExpression("label", date.getValueExpression("label"));
        html5Date.setValueExpression("value", date.getValueExpression("value"));
        // we need to use the converter from original date item, creating new DateTimeConverter instance
        // causes java.lang.ClassCastException for some unknown reason:
        // Value "2005-06-24" is not of type java.util.Date, it is class oracle.jbo.domain.Date
        DateTimeConverter conv = (DateTimeConverter) date.getConverter();
        date.setConverter(null);
        conv.setPattern("yyyy-MM-dd");
        html5Date.setConverter(conv);
        date.getParent().getChildren().add(index + 1, html5Date);
        date.setRendered(false);
    }
}

Using the HTML date input type on an iPad shows the iOS-native date picker (dutch language):

html5date

On the HTC-Android device, the date picker looks like this:

html5dateAndroid

In a similar way, you can add the usage property to af:inputText components that represent an email or number item. You only need to have a way to identify these item types in a generic way. For number items, this could be done by checking whether the af:inputText has an af:numberConverter specified:

private void processNumberField(UIComponent target) {
    if (target instanceof RichInputText) {
        RichInputText field = (RichInputText) target;
        Converter converter = field.getConverter();
        if (converter instanceof NumberConverter) {
            field.setUsage("number");
        }
    }
}

This code will cause a numeric keyboard to show up on the iPad when tapping in the ManagerId field:

html5number

Using Client-Side Responsive Design Techniques

This articles described server-side adaptive design techniques. You can use these techniques in combination with client-side responsive design techniques using CSS media queries as desired. Here are some links to good articles on using CSS media queries in ADF Faces:

Note that while using CSS media queries is a very popular technique to optimize mobile rendering, you should be aware that all changes are made on the client, which means that the full JSF UI component tree is still sent to the mobile browser. If you want to hide significant parts of your desktop user interface to optimize mobile rendering it will be more performant to do this server-side.

Conclusion

With some simple JavaScript we can gather additional screen sizing information from the browser agent. This information can then be used to implement server-side adaptive design techniques to enhance mobile rendering without modifying each and every page or page fragment. This is a fast and cost-effective way to go mobile with ADF. However, as explained in the first article of this series Understanding the Options, you might want to consider more significant redesign, as well as the use of ADF Mobile to truly optimize the mobile user experience.

You can download the sample application for both JDeveloper 11.1.1.7 and JDeveloper 12.1.2.

Add Your Comment