Draggable Symbols II
In this project we will start with the basic DraggableSymbols project developed in the previous section and extend it to do more sophisticated things.
Create a new project in Eclipse: File > New > Android Project, and select
First we copy some resources. Open the course images directory and copy the files green_square.png, red_square.png, and yellow_square.png to the res/drawable-hdpi directory of the DragSymbols project.
Import the class SymbolDragger from the DraggableSymbols project: right-click on src/<namespace>dragsymbols, select Import > General > File System, then click Next and Browse. Navigate to the directory containing the file SymbolDragger.java, click OK, select the file, and then click Finish. Eclipse will indicate an error in this imported file because the package name is wrong (force a rebuild if no error shows initially). Fix that by editing the first line of the imported file SymbolDragger.java to
package com.lightcone.dragsymbols;
which gives it the appropriate package name for this project.
Now edit the file DragSymbols.java to give it the content
package com.lightcone.dragsymbols;
import android.app.Activity;
import android.os.Bundle;
import android.view.ViewGroup;
/*
Demonstration of one way to put a set of draggable symbols on screen. Define symbols as image
files in res/drawable-hdpi/filename.png (or .jpg or .gif files). Refer to them then by
R.drawable.filename in the array symbolIndex, and give the initial x and y coordinates for
each symbol in the arrays X and Y.
*/
public class DragSymbols extends Activity {
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Define the symbols and their initial coordinates in arrays. No limit in principle
// to how many. Coordinates are measured from the upper left corner of the screen,
// with x increasing to the right and y increasing downward
float [] X = {2, 48, 94}; // Initial x coord in pixels of upper left corner of symbol
float [] Y = {2, 2, 2}; // Initial y coord in pixels of upper left corner of symbol
// The Drawable corresponding to the symbol. R.drawable.file refers to file.png, .jpg,
// or .gif stored in res/drawable-hdpi (referenced from code without the extension).
int[]symbolIndex = {R.drawable.red_square, R.drawable.green_square, R.drawable.yellow_square};
// Instantiate a SymbolDragger instance (which subclasses View), passing to it in the
// constructor the context (this) and the above arrays. Then set the content view to
// this instance of SymbolDragger (so the layout is being specified entirely by SymbolDragger,
// with no XML layout file). The resulting view should then place draggable symbols with
// content and initial position defined by the above arrays on the screen.
SymbolDragger view = new SymbolDragger(this, X, Y, symbolIndex);
view.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.FILL_PARENT,
ViewGroup.LayoutParams.FILL_PARENT));
setContentView(view);
}
}
which is the same as DraggableSymbols.java in the DraggableSymbols project except for the package name. If you now execute this you should get the same results as for the project DraggableSymbols.
Before proceeding, let's use the global rename facility of Eclipse to change the name of the class SymbolDragger. (We do this for clarity, since we are going to alter its content substantially from that of the corresponding class in DraggableSymbols.)
Scroll through the code in both classes for this project and notice the changes; confirm that renaming has not changed any functionality and everything works as before by doing a clean build (Project > Clean) and executing the app.
A typical application of the dragging capability that we have implemented would be to have a "source" of symbols off-stage so that when they are dragged a new instance of the object is created and dragged to the stage, but the original "source" of the symbols remains. Let's implement that capability. Basically, we have to modify DragginSlayer so that a set of original symbols remains at fixed positions when we drag the new symbols to the stage. To implement that in simplest form, we add a new set of Drawables symbol0, with coordinates X0 and Y0, that do not change as the objects corresponding to symbol are dragged about the screen. We modify DragginSlayer.java so that it reads (with the changes relative to the original SymbolDragger.java marked in red)
package com.lightcone.dragsymbols;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
/*
Demonstration of one way to put a set of draggable symbols on screen.
Adapted loosely from material discussed in
http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
See also
http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html
This example requires only API 3 (Android 1.5).
*/
public class DragginSlayer extends View {
// Colors for background and text
static final int BACKGROUND_COLOR = Color.argb(255, 0, 0, 0);
static final int HEADER_COLOR = Color.argb(255, 30, 30, 30);
static final int TEXT_COLOR = Color.argb(255,255,255,0);
private int numberSymbols; // Total number of symbols to use
private Drawable [] symbol; // Array of symbols (dimension numberSymbols)
private int [] symbolIndex; // Index of drawable resource (R.drawable.symbol)
private Drawable [] symbol0; // Array of symbols (dimension numberSymbols)
private float [] X0; // Initial x coordinate, upper left corner of symbol i
private float [] Y0; // Initial y coordinate, upper left corner of symbol i
private float [] X; // Current x coordinate, upper left corner of symbol i
private float [] Y; // Current y coordinate, upper left corner of symbol i
private int [] symbolWidth; // Width of symbol i
private int [] symbolHeight; // Height of symbol i
private float [] lastTouchX; // x coordinate of symbol i at last touch
private float [] lastTouchY; // y coordinate of symbol i at last touch
private int symbolSelected; // Index of symbol last touched (-1 if none selected)
private Paint paint;
// Following define upper left and lower right corners of display stage rectangle
private int stageX1 = 0;
private int stageY1 = 45;
private int stageX2 = 500;
private int stageY2 = 500;
private boolean isDragging = false; // True if some symbol is currently being dragged
public DragginSlayer(Context context, float[]X, float[]Y, int[]symbolIndex) {
// Call through to simplest constructor of View superclass
super(context);
// Set up local arrays defining symbols and their positions
this.X = X;
this.Y = Y;
this.symbolIndex = symbolIndex;
numberSymbols = X.length;
X0 = new float[numberSymbols];
Y0 = new float[numberSymbols];
symbol0 = new Drawable[numberSymbols];
symbol = new Drawable[numberSymbols];
symbolWidth = new int[numberSymbols];
symbolHeight = new int[numberSymbols];
lastTouchX = new float[numberSymbols];
lastTouchY = new float[numberSymbols];
// Fill the symbol arrays with data
for(int i=0; i<numberSymbols; i++){
X0[i] = X[i];
Y0[i] = Y[i];
symbol[i] = context.getResources().getDrawable(symbolIndex[i]);
symbolWidth[i] = symbol[i].getIntrinsicWidth();
symbolHeight[i] = symbol[i].getIntrinsicHeight();
symbol[i].setBounds(0,0,symbolWidth[i],symbolHeight[i]);
symbol0[i] = context.getResources().getDrawable(symbolIndex[i]);
symbol0[i].setBounds(0,0,symbolWidth[i],symbolHeight[i]);
}
// Set up the Paint object that will control format of screen draws
paint = new Paint();
paint.setAntiAlias(true);
paint.setTextSize(14);
paint.setStrokeWidth(0);
}
/* Process MotionEvents corresponding to screen touches and drags. MotionEvent
reports movement (mouse, pen, finger, trackball) events. The MotionEvent method
getAction() returns the kind of action being performed as an integer constant of
the MotionEvent class, with possible values ACTION_DOWN, ACTION_MOVE, ACTION_UP,
and ACTION_CANCEL. Thus we can switch on the returned integer to determine the
kind of event and the appropriate action.
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
// MotionEvent class constant signifying a finger-down event
case MotionEvent.ACTION_DOWN: {
isDragging = false;
// Get coordinates of touch event
final float x = ev.getX();
final float y = ev.getY();
// Initialize
symbolSelected = -1; // -1 if touch not within current bounds of some symbol
// Determine if touch within bounds of one of the symbols
for(int i=0; i<numberSymbols; i++){
if((x>X[i] && x<(X[i]+symbolWidth[i])) && (y>Y[i]
&& y<(Y[i]+symbolHeight[i]))) {
symbolSelected = i;
break;
}
}
// If touch within bounds of a symbol, remember start position for this symbol
if(symbolSelected > -1){
lastTouchX[symbolSelected] = x;
lastTouchY[symbolSelected] = y;
}
break;
}
// MotionEvent class constant signifying a finger-drag event
case MotionEvent.ACTION_MOVE: {
// Only process if touch selected a symbol
if(symbolSelected > -1 ){
isDragging = true;
final float x = ev.getX();
final float y = ev.getY();
// Calculate the distance moved
final float dx = x - lastTouchX[symbolSelected];
final float dy = y - lastTouchY[symbolSelected];
// Move the object selected
X[symbolSelected] += dx;
Y[symbolSelected] += dy;
// Remember this touch position for the next move event of this object
lastTouchX[symbolSelected] = x;
lastTouchY[symbolSelected] = y;
// Request a redraw
invalidate();
}
break;
}
// MotionEvent class constant signifying a finger-up event
case MotionEvent.ACTION_UP:
isDragging = false;
invalidate(); // Request redraw
break;
}
return true;
}
// This method will be called each time the screen is redrawn. The draw is
// on the Canvas object, with formatting controlled by the Paint object.
// When to redraw is under Android control, but we can request a redraw
// using the method invalidate() inherited from the View superclass.
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw backgrounds
drawBackground(paint,canvas);
// Draw all draggable symbols at their current dragged locations
for(int i=0; i<numberSymbols; i++){
canvas.save();
canvas.translate(X[i],Y[i]);
symbol[i].draw(canvas);
canvas.restore();
}
}
// Method to draw the background for the screen. Invoked from onDraw each time
// the screen is redrawn.
private void drawBackground(Paint paint, Canvas canvas){
// Draw header bar background
paint.setColor(HEADER_COLOR);
canvas.drawRect(0,0,stageX2,stageY2,paint);
// Draw main stage background
paint.setColor(BACKGROUND_COLOR);
canvas.drawRect(stageX1,stageY1,stageX2,stageY2,paint);
// Draw image of symbols at their original locations to denote source
// (But presently set up so that only one instance of each symbol can
// be dragged onto the stage.)
for(int i=0; i<numberSymbols; i++){
canvas.save();
canvas.translate(X0[i],Y0[i]);
symbol0[i].draw(canvas);
canvas.restore();
}
// While a symbol is being dragged, display its x and y coordinates in a readout
if(isDragging){
paint.setColor(TEXT_COLOR);
canvas.drawText("X = "+X[symbolSelected], 200, 18, paint);
canvas.drawText("Y = "+Y[symbolSelected], 200, 33, paint);
}
}
}
Everything behaves as before, except that in the method drawBackground there is a new block of code that always draws the symbols at their original positions, in addition to their new positions.
If you try it in an emulator or on a phone you should get something similar to the following figures.
![]() |
![]() |
On the left side we see the screen in its initial state and on the right side the yellow and green squares have already been dragged and positioned, and the red square is currently being dragged (with the readout showing its current position). Notice that the original "source" images remain, off the top of the stage.
| A project that implements the application as developed to this point is archived at the link DraggableSymbols2. (In that project the class SymbolDragger has not been renamed to DragginSlayer, but the functionality is the same.) |
The preceding example is a simple implementation of the idea, but it allows only a single instance of each of the three color squares to be dragged to the stage. If you attempt to drag additional instances to the stage nothing will happen. Let's now implement the bookkeeping that permits multiple instances of each symbol to be dragged to the stage and positioned. The essential changes required are
The modified form of DragginSlayer.java that accomplishes these tasks is illustrated in the following listing.
package com.lightcone.dragsymbols;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Toast;
/*
Demonstration of one way to put a set of draggable symbols on screen.
Adapted loosely from material discussed in
http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
See also
http://android-developers.blogspot.com/2010/07/how-to-have-your-cupcake-and-eat-it-too.html
This example requires only API 3 (Android 1.5).
*/
public class DragginSlayer extends View {
// Colors for background and text
static final int BACKGROUND_COLOR = Color.argb(255, 0, 0, 0);
static final int HEADER_COLOR = Color.argb(255, 30, 30, 30);
static final int TEXT_COLOR = Color.argb(255,255,255,0);
private int numberSymbols; // Total number of symbols to use
private int numberInstances; // Total number of symbol instances onstage
private int maxInstances; // Maximum number of symbol instances permitted onstage
private Drawable [] symbol; // Array of symbols (dimension numberSymbols)
private int [] symbolIndex; // Index of drawable resource (R.drawable.symbol)
private Drawable [] symbol0; // Array of symbols (dimension numberSymbols)
private float [] X0; // Initial x coordinate, upper left corner of symbol i
private float [] Y0; // Initial y coordinate, upper left corner of symbol i
private float [] X; // Current x coordinate, upper left corner of symbol i
private float [] Y; // Current y coordinate, upper left corner of symbol i
private int [] symbolWidth; // Width of symbol i
private int [] symbolHeight; // Height of symbol i
private float [] lastTouchX; // x coordinate of symbol i at last touch
private float [] lastTouchY; // y coordinate of symbol i at last touch
private int symbolSelected; // Index of symbol last touched (-1 if none selected)
private int instanceSelected; // Index of symbol instance last touched (-1 if none)
private Paint paint; // Paint object holding draw formatting information
// Following define upper left and lower right corners of display stage rectangle
private int stageX1 = 0;
private int stageY1 = 45;
private int stageX2 = 500;
private int stageY2 = 500;
private boolean isDragging = false; // True if any symbol is currently being dragged
private Context context;
public DragginSlayer(Context context, float[]XX, float[]YY, int[]symbolIndex) {
// Call through to simplest constructor of View superclass
super(context);
this.context = context;
// Initialize instance counters
maxInstances = 12;
numberInstances = 0;
// Set up local arrays defining symbols and their positions
this.X0 = XX;
this.Y0 = YY;
this.symbolIndex = symbolIndex;
numberSymbols = X0.length;
this.X = new float[maxInstances];
this.Y = new float[maxInstances];
symbol0 = new Drawable[numberSymbols];
symbol = new Drawable[maxInstances];
symbolWidth = new int[numberSymbols];
symbolHeight = new int[numberSymbols];
lastTouchX = new float[maxInstances];
lastTouchY = new float[maxInstances];
// Fill the symbol arrays with data
for(int i=0; i<numberSymbols; i++){
symbol0[i] = context.getResources().getDrawable(symbolIndex[i]);
symbolWidth[i] = symbol0[i].getIntrinsicWidth();
symbolHeight[i] = symbol0[i].getIntrinsicHeight();
symbol0[i].setBounds(0,0,symbolWidth[i],symbolHeight[i]);
}
// Set up the Paint object that will control format of screen draws
paint = new Paint();
paint.setAntiAlias(true);
paint.setTextSize(13);
paint.setStrokeWidth(0);
}
/* Process MotionEvents corresponding to screen touches and drags. MotionEvent
reports movement (mouse, pen, finger, trackball) events. The MotionEvent method
getAction() returns the kind of action being performed as an integer constant of
the MotionEvent class, with possible values ACTION_DOWN, ACTION_MOVE, ACTION_UP,
and ACTION_CANCEL. Thus we can switch on the returned integer to determine the
kind of event and the appropriate action.
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
final int action = ev.getAction();
switch (action) {
// MotionEvent class constant signifying a finger-down event
case MotionEvent.ACTION_DOWN: {
isDragging = false;
// Get coordinates of touch event
final float x = ev.getX();
final float y = ev.getY();
// Initialize symbol and instance indices
symbolSelected = -1; // -1 if touch not within current bounds of symbol source
instanceSelected = -1; // -1 if touch not within bounds of symbol instance
// Determine if touch within bounds of one of the symbol sources offstage
for(int i=0; i<numberSymbols; i++){
if((x>X0[i] && x<(X0[i]+symbolWidth[i])) && (y>Y0[i]
&& y<(Y0[i]+symbolHeight[i]))) {
symbolSelected = i; // Index of symbol source touched
break;
}
// Warn if max number of instances has been reached (it won't create any more)
if(numberInstances == maxInstances){
String toaster = "Maximum number of instances ";
toaster += "("+maxInstances+") has been reached.";
Toast.makeText(context, toaster, Toast.LENGTH_LONG).show();
}
}
// Determine if touch within bounds of one of the symbol instances onstage
for(int i=0; i<numberInstances; i++){
if((x>X[i] && x<(X[i]+symbol[i].getIntrinsicWidth())) && (y>Y[i]
&& y<(Y[i]+symbol[i].getIntrinsicHeight() ))) {
instanceSelected = i; // Index of symbol instance touched
break;
}
}
// If touch within bounds of a symbol source or instance, remember start
// position for this symbol
if(symbolSelected > -1 || instanceSelected > -1){
if(instanceSelected > -1)lastTouchX[instanceSelected] = x;
if(instanceSelected > -1)lastTouchY[instanceSelected] = y;
}
break;
}
// MotionEvent class constant signifying a finger-drag event
case MotionEvent.ACTION_MOVE: {
// Only process if initial touch selected a symbol and not background
// If touch and drag were on symbol source, and this hasn't yet been processed, first create a
// new symbol instance (but only if the max number of instances will not be exceeded). Do it
// here rather than in ACTION_DOWN so that just pressing the source symbol without a drag will
// not create a new instance.
if(symbolSelected > -1 && instanceSelected == -1 && numberInstances < maxInstances){
symbol[numberInstances] = context.getResources().getDrawable(symbolIndex[symbolSelected]);
symbol[numberInstances].setBounds(0,0,symbolWidth[symbolSelected],symbolHeight[symbolSelected]);
instanceSelected = numberInstances;
numberInstances ++;
}
// Drag the instance, if selected (either an old instance or one just created)
if(instanceSelected > -1 ){
isDragging = true;
final float x = ev.getX();
final float y = ev.getY();
// Calculate the distance moved
final float dx = x - lastTouchX[instanceSelected];
final float dy = y - lastTouchY[instanceSelected];
// Move the instance selected
X[instanceSelected] += dx;
Y[instanceSelected] += dy;
// Remember this touch position for the next move event of this object
lastTouchX[instanceSelected] = x;
lastTouchY[instanceSelected] = y;
// Request a redraw
invalidate();
}
break;
}
// MotionEvent class constant signifying a finger-up event
case MotionEvent.ACTION_UP:
isDragging = false;
invalidate(); // Request redraw
break;
}
return true;
}
// This method will be called each time the screen is redrawn. The draw is
// on the Canvas object, with formatting controlled by the Paint object.
// When to redraw is under Android control, but we can request a redraw
// using the method invalidate() inherited from the View superclass.
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Draw backgrounds
drawBackground(paint,canvas);
// Draw all draggable symbols at their current dragged locations
for(int i=0; i<numberInstances; i++){
canvas.save();
canvas.translate(X[i],Y[i]);
symbol[i].draw(canvas);
canvas.restore();
}
}
// Method to draw the background for the screen. Invoked from onDraw each time
// the screen is redrawn.
private void drawBackground(Paint paint, Canvas canvas){
// Draw header bar background
paint.setColor(HEADER_COLOR);
canvas.drawRect(0,0,stageX2,stageY2,paint);
// Draw main stage background
paint.setColor(BACKGROUND_COLOR);
canvas.drawRect(stageX1,stageY1,stageX2,stageY2,paint);
// Draw image of symbols at their original locations to denote source
for(int i=0; i<numberSymbols; i++){
canvas.save();
canvas.translate(X0[i],Y0[i]);
symbol0[i].draw(canvas);
canvas.restore();
}
// While a symbol is being dragged, display its x and y coordinates in a readout
if(isDragging){
paint.setColor(TEXT_COLOR);
canvas.drawText("Instance "+instanceSelected, 200, 12, paint);
canvas.drawText("X = "+X[instanceSelected], 200, 27, paint);
canvas.drawText("Y = "+Y[instanceSelected], 200, 42, paint);
}
}
}
Now if you execute this on a device or emulator, you should be able to drag multiple instances of each symbol onto the stage and reposition these instances at will. The following figures illustrate.
![]() |
![]() |
For this case maxInstances = 12. In the left figure the user has already dragged 6 assorted instances onto the stage and is presently dragging the 7th (the red square near the lower left). The readout gives the instance number (counted from 0) and the position as the red square is being dragged. In the right figure the user has dragged 12 instances onto the stage and has just tried to drag another. She gets a popup warning that the maximum number of instances has been reached, and the app refuses to add another to the stage. She can still drag the 12 instances already on the stage to reposition at will.