Previous  | Next  | Home

Draggable Symbols I


 

For many applications in design, modeling, games, and so on, it is important for the user to be able to drag symbols from one part of the display screen to another. For example, in touchscreen Android phones it is common to move application icons to different positions or different folders by long-pressing (holding down longer than a tap) on the icon with a finger, dragging it to the new location, and then raising your finger. In this project we develop some techniques for dragging arbitrary user-defined symbols around the screen. The primary technology that we shall use involves

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

  2. The Canvas class, which manages draws and redraws on the screen (see the Canvas and Drawables developer guide).

  3. The Paint class, 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. The class Display, which provides information about the size and pixel density of the device screen to enable a screen layout that adapts to different devices.

Although the examples that we shall discuss here and in the following Draggable Symbols II are relatively simple, they provide an introduction to implementing motion events and screen animation that can be the basis for much more sophisticated applications.

 

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


Almost everything that we shall do will work under any version of Android but we are going to use some methods to query the properties of the device screen that were introduced in Android 3.2. Thus the minimum SDK has been set to API Level 13.

 

Setting Up the Problem

Rather than putting this example together piece by piece, we shall give the complete listing for the two classes that we shall use and then dissect the functionality of each. In this example, we do the complete layout using Java (there will be no XML files needed at all in the res/layout directory), so all the action will involve just two Java classes and some Drawable image resource files that they interact with.

 

Copying Resources

Let's first copy some resources that we will use. 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 DraggableSymbols project. You may then need to left-click on res/drawable-hdpi in the Eclipse Package Explorer pane and select Refresh to get these files to appear in Eclipse.

 

The Class SymbolDragger

The primary purpose of the MainActivity class will be to set up some resource arrays and then to pass them to an instance of the class SymbolDragger, which will do all the work. So let's create SymbolDragger. Right-click on src/<namespace>/draggablesymbols and select New > Class to create the new class file SymbolDragger.java, with superclass android.view.View. Edit this file so that it has the content

   package <YourNamespace>.draggablesymbols;
   
   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 SymbolDragger 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 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 SymbolDragger(Context context) {
                  super(context);
         }
   
         public SymbolDragger(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;
                  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++) {
                           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]);
                  }
   
                  // 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);
   
                  // 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);
                  }
         }
   }

This class should compile without error.

 

The Class MainActivity

Now edit the file MainActivity.java in src/<namespace>/draggablesymbols so that it reads

   package <YourPackageIdentifier>.draggablesymbols;
   
   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.
                  */
   
                  SymbolDragger view = new SymbolDragger(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;
         }
   
   }

This class also should compile without error.


It isn't essential, but to emphasize that our layout has been created entirely in Java, you can go to the res/layout directory of the project and delete the activity_main.xml file, since we are not using it. This file was produced automatically by Eclipse when the project was created as the default layout for the initial screen of the application, but we changed the view in MainActivity set by the argument of setContentView() from the default R.layout.activity_main to our own Java-created view object.

 

Take It Out for a Spin

If you now execute on an emulator or device by right-clicking on the project and selecting Run As > Android Application (or selecting the Run dropdown menu at the top), you should get screens like the following figures,



The left figure corresponds to the initial state, before anything has been dragged. In the right figure the green and red symbols already have been dragged to new positions by touching with the finger on a touchscreen and dragging to the new position. The yellow symbol is in the process of being dragged to a new position, so a dynamically updated readout of its position in pixels is being displayed as it is being dragged. Also, you should find that nothing happens if you press or drag on the screen background instead of one of the symbols.

 

How It Works

The two classes defined in this project are heavily commented, so you should be able to understand what the code is doing by reading through it with systematic attention to the documentation. But some of the comments are a little terse, so let's give a somewhat less abridged version here.

 

MainActivity

The class MainActivity is the entry point for the app (notice in AndroidManifest.xml that it is the only Activity registered), and extends Activity, as is normal. However, we have replaced the usual specification of the initial screen layout through an XML resource file by the SymbolDragger instance view, which is subclassed from View, in the last lines of the onCreate() method. Thus our layout is going to be defined by the Java class SymbolDragger described below.

We also have set up some arrays in MainActivity that will hold the initial positions of the draggable symbols that we wish to place on the screen (the arrays X and Y, which hold pixel coordinates as floats). Finally, we have set up an array symbolIndex that holds the integer references to the Drawable image resources that we placed in the res/drawable-hdpi folder. For example, the integer R.drawable.red_square is a reference to the file res/drawable-hdpi/red_square.png that the class SymbolDragger will use to access this image resource. These arrays of data are passed as arguments to the constructor of SymbolDragger that we instantiate as the screen view.

 

SymbolDragger

The Java class SymbolDragger, through the view instance created by MainActivity, does most of the work. It

Let's now describe briefly the main blocks of code that accomplish these tasks in SymbolDragger.

  1. The SymbolDragger constructor copies the arrays passed to it from MainActivity into its own local arrays and uses that information to construct additional arrays of information like the dimensions of images, and then uses that to produce the array symbols, which contains objects of type Drawable that will correspond to the visual representation of symbols to be displayed and manipulated onscreen. Finally the constructor sets up the Paint object that will control formatting during screen redraws.

  2. The processing of events is implemented by overriding the onTouchEvent(MotionEvent ev) method inherited from View, which processes touch-screen motion events. These correspond to objects of the MotionEvent class, which are used to report movement (mouse, pen, finger, trackball) events. TheMotionEvent 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:


  3. The class then uses a switch statement on these possible MotionEvent constants returned by getAction() to process the different possibilities.


  4. The onDraw(Canvas) method inherited from View is overriden to define the look of the screen. 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. Basically, onDraw calls drawBackground to draw all of the screen except the draggable symbols, and then draws all the onscreen draggable symbols at their current positions by using the translate(dx,dy) method of Canvas to translate by dx in the x coordinate and dy in the y coordinate from their previous position.

  5. The drawBackground(canvas, paint) method has the task of drawing everything except the draggable symbols. It first draws the header bar and then the background for the main stage, and finally draws a coordinate readout on the screen if one of the symbols is currently being dragged. Since drawBackground is called before any draggable symbols are drawn, the symbols will always be above anything in the background.

When to redraw the screen is under the control of Android, but the invalidate() method of View requests a redraw (through a call to onDraw(Canvas), if the screen is visible) as soon as possible.


The View method invalidate() marks the entire screen as needing a redraw. The overloaded form invalidate(Rect dirtyRectangle) marks only the portion of the screen defined by the Rect dirtyRectangle as "dirty" and in need of a redraw. Use of this latter form can be important in achieving rapid and smooth response if only a fixed portion of a complex display is changing because of some action. In our example, we have only a few symbols that can be dragged anywhere on the screen, so we request a redraw of the entire screen each time the status of a symbol changes.

This completes the basic demonstration of how to drag some symbols around the screen. We shall now go on in Draggable Symbols II to add some additional features.


The complete project for the application described above is archived at the link DraggableSymbols. Instructions for installing it in Eclipse may be found in Packages for All Projects.

Last modified: April 4, 2014


Previous  | Next  | Home