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 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: Draggable Symbols
Company Domain:< YourNamespace >
Package Name: <YourNamespace> . draggablesymbols
Project Location: <ProjectPath> DraggableSymbols
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.

 

Setting Up the Problem

Rather than putting this example together piece by piece, we shall give the complete listing for the two classes to be used and then dissect the functionality of each. In this example, the complete layout will be done 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 will be needed. Open the course images directory and copy the files green_square.png, red_square.png, and yellow_square.png to the res/drawable directory of the DraggableSymbols project. You may then need to left-click on res/drawable in the Android Studio package explorer pane and select Refresh to get these files to appear in Android Studio. Alternatively, you can copy the files with a mouse and paste them directly into res/drawable in the project pane of Android Studio.

 

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 java/<namespace>/draggablesymbols and select New > Java Class. In the resulting popup give the name SymbolsDragger and select Class as the kind. This will create the new class file SymbolDragger.java. 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.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 SymbolDragger extends View { // Colors for background and text private static final int BACKGROUND_COLOR = Color.argb(255, 230, 230, 230); private static final int HEADER_COLOR = Color.argb(255, 210, 210, 210); 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 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++) { // 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]); } // 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); // 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); } } }

There will be some errors (things marked in red and wiggly-underlined in red) as Android studio compiles this on the fly. Ignore them. They are because the class just defined is referencing variables that have not been inserted yet in the class MainActivity.java, and that will be fixed in the next step.

 

The Class MainActivity

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


package <YourPackageIdentifier>.draggablesymbols; import android.os.Build; import android.os.Bundle; import android.graphics.Point; import android.util.Log; import android.view.Display; import android.view.ViewGroup; import android.support.v7.app.AppCompatActivity; public class MainActivity extends AppCompatActivity { 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 (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++) { // getDrawable(int id) is deprecated as of API version 22 in favor of // getDrawable(int id, Theme theme). See // https://developer.android.com/reference/android/content/res/Resources.html. // Handle as follows. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { symbolWidth[i] = this.getResources().getDrawable(symbolIndex[i],this.getTheme()) .getIntrinsicWidth(); symbolHeight[i] = this.getResources().getDrawable(symbolIndex[i], this.getTheme()) .getIntrinsicHeight(); } else { 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); } }

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 it isn't being used. This file was produced automatically by Android Studio 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, 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 yellow 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 green 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 folder. For example, the integer R.drawable.red_square is a reference to the file res/drawable/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 is instantiated 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 on GitHub at the link DraggableSymbols. Instructions for installing it in Android Studio may be found in Packages for All Projects.

Last modified: July 1, 2016


Exercises

1. Modify this app so that the user cannot drag any part of a symbol off the screen.

2. Compute the velocity of a symbol (change in position divided by change in time for small changes) as it is being dragged (in units of pixels/unit time), and output this quantitity to the screen continuously as the symbol is being dragged.

3. Compute the acceleration (change in velocity divided by change in time for small changes) of a symbol as it is being dragged [in units of pixels/(unit time)2], and output this quantitity to the screen continuously as the symbol is being dragged. The acceleration determined here and the velocity computed in the previous exercise, could be important in gaming applications, for example.


Previous  | Next  | Home