Android Tutorial: Event Listeners

In this article, you will learn how to handle events and write event listeners. Most Android programs are interactive.

Event Listeners

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 1.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 1.1: Listener interfaces in View

Once you create an implementation of a listener interface, you can pass it to the appropriate setOnXXXListenermethod 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. [apcode language="java"]

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 
    }

    ...
}


[/apcode]
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.

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. There is nothing out of ordinary here and you should not find it difficult to understand.

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.xmland located under the res/layoutdirectory. The layout file is presented.

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.

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 1.1.

Figure 1.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.OnTouchListenerinterface 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 1.2.

Figure 1.2: The GestureDemo application

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

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 onCreatemethod of the MainActivity class.

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 to onTouch, 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);

In this article, 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.

You can see the previous article Android Tutorial: Layouts

Leave a Reply

Your email address will not be published.