Listeners - Android Application Development: A Beginner's Tutorial (2015)

Android Application Development: A Beginner's Tutorial (2015)

Chapter 5. Listeners

Like many GUI systems, Android is event based. User interaction with a view in an activity may trigger an event and you can write code that gets executed when the event occurs. The class that contains code to respond to a certain event is called an event listener.

In this chapter you will learn how to handle events and write event listeners.

Overview

Most Android programs are interactive. The user can interact with the application easily thanks to the event-driven programming paradigm the Android framework offers. Examples of events that may occur when the user is interacting with an activity are click, long-click, touch, key, and so on.

To make a program respond to a certain event, you need to write a listener for that event. The way to do it is by implementing an interface that is nested in the android.view.View class. Table 5.1 shows some of the listener interfaces in View and the corresponding method in each interface that will get called when the corresponding event occurs.

Interface

Method

OnClickListener

onClick()

OnLongClickListner

OnLongClick()

OnFocusChangeListener

OnFocusChange()

OnKeyListener

OnKey()

OnTouchListener

OnTouch()

Table 5.1: Listener interfaces in View

Once you create an implementation of a listener interface, you can pass it to the appropriate setOnXXXListener method of the view you want to listen to, where XXX is the event name. For example, to create a click listener for a button, you would write this in your activity class.

private OnClickListener clickListener = new OnClickListener() {

public void onClick(View view) {

// code to execute in response to the click event

}

};

protected void onCreate(Bundle savedValues) {

...

Button button = (Button)findViewById(...);

button.setOnClickListener(clickListener);

...

}

Alternatively, you can make your activity class implement the listener interface and provide an implementation of the needed method as part of the activity class.

public class MyActivity extends Activity

implements View.OnClickListener {

protected void onCreate(Bundle savedValues) {

...

Button button = (Button)findViewById(...);

button.setOnClickListener(this);

...

}

// implementation of View.OnClickListener

@Override

public void onClick(View view) {

// code to execute in response to the click event

}

...

}

In addition, there is a shortcut for handling the click event. You can use the onClick attribute in the declaration of the target view in the layout file and write a public method in the activity class. The public method must have no return value and take a View argument. For example, if you have this method in your activity class

public void showNote(View view) {

// do something

}

you can use this onClick attribute in a view to attach the method to the click event of that view.

<Button android:onClick="showNote" .../>

In the background, Android will create an implementation of the OnClickListener interface and attach it to the view.

In the sample applications that follow you will learn how to write event listeners.

Note

A listener runs on the main thread. This means you should use a different thread if your listener takes a long time (say, more than 200ms) to run. Or else, your application will look unresponsive during the execution of the listener code. You have two choices for solving this. You can either use a handler or an AsyncTask. The handler is covered in Chapter 2, “Handling the Handler” and AsyncTask in Chapter 23, “Asynchronous Tasks.” For long-running tasks, you should also consider using the Java Concurrency Utilities.

Using the onClick Attribute

As an example of using the onClick attribute to handle the click event of a view, consider the MulticolorClock project that accompanies this book. It is a simple application with a single activity that shows an analog clock that can be clicked to change its color. AnalogClock is one of the widgets available on Android, so writing the view for the application is a breeze. The main objective of this project is to demonstrate how to write a listener by using a callback method in the layout file.

The manifest for MulticolorClock is given in Listing 5.1. There is nothing out of ordinary here and you should not find it difficult to understand.

Listing 5.1: The manifest for MulticolorClock

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="com.example.multicolorclock"

android:versionCode="1"

android:versionName="1.0" >

<uses-sdk

android:minSdkVersion="8"

android:targetSdkVersion="17" />

<application

android:allowBackup="true"

android:icon="@drawable/ic_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

<activity

android:name="com.example.multicolorclock.MainActivity"

android:label="@string/app_name" >

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

</application>

</manifest>

Now comes the crucial part, the layout file. It is called activity_main.xml and located under the res/layout directory. The layout file is presented in Listing 5.2.

Listing 5.2: The layout file in MulticolorClock

<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=".MainActivity">

<AnalogClock

android:id="@+id/analogClock1"

android:layout_width="wrap_content"

android:layout_height="wrap_content"

android:layout_alignParentTop="true"

android:layout_centerHorizontal="true"

android:layout_marginTop="90dp"

android:onClick="changeColor"

/>

</RelativeLayout>

The layout file defines a RelativeLayout containing an AnalogClock. The important part is the onClick attribute in the AnalogClock declaration.

android:onClick="changeColor"

This means that when the user presses the AnalogClock widget, the changeColor method in the activity class will be called. For a callback method like changeColor to work, it must have no return value and accept an argument of type View. The system will call this method and pass the widget that was pressed.

The changeColor method is part of the MainActivity class shown in Listing 5.3.

Listing 5.3: The MainActivity class in MulticolorClock

package com.example.multicolorclock;

import android.app.Activity;

import android.graphics.Color;

import android.os.Bundle;

import android.view.Menu;

import android.view.View;

import android.widget.AnalogClock;

public class MainActivity extends Activity {

int counter = 0;

int[] colors = { Color.BLACK, Color.BLUE, Color.CYAN,

Color.DKGRAY, Color.GRAY, Color.GREEN, Color.LTGRAY,

Color.MAGENTA, Color.RED, Color.WHITE, Color.YELLOW };

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.activity_main);

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

// Inflate the menu; this adds items to the action bar if it

// is present.

getMenuInflater().inflate(R.menu.menu_main, menu);

return true;

}

public void changeColor(View view) {

if (counter == colors.length) {

counter = 0;

}

view.setBackgroundColor(colors[counter++]);

}

}

Pay special attention to the changeColor method in the MainActivity class. When the user presses (or touches) the analog clock, this method will be called and receive the clock object. To change the clock’s color, call its setBackgroundColor method, passing a color object. In Android, colors are represented by the android.graphics.Color class. The class has pre-defined colors that make creating color objects easy. These pre-defined colors include Color.BLACK, Color.Magenta, Color.GREEN, and others. The MainActivity class defines an array of ints that contains some of the pre-defined colors in android.graphics.Color.

int[] colors = { Color.BLACK, Color.BLUE, Color.CYAN,

Color.DKGRAY, Color.GRAY, Color.GREEN, Color.LTGRAY,

Color.MAGENTA, Color.RED, Color.WHITE, Color.YELLOW };

There is also a counter that points to the current index position of colors. The changeColor method inquiries the value of counter and changes it to zero if the value is equal to the array length. It then passes the pointed color to the setBackgroundColor method of the AnalogClock.

view.setBackgroundColor(colors[counter++]);

The application is shown in Figure 5.1.

image

Figure 5.1: The MulticolorClock application

Touch the clock to change its color!

Implementing A Listener

As a second example, the GestureDemo application shows you how to implement the View.OnTouchListener interface to handle the touch event. To make it simple, the application only has one activity that contains a grid of cells that can be swapped. The application is shown in Figure 5.2.

image

Figure 5.2: The GestureDemo application

Each of the images is an instance of the CellView class given in Listing 5.4. It simply extends ImageView and adds x and y fields to store the position in the grid.

Listing 5.4: The CellView class

package com.example.gesturedemo;

import android.content.Context;

import android.widget.ImageView;

public class CellView extends ImageView {

int x;

int y;

public CellView(Context context, int x, int y) {

super(context);

this.x = x;

this.y = y;

}

}

There is no layout class for the activity as the layout is built programmatically. This is shown in the onCreate method of the MainActivity class in Listing 5.5.

Listing 5.5: The MainActivity class

package com.example.gesturedemo;

import android.app.Activity;

import android.graphics.drawable.Drawable;

import android.os.Bundle;

import android.view.Gravity;

import android.view.Menu;

import android.view.MotionEvent;

import android.view.View;

import android.view.View.OnTouchListener;

import android.view.ViewGroup;

import android.widget.ImageView;

import android.widget.LinearLayout;

public class MainActivity extends Activity {

int rowCount = 7;

int cellCount = 7;

ImageView imageView1;

ImageView imageView2;

CellView[][] cellViews;

int downX;

int downY;

boolean swapping = false;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

LinearLayout root = new LinearLayout(this);

LinearLayout.LayoutParams matchParent =

new LinearLayout.LayoutParams(

LinearLayout.LayoutParams.MATCH_PARENT,

LinearLayout.LayoutParams.MATCH_PARENT);

root.setOrientation(LinearLayout.VERTICAL);

root.setGravity(Gravity.CENTER_VERTICAL);

addContentView(root, matchParent);

// create row

cellViews = new CellView[rowCount][cellCount];

LinearLayout.LayoutParams rowLayoutParams =

new LinearLayout.LayoutParams(

LinearLayout.LayoutParams.MATCH_PARENT,

LinearLayout.LayoutParams.WRAP_CONTENT);

ViewGroup.LayoutParams cellLayoutParams =

new ViewGroup.LayoutParams(

ViewGroup.LayoutParams.WRAP_CONTENT,

ViewGroup.LayoutParams.WRAP_CONTENT);

int count = 0;

for (int i = 0; i < rowCount; i++) {

CellView[] cellRow = new CellView[cellCount];

cellViews[i] = cellRow;

LinearLayout row = new LinearLayout(this);

row.setLayoutParams(rowLayoutParams);

row.setOrientation(LinearLayout.HORIZONTAL);

row.setGravity(Gravity.CENTER_HORIZONTAL);

root.addView(row);

// create cells

for (int j = 0; j < cellCount; j++) {

CellView cellView = new CellView(this, j, i);

cellRow[j] = cellView;

if (count == 0) {

cellView.setImageDrawable(

getResources().getDrawable(

R.drawable.image1));

} else if (count == 1) {

cellView.setImageDrawable(

getResources().getDrawable(

R.drawable.image2));

} else {

cellView.setImageDrawable(

getResources().getDrawable(

R.drawable.image3));

}

count++;

if (count == 3) {

count = 0;

}

cellView.setLayoutParams(cellLayoutParams);

cellView.setOnTouchListener(touchListener);

row.addView(cellView);

}

}

}

@Override

public boolean onCreateOptionsMenu(Menu menu) {

getMenuInflater().inflate(R.menu.menu_main, menu);

return true;

}

private void swapImages(CellView v1, CellView v2) {

Drawable drawable1 = v1.getDrawable();

Drawable drawable2 = v2.getDrawable();

v1.setImageDrawable(drawable2);

v2.setImageDrawable(drawable1);

}

OnTouchListener touchListener = new OnTouchListener() {

@Override

public boolean onTouch(View v, MotionEvent event) {

CellView cellView = (CellView) v;

int action = event.getAction();

switch (action) {

case (MotionEvent.ACTION_DOWN):

downX = cellView.x;

downY = cellView.y;

return true;

case (MotionEvent.ACTION_MOVE):

if (swapping) {

return true;

}

float x = event.getX();

float y = event.getY();

int w = cellView.getWidth();

int h = cellView.getHeight();

if (downX < cellCount - 1

&& x > w && y >= 0 && y <= h) {

// swap with right cell

swapping = true;

swapImages(cellView,

cellViews[downY][downX + 1]);

} else if (downX > 0 && x < 0

&& y >=0 && y <= h) {

// swap with left cell

swapping = true;

swapImages(cellView,

cellViews[downY][downX - 1]);

} else if (downY < rowCount - 1

&& y > h && x >= 0 && x <= w) {

// swap with cell below

swapping = true;

swapImages(cellView,

cellViews[downY + 1][downX]);

} else if (downY > 0 && y < 0

&& x >= 0 && x <= w) {

// swap with cell above

swapping = true;

swapImages(cellView,

cellViews[downY - 1][downX]);

}

return true;

case (MotionEvent.ACTION_UP):

swapping = false;

return true;

default:

return true;

}

}

};

}

The MainActivity class contains a View.OnTouchListener called touchListener that will be attached to every single CellView in the grid. The OnTouchListener interface has an onTouch method that must be implemented. Here is the signature of onTouch.

public boolean onTouch(View view, MotionEvent event)

The method should return true if it has consumed the event, which means that the event should not propagate to other views. Otherwise, it should return false.

A single touch action by the user causes the onTouch method to be called several times. When the user touches the view, the method is called. When the user moves his/her finger, onTouch is called. Likewise, onTouch is called when the user lifts his/her finger. The second argument toonTouch, a MotionEvent, contains the information about the event. You can inquire what type of action is triggering the event by calling the getAction method on the MotionEvent.

int action = event.getAction();

The return value will be one of the static final ints defined in the MotionEvent class. For this application we are interested in MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE, and MotionEvent.ACTION_UP. When the user touches the view, the getAction method returns a MotionEvent.ACTION_DOWN. The code simply stores the location of the event to the x and y fields and returns true.

case (MotionEvent.ACTION_DOWN):

downX = cellView.x;

downY = cellView.y;

return true;

If the user moves his/her finger to a neighboring cell, the touch action will return a MotionEvent.ACTION_MOVE and you need to swap the images of the original cell and the destination cell and set the swapping field to true. This would prevent another swapping before the finger is lifted.

Finally, when the user lifts his/her finger, the swapping field is set to false to enable another swapping.

The layout for the activity is built dynamically in the onCreate method of the activity class. Each CellView is passed the OnTouchListener so that the listener will handle the CellView’s touch event.

cellView.setOnTouchListener(touchListener);

Summary

In this chapter you learned the basics of Android event handling and how to write listeners by implementing a nested interface in the View class. You have also learned to use the shortcut for handling the click event.