Previous  | Next  | Home

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. As in that project, we shall make extensive use of

  1. MotionEvent, which detects and manages events on the device touchscreen.

  2. Canvas, which manages draws and redraws on the screen.

  3. Paint, which holds the style and color information about objects to be drawn on the screen.

  4. Drawable, which provides a generic API for dealing with visual resources.

  5. Display, which provides information about the size and pixel density of the device screen.

The techniques illustrated here are relatively simple but can be the basis for much more sophisticated applications involving motion events and screen animation.

 

Creating the Project in Android Studio

Following the general procedure in Creating a New Project, either choose Start a new Android Studio project from the Android Studio homepage, or from the Android Studio interface choose File > New > New Project. Fill out the fields in the resulting screens as follows,


Application Name: DragSymbols
Company Domain:< YourNamespace >
Package Name: <YourNamespace> . dragsymbols
Project Location: <ProjectPath> DragSymbols
Target Devices: Phone and Tablet; Min SDK API 15
Add an Activity: Empty Activity
Activity Name: MainActivity (check the Generate Layout File box)
Layout Name: activity_main

where your namespace should be substituted for <YourNamespace> (com.lightcone in my case) and <ProjectPath> is the path to the directory where you will store this Android Studio Project (/home/guidry/StudioProjects/ in my case). If you have chosen to use version control for your projects, go ahead and commit this project to version control.

The starting point of this will be a clone of the project DraggableSymbols that we already completed, which we will produce by copying some files from the earlier project to this one.

 

Copying Resources

First we copy some resources. Follow the instruction in the Copying Resources subsection of DraggableSymbols to copy the files green_square.png, red_square.png, and yellow_square.png to the res/drawable directory of the DragSymbols project. (Or you can copy them directly from res/drawable in the DraggableSymbols project and paste them into the corresponding directory in DragSymbols using Android Studio.)

 

Import the Class Files from the Previous Project

Import the classes SymbolDragger and ActivityMain from the DraggableSymbols project completed earlier:

  1. Open the project DraggableSymbols in a separate window in Android Studio.

  2. Copy the files MainActivity.java and SymbolDragger.java from app/java/com.lightcone.draggablesymbols with the mouse.

  3. Paste the copied files into app/java/com.lightcone.dragsymbols in DragSymbols. (You will get a warning that you are going to overwrite the existing file MainActivity.java with the new one; chose Overwrite.)

Now you can close the DraggableSymbols project. Android Studio will probably do it automatically, but check to be sure in DragSymbols that the two new files have the correct package name for this project in the first line (package com.lightcone.dragsymbols;). DragSymbols should compile without errors and if you now execute this on a device or AVD you should get the same results as for the project DraggableSymbols, since at this point we have simply reproduced that project with a new name.

 

Renaming SymbolDragger to DragginSlayer

Before proceeding, let's use the global refactoring facility of Android Studio to change the name of the class SymbolDragger. (This is done for clarity, since we are going to alter its content substantially from that of the corresponding class in DraggableSymbols, and to illustrate an important technique.)

  1. Right-click on java/com.lightcone.dragsymbols/SymbolDragger and select Refactor > Rename.

  2. On the resulting screen, insert DragginSlayer for the new name, keep the default checked boxes, and click Refactor.

  3. Android Studio should now consistently and systematically change all SymbolDragger references to the new name DragginSlayer, throughout the project.

Confirm that renaming has not changed any functionality and everything works as before by executing the app. You should find the same result as before. Now let's add some additional functionality.

 

Creating a Source of Symbols

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, DragginSlayer must be modified so that symbols remains at fixed positions when we drag copies of them 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 <YourPackageIdentifier>.dragsymbols; import android.content.Context; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.drawable.Drawable; import android.os.Build; 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 */ public class DragginSlayer extends View { // Colors for background and text private static final int BACKGROUND_COLOR = Color.argb(255, 210, 210, 210); private static final int HEADER_COLOR = Color.argb(255, 190, 190, 190); private static final int TEXT_COLOR = Color.argb(255, 0, 0, 0); private int numberSymbols; // Total number of symbols to use private Drawable[] symbol; // Array of symbols (dimension numberSymbols) private float[] X; // Current x coordinate, upper left corner of symbol private float[] Y; // Current y coordinate, upper left corner of 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 int[] symbolWidth; // Width of symbol private int[] symbolHeight; // Height of symbol private float[] lastTouchX; // x coordinate of symbol at last touch private float[] lastTouchY; // y coordinate of symbol at last touch private int symbolSelected; // Index of symbol last touched (-1 if none) private Paint paint; // Following define upper left and lower right corners of display stage rectangle private int stageX1 = 0; private int stageY1 = MainActivity.topMargin; private int stageX2 = MainActivity.screenWidth; private int stageY2 = MainActivity.screenHeight; private boolean isDragging = false; // True if some symbol is being dragged // Simplest default constructor. Not used, but prevents a warning message. public DragginSlayer(Context context) { super(context); } 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 symbol positions with the initial // positions passed as arguments in the constructor this.X = X; this.Y = Y; 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]; // Handle method getDrawable deprecated as of API 22 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Theme required but set to null since no styling for it symbol[i] = context.getResources().getDrawable(symbolIndex[i],null); } else { 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(36); 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. Will be -1 if not within the current bounds of some // symbol. symbolSelected = -1; // 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. Note that we are simply // illustrating how to drag symbols. In an actual application, // you would probably want to add some logic to confine the symbols // to a region the size of the visible stage or smaller. 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 locations for (int i = 0; i < numberSymbols; i++) { canvas.save(); canvas.translate(X[i], Y[i]); symbol[i].draw(canvas); canvas.restore(); } isDragging = false; } // 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(); } // If dragging a symbol, display its x and y coordinates in a readout if (isDragging) { paint.setColor(TEXT_COLOR); canvas.drawText("X = " + X[symbolSelected], MainActivity.screenWidth / 2, MainActivity.topMargin / 2 - 10, paint); canvas.drawText("Y = " + Y[symbolSelected], MainActivity.screenWidth / 2, MainActivity.topMargin / 2 + 25, 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.

 

Trying It Out

If you try it in an emulator or on a phone you should get something similar to the following figures.



where 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, in the gray bar at the top of the stage.

 

Allowing for Multiple Instances of Each Symbol

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

  1. We must keep track separately of the "source" images positioned offstage, which do not move, and instances of these that we drag onto the stage and can move around. We use the arrays symbol0, X0, and Y0 to keep track of the source drawables and positions, respectively, and use source, X, and Y to keep track of the corresponding quantities for the instances dragged onstage. The dimension of the source arrays is set by the arrays passed through the constructor; the dimension of the instance arrays is set by the variable maxInstances. The currently selected index in the source arrays is tracked by symbolSelected and the corresponding index in the instance arrays is tracked by instanceSelected. If a touch is not on a source symbolSelected = -1, and if a touch is not on an already-created instance, instanceSelected = -1.

  2. We continue to create the offstage source objects source0 in the constructor since their number does not change, but now we must move the creation of the instance objects source to the touchEvent handlers, since the instances are created dynamically by dragging a source onstage.

  3. Within the event handlers we must now distinguish between a touch and drag on an offstage source, which creates a new instance and then drags it to a position on the stage, and a touch and drag on an instance already onstage, which just moves that instance to a new position without creating a new instance.

  4. We should not allow the user to create more than maxInstances total instances, and warn the user when the maximum number has been reached.

The modified form of DragginSlayer.java that accomplishes these tasks is illustrated in the following code listing.


package <YourPackageIdentifier>.dragsymbols; 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.os.Build; 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 */ public class DragginSlayer extends View { // Colors for background and text private static final int BACKGROUND_COLOR = Color.argb(255, 210, 210, 210); private static final int HEADER_COLOR = Color.argb(255, 190, 190, 190); private static final int TEXT_COLOR = Color.argb(255, 0, 0, 0); private static final int maxInstances = 12; // Max symbol instances permitted onstage private int numberSymbols; // Total number of symbols to use private int numberInstances; // Total number of symbol instances onstage private Drawable[] symbol; // Array of symbols (dimension numberSymbols) private int[] symbolIndex; // Index of Drawable resource (R.drawable.symbol) private float[] X; // Current x coordinate, upper left corner of symbol private float[] Y; // Current y coordinate, upper left corner of 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 int[] symbolWidth; // Width of symbol private int[] symbolHeight; // Height of symbol private float[] lastTouchX; // x coordinate of symbol at last touch private float[] lastTouchY; // y coordinate of symbol at last touch private int symbolSelected; // Index of symbol last touched (-1 if none) private int instanceSelected; // Index of symbol instance last touched (-1 if none) private Paint paint; // Following define upper left and lower right corners of display stage rectangle private int stageX1 = 0; private int stageY1 = MainActivity.topMargin; private int stageX2 = MainActivity.screenWidth; private int stageY2 = MainActivity.screenHeight; private boolean isDragging = false; // True if some symbol is being dragged private Context context; // Simplest default constructor. Not used, but prevents a warning message. public DragginSlayer(Context context) { super(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 counter numberInstances = 0; // Set up local arrays defining symbol positions with the initial // positions passed as arguments in the constructor 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++) { // Handle method getDrawable deprecated as of API 22 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Theme required but set to null since no styling for it symbol0[i] = context.getResources().getDrawable(symbolIndex[i], null); } else { 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(36); 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. */ // See android.view.View#onTouchEvent(android.view.MotionEvent) @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. Will be -1 if not within the current bounds of some // symbol. symbolSelected = -1; // -1 if 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; // Warn if max number of instances has been reached (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(); } break; } } // Determine if touch within bounds of one of the symbol instances onstage for (int i = 0; i < numberInstances; i++) { int width = symbol[i].getIntrinsicWidth(); int height = symbol[i].getIntrinsicHeight(); if ((x > X[i] && x < (X[i] + width)) && (y > Y[i] && y < (Y[i] + height))) { instanceSelected = i; // Index of instance touched break; } } // If touch within bounds of 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 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) { // Handle method getDrawable deprecated as of API 22 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { // Theme required but set to null since no styling for it symbol[numberInstances] = context.getResources().getDrawable(symbolIndex[symbolSelected], null); } else { 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 object selected. Note that we are simply // illustrating how to drag symbols. In an actual application, // you would probably want to add some logic to confine the symbols // to a region the size of the visible stage or smaller. 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 locations for (int i = 0; i < numberInstances; i++) { canvas.save(); canvas.translate(X[i], Y[i]); symbol[i].draw(canvas); canvas.restore(); } isDragging = false; } // 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(); } // If dragging a symbol, display its instance number and x and y coordinates as dragged if (isDragging) { paint.setColor(TEXT_COLOR); canvas.drawText("Instance " + instanceSelected, MainActivity.screenWidth/2, MainActivity.topMargin/2 - 50, paint); canvas.drawText("X = " + X[instanceSelected], MainActivity.screenWidth/2, MainActivity.topMargin/2 + 0, paint); canvas.drawText("Y = " + Y[instanceSelected], MainActivity.screenWidth/2, MainActivity.topMargin/2 + 50, 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 instances already on the stage at will. The following figures illustrate.



For this case maxInstances = 12 has been chosen. 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. Any of the 12 instances already on the stage can be dragged to reposition it, but the code logic forbids adding new instances once the limit has been reached.

It would be a relatively simple matter to add logic to the code permitting the user to remove instances from the stage (for example, upon a shift-click on a symbol instance). We leave that as an exercise.


The complete project for the application described above is archived on GitHub at the link DragSymbols. Instructions for installing it in Android Studio may be found in Packages for All Projects.

Last modified: July 2, 2016


Exercises

1. Add logic to DragginSlayer.java permitting the user to remove instances from the stage after they have been added by double-tapping on a symbol instance. Adjust the bookkeeping accordingly so that the number of instances onstage at any one time is still limited by maxInstances.


Previous  | Next  | Home