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

Create a new project in Eclipse by selecting New > Android Project (or File > Project > Android > Android Project > Next) and filling in the following information on the screen that results.

Click Next and then accept the defaults on the remaining screens to create the project DragSymbols.

 

Copying Resources

First we copy some resources. Open the 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. Right-click on res/drawable-hdpi in the Package Explorer pane of Eclipse and select Refresh, which should cause these image resources to appear under drawable-hdpi.

 

Import the SymbolDragger Class from the Previous Project

Import the class SymbolDragger from the DraggableSymbols project:

  1. Right-click on src/<namespace>dragsymbols,

  2. select Import > General > File System, then

  3. click Next and Browse.

  4. 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 <YourPackageIdentifier>.dragsymbols;

which gives it the appropriate package name for this project. Eclipse will still indicate several errors, but that is because of some variables in MainActivity.java that we have note defined yet. Let's fix that.

 

Edit the MainActivity Class

Edit the file MainActivity.java to give it the content

   package <YourPackageIdentifier>.dragsymbols;
    
   import android.os.Bundle;
   import android.app.Activity;
   import android.graphics.Point;
   import android.util.Log;
   import android.view.Display;
   import android.view.Menu;
   import android.view.ViewGroup;
   
   public class MainActivity extends Activity {
   
         private static final String TAG = "Dragger";
   
         // Screen dimensions and positioning offsets
         public static int screenWidth;
         public static int screenHeight;
         public static int topMargin = 0;
         private static final int xoff = 2;
         private static final int yoff = 2;
         private static final int xgap = 1;
   
         @Override
         protected void onCreate(Bundle savedInstanceState) {
                  super.onCreate(savedInstanceState);
   
                  // Get the screen dimensions
                  Display display = getWindowManager().getDefaultDisplay();
                  Point size = new Point();
                  display.getSize(size);
                  screenWidth = size.x;
                  screenHeight = size.y;
   
                  Log.i(TAG, "Screen width=" + screenWidth + " height=" + screenHeight);
   
                  // Put the reference to the symbols to be used in an array.
                  // The Drawable corresponds 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 };
   
                  int numberSymbols = symbolIndex.length; // Total number of symbols to
                  // use
                  int[] symbolWidth = new int[numberSymbols]; // Width of symbol in pixels
                  int[] symbolHeight = new int[numberSymbols]; // Height of symbol in
                  // pixels
   
                  // Determine the height and width of the symbols for positioning issues
   
                  for (int i = 0; i < numberSymbols; i++) {
   
                           symbolWidth[i] = this.getResources().getDrawable(symbolIndex[i])
                           .getIntrinsicWidth();
                           symbolHeight[i] = this.getResources().getDrawable(symbolIndex[i])
                           .getIntrinsicHeight();
                           Log.i(TAG, "Symbol width=" + symbolWidth[i] + " height="
                                          + symbolHeight[i]);
   
                           // Set top margin (header) area equal to height of tallest symbol
                           if (topMargin < symbolHeight[i])
                                 topMargin = symbolHeight[i];
                  }
   
                  // Initial location of symbols. Coordinates are measured from the upper
                  // left corner of the screen, with x increasing to the right and y
                  // increasing downward.
   
                  // Initial x coord in pixels for upper left corner of symbol
                  float[] X = new float[numberSymbols];
                  X[0] = xoff;
                  X[1] = xoff + xgap + symbolWidth[0];
                  X[2] = xoff + 2 * xgap + symbolWidth[0] + symbolWidth[1];
   
                  // Initial y coord in pixels for upper left corner of symbol
                  float[] Y = new float[numberSymbols];
                  Y[0] = Y[1] = Y[2] = yoff;
   
                  /*
                  * 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 initial content and position defined by the above arrays on the
                  * screen.
                  */
   
                  DragginSlayer view = new DragginSlayer(this, X, Y, symbolIndex);
                  view.setLayoutParams(new ViewGroup.LayoutParams(
                                 ViewGroup.LayoutParams.MATCH_PARENT,
                                 ViewGroup.LayoutParams.MATCH_PARENT));
                  setContentView(view);
         }
   
         @Override
         public boolean onCreateOptionsMenu(Menu menu) {
                  // Inflate the menu; this adds items to the action bar if it is present.
                  getMenuInflater().inflate(R.menu.main, menu);
                  return true;
         }
   }

which is the same as MainActivity.java in the DraggableSymbols project except for the package name. This should cause all the red x's to disappear (if they don't immediately, try Project > Clean > Clean all projects). If you now execute this you should get the same results as for the project DraggableSymbols, since at this point we have essentially reproduced that project.

 

Renaming SymbolDragger to DragginSlayer

Before proceeding, let's use the global refactoring 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, and to illustrate a technique.)

  1. Right-click on SymbolDragger.java and select Refactor > Rename.

  2. On the resulting screen, insert DragginSlayer for the New name, and confirm that the Update reference box is checked and all other boxes are unchecked.

  3. Click Finish.

  4. Eclipse 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 doing a clean build (Project > Clean) and executing the app.

 

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, 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 <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.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
         private static final int BACKGROUND_COLOR = Color.argb(255, 200, 200, 200);
         private static final int HEADER_COLOR = Color.argb(255, 100, 100, 100);
         private 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 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];
                           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(18);
                  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 + 20, 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, off 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 listing.

   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.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 API 13 (Android 3.2) or above because of some specific methods used.   
   */
   
   public class DragginSlayer extends View {
   
         // Colors for background and text
         private static final int BACKGROUND_COLOR = Color.argb(255, 200, 200, 200);
         private static final int HEADER_COLOR = Color.argb(255, 100, 100, 100);
         private static final int TEXT_COLOR = Color.argb(255, 255, 255, 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++) {
                           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(18);
                  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.
            */
   
         /* (non-Javadoc)
            * @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 symbol and instance indices 
   
                           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){ 
   
                                 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 x and y coordinates as dragged
                  if (isDragging) {
                           paint.setColor(TEXT_COLOR);
                           canvas.drawText("Instance "+instanceSelected,
                                          MainActivity.screenWidth / 2,
                                          MainActivity.topMargin / 2 - 15, paint);
                           canvas.drawText("X = " + X[instanceSelected],
                                          MainActivity.screenWidth / 2,
                                          MainActivity.topMargin / 2 + 5, paint);
                           canvas.drawText("Y = " + Y[instanceSelected],
                                          MainActivity.screenWidth / 2,
                                          MainActivity.topMargin / 2 + 25, 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 we have chosen 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 one of the 12 instances already on the stage 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 at the link DraggableSymbols2. Instructions for installing it in Eclipse may be found in Packages for All Projects.

Last modified: April 4, 2014


Exercises

1. Add logic to DragginSlayer.java permitting the user to remove instances from the stage after they have been added by shift-clicking 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