Tuesday Dec 22, 2015

Android 9-patch splash screen images

A very handy, but little advertised feature introduced in MAF 2.2.0 is the ability to use 9-patch splash screen images for Android.

By using 9-patch splash screen images, you are able to define how your splash screen images will stretch to fit all Android device screens.

You define which area(s) of the image can be stretched, whilst ensuring that other areas such as those containing a logo or text will remain a constant size.

What is a 9-patch image?

A 9-patch image is a standard PNG image that includes an extra 1-pixel wide border and is saved with a .9.png extension.

The border is used to indicate which area(s) of the image can be stretched and optionally which area(s) of the image are fillable (i.e. can be overlaid with text when the image is used as a background image for an Android View object, such as a Button).

To start with, all border pixels must be completely transparent. You then indicate the stretchable and fillable areas by adding black pixels to the corresponding border areas.

For the purpose of a splash screen image, you only need to define the stretchable areas of the image. The top and left borders are used for this purpose. A black pixel in the top border row indicates that the image pixels in that column may be stretched horizontally. A black pixel in the left border column indicates that the image pixels in that row may be stretched vertically.

The following example 9-patch image has a blue background, white log and red box.  I've doubled its size here so you can more easily see the black pixels in the top and left borders.

The black pixels in the top and left borders ensure that only the blue background portion of the image will be stretched when the available display area is larger than the image size.  The center logo and the outer red box will remain static.

For more information, the official Android documentation on 9-patch can be found here and a really good tutorial can be found here.

Creating a 9-patch splash screen image

The best place to start is with your existing set of PNG splash screen images.

Since 9-patch images can only stretch, not shrink, ensure that you start with an image size equal to or smaller than the size of the smallest area in which you expect your app to be launched (remember that some Android versions support launching apps into a window that does not fill device’s entire screen).

For best results with images that contain logos and text, create a separate 9-patch image for each screen density. You may find that a single 9-patch image works in both portrait and landscape mode for the specified density, which will reduce the number of splash screen images you need to create and maintain.

The Android SDK provides a WYSIWYG editor called draw9patch that enables you to create a 9-patch image from an existing PNG image. It is very easy to use and dynamically shows you how your image will stretch. More information on this tool can be found here.

If for some reason you would rather use your favorite image editor, remember the following 9-patch image constraints:

  • The border must be only 1-pixel wide and must surround the entire image, even if you don't wish to define the fillable area.  Thus a 100x100 pixel image becomes a 102x102 pixel 9-patch image.
  • The border must consist entirely of only fully transparent or solid black pixels.  A slight difference in color or alpha level will fail silently at runtime.
  • The image file must have a .9.png extension.  

Using 9-patch splash screen images in your MAF app

Having created 9-patch versions of your splash screen images for each Android screen density and orientation, copy them into your MAF project and reference them in your Android deployment profile.

Conclusion

By using 9-patch splash screen images, you are able to define how your splash screen images will stretch to fit all Android device screens or windows in which your app is launched.

Creating 9-patch splash screen images from your existing PNG splash screen images is simple using the draw9patch tool available in the Android SDK.

The above information is only relevant to Android, since iOS does not support 9-patch.

Getting ready for 2016: Learn Oracle Mobile Cloud Service

As we approach the end of a year and the start of a new one, it's a good time to start considering new challenges and opportunities. We'd like to suggest Oracle Mobile Cloud Service (MCS) is a good choice to investigate in 2016 as the enterprise world increasingly adopts mobile solutions.

Oracle Mobile Cloud Service has a simple premise, to accelerate enterprise mobile development by simplifying common mobile development tasks like integration, push notifications, user management, data offline synchronization and more, regardless if you're working with Android, iOS, Oracle Mobile Application Framework and other mobile platforms. Personally having spoken about MCS to many clients since the introduction of MCS mid 2015, there is a certain point in every discussion around their mobile plans where they don't first understand the worth of MCS as they learn the platform, but then there's a near audible "click", and all the ideas thereafter can't imagine a solution without MCS.

To help you with your 2016 goals Oracle have published a free YouTube training channel on Oracle Mobile Cloud Service. And we think you'll find this set of videos special, as it was specifically designed to be easily digestible. Rather than the 1hr+ slog many other technical videos follow, this channel uses videos typically around 10minutes in length so you can fit them in while catching the bus, squeezing some time in between meetings, or just sitting there trying to digest your Christmas lunch!

The overall channel can be found here, but, it is also broken into several playlists so you can watch videos relating to specific topic areas you are interested in:

Alternatively if you want to just look at the "big picture" and skip the details, or are only interested in MCS from an Android and iOS perspective, we've also created playlists that gather just the pertinent videos for you:

Oh and of course if you're interested in picking up Oracle Mobile Application Framework (MAF) we have an array of videos ready for you too:

Overall once the New Year rolls in, Oracle has you covered in helping you learn these new products in 2016.

Monday Dec 21, 2015

Should brands build multiple mobile apps?

Author: Ian Wallis, Mobile Technical Director, Oracle EMEA

There is definitely a trend by brands towards multiple consumer mobile apps but mainly for very popular services that users use regularly in order to speed up their access to specific services. An example is Facebook breaking out Messenger into a separate app.

If we take Banking as an example, it is still early days in this shift, and I see near term potential for a maximum of one or two apps per brand. Barclays had great success with their PingIt mobile app, but HSBC recently withdrew their Fast Banking access app in the UK to try to get users onto their main Mobile Banking app. Interestingly, one of the banking industry’s biggest challenge is that millenials (18-30 year olds) are flocking to their mobile app channel but the banks only have a limited set of their products available on the mobile channel vs their online website channel. This is a big headache for their Product Managers.

In a perfect world, Digital focused brands would have one mobile app and know who the user is and what the user wants to do next rather than getting the user to do multiple downloads and have to choose which app to engage with. Most companies do not have this capability yet and should consider how to increase their contextual capability.

If a company is driving different branding for millenials versus other age groups or to a different segment of their market, then a separate dedicated more trendy or applicable app my apply.

There are a number of exceptions to the one app strategy:

1. Frequently used apps - where mobile apps are used multiple times a day and a separate branded app would help reduce navigation and reduce time for the user getting to value – this is more applicable to the likes of Google and Facebook or something like the Barclays PingIt example

2. Apps targeted at Millenials or other age groups and segments – usually with a different brand flavor to the main mobile application. See recent Oracle Mobile research on this here on millennial’s

3. Event apps - e.g. for specific events like sports, music festivals, elections etc…

4. Media companies – where they are looking to expand their presence and expand screen time for advertising and 'mobile moments'

5. Business to Employee scenarios - there is definitely a move to lots of micro apps for specific jobs and tasks done by employees

I am sure there are many other exceptions and would love to hear back on this @wallisi. Forbes published an interesting article on this last year here.

If you would like to read more about Oracle’s Mobile Cloud Services and how we can support brands with multiple apps and immersive mobile experiences take a look at our eBook here.

Follow @OracleMobile

Monday Dec 14, 2015

Estapar Makes Parking Easy with Oracle Mobile

Going to Latin America? Looking for parking as you're checking out the attractions? You can use Estapar's new mobile app (Escaper is the largest parking company in Latin America) to find a great parking space. They built and deployed their mobile solution in a few months using Oracle MAF and their next step is to further expand their mobile strategy with Oracle Mobile Cloud Service. Check out the video below.

Follow @OracleMobile

Monday Nov 30, 2015

Gartner: Mobile Trends and Insights

Enterprise Mobile Trends and Insights Featuring Gartner
Mobile has quickly become a ubiquitous part of our everyday life. Today we expect the convenience and ease of use capabilities achieved from mobile devices to permeate into the workplace. How should enterprises embrace mobile technology as they embark on their mobile journey? What impacts will Cloud and Internet of Things have on Mobile? Get these answers and much more from Gartner Research Director, Richard Marshall in this short webcast.   (registration required) 

Follow @OracleMobile

Wednesday Nov 25, 2015

Quick Note: How to validate an input field in a list view

This article is a quick note on how to validate an input field added to an amx:listView and how to reset the list item value back to the original value. The use case in the sample code snippet is a field service application that technicians use to protocol service incidents, the status of a repair and the time spent on it. The validation should be such that the time the technician worked on an incident doesn't go beyond the 8 hours of the working shift.

About the Data Model

I like the Mobile Application Framework  data control as an abstraction layer, but I also like to be in control. Because of this, independent of the service or data source (SQLite) to access, I always work through a POJO model. For this sample, the simple POJO model I created includes to classes.

The entity

 public class ServiceAssignment {
    
    String orderId = null;
    String customerName = "";
    String incident = "";
    String repairStatus="";
    Integer repairHours = 0;
    private PropertyChangeSupport propertyChangeSupport = 
                              new PropertyChangeSupport(this);


    public ServiceAssignment() {
        super();
    }


    public void setOrderId(String orderId) {
        String oldOrderId = this.orderId;
        this.orderId = orderId;
        propertyChangeSupport.firePropertyChange("orderId", oldOrderId, orderId);
    }

    public String getOrderId() {
        return orderId;
    }

    public void setCustomerName(String customerName) {
        String oldCustomerName = this.customerName;
        this.customerName = customerName;
        propertyChangeSupport.firePropertyChange("customerName", 
                                  oldCustomerName, customerName);
    }

    public String getCustomerName() {
        return customerName;
    }

    public void setIncident(String incident) {
        String oldIncident = this.incident;
        this.incident = incident;
        propertyChangeSupport.firePropertyChange("incident", 
                                          oldIncident, incident);
    }

    public String getIncident() {
        return incident;
    }

    public void setRepairStatus(String repairStatus) {
        String oldRepairStatus = this.repairStatus;
        this.repairStatus = repairStatus;
        propertyChangeSupport.firePropertyChange("repairStatus", 
                                 oldRepairStatus, repairStatus);
    }

    public String getRepairStatus() {
        return repairStatus;
    }

    public void setRepairHours(Integer repairHours) {
        Integer oldRepairHours = this.repairHours;
        this.repairHours = repairHours;
        propertyChangeSupport.firePropertyChange("repairHours",
                                 oldRepairHours, repairHours);
    }

    public Integer getRepairHours() {
        return repairHours;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
}

The facade (becoming the data control)

public class TechnicianAssignments {
    
    
    protected transient ProviderChangeSupport   providerChangeSupport 
                                     = new ProviderChangeSupport(this);  
    
    private ArrayList<ServiceAssignment> assignments = 
                                        new ArrayList<ServiceAssignment>();

    private PropertyChangeSupport propertyChangeSupport = 
                                          new PropertyChangeSupport(this);

    public TechnicianAssignments() {
        super();
        
        
        ServiceAssignment sa1 = new ServiceAssignment();
        
        sa1.setOrderId("1");
        sa1.setIncident("washing machine - short circuit");
        sa1.setRepairStatus("fixed");
        sa1.setRepairHours(new Integer(1));
        sa1.setCustomerName("John Muller");
        
        ServiceAssignment sa2 = new ServiceAssignment();
        
        sa2.setOrderId("2");
        sa2.setIncident("TV set broken");
        sa2.setRepairStatus("fixed");
        sa2.setRepairHours(new Integer(3));
        sa2.setCustomerName("Bill Spender");
        
        ServiceAssignment sa3 = new ServiceAssignment();
        
        sa3.setOrderId("3");
        sa3.setIncident("Cofee machine doesn't clean");
        sa3.setRepairStatus("pending");
        sa3.setRepairHours(new Integer(0));
        sa3.setCustomerName("Gaby Crown");
        
        ServiceAssignment sa4 = new ServiceAssignment();
        
        sa4.setOrderId("4");
        sa4.setIncident("dish washer - leaks water");
        sa4.setRepairStatus("fixed");
        sa4.setRepairHours(new Integer(5));
        sa4.setCustomerName("John King");
        
        assignments.add(sa1);
        assignments.add(sa2);
        assignments.add(sa3);
        assignments.add(sa4);
        
    }

    public void setAssignments(ArrayList<ServiceAssignment> assignments) {
        ArrayList<ServiceAssignment> oldAssignments = this.assignments;
        this.assignments = assignments;
        propertyChangeSupport.firePropertyChange("assignments", 
                                       oldAssignments, assignments);
    }

    public void refreshAssignmentsList(){
        //refresh the list for the UI to refresh
        propertyChangeSupport.firePropertyChange("assignments", 
                                           null, assignments);
    }

    public ArrayList<ServiceAssignment> getAssignments() {
        return assignments;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
    
    public void addProviderChangeListener(ProviderChangeListener l) {  
         providerChangeSupport.addProviderChangeListener(l);  
      }  
      public void removeProviderChangeListener(ProviderChangeListener l) {  
         providerChangeSupport.removeProviderChangeListener(l);  
      }  
}

Please have a special interest in the following method added to the data control facade:

public void refreshAssignmentsList(){
        //refresh the list for the UI to refresh
        propertyChangeSupport.firePropertyChange("assignments", null, assignments);
    }

This method invokes the provider change event for the "assignments" list that then propagates the event notification to the list for the  list view to refresh. This is important to ensure that the value change issued by the validation is displayed on the screen.

The View

The amx view renders a list based on the "assignments" collection that is exposed on the data control.

<amx:listView var="row" value="#{bindings.assignments.collectionModel}"
                  fetchSize="#{bindings.assignments.rangeSize}"
                  selectedRowKeys="#{bindings.assignments.collectionModel.selectedRow}"
                  selectionListener="#{bindings.assignments.collectionModel.makeCurrent}" 
                  showMoreStrategy="autoScroll"
                  bufferStrategy="viewport" id="lv1">
      <amx:listItem id="li1"> ... </amx:listItem>
</amx:listView

 The list view is configured such that selecting a list item will automatically (or automagically) set the selected row as current in the MAF binding layer. The input component that exposes the working hours gets a value change listener that points to a managed bean to evaluate the user entry and - if needed - to correct it.

The managed bean code

The managed bean can live in viewScope as there is no state it needs to keep across page navigation. The code simply checks whether the user provided value is above 8 hours or below 0 (though I never experienced a field service to not at least charge for one hour. So probably 0 is for warranty cases)

public class RepairAssignmentsBacking {
    public RepairAssignmentsBacking() {
    }

    public void onRepairHoursChange(ValueChangeEvent valueChangeEvent) {
        
          Integer newVal = new Integer((String) valueChangeEvent.getNewValue());
          Integer oldVal = new Integer((String) valueChangeEvent.getOldValue());
        
          
          //validation condition
          if(newVal>8 || newVal<0){
              
             BasicIterator iterator = (BasicIterator) AdfmfJavaUtilities.getELValue
                                      ("#{bindings.assignmentsIterator.iterator}");          

             ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
             
             sa.setRepairHours(oldVal);
             
             DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                 iterator.getDataControl().getName());

             TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
             
             //refresh UI
              dcImpl.refreshAssignmentsList();
                            
              throw new AdfException("The working time cannot be greater than 8 hours", AdfException.ERROR);
          }
    }
}

As you can see, the trick really is to know about the iterator that servers the values for the listView component. If te underlying data control is POJO based, then accessing the base iterator (shown in the code above) and calling getDataProvider() gives you access to the entity bean that represents the row. This is where you perform the update.

But this is not the whole 9 years we need to go to get the use case working. After updating the entity you need to refresh the collection the entity is located in. For this, a method has been exposed on the POJO DC (explained earlier) to trigger the refresh of the collection by invoking the provider change event for the "assignments" property.

DataControl dc = AdfmfJavaUtilities.getDataControlById(
                   iterator.getDataControl().getName());

TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
             
//refresh UI
dcImpl.refreshAssignmentsList();

Btw.: If you don't like the end user notification via an ADFException, which I agree is not a good user experience, you can

i) define a managed bean and an error message component / area in the amx. The managed bean is updated from the validator and thanks to property change listeners would update the error message instantly.

Or, ii) you use a custom popup as explained here https://blogs.oracle.com/mobile/entry/how_to_open_and_close

----

QUIZ YOURSELF: There is a design flaw in the code above! Can you spot it? No? Then read on.

----

A Problem you may run into with this ...

The managed bean code so far assumes that the input data is validated while the row currency doesn't change. This however may happen if validation is in a value change listener and thus - for the use case I describe - can be considered a design flow in my sample.

So lets fine tune the sample code to ensure it works even if the row currency canges to avoid updating the wrong row. To avoid the problem, you add a set property listener to the list item in the view to memorize the selected row in the managed bean. This way, the validation code can explicitly set the current row in the model to the row that the changes should be applied to.

Step 1 Add a PropertlyListener to the list item

    <amx:listItem ...
        <amx:setPropertyListener id="spl1" 
                                 from="#{row.rowKey}" 
                                 to="#{viewScope.RepairAssignmentsBacking.rowKey}"
                                 type="action"/>
      </amx:listItem

The row key of a list item is set to a property defined in the managed bean that also performs the validation. Because the bean is in view scope, the property value becomes accessible to the validation code

Step 2 Set the current row

if(newVal>8 || newVal<0){
           
      BasicIterator iterator = (BasicIterator) AdfmfJavaUtilities.getELValue(
                                   "#{bindings.assignmentsIterator.iterator}");    
            
      //in case of row currency change, this is the key of the newly selected row
      Object currentRowKey = iterator.getCurrentRowKey();
      //set the current row back to the row that the values should be reset for
      iterator.setCurrentRowWithKey((String) this.rowKey);
      ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
             
      sa.setRepairHours(oldVal);
           
      //now set the current row back to the row the user navigated to (if at all)
      iterator.setCurrentRowWithKey((String) currentRowKey);            
           
      DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                      iterator.getDataControl().getName());

      TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
           
      //refresh UI
      dcImpl.refreshAssignmentsList();
                          
     throw new AdfException("The working time cannot be greater than 8 hours"
                            , AdfException.ERROR);
 }


The Complete Managed Bean Code (with the change)

The corrected managed bean code shown below highlights the changes in bold. 

 public class RepairAssignmentsBacking {
    
    private Object rowKey = null;
    private PropertyChangeSupport propertyChangeSupport = 
                                          new PropertyChangeSupport(this);

    public RepairAssignmentsBacking() {
    }

    public void onRepairHoursChange(ValueChangeEvent valueChangeEvent) {
        
          Integer newVal = new Integer((String) valueChangeEvent.getNewValue());
          Integer oldVal = new Integer((String) valueChangeEvent.getOldValue());
        
          
          //validation condition
                    if(newVal>8 || newVal<0){
              
             BasicIterator iterator = 
            (BasicIterator) AdfmfJavaUtilities.getELValue(
                             "#{bindings.assignmentsIterator.iterator}");    
             
             //in case of row currency change, this is the key of the newly selected row
             Object currentRowKey = iterator.getCurrentRowKey();
             //set the current row back to the row that the values should be reset for
             iterator.setCurrentRowWithKey((String) this.rowKey);
             ServiceAssignment sa = (ServiceAssignment) iterator.getDataProvider();
             
             sa.setRepairHours(oldVal);
             
             //now set the current row back to the row the user navigated to (if at all)
             iterator.setCurrentRowWithKey((String) currentRowKey);            
             
             DataControl dc = AdfmfJavaUtilities.getDataControlById(
                                                    iterator.getDataControl().getName());

             TechnicianAssignments dcImpl = (TechnicianAssignments)dc.getDataProvider();
             
             //refresh UI
              dcImpl.refreshAssignmentsList();
                            
              throw new AdfException("The working time cannot be greater than 8 hours", 
                                     AdfException.ERROR);
          }
    }


    public void setRowKey(Object rowKey) {
        Object oldRowKey = this.rowKey;
        this.rowKey = rowKey;
        propertyChangeSupport.firePropertyChange("rowKey", oldRowKey, rowKey);
    }

    public Object getRowKey() {
        return rowKey;
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propertyChangeSupport.removePropertyChangeListener(l);
    }
}

About

This blog is is dedicated to announcements,tips and tricks and other items related to developing, integrating, securing, and managing mobile applications using Oracle's Mobile Platform. It is created and maintained by the Oracle Mobile product development team.

Archive of past entries

Even More Mobile Development Blogs

Oracle A-Team Site - Mobile Related Entries

Code samples from the Community

Fusion Middleware Blogs

Search

Archives
« February 2016
SunMonTueWedThuFriSat
 
2
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
     
       
Today