Taste of Android :: Part-9


Bhaskar S 07/06/2013


Android Application Development - VIII

Thus far we have built and demonstrated simple applications involving basic user interfaces leveraging the Android Activity component.

What we have not covered in these applications is the layout orientation change. . In a real device, one can hold the device vertical or horizontal changing the orientation of the application user interface accordingly.

How do we simulation the orientation change in the Virtual Android Device ? Once the Virtual Android Device is launched, pressing either CTRL+F11 or CTRL+F12 will change the orientation of the Virtual Android Device.

Screen orientation change will cause an Activity to be destroyed and recreated. This is because the screen orientation change might need to load alternative set of resources (such as the layout, images etc).

To offer a seemless user experience when the user changes the screen orientation in our Stock Ticker application, we need to persist the user interface state (stock prices) and restore it on orientation change. This is where we leverage the Android Activity callback method onSaveInstanceState() to persist user interface state. This method is called before the callback method onStop() and before the Activity is destroyed.

We will now demonstrate the seemless user experience of screen orientation change by creating a new Android application named DroidStockTicker3.

This application is a modified version of the application DroidStockTicker2 leveraging the callback methods onSaveInstanceState() , onStart(), and onStop().

Without further ado, lets get started. We will not go step-by-step to show the various screens since we already did that in Part-2 for the DroidTipCalculator application.

The contents of the dimens.xml file to look like the one shown in the listing 9.1 below:

Listing-9.1
<?xml version="1.0" encoding="utf-8"?>
<resources>

<dimen name="cell_width">0dp</dimen>
<dimen name="list_padding">5dp</dimen>

</resources>

The contents of the strings.xml file to look like the one shown in the listing 9.2 below:

Listing-9.2
<?xml version="1.0" encoding="utf-8"?>
<resources>

<!-- Text references -->

<string name="app_name">DroidStockTicker3</string>
<string name="action_settings">Settings</string>

<string name="tick_image">Tick Image</string>

<!-- Array references -->

<string-array name="stocks">
<item>AAPL</item>
<item>AMZN</item>
<item>CSCO</item>
<item>GOOG</item>
<item>IBM</item>
<item>INTC</item>
<item>LNKD</item>
<item>MSFT</item>
<item>NFLX</item>
<item>ORCL</item>
</string-array>

<string-array name="opens">
<item>450</item>
<item>267</item>
<item>24</item>
<item>871</item>
<item>209</item>
<item>24</item>
<item>169</item>
<item>35</item>
<item>223</item>
<item>34</item>
</string-array>

</resources>

The contents of the main activity layout in file activity_stock_ticker.xml will look like the one shown in the listing 9.3 below:

Listing-9.3
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".StockTickerActivity" >

<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

</LinearLayout>

The contents of each list item layout in file list_item_row.xml will look like the one shown in the listing 9.4 below:

Listing-9.4
<TableLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >

<TableRow>
<TextView
android:id="@+id/symbol"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<TextView
android:id="@+id/price"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<TextView
android:id="@+id/change"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
android:text=""
/>

<ImageView
android:id="@+id/tick"
android:contentDescription="@string/tick_image"
android:layout_width="@dimen/cell_width"
android:layout_height="wrap_content"
android:layout_weight="1"
android:padding="@dimen/list_padding"
/>
</TableRow>

</TableLayout>

The Java class TickSignal is shown in the listing 9.5 below:

Listing-9.5
package com.polarsparc.android.droidstockticker3;

public enum TickSignal {
UP, FLAT, DOWN;
}

The Java class StockTicker is shown in the listing 9.6 below:

Listing-9.6
package com.polarsparc.android.droidstockticker3;

import java.io.Serializable;

public class StockTicker implements Serializable {
private static final long serialVersionUID = 1L;

private String symbol;
private float open;
private float change;

public String getSymbol() {
return symbol;
}

public void setSymbol(String symbol) {
this.symbol = symbol;
}

public float getPrice() {
return open+change;
}

public void setOpen(float open) {
this.open = open;
}

public float getChange() {
return change;
}

public void setChange(float change) {
this.change = change;
}

public TickSignal getTickSignal() {
if (change > 0.0f) {
return TickSignal.UP;
}
else if (change < 0.0f) {
return TickSignal.DOWN;
}
return TickSignal.FLAT;
}
}

The Java class TickerArrayAdapter is shown in the listing 9.7 below:

Listing-9.7
package com.polarsparc.android.droidstockticker3;

import java.util.List;

import android.content.Context;
import android.widget.ArrayAdapter;
import android.view.View;
import android.view.ViewGroup;
import android.view.LayoutInflater;
import android.widget.TextView;
import android.widget.ImageView;
import android.graphics.Color;

public class TickerArrayAdapter extends ArrayAdapter<StockTicker> {
private List<StockTicker> items;

public TickerArrayAdapter(Context context, List<StockTicker> items) {
super(context, R.layout.list_item_row, items);
this.items = items;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
LayoutInflater inflater = (LayoutInflater) getContext().
getSystemService(Context.LAYOUT_INFLATER_SERVICE);

convertView = inflater.inflate(R.layout.list_item_row, parent, false);
}

// Symbol
TextView symbol = (TextView)convertView.findViewById(R.id.symbol);
symbol.setText(items.get(position).getSymbol());
symbol.setTextColor(Color.BLUE);

// Price
TextView price = (TextView)convertView.findViewById(R.id.price);
price.setText(String.format("%.02f", items.get(position).getPrice()));
price.setTextColor(Color.BLACK);

// Change
TextView change = (TextView)convertView.findViewById(R.id.change);
change.setText(String.format("%.02f", items.get(position).getChange()));

// Tick Image
ImageView tick = (ImageView)convertView.findViewById(R.id.tick);
switch (items.get(position).getTickSignal()) {
case UP:
tick.setImageResource(R.drawable.up_tick);
change.setTextColor(Color.GREEN);
break;
case FLAT:
tick.setImageResource(R.drawable.flat_tick);
change.setTextColor(Color.GRAY);
break;
case DOWN:
tick.setImageResource(R.drawable.down_tick);
change.setTextColor(Color.RED);
break;
}

return convertView;
}
}

Finally, we have the java source file corresponding to the main stock ticker Activity called StockTickerActivity.java.

The contents of the java source file StockTickerActivity.java will look like the one shown in the listing 9.8 below:

Listing-9.8
package com.polarsparc.android.droidstockticker3;

import java.util.ArrayList;
import java.util.Random;

import com.polarsparc.android.droidstockticker3.R;

import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.os.AsyncTask;

public class StockTickerActivity extends Activity {
private static String TAG = "StockTickerActivity3";
private static String KEY = "TICKER_ITEMS";

private ArrayList<StockTicker> items = null;

private ArrayAdapter<StockTicker> adapter;

private TickerUpdateTask task = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_stock_ticker);

Object value = null;
if (savedInstanceState != null) {
value = savedInstanceState.getSerializable(KEY);
}

if (value != null) {
items = (ArrayList<StockTicker>) value;

Log.i(TAG, "StockTickerActivity - onCreate() called (restart from saved) !!!");
}
else {
items = new ArrayList<StockTicker>();
String[] stocks = getResources().getStringArray(R.array.stocks);
String[] opens = getResources().getStringArray(R.array.opens);
if (stocks != null) {
for (int i = 0; i < stocks.length; i++) {
StockTicker st = new StockTicker();
st.setSymbol(stocks[i]);
st.setOpen(Float.valueOf(opens[i]));
st.setChange(0.0f);

items.add(st);
}
}

Log.i(TAG, "StockTickerActivity - onCreate() called (create new) !!!");
}

adapter = new TickerArrayAdapter(this, items);

final ListView list = (ListView) findViewById(R.id.list);
list.setAdapter(adapter);
}

@Override
protected void onStart() {
super.onStart();

task = new TickerUpdateTask();
task.execute();

Log.i(TAG, "StockTickerActivity - onStart() called !!!");
}

@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);

Log.i(TAG, "StockTickerActivity - onSaveInstanceState() called !!!");

if (items != null) {
outState.putSerializable(KEY, items);
}
}

@Override
protected void onStop() {
super.onStop();

if (task != null) {
task.cancel(true);
}

Log.i(TAG, "StockTickerActivity - onStop() called !!!");
}

private class TickerUpdateTask extends AsyncTask<Void, Void, Void> {
@Override
protected Void doInBackground(Void... param) {
Log.i(TAG, "TickerUpdateTask <" + Thread.currentThread().getName() +
"> - doInBackground() called !!!");

Random rand = new Random();

for (;;) {
try {
// Sleep for 10 seconds
Thread.sleep(10000);

if (isCancelled()) {
break;
}

synchronized (items) {
for (int i = 0; i < items.size(); i++) {
float change = rand.nextFloat() * 10;
if (rand.nextInt(100) % 2 == 1) {
change = change * -1.0f;
}
StockTicker st = items.get(i);
st.setChange(change);
}
}

// Update progress
publishProgress();
}
catch (Throwable t) {
Log.e(TAG, "TickerUpdateTask <" + Thread.currentThread().getName() +
"> - doInBackground() exception !!!", t);
}
}

Log.i(TAG, "TickerUpdateTask <" + Thread.currentThread().getName() +
"> - doInBackground() exit !!!");

return null;
}

@Override
protected void onProgressUpdate(Void... progress) {
adapter.notifyDataSetChanged();
}

@Override
protected void onPostExecute(Void result) {
Log.i(TAG, "TickerUpdateTask <" + Thread.currentThread().getName() +
"> - onPostExecute() never called !!!");
}
}
}

From the code Listing 9.8 above, the following are some of the interesting details:

We are now ready to test our DroidStockTicker3 application on the virtual Android device we created in Part-1. We will create a Run Configuration for DroidStockTicker3 as we did in Part-2 for DroidTipCalculator.

Once the run configuration for DroidStockTicker3 is ready, we will Run the application and the application will come to life as shown in the following figure 9.1 below:

Stock Ticker3 App Image
Figure-9.1

Wait about 10 seconds and the device screen will be refreshed as shown in the following figure 9.2 below:

Stock Ticker3 App Image
Figure-9.2

Press CTRL+F12 to change the orientation of the screen from vertical to horizontal and the virtual device screen will appear as shown in the following figure 9.3 below:

Stock Ticker3 App Image
Figure-9.3

The following figure 9.4 below shows the output from the console to give us an idea of what is happening behind the scenes:

Stock Ticker3 App Image
Figure-9.4

An application can also fix the screen orientation to either portrait or landscape.

There are many mobile apps that take this approach - Angry Birds being one of the popular ones that has a horizontal orientation.

In order to fix the screen orientation to portrait for our DroidStockTicker3 application, we will modify the AndroidManifest.xml to look like the one shown in the listing 9.9 below:

Listing-9.9
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.polarsparc.android.droidstockticker3"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="14" />

<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.polarsparc.android.droidstockticker3.StockTickerActivity"
android:label="@string/app_name"
android:screenOrientation="portrait" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

Restart the DroidStockTicker3 application in the Virtual Android Device and press CTRL+F12 to change the orientation of the screen from vertical to horizontal and the virtual device screen will appear as shown in the following figure 9.5 below:

Stock Ticker3 App Image
Figure-9.5

As can be observed, even though the virtual device is horizontal, the orientation of the DroidStockTicker3 application is still vertical.

The following figure 9.6 below shows the output from the console to give us an idea of what is happening behind the scenes when the orientation is fixed at portrait:

Stock Ticker3 App Image
Figure-9.6

References

Taste of Android :: Part-1

Taste of Android :: Part-2

Taste of Android :: Part-3

Taste of Android :: Part-4

Taste of Android :: Part-5

Taste of Android :: Part-6

Taste of Android :: Part-7

Taste of Android :: Part-8