Orientation Aware Apps in Sencha Touch Part 2 of 2

   Sencha

In part one of this series, Tim Eagan introduced us to the challenge of developing apps which can support the changing of device orientation. While the techniques outlined in part one will account for the majority of our requirements, it’s important to understand our options when things get complicated. In this last part of this series, we’ll walk through a scripting approach to handling orientation change in your Sencha Touch apps.

Complexity happens quickly

As Sencha Touch developers, we know how much of a challenge it is to properly handle orientation change in our apps. However with any challenge comes great opportunity and orientation change is no different. Armed with the knowledge that we have multiple screen resolutions to account for coupled with two different orientations, the user experience has a chance for dramatic customization. For example take the idea of developing a list/detail type view to a mini-tablet. In a card layout this will look fine in portrait but in landscape it might be a little vertically ‘short’. A better way would be to present this type of view as an hbox in landscape.

Can profiles help?

Up to this point we’ve talked about delivering custom experiences based on screen resolution (orientation change). While this sounds like it fits well with Sencha Touch profiles, you’re right, but it’s important to understand some key differences.

Sencha Touch profiles are used to deliver custom code to a group of devices or even specific device. The choice of what to load can be based on anything but typically it would be device type (phone, tablet, mini-tablet). With profiles, this decision has to be made at launch. This makes profiles a static solution when we’re trying to solve the dynamic problem of orientation change.

Dynamically loading new experiences

While Sencha profiles aren’t able to help us much with orientation change, we still have some techniques at our disposal. Let’s implement the list/detail example mentioned earlier.

We essentially want to swap out a card layout for an hbox layout. Knowing that styles can’t handle this and dynamically assigning new layouts at runtime is not supported, we need to come up with something else.

Let’s start by reviewing the code we’ll use to represent the portrait and landscape experiences. The portrait experience will be a card layout using an Ext.navigation.View http://docs.sencha.com/touch/2.3.1/#!/api/Ext.navigation.View container as shown in Figure 1 below.

Ext.define('MyApp.view.portrait.MasterDetail', {
    extend : 'Ext.navigation.View',
    xtype  : 'portraitmasterdetail',

    requires : [
        'Ext.dataview.List'
    ],

    config : {
        items  : [
            {
                xtype   : 'list',
                title   : 'Master',
                itemId  : 'master',
                itemTpl : '{title}'
            }
        ],

        store  : undefined
    },

    initialize : function() {
        var me = this,
            master;

        me.callParent();

        me.down('#master').on({
            select : me.onMasterSelect,
            scope  : me
        });
    },

    onMasterSelect : function(list, record) {
        this.push({
            xtype            : 'component',
            itemId           : 'detail',
            title            : 'Detail',
            styleHtmlContent : true,
            data             : record.getData(),
            tpl              : ''.concat(
                '<h1>{title}</h1>',
                '{detail}'
            )
        });
    },

    applyStore : function(value) {
        var me     = this,
            master = me.down('#master');

        master &amp;&amp; master.setStore(value);

        return value;
    }
});

Figure 1 – Portrait view

This view has some features baked in such as supporting the configuration of a store on the navigation view itself. When the store is defined on this container it will be applied to the list component which renders as the initial view. When a list item is tapped, the detail view is pushed on top of the card deck.

The landscape experience will be a container with an HBox http://docs.sencha.com/touch/2.3.1/#!/api/Ext.layout.HBox layout as shown in Figure 2 below.

Ext.define('MyApp.view.landscape.MasterDetail', {
    extend : 'Ext.Container',
    xtype  : 'landscapemasterdetail',

    config : {
        layout : 'hbox',
        cls    : 'landscape-master-detail',
        items  : [
            {
                xtype   : 'list',
                flex    : 1,
                itemId  : 'master',
                store   : 'mystore',
                itemTpl : '{title}'
            },

            {
                xtype            : 'component',
                itemId           : 'detail',
                styleHtmlContent : true,
                flex             : 2,
                tpl              : ''.concat(
                    '<h1>{title}</h1>',
                    '{detail}'
                )
            }
        ],

        store : undefined
    },

    initialize : function() {
        var me = this,
            master;

        me.callParent();

        me.down('#master').on({
            select : me.onMasterSelect,
            scope  : me
        });
    },

    onMasterSelect : function(list, record) {
        this.down('#detail').setData(record.getData());
    },

    applyStore : function(value) {
        var me     = this,
            master = me.down('#master');

        master &amp;&amp; master.setStore(value);

        return value;
    }
});

Figure 2 – Landscape view

Notice some traits the portrait and landscape code have in common. First, the master and detail sub-views have an itemId http://docs.sencha.com/touch/2.3.1/#!/api/Ext.Component-cfg-itemId of #master and #detail. Also each of the main containers support the definition of a store. These are two important traits will be key to leveraging our orientation container.

To complete the story, the code we’ll use to define the store is shown in Figure 3 below.

Ext.define('MyApp.store.MyStore', {
    extend : 'Ext.data.Store',

    config : {
        storeId : 'mystore',
        data    : [
            {
                id     : 1,
                title  : 'Item 1',
                detail : 'Item 1 details'
            },

            {
                id     : 2,
                title  : 'Item 2',
                detail : 'Item 2 details'
            },

            {
                id     : 3,
                title  : 'Item 3',
                detail : 'Item 3 details'
            }
        ]
    }
});

Figure 3 – The store used for both the portrait and landscape views

The store http://docs.sencha.com/touch/2.3.1/#!/api/Ext.data.Store is a simple in-memory store of data that is registered with the storeId of ‘mystore.’

Creating the orientation container

The custom container we’ll use to manage the swapping of views to support portrait and landscape orientations is simpler than you may think. The source code for the orientation container is shown in Figure 4 below.

Ext.define('MyApp.view.OrientationContainer', {
    extend : 'Ext.Container',
    xtype  : 'orientationcontainer',

    config : {
        layout : 'card',

        portrait  : undefined,
        landscape : undefined
    },

    initialize : function() {
        var me = this;

        Ext.Viewport.on({
            orientationchange : me.initView,
            scope             : me
        });

        me.initView();

        me.callParent();
    },

    initView : function() {
        var me          = this,
            orientation = Ext.Viewport.getOrientation(),
            viewConfig  = orientation === 'portrait' ? me.getPortrait() : me.getLandscape(),
            oldView     = me.getActiveItem(),
            newView;

        // add the new view to the container
        newView = me.add(viewConfig);

        // remove the old view
        me.remove(oldView);
    }
});

Figure 4 – Orientation container

The orientation container has two custom properties, ‘portrait’ and ‘landscape’ which are used to contain the views specific to their orientation. When this container is initialized, the orientationchange http://docs.sencha.com/touch/2.3.1/#!/api/Ext.Viewport-event-orientationchange event is handled by the ‘initView’ method. This method drives the whole process as it will add the new view based on the currently detected orientation, then remove the old view.

To implement this container we could use the the configuration shown in Figure 5 below.

{
    xtype     : 'orientationcontainer',
    title     : 'List',
    defaults  : {
        store : 'mystore'
    },
    portrait  : {
        xtype : 'portraitmasterdetail'
    },
    landscape : {
        xtype    : 'landscapemasterdetail'
    }
}

Figure 5 – Implementing the orientation container

The orientation container takes advantage of a key feature of Sencha Touch containers; the defaults http://docs.sencha.com/touch/2.3.1/#!/api/Ext.Container-cfg-defaults config option. This is used to apply the same store to the children of this container. So as views are swapped out based on orientation, they’re sure to use the same store.

This is great, but what about state?

Our orientation container is working just fine but we have a problem. As portrait and landscape views are rendered, we lose the state of the view. Each time the orientation is changed, the old view is destroyed and the new view is re-created.

To fix this, we’ll modify the orientation container as shown in figure 6 below.

Ext.define('MyApp.view.OrientationContainer', {
    extend : 'Ext.Container',
    xtype  : 'orientationcontainer',

    config : {
        layout : 'card',

        portrait  : undefined,
        landscape : undefined
    },

    initialize : function() {
        var me = this;

        Ext.Viewport.on({
            orientationchange : me.initView,
            scope             : me
        });

        me.initView();

        me.callParent();
    },

    initView : function() {
        var me          = this,
            orientation = Ext.Viewport.getOrientation(),
            viewConfig  = orientation === 'portrait' ? me.getPortrait() : me.getLandscape(),
            oldView     = me.getActiveItem(),
            newView;

        // add the new view to the container
        newView = me.add(viewConfig);

        // run the optional callback
        viewConfig.callback &amp;&amp; viewConfig.callback.call((viewConfig.scope || newView), newView, oldView);

        // remove the old view
        me.remove(oldView);
    }
});

Figure 6 – Adding support for a callback to the orientation container

We’ve added just one more line to the orientation container which adds support for running an optional callback function for a given view. This callback supports the assignment of an optional scope. If no scope is defined, the callback will execute under the scope of the new view.

We can use this to our advantage with our master/detail example, shown below in Figure 7.

{
    xtype     : 'orientationcontainer',
    title     : 'List',
    defaults  : {
        store : 'mystore'
    },
    portrait  : {
        xtype : 'portraitmasterdetail'
    },
    landscape : {
        xtype    : 'landscapemasterdetail',
        callback : function(newView, oldView) {
            var oldDetail = oldView &amp;&amp; oldView.down('#detail'),
                newList   = newView.down('#master'),
                selected;

            if(oldDetail) {
                selected = newList.getStore().findExact('id', oldDetail.getData().id);
            }
            else {
                selected = newList.getStore().first();
            }

            newList.select(selected);
        }
    }
}

Figure 7 – Implementing a callback

In the case where a detail view is selected in portrait view, when switched to landscape view, we want that selection to be preserved. Our callback does just that by first detecting whether the old view was showing its detail view at the time of orientation change. If so, then the landscape view will pre-select the entry and automatically show its version of the detail. Otherwise, the first selection is shown.

Moving forward

While this served as a valid solution to handling complex orientation change requirements, it’s not the only technique. More than likely the solutions you apply to your orientation change requirements will be diverse and should be evaluated on a case-by-case basis.


Like What You See?

Got any questions?