Università
Degli
Studi di Parma
2015 - Parma Alessandro Grazioli
Distributed Systems Group
Cross-platform Programming
Titanium Alloy Tutorial
http://dsg.ce.unipr.it/
Alessandro Grazioli
http://dsg.ce.unipr.it/?q=node/37 [email protected]
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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 ProjectDistributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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>
<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>
Create a new project
tabOneView.xml code
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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>
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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'
}
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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 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
Defining controllers
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
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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 viewsDistributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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>
-
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>
Specifying the map module
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
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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 } }
-
Create file app/styles/annotation.tss and add the following code
"Annotation" : {
animate : true,
pincolor : Titanium.Map.ANNOTATION_RED }
Setting map’s views style
Map’s properties are
described in
2
slides
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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 }
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
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 andlongitudeDelta 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
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
To display a map, it is necessary to add the ti.mapmodule
-
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 addingto 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 thisDistributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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})
;
})
;
})
;
Geocoding
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.
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
‣
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 }); };
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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, acallback 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 Annotationcontroller
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)Editing controllers
The OR means that each variable can assume the value passed as a parameter when the controller is created, or a default value
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
Events can be used to provide interactions between different controllers-
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 fromfunction 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‣
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 }); });
Events management
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; case 1: { removePinAlert.hide(); } break; default: break; } }); removePinAlert.show(); break; // exit from for cycle
} }
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma
-
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); });
Contacts management
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 }); } $.contactsTable.setData(data); }
Distributed Systems Group
Alessandro Grazioli 2015 - Parma
Università Degli Studi di Parma