Starter Step

Archive for the ‘Mobile’ Category

Another UI component we found missing in Titanium Mobile was a TabStrip, like the one you would find in Facebook’s Three20 project for the iPhone where you can have as many tabs as you like and have them fit nicely on the top of the screen because they are scrollable.

After spending some time we came up with a solution that we believe works really well. It takes in any number of labels and will dynamically size the horizontal scroll width and provide arrow indicators that more tabs exist in either direction. Just pass in an onselect function and you can get the tab index for a selected tab – with this you can change the data on the screen. The code below uses the days of the week as an example and will swap out the table view’s data based on the tab selected.

function createTabStrip(options) {
 options = options || {};
 params = {
   labels: options.labels || [],
   onselect: options.onselect || null,
   top: options.top || .1,
   height: options.height || 40,
   backgroundColor: options.backgroundColor || '#000',
   gradientColor: options.gradientColor || '#444',
   selectedColor: options.selectedColor || '#fff',
   unselectedColor: options.unselectedColor || '#999',
   fontSize: options.fontSize || 14
 }

 var labelViews = [];
 var lastSelectedLabel = null;
 var totalWidth = 0;

 var containerView = Titanium.UI.createView({
   top:params.top,
   height:params.height,
   width:320,
   backgroundColor:params.backgroundColor,
   backgroundGradient: {
     type:'linear',
     colors:[
       {color:params.gradientColor,position:0.0},
       {color:params.backgroundColor,position:0.50},
       {color:params.gradientColor,position:1.0}
     ]
   }
 });

 var leftArrow = Ti.UI.createLabel({
   text:String.fromCharCode(171),
   font:{fontSize:params.height / 2,fontWeight:'bold'},
   color:params.selectedColor,
   height:params.height,
   width:15,
   top:params.top,
   left:2,
   textAlign:'left',
   visible:false
 });
 containerView.add(leftArrow);

 var scrollView = Titanium.UI.createScrollView({
   layout:'horizontal',
   top:params.top,
   left:17,
   height:params.height,
   width:286
 });
 containerView.add(scrollView);

 var rightArrow = Ti.UI.createLabel({
   text:String.fromCharCode(187),
   font:{fontSize:params.height / 2,fontWeight:'bold'},
   color:params.selectedColor,
   height:params.height,
   width:15,
   top:params.top,
   right:2,
   textAlign:'right',
   visible:false
 });
 containerView.add(rightArrow);

 scrollView.addEventListener('scroll', function(e) {
   leftArrow.visible = e.x > 5;
   rightArrow.visible = e.x < scrollView.contentWidth - scrollView.width;
 });

 containerView.labels = function(labels) {
   params.labels = labels;
   resetLabels();
 }

 containerView.selectTab = function(index) {
   select(labelViews[index]);
 }

 function resetLabels() {
   totalWidth = 0;
   labelViews = [];
   var oldLabels = scrollView.children;
   if (oldLabels) {
     for (var i = 0,count = oldLabels.length; i < count; i++) {
       scrollView.remove(oldLabels[i]);
     }
   }

   var labels = params.labels;
   for (var i = 0,count = labels.length; i < count; i++) {
      var button = createButton(labels[i], i);
      scrollView.add(button);
   }
   scrollView.contentWidth = totalWidth;
   rightArrow.visible = totalWidth > scrollView.width;
 }

 function createButton(title, index) {
   var buttonView = Ti.UI.createView({
     top:params.top,
     height:params.height
   });

   var label = Ti.UI.createLabel({
     text:title,
     font:{fontSize:params.fontSize,fontStyle:'bold'},
     width:'auto',
     textAlign:'center',
     height:params.height,
     touchEnabled:false
   });
   label.index = index;
   labelViews.push(label);
   buttonView.add(label);
   showAsUnSlected(label);
   buttonView.addEventListener('click', function(e) {
     select(e.source.children[0]);
   });
   buttonView.width = label.size.width + 20;
   totalWidth += buttonView.width;

   if (index == 0) {
     showAsSelected(label);
   }
   return buttonView;
 }

 function select(label) {
   if (lastSelectedLabel) {
     showAsUnSlected(lastSelectedLabel);
   }
   showAsSelected(label)
   if (params.onselect) {
     params.onselect(label.index);
   }
 }

 function showAsSelected(label) {
   label.color = params.selectedColor;
   label.getParent().borderWidth = 1;
   lastSelectedLabel = label;
 }

 function showAsUnSlected(label) {
   label.color = params.unselectedColor;
   label.getParent().borderWidth = 0;
 }

 resetLabels();
 return containerView;
}


var tableData = [
 [{title:'Monday Data'}],
 [{title:'Tuesday Data'}],
 [{title:'Wednesday Data'}],
 [{title:'Thursday Data'}],
 [{title:'Friday Data'}],
 [{title:'Saturday Data'}],
 [{title:'Sunday Data'}]
];

var window = Titanium.UI.createWindow({
 fullscreen:false,
 backgroundColor:'#fff',
 title:'TabStrip'
});

var tabStripView = createTabStrip({
 labels:['Monday','Tuesday','Wednesday','Thursday','Friday','Saturday','Sunday'],
 onselect: function(index) {
    //This is so you don't get flickering
    tableView.hide();
    tableView.setData(tableData[index]);
    setTimeout(function() {
        tableView.show();
    }, 300);
 }
});

window.add(tabStripView);

var tableView = Ti.UI.createTableView({
 top:tabStripView.height
});
window.add(tableView);

var navGroup = Ti.UI.iPhone.createNavigationGroup({
 window:window
});

var main = Ti.UI.createWindow();
main.add(navGroup);
main.open();

tabStripView.selectTab(0);

We have started looking at Appcelerator’s Titanium Mobile SDK for developing iPhone and Android apps using Javascript…unfortunately they still have a ways to go with the promise of being cross-platform in supporting iPhone and Android with a single code base.  However if you are just targeting iPhone, then you must take a look at the latest Titanium 1.6 SDK.

We did however find a vital UI component missing from Titanium – the ability to view remote images (thumbnails) in a grid-like fashion with built in paging.  We knew that this would be a showstopper for us so I took a stab at building my own by wiring together pieces of the current Titanium UI components.  To try it out just follow the getting started instructions on Appcelerator’s site and then copy and paste the code below into the app.js file that is generated for you.  Please note that this only works on a Mac since it’s targeting the iPhone.  And it’s not the prettiest code and could use some refactoring before going prime-time.

The thumbnail viewer requires a plixi user id.  It uses this id to call the plixi api and retrieve a user’s uploaded photos for viewing as thumbnails.  The example below uses Arsenio Hall’s plixi id 🙂

function createThumbnailViewer(props) {

 var that = {};
 var xhr = Titanium.Network.createHTTPClient();

 var photosWindow = Titanium.UI.createWindow({
   fullscreen:false,
   backgroundColor:'#fff',
   title:props.title
 });
 that.window = photosWindow;

 var refreshButton = Ti.UI.createButton({
   title:'Refresh',
   width:40,
   height:30
 });
 var photosContainer = Titanium.UI.createView();

 var imageUrlLists = [];
 var index = -1;
 var total = 0;

 var heightSpacer = 3.5;
 var widthSpacer = 8;
 var topOffset = heightSpacer;
 var imageDim = 70;
 var pageSize = 20;
 for (var i = 0,count = pageSize; i < count; i += 4) {
   var leftOffset = widthSpacer;
   for (var j = 0; j < 4; j++) {
     var photoView = Ti.UI.createImageView({
       top:topOffset,
       left:leftOffset,
       height:imageDim,
       width:imageDim
     });
     photosContainer.add(photoView);
     leftOffset += imageDim + widthSpacer;
   }
   topOffset += imageDim + heightSpacer;
 }

 photosWindow.add(photosContainer);

 var flexSpace = Titanium.UI.createButton({
   systemButton:Titanium.UI.iPhone.SystemButton.FLEXIBLE_SPACE
 });

 var previousButton = Ti.UI.createButton({
   title:'Prev',
   width:50,
   height:10
 });
 previousButton.addEventListener('click', function() {
   previous();
 });
 var nextButton = Ti.UI.createButton({
   title:'Next',
   width:50,
   height:10
 });
 nextButton.addEventListener('click', function() {
   next();
 });
 var nativespinner = Titanium.UI.createButton({
   systemButton:Titanium.UI.iPhone.SystemButton.SPINNER
 });

 var labelContainer = Ti.UI.createView({width:100,height:20});

 var label = Ti.UI.createLabel({
 color:'#fff'
 });

 labelContainer.add(label);

 photosWindow.toolbar = [previousButton, flexSpace, labelContainer, flexSpace, nextButton];

 refreshButton.addEventListener('click', function() {
   that.start();
 });

 that.start = function() {
   index = -1;
   imageUrlLists = [];
   total = 0;
   next();
 }

 photosWindow.rightNavButton = refreshButton;

 function next() {
   index++;
   if (index >= imageUrlLists.length) {
     load();
   } else {
     applyImageUrls();
   }
 }

 function previous() {
   index--;
   if (index >= 0) {
     applyImageUrls();
   }
 }

 function applyImageUrls() {
   var imageUrlList = imageUrlLists[index];
   label.text = (index * pageSize + imageUrlList.length) + " of " + total;
   var photos = photosContainer.children;
   var i = 0,count = imageUrlList.length;
   while (i < count) {
     photos[i].image = imageUrlList[i];
     i++;
   }
   if (count < pageSize) {
     while (i < pageSize) {
       photos[i].image = null;
       i++;
     }
   }
   previousButton.enabled = index > 0;
   nextButton.enabled = imageUrlLists.length * pageSize <= total;
 }

 function load() {
   xhr.onload = function() {
     var json = JSON.parse(this.responseText);
     var list = json.List;
     total = json.CollectionCount;
     var imageUrlList = [];
     var i = 0, count = list.length;
     while (i < count) {
       imageUrlList[i] = list[i].ThumbnailUrl;
       i++;
     }
     imageUrlLists[index] = imageUrlList;
     photosWindow.toolbar = [previousButton, flexSpace, labelContainer, flexSpace, nextButton];
     applyImageUrls();
   };
   previousButton.enabled = false;
   nextButton.enabled = false;
   photosWindow.toolbar = [previousButton, flexSpace, nativespinner, flexSpace, nextButton];
   var url = "http://api.plixi.com/api/tpapi.svc/json/users/"+props.plixiUserId+"/photos?getcount=true&ps=" + pageSize + "&ind=" + (index * pageSize);
   Ti.API.info("Calling url  = " + url);
   xhr.open("GET", url);
   xhr.send();
 }
 return that;
}

var thumbnailViewer = createThumbnailViewer({
 plixiUserId:'6236650',
 title:'Aresenio Hall'
});

var navGroup = Ti.UI.iPhone.createNavigationGroup({
 window:thumbnailViewer.window
});

var main = Ti.UI.createWindow();
main.add(navGroup);
main.open();

thumbnailViewer.start();

We recently released our popular mobile travel application TripCase on Android, and though we liked developing with the Android framework more than iphone, blackberry, and winmo, we ran into some cumbersome concepts like the Android ActivityTripCase has many forms, work-flows, viewable and editable data, interactions with the device, and makes many calls to our Rails back-end server.  So in order to streamline our development efforts on Android we created a small micro-framework that we are now calling DynaDroid and would like to open it up to the Android community for use.

Here are some of the features you get with DynaDroid:

And here is what is coming:

And it’s very easy to get started…just checkout the DynaDroid source from the subversion repository found here.  The source is setup as an example Android application so all you have to do is import it into Eclipse or IntelliJ and just run it.

We would really like your feedback on the framework and if you have any questions or issues please visit the DynaDroid Google Group.

Enjoy!

Tags: , ,


  • Dave: I can tell you're a ruby guy because you forgot the 'return' keyword. Thanks for the tip though!
  • Chandrashekhar H M: Hi, Thanks its working fine in iOS 6 but not in iOS 7.0. Any Suggestion on this.
  • Coeur: To change a rootViewController, without all this TVNavigationController : myNewRoot = [[UIViewController alloc] init]; myNavigationController.view