Alloy - DSG - Università degli Studi di Parma

Università Degli Studi di Parma
Distributed Systems Group
Cross-platform Programming
Titanium Alloy Tutorial
Alessandro Grazioli
http://dsg.ce.unipr.it/?q=node/37
http://dsg.ce.unipr.it/
Alessandro Grazioli
alessandro.grazioli81@gmail.com
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Create a new project
-
Create a new project by selecting File → New → Project
Select Alloy on the left section of the wizard and choose Mobile App Project → Default Alloy Project
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Create a new project
-
We’ll add 2 tabs to our app
‣
Create files tabOneView.xml and tabTwoView.xml for the tab views in app/views
<Alloy>
<Tab id='first_tab' title='Tab 1' icon="KS_nav_views.png">
<Window title='Tab view one' class='container'>
<Label>I am Window 1</Label>
<Button id='open_button'>Open Child Window</Button>
</Window>
</Tab>
</Alloy>
tabOneView.xml code
<Alloy>
<Tab id='second_tab' title='Tab 2' icon="KS_nav_ui.png">
<Window title='Tab view two' class='container'>
<TableView id='contactsTable'></TableView>
</Window>
</Tab>
</Alloy>
tabTwoView.xml code
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Defining a Tab
-
Each element of the view requires an id so that the controller can access it
Images are taken from app/images directory
The view includes a Tab with a nested Window including a button
We’ll now define the first tab’s child window
‣
Create file tabOneViewChild.xml in app/views
<Alloy>
<Window id="first_tab_child_window" title='Tab view one child' class='container'>
</Window>
</Alloy>
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Setting the style
-
The style properties for a view have to be specified in a file having the same name of the view file and .tss extension (e.g., the
style for index.xml view must be specified in index.tss file)
-
Style files have to be placed in app/styles directory
You can also define a file named app.tss to include all the styles that have to be applied to every element in the app
‣
Create such a file and add the following code
".container": {
backgroundColor:"white"
}
-
Now, every element having class container will have a white background, regardless of the file where it has been defined
If you want to specify the position of tabOneView’s button, you can create tabOneView.tss file and set its layout as:
"#open_button": {
position: 'absolute',
top: '20px'
}
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Defining a TabGroup
-
Now that the tab views are ready, we’ll build the tab group to contain them in index.xml
To specify the views to be included, use the Require element
<Alloy>
<TabGroup>
<Require src="tabOneView" />
<Require src="tabTwoView" />
</TabGroup>
</Alloy>
-
The tabs code can of course be included directly in index.xml file, but the use of Require elements
makes the code more modular since the functionality for each component is separated into a
specific controller file
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Defining controllers
-
Now you can implement the controllers in app/controllers
index.js is the controller for the view defined in index.html
You can create the controller for tabOneView view in file tabOneView.js and add a function executed when
the user presses the button
$.open_button.addEventListener('click', function(e) {
var tabViewOneChildController = Alloy.createController('tabOneViewChild');
tabViewOneChildController.openMainWindow($.first_tab);
});
The code refers to a function defined in
tabOneViewChild.js which takes a Tab
as param and opens a window in it.
Such a function is exported with
openMainWindow name
-
The event listener $.open_button is called when the user presses the button having id open_button (the $.
notation allows you to access elements by id)
-
Alloy.createController(‘ID’) returns the controller for the view whose id is passed as a parameter
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Defining controllers
-
We’ll now define openMainWindow method
‣
Create app/controllers/tabOneViewChild.js file and add the following content
function openMainWindow(tab){
tab.open($.first_tab_child_window);
}
exports.openMainWindow = openMainWindow;
‣
The code defines a function which takes a Tab as parameter and opens a window in it (the type
Tab is required to be able to call method open for the parameter)
‣
The function is exported with name openMainWindow
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Adding a map
-
We’ll now add a map and a text field to look for locations to tabOneViewChild by editing tabOneView.xml as follows
<Alloy>
<Window id="first_tab_child_window" title='Tab view one child' class='container'>
<Require src="addressField" id="addressField" />
<Require src="map" id="map" />
</Window>
</Alloy>
-
-
The code requires two additional views to:
‣
display a map (map.xml)
‣
search for an address and center the map on it (addressField.xml)
In the following we define the required views
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Specifying the map module
-
The map is not part of Titanium, so we need to use a proper module named Ti.Map (we will add it to the project in 4 slides)
Create file app/views/map.xml and add the following code
<Alloy>
<View id="map" ns="Ti.Map" >
<Require src="annotation" title="Annotation" />
</View>
</Alloy>
-
All UI components specified in the views are
prefixed with Titanium.UI for convenience.
However, to use a component not part of the
Titanium.UI namespace, you need to use
the ns attribute – Ti.Map will be used to
interact with the map
The map view requires a view, named annotation, which represents a labeled point of interest (POI) the user can click
‣
Create file app/views/annotation.xml and add the following code
<Alloy>
<Annotation id="annotation" />
</Alloy>
-
Create app/views/addressField.xml file and add the following code
<Alloy>
<View class="addressField">
<TextField id="textField" hintText="Enter an address" />
<Button id="searchButton" title="Search" />
</View>
</Alloy>
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Setting map’s views style
-
We’ll now define the style for the required views
Create file app/styles/map.tss and add the following code
"#map" : {
mapType : 'Ti.Map.STANDARD_TYPE',
top : '50dp',
animate : true,
regionFit : true,
userLocation : true,
region : {
latitude : Alloy.Globals.LATITUDE_BASE,
longitude : Alloy.Globals.LONGITUDE_BASE,
latitudeDelta : 0.1,
longitudeDelta : 0.1
}
}
-
Map’s properties are
described in 2 slides
Create file app/styles/annotation.tss and add the following code
"Annotation" : {
animate : true,
pincolor : Titanium.Map.ANNOTATION_RED
}
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Setting map’s views style
-
Create file app/styles/addressField.tss and add the following code
"TextField" : {
height : '40dp',
top : '5dp',
left : '5dp',
right : '150dp',
style : Ti.UI.INPUT_BORDERSTYLE_ROUNDED,
backgroundColor : '#fff',
paddingLeft : '5dp'
}
"Button" : {
font : {
fontSize : '20dp',
fontWeight : 'bold'
},
top : '5dp',
height : '40dp',
width : '150dp',
right : '5dp'
}
".addressField" : {
backgroundColor : '#E0E0E0',
height : '50dp',
top : 0
}
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Map attributes
Map attributes
-
mapType indicates what type of map should be displayed (possible values are: Ti.Map.STANDARD_TYPE,
Ti.Map.SATELLITE_TYPE and Ti.Map.HYBRID_TYPE)
-
animate is a boolean that indicates whether or not map actions, like opening and adding annotations, should be
animated
-
regionFit is a boolean that indicates if the map should attempt to fit the region (MapView) in the visible view
userLocation is a boolean that indicates if the map should show the user's current device location as a pin on the map
region is an object that contains the 4 properties defining the visible area of the MapView
‣
latitude and longitude represent the center of the map and are set based on the two variables defined in alloy.js file
‣
The same latitude and longitude of a region can be represented with a different level of zoom via the latitudeDelta and
longitudeDelta properties (they respectively represent the latitude north and south, and the longitude east and west,
from the center of the map that will be visible) - the smaller the delta values, the closer the zoom on the map
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Adding the map module to the project
-
To display a map, it is necessary to add the ti.map
module
-
Open tiapp.xml and click on + on the modules side
Choose ti.map module and add it
To use the module, you need to specify it by adding
to alloy.js file the following code
Alloy.Globals.LATITUDE_BASE = 44.765;
Alloy.Globals.LONGITUDE_BASE = 10.3;
if (OS_IOS || OS_ANDROID) {
Ti.Map = require('ti.map');
}
-
The first ad second line define two variables that will represent the center of the map
The if block specifies that Ti.Map calls refer to ti.map module (the if condition is due to the fact that the map module
is just available for iOS and Android platforms) – the ns attribute of map.xml file refers to this
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Geocoding
-
To geocode the addresses provided by the user, you can use a script provided by Appcelerator in the
Alloy Samples page (https://github.com/appcelerator/alloy/tree/master/samples/mapping/lib), namely
geo.js, and store it in app/lib
-
Define the controllers for the new views
‣
Create file app/controllers/addressField.js and add the following code
var geo = require('geo');
$.searchButton.addEventListener('click', function(e) {
$.textField.blur();
geo.forwardGeocode($.textField.value, function(geodata) {
$.trigger('addAnnotation', {geodata: geodata});
});
});
searchButton click event listener executes a function called
forwardGeocode from geo.js which computes the latitude and longitude
corresponding to the address the user provided; its second parameter is a
callback to be executed upon correct coordinates retrieval.
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Adding map controller
‣
Create file app/controllers/map.js and add the following code
var annotations = new Array();
exports.addAnnotation = function(geodata) {
var annotation = Alloy.createController('annotation', {
title : geodata.title,
latitude : geodata.coords.latitude,
longitude : geodata.coords.longitude
});
annotations.push(annotation);
$.map.addAnnotation(annotation.getView());
};
$.map.setLocation({
latitude : geodata.coords.latitude,
longitude : geodata.coords.longitude,
latitudeDelta : 1,
longitudeDelta : 1
});
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Editing controllers
-
Add a function to tabOneViewChild controller so that when the user presses searchButton, and its callback executes,
function addAnnotation defined in map controller is called ($.map.addAnnotation)
$.addressField.on('addAnnotation', function(e) {
$.map.addAnnotation(e.geodata);
});
-
searchButton callback calls forwardGeocode function defined in geo.js which takes, as second parameter, a
callback triggering function addAnnotation - since such a function is not defined in tabOneViewChild controller, the
code we add uses the on method to call addAnnotation function defined in map.js
-
addAnnotation function adds a pin in the location searched by the user – to do so, you need to define the Annotation
controller
The OR means that each variable can assume the value passed
as a parameter when the controller is created, or a default value
var args = arguments[0] || {};
$.annotation.title = args.title || '';
$.annotation.latitude = args.latitude || Alloy.Globals.LATITUDE_BASE;
$.annotation.longitude = args.longitude || Alloy.Globals.LONGITUDE_BASE;
-
The controller for a new annotation is created by addAnnotation method in map.js (see previous slide)
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Events management
-
Events can be used to provide interactions between
different controllers
-
Ti.App.addEventListener("app:clickedAnnotation", function(evt) {
var indexOfClickedElement;
for(var i = 0; i < annotations.length; i++) {
var currentAnnotationTitle = annotations[i].getView().title;
if(currentAnnotationTitle == evt.title) {
// The index of the clicked annotation in the array
indexOfClickedElement = i;
var removePinAlert = Titanium.UI.createAlertDialog({
message: 'Remove pin?',
buttonNames: ['Confirm', 'Cancel']
});
removePinAlert.addEventListener('click', function(e) {
// Clicked cancel, first check is for iphone, second for android
if (e.cancel === e.index || e.cancel === true) { return; }
switch (e.index) {
case 0: {
annotations.splice(indexOfClickedElement, 1);
$.map.removeAnnotation(evt.title);
}
break;
We can modify map.js so that, when a user clicks on a pin
on the map, such a pin is removed upon confirm
‣
Add an array for the annotations in map.js
‣
Each time an annotation is added to the map from
function addAnnotation in map.js, also add it to the
array
‣
Add an event listener for the clicked pin and ask the
user if he/she wants to remove the pin
‣
case 1: {
removePinAlert.hide();
}
break;
We also have to modify annotation controller to fire an
event when the user clicks the annotation itself
$.annotation.addEventListener('click',
function(e) {
Ti.App.fireEvent("app:clickedAnnotation", {
title : e.source.title
});
});
default:
break;
}
});
removePinAlert.show();
break; // exit from for cycle
}
}
});
Alessandro Grazioli
2015 - Parma
Università Degli Studi di Parma
Contacts management
-
Alloy can access native APIs such as the phone contacts
We can display in tabTwoView a table listing all contacts
Also, we can add a listener to the table so that, when the user
clicks a row, an alert displaying the information of the contacts is
presented
$.contactsTable.addEventListener("click", function(e) {
var contactDetails = e.source.title + "\n";
for(var temp in e.source.phone) {
var temp_numbers = e.source.phone[temp];
for(var k=0;k<temp_numbers.length; k++) {
var temp_num = temp_numbers[k];
temp_num = temp_num.replace(/[^\d.]/g, "");
contactDetails += temp_num + "\n";
}
}
alert("Clicked " + contactDetails);
});
Distributed Systems Group
var addressBookDisallowed = function() {
alert("Cannot access address book");
};
if (Ti.Contacts.contactsAuthorization ==
Ti.Contacts.AUTHORIZATION_AUTHORIZED) {
renderContacts();
} else if (Ti.Contacts.contactsAuthorization ==
Ti.Contacts.AUTHORIZATION_UNKNOWN) {
Ti.Contacts.requestAuthorization(function(e) {
if (e.success) {
renderContacts();
} else {
addressBookDisallowed();
}
});
} else { addressBookDisallowed(); }
var data = [];
function renderContacts() {
var contacts = Ti.Contacts.getAllPeople();
data = [];
for (var i = 0; i < contacts.length; i++) {
var title = contacts[i].fullName;
var phone = contacts[i].phone;
if (!title || title.length === 0) {
title = "(no name)";
}
data.push({
title : title,
phone : phone
});
}
Alessandro Grazioli
}
$.contactsTable.setData(data);
2015 - Parma
Università Degli Studi di Parma
Distributed Systems Group
Final result
Alessandro Grazioli
2015 - Parma