Objectives

Layout a new Android project with a single activity. Design this activity to permit simple sports activities to be specified. Implement the Activity class to support these controls

Create Project

Download and install the latest Android/Ecliose SDL

Create a new Android application, with the following properties:

Note the 'Minimum Required SDK' which may be different from the default.

Accept all the defaults in the wizard - except the name of the main activity:

The project will look like this:

With the activity_donate open in the visual designer:

It is important to become familiar with the structure and purpose of the three panes surrounding the Donation 'canvas':

Pallette:

Outline

Properties

These views are closely related - and you will need to monitor the information displayed there continually as you evolve the appearance of your activities screens.

Layout Donation Activity

For this lab, out objective is to reproduce in Android this feature from this web app:

In eclipse, delete the current 'Hello World' text, and drag and drop a new'LargeText' form widget onto the canvas. Look closely at the following:

Note carefully the following features:

  • the guides tyeing the text to the left, top and right corner
  • in Outline - the name of the control has been changed from a default to 'donateTitle'. This is changed by selecting the item in outline, and selecting 'Edit ID' from the context menu.
  • in Properties - where we entered 'Welcome Homer' into the text field

Recreate the above precisely.

A 'Lint warning' will have popped up on the left:

and accept the offered id.

Locate the following two files and inspect them closely:

res/layout/activity_create.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="org.pacemaker.pacemaker.CreateActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="@string/enter_activity_details"
        android:id="@+id/CreateActivityDetails"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />
</RelativeLayout>

res/values/strings.xml

<resources>
    <string name="app_name">Pacemaker</string>
    <string name="enter_activity_details">Enter Activity Details</string>
</resources>

Note the relationship between 'enter_activity_details' in both files.

Donate Button

Place a button directly on to the activity - attached to the bottom of the screen as shown:

Android Visual Designer

Rename both the button and the text field:

Outline View

You can do this by selecting the item in the outline view->Go To Declaration. Enter the more meaningful names in there.

The resource files will now be:

activity_create.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="org.pacemaker.pacemaker.CreateActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="@string/enter_activity_details"
        android:id="@+id/createActivityTitle"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Create Activity"
        android:id="@+id/createActivityButton"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

strings.xml

<resources>
    <string name="app_name">Pacemaker</string>
    <string name="enter_activity_details">Enter Activity Details</string>
</resources>

If there is a deviation from the above - retrace your steps (delete the button) until you can match the above.

We can now switch our attention to the Java Activity class CreateActivity

package org.pacemaker.pacemaker;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class CreateActivity extends AppCompatActivity
{

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

For any 'controls' a user can interact with we usually find it useful to associate a class member with that object. Currently we have one only on - a Button. The text fields we dont consider 'interactive' as such, so we will not include those.

Insert the following new field into the class:

  private Button createActivityButton;

The class will have to be imported. The class name will always match the name in the Palette:

We are free to call the variable anything we like. However, in order to keep confusion to a minimum, always call the variable by the same name you used in the Component Tree view:

In onCreate - we need to initialise this variable:

    createActivityButton = (Button) findViewById(R.id.createActivityButton);

We might also add a logging message so we can have some feedback as the app launches:

    Log.v("Pacemaker", "got the CreateActivity button");

This is the complete activity class:

package org.pacemaker;

import android.os.Bundle;
import android.app.Activity;
import android.util.Log;
import android.view.Menu;
import android.widget.Button;

public class CreateActivity extends Activity
{
  private Button createActivityButton;

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

    createActivityButton = (Button) findViewById(R.id.createActivityButton);

    Log.v("Pacemaker", "got the CreateActivity button");
  }

  @Override
  public boolean onCreateOptionsMenu(Menu menu)
  {
    // Inflate the menu; this adds items to the action bar if it is present.
    getMenuInflater().inflate(R.menu.create, menu);
    return true;
  }
}

Finding the log message can be difficult, unless you set a filter or just a simple search for the Pacemaker string:

Documentation

The android documentation is particularly helpful and well designed. These are the two key starting points:

The first is designed to be read though as a guide, perhaps independent of any work in eclipse. You should get into the habit of devoting an hour or two a week just reading this section.

The Reference guide should always be open as you are working on labs or projects, and you should make a serious effort to get to grips with at least some of the information here.

Taking the Button class we have just started using. We can immediately find the reference just by knowing the import statement in our Activity class:

import android.widget.Button;

.. translates to

(note the last three segments match the package name). Open this page now. Read just as far as the "Button Style" heading. There seems to be two ways of learning when an button event occurs. The first method is using the event handler/listener - but a second easier method is also available.

Try this now. Bring in a new method into Donate class:

  public void createActivityButtonPressed (View view)
  {
    Log.v("Pacemaker", "CreateActivity Button Pressed!");
  }

Then, edit the activity_donate xml file - and add a new attribute into the Button xml fragment:

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Create Activity"
        android:id="@+id/createActivityButton"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:onClick="createActivityButtonPressed"/>

(the very last entry)

Save everything and execute the app, and monitor the log as you press the button:

We now have our first interaction working!

Research + New Control Layout

Recall the UI we are trying to implement:

We need a Radio Buttons, some sort of selection/combo box + a progress bar. These can be found in various locations in the pallette:

RadioGroup, ProgressBar and NumberPicker seem likely candidates. The names of these controls are exactly as advertised, and we can expect them to be in the 'widgets' package. To verify this, try importing them at the top of the activity class:

import android.widget.NumberPicker;
import android.widget.TextView;

... and we can bring in three fields into the class:

  private Button       createActivityButton;
  private TextView     activityType;
  private TextView     activityLocation;
  private NumberPicker distancePicker;

We can also open up three pages of documentation - which we can reverse engineer from the package/class names:

Note this time we have gone to the Activity class before actually creating the controls. We should do this now - and remember to use the same names (for the IDs) as we create the controls.

Getting the layout +id names as shown above may take some practice. However, it is an essential skill to get on top of, even it it takes a lot of trial and error.

For reference purposes (try to do it your self), these are the relevant generated xml files:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="org.pacemaker.pacemaker.CreateActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="@string/enter_activity_details"
        android:id="@+id/createActivityTitle"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/create_activity"
        android:id="@+id/createActivityButton"
        android:layout_alignParentBottom="true"
        android:layout_centerHorizontal="true"
        android:onClick="createActivityButtonPressed"/>

    <EditText
        android:id="@+id/activityType"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/createActivityTitle"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/createActivityTitle"
        android:layout_marginTop="27dp"
        android:ems="10"
        android:hint="@string/activityTypeHint" >

        <requestFocus />
    </EditText>

    <EditText
        android:id="@+id/activityLocation"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignLeft="@+id/activityType"
        android:layout_alignParentRight="true"
        android:layout_below="@+id/activityType"
        android:layout_marginTop="20dp"
        android:ems="10"
        android:hint="@string/activityLocationHint" />

    <TextView
        android:id="@+id/distanceLabel"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="50dp"
        android:text="@string/distanceLabel"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:layout_below="@+id/activityLocation"
        android:layout_alignRight="@+id/createActivityButton"
        android:layout_alignEnd="@+id/createActivityButton"
        android:layout_above="@+id/createActivityButton"
        android:layout_alignLeft="@+id/createActivityButton"
        android:layout_alignStart="@+id/createActivityButton" />

    <NumberPicker
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/numberPicker"
        android:layout_below="@+id/activityLocation"
        android:layout_toRightOf="@+id/createActivityButton"
        android:layout_toEndOf="@+id/createActivityButton" />

</RelativeLayout>
<resources>
    <string name="app_name">Pacemaker</string>
    <string name="enter_activity_details">Enter Activity Details</string>
    <string name="activityTypeHint">Enter Activity type...</string>
    <string name="activityLocationHint">Enter Location...</string>
    <string name="distanceLabel">Distance</string>
    <string name="create_activity">Create Activity</string>
</resources>

If we have our naming conventions right - then we can bind to these new controls in onCreate:

    createActivityButton = (Button)       findViewById(R.id.createActivityButton);
    activityType         = (TextView)     findViewById(R.id.activityType);
    activityLocation     = (TextView)     findViewById(R.id.activityLocation);
    distancePicker       = (NumberPicker) findViewById(R.id.numberPicker);

This is the complete CreateActivity class:

package org.pacemaker.pacemaker;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.NumberPicker;
import android.widget.TextView;


public class CreateActivity extends AppCompatActivity
{
  private Button       createActivityButton;
  private TextView     activityType;
  private TextView     activityLocation;
  private NumberPicker distancePicker;

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

    createActivityButton = (Button)       findViewById(R.id.createActivityButton);
    activityType         = (TextView)     findViewById(R.id.activityType);
    activityLocation     = (TextView)     findViewById(R.id.activityLocation);
    distancePicker       = (NumberPicker) findViewById(R.id.numberPicker);

    Log.v("Pacemaker", "got the CreateActivity button");
  }

  public void createActivityButtonPressed (View view)
  {
    Log.v("Pacemaker", "CreateActivity Button Pressed!");
  }
}

NumberPicker

This is our reference documentation:

which is a little overwhelming. Back in the guides:

we might find some useful tutorial type introduction to this control - under 'User Interface' - 'Input Controls'

.. and this is the page on 'pickers'

This documentation is concerned with Fragments - a concept that may be difficult to grasp initially, and also explores the usage of date and time pickers.

We can get up and running without this much fuss. Returning to the documentation, these three methods should be sufficient initially:

In onCreate, initialise the values:


    distancePicker.setMinValue(0);
    distancePicker.setMaxValue(20);

And in createActivityButtonPressed:

  public void createActivityButtonPressed (View view) 
  {
    int distance = distancePicker.getValue();
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }

Run this now - and verify that it operates as expected (see the actual amounts in the log view).

List Activities

We would like to display the activities we have just created, mirroring the approach taken by the web application:

Select the package containing the CreateActivity class, and create a new activity using the wizard (New->Android->Android Activity:

Accept all the defaults, and call the activity ActivitiesList:

In the designer, place a Large Text Field + a ListView (from the Composite panel) directly on to the canvas like this:

Note the names used for the IDs in the Outline view.

Activities Model

Create a new class to represent an individual (user) activity - called MyActivity to avoid interference from the Android Activity namespace:

package org.pacemaker.pacemaker;

import static com.google.common.base.Objects.toStringHelper;
import com.google.common.base.Objects;

public class MyActivity
{
  public Long   id;
  public String type;
  public String location;
  public double distance;

  public MyActivity()
  {
  }

  public MyActivity(String type, String location, double distance)
  {
    this.type      = type;
    this.location  = location;
    this.distance  = distance;
  }

  @Override
  public String toString()
  {
    return toStringHelper(this).addValue(id)
        .addValue(type)
        .addValue(location)
        .addValue(distance)
        .toString();
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof MyActivity)
    {
      final MyActivity other = (MyActivity) obj;
      return Objects.equal(type, other.type)
          && Objects.equal(location,  other.location)
          && Objects.equal(distance,  other.distance);
    }
    else
    {
      return false;
    }
  }

  @Override
  public int hashCode()
  {
    return Objects.hashCode(this.id, this.type, this.location, this.distance);
  }
}

This use the guava library for utility support:

To include this in your project, you need to edit build.gradle and include guava as an additional dependency:

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.1.1'
    compile group: 'com.google.guava', name: 'guava', version: '18.0'
}

Back in the CreateActivity, we might create a list to store the activities:

  private List<MyActivity> activities = new ArrayList<MyActivity>();

...and populate this list in createActivityButtonPressed:

  public void createActivityButtonPressed (View view) 
  {  
    double distance = distancePicker.getValue();
    MyActivity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    activities.add(activity);
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }

Rendering the Activities

Place a new button on the CreateAvtivity panel to trigger the display of the activities list:

... and install an event handler for this button:

  public void listActivityButtonPressed (View view) 
  {
    Log.v("Pacemaker", "List Activities Button Pressed");
  }

Manually 'wire-up' the event handler by inserting the appropriate entry in the layout file:

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/list_activities"
        android:id="@+id/listActivitiesButton"
        android:layout_alignParentBottom="true"
        android:layout_alignRight="@+id/createActivityButton"
        android:layout_alignEnd="@+id/createActivityButton"
        android:layout_alignLeft="@+id/createActivityButton"
        android:onClick="listActivityButtonPressed"
        android:layout_alignStart="@+id/createActivityButton"/>

(second last entry)

To switch views, we start a new activity with the correct intent:

  public void listActivityButtonPressed (View view) 
  {
    Log.v("Pacemaker", "List Activityies Button Pressed");
    Intent intent = new Intent(this, ActivitiesList.class);
    startActivity (intent);
  }

Exercises

Archive of lab so far:

Exercise 1:

We have a list of activities in the CreateActivity class:

  private ArrayList<MyActivity> activities = new ArrayList<MyActivity>();

Which is populated in the button event handler:

  public void createActivityButtonPressed (View view) 
  {  
    double distance = distancePicker.getValue();
    Activity activity = new MyActivity (activityType.getText().toString(), activityLocation.getText().toString(), distance);

    activities.add(activity);
    Log.v("Pacemaker", "CreateActivity Button Pressed with " + distance);
  }

How do we display this in the ActivityList?

public class ActivitiesList extends Activity
{

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

To populate the array - you will need to use an adapter:

This is a example of the adapter pattern:

But you will need to find a way of getting the arraylist of activities to the view. One technique is based on the memento pattern:

In android, this technique is implemented using the Parcelable class:

See if you can have the list of activities displayed in the ActivitiesList view.

The next lab will go though both options in solving this problem.