πŸ“œ ⬆️ ⬇️

Recognition of custom gestures

Recently, when developing a game for Android, I was faced with the problem of implementing work with custom gestures. The standard Android SDK has a GestureDetector class ( here is a demonstration of working with this class) , however, not all the gestures I needed were implemented, and some of them did not work as I should (onLongPress, for example, did not work only on a long touch, but also on a long touch with a finger on the screen) . In addition to games, gestures can also be used in ordinary applications. They can replace some interface elements, thus making it easier. Gestures are already used in so many applications for touch input devices and this gives us the right to assume that the user is already familiar with them. Today we are implementing in our application the recognition of long press , double touch , pinch open , pinch close and others.

Hello, Habr!

Preproduction


For an example of working with a touch screen, multitouch and gestures, I decided to implement a simple graphic editor. What should be our graphic editor? A small dragging canvas, the distance to which you can change, spreading and bringing your fingers. Also, in order not to confuse dragging the canvas and drawing on it, we must implement a mode change on a long touch. By double-tapping you can display the color selection window.
')

Application Basis


First, create the Android Application Project in Eclipse. Our project will not use any features from the new SDKs, so API 8 (Android 2.2) , for example, can be supplied as the Minimum Required SDK. MainActivity (if it is not present , then it will need to be created) we will result in the following form:

import android.os.Bundle; import android.app.Activity; public class MainActivity extends Activity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(new ApplicationView(this)); } } 


Accordingly, we need to create an ApplicationView class that will inherit from View:

 import android.view.View; import android.content.Context; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Canvas; public class ApplicationView extends View { Paint paint = new Paint(); //  ,  static float density; public ApplicationView(Context context) { super(context); density = getResources().getDisplayMetrics().density; this.setBackgroundColor(Color.GRAY); //     paint.setStrokeWidth(5*density); //     5dp } @Override protected void onDraw(Canvas canvas) { } } 


Now we need to figure out how we will display the canvas. We create a Canvas by passing it to the Bitmap parameters. In this case, when drawing something on our canvas, it is displayed on a bitmap. We also need a variable position of our canvas and the distance to it. Add several variables to the class ApplicationView:

 Canvas canvas; //  Bitmap image; //   float zoom = 500; //    Point position = new Point(50, 50); //   


In the class constructor, initialize the canvas and its contents, and also paint it with white:

 image = Bitmap.createBitmap(500, 500, Config.ARGB_4444); //    canvas = new Canvas(image); //   canvas.drawColor(Color.WHITE); //     


In the onDraw method, display it:

 canvas.translate(position.x, position.y); //   canvas.scale(zoom / 500, zoom / 500); //     canvas.drawBitmap(image, 0, 0, paint); //   


If we launch our application now, we will see a white square on a gray background.

To make better
You can enhance the application by making it fullscreen and adding landscape orientation. To do this, change the parameters of our Activity:

 <activity android:name=".MainActivity" android:label="@string/title_activity_main" android:screenOrientation="landscape"> 


And also, for full screen, change the style-files. For API 10 and below (values ​​/ styles.xml) :

 <resources> <style name="AppTheme" parent="android:Theme.Light.NoTitleBar.Fullscreen" /> </resources> 


For API 11 and higher (values-v11 / styles.xml) :

 <resources> <style name="AppTheme" parent="android:Theme.Holo.Light.NoActionBar.Fullscreen" /> </resources> 


For API 14 (values ​​/ styles-v14.xml) :

 <resources> <style name="AppTheme" parent="android:Theme.DeviceDefault.Light.NoActionBar.Fullscreen" /> </resources> 


The implementation of the touch screen


We need to start interacting with the touchscreen by overriding the onTouchEvent method (for more on working with the touchscreen here ) :

 ArrayList<Finger> fingers = new ArrayList<Finger>(); //  ,    @Override public boolean onTouchEvent(MotionEvent event) { int id = event.getPointerId(event.getActionIndex()); //   int action = event.getActionMasked(); //  if(action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) fingers.add(event.getActionIndex(), new Finger(id, (int)event.getX(), (int)event.getY())); else if(action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) fingers.remove(fingers.get(event.getActionIndex())); //  ,    else if(action == MotionEvent.ACTION_MOVE){ for(int n = 0; n < fingers.size(); n++){ //     fingers.get(n).setNow((int)event.getX(n), (int)event.getY(n)); } //checkGestures(); invalidate(); } return true; } 


We see that when you touch your finger, we add an object of the Finger type to the list of fingers. When we lift a finger, we remove this object, and when we move our fingers, we update the coordinates of all objects. As a result, the fingers list contains all the touches with the current and previous coordinates. Finger class:

 import android.graphics.Point; public class Finger { public int ID; //   public Point Now; public Point Before; boolean enabled = false; //      public Finger(int id, int x, int y){ ID = id; Now = Before = new Point(x, y); } public void setNow(int x, int y){ if(!enabled){ enabled = true; Now = Before = new Point(x, y); }else{ Before = Now; Now = new Point(x, y); } } } 


Validation
We can display all touches by simple manipulations. To do this, in the onDraw method, enter this:

 canvas.restore(); //      for(int i = 0; i < fingers.size(); i++){ //       canvas.drawCircle(fingers.get(i).Now.x, fingers.get(i).Now.y, 40 * density, paint); } 


Moving the canvas


To implement the canvas transfer, we need to call the checkGestures method, which will work with touches, in the onTouchEvent method, in the object transfer unit. The call to this method is already there, but under the comment. Uncomment it and write the method itself:

 public void checkGestures(){ Finger point = fingers.get(0); //    position.x += point.Now.x - point.Before.x; //   position.y += point.Now.y - point.Before.y; } 


You can run and drag your finger and if everything is done correctly, the canvas should be dragged behind it.

Change the distance to the canvas



To implement this gesture, you need to divide the entire contents of the method into multitouch (when there is more than one finger) and the usual touch. If it is multitouch, then we will constantly check the current distance between two fingers and the past. Change the contents of the checkGestures method:

 Finger point = fingers.get(0); if(fingers.size() > 1){ // Multitouch //     (now)   (before) float now = checkDistance(point.Now, fingers.get(1).Now); float before = checkDistance(point.Before, fingers.get(1).Before); float oldSize = zoom; //     zoom = Math.max(now - before + zoom, density * 25); //     position.x -= (zoom - oldSize) / 2; //    position.y -= (zoom - oldSize) / 2; }else{ //   position.x += point.Now.x - point.Before.x; position.y += point.Now.y - point.Before.y; } 


In this section of the code, the checkDistance method was used, which needs to be added to the ApplicationView. Here is his code:

 static float checkDistance(Point p1, Point p2){ //       return FloatMath.sqrt((p1.x - p2.x)*(p1.x - p2.x)+(p1.y - p2.y)*(p1.y - p2.y)); } 


Now when you touch two fingers and mixing / breeding them, the distance to the canvas will change. If you have an emulator, it will not work.

Mode change



We need to create a variable that will be responsible for the mode. I called it a boolean drawingMode. To implement a long touch, we have to use a method that will be called in time. There are several options for the development of events:
  1. we write our code in the onDraw method;
  2. we write our code in onTouchEvent;
  3. we create a timer and write code into it;
  4. we create a Handler and call our Runnable using the postDelayed method;

In the onDraw method, in my opinion, only the display of graphics should be performed. In onTouchEvent, you can only write with the fact that the finger will move, thereby constantly calling this method. The timer works constantly and it is a little annoying, so we will use the fourth option. In the ApplicationView class, we create a handler variable of type Handler, and also add an onTouchEvent line to the if block (if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN)) block:

 handler.postDelayed(longPress, 1000); 


Now you need to create a Runnable named longPress:

 Runnable longPress = new Runnable() { public void run() { } }; 


Now, in the Finger class, we need to create a long type wasDown variable, which will contain the time of pressing (useful for double-tapping) . In the constructor of this variable, you must set the value of System.currentTimeMillis (). We also need to add a StartPoint variable of type Point, which will contain the starting position of the finger. It must contain the value that was passed to the constructor, or the first time you call setNow. We also need to create a boolean-type enabledLongTouch variable that represents the suitability of this touch for the event we are implementing. We need to constantly check whether the finger has moved too far from the start. This functionality can be implemented in setNow. The result should be something like this:

 import android.graphics.Point; public class Finger { public int ID; public Point Now; public Point Before; public long wasDown; boolean enabled = false; public boolean enabledLongTouch = true; Point startPoint; public Finger(int id, int x, int y){ wasDown = System.currentTimeMillis(); ID = id; Now = Before = startPoint = new Point(x, y); } public void setNow(int x, int y){ if(!enabled){ enabled = true; Now = Before = startPoint = new Point(x, y); }else{ Before = Now; Now = new Point(x, y); if(ApplicationView.checkDistance(Now, startPoint) > ApplicationView.density * 25) enabledLongTouch = false; } } } 


Now in the run method of our Runnable, we can check whether this is a long touch:

 if(fingers.size() > 0 && fingers.get(0).enabledLongTouch){ fingers.get(0).enabledLongTouch = false; drawingMode = !drawingMode; vibrator.vibrate(80); } 


To make better
To make a long touch better, you need to turn on a light vibration when it is activated. To do this, simply create a vibrator variable of type Vibrator and in the constructor set its value as follows:
 vibrator = (Vibrator)context.getSystemService(Context.VIBRATOR_SERVICE); 

Important: to work with vibration in manifest, the following line should be:
 <uses-permission android:name="android.permission.VIBRATE"/> 

Then in the run method, our timer at the end of the test for a long touch can be entered:
 vibrator.vibrate(80); 


Drawing



Now we are implementing drawing on canvas and resizing the brush. To do this, we will divide each part of the checkGestures method into two more parts: the drawing mode and the normal mode. In the drawing mode when you touch, we will simply draw a line, and in the drawing mode with multitouch we will change the size of the brush. This is what the checkGestures method will look like:

 Finger finger = fingers.get(0); if(fingers.size() > 1){ float now = checkDistance(finger.Now, fingers.get(1).Now); float before = checkDistance(finger.Before, fingers.get(1).Before); if(!drawingMode){ float oldSize = zoom; zoom = Math.max(now - before + zoom, density * 25); position.x -= (zoom - oldSize) / 2; position.y -= (zoom - oldSize) / 2; }else paint.setStrokeWidth(paint.getStrokeWidth() + (now - before) / 8); }else{ if(!drawingMode){ position.x += finger.Now.x - finger.Before.x; position.y += finger.Now.y - finger.Before.y; }else{ float x1 = (finger.Before.x-position.x)*500/zoom; //   float x2 = (finger.Now.x-position.x)*500/zoom; //    float y1 = (finger.Before.y-position.y)*500/zoom; //   float y2 = (finger.Now.y-position.y)*500/zoom; canvas.drawLine(x1, y1, x2, y2, paint); //   canvas.drawCircle(x1, y1, paint.getStrokeWidth() / 2, paint); //   canvas.drawCircle(x2, y2, paint.getStrokeWidth() / 2, paint); cursor = finger.Now; } } 


In the last line I set the value to nobody cursor. It is a variable of type Point containing the coordinates of the cursor. The cursor is only needed to navigate the size of the brush. To display the cursor in the onDraw method, add:

 if(drawingMode){ int old = paint.getColor(); //    paint.setColor(Color.GRAY); //     canvas.drawCircle((cursor.x-position.x)*500/zoom, (cursor.y-position.y)*500/zoom, paint.getStrokeWidth() / 2, paint); //      paint.setColor(old); //    } 


Now we can move the canvas, zoom in, move it away, switch to drawing mode, draw, change the size of the brush. It remains only to realize the choice of color.

Color selection



The choice of color occurs after double-tapping on the screen. To do this, in ApplicationView you need to create a variable that saves the last touch on the screen and a variable that saves the coordinates of this touch. The first one will be called lastTapTime of type long, and the second one - lastTapPosition of type Point. Then change the onTouchEvent method:

 int id = event.getPointerId(event.getActionIndex()); int action = event.getActionMasked(); if(action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) fingers.add(event.getActionIndex(), new Finger(id, (int)event.getX(), (int)event.getY())); else if(action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP){ Finger finger = fingers.get(event.getActionIndex()); //    //     100,       200, //             25dp if(System.currentTimeMillis() - finger.wasDown < 100 && finger.wasDown - lastTapTime < 200 && finger.wasDown - lastTapTime > 0 && checkDistance(finger.Now, lastTapPosition) < density * 25){ //     } lastTapTime = System.currentTimeMillis(); //     lastTapPosition = finger.Now; //     fingers.remove(fingers.get(event.getActionIndex())); }else if(action == MotionEvent.ACTION_MOVE){ for(int n = 0; n < fingers.size(); n++){ fingers.get(n).setNow((int)event.getX(n), (int)event.getY(n)); } checkGestures(); } return true; 


It remains for us to implement the color selection dialog. Where a touch occurs (marked with a comment) , we write:

 Builder builder = new AlertDialog.Builder(getContext()); String[] items = {"", "", "", "", "", "", "", ""}; final AlertDialog dialog = builder.setTitle("  ").setItems(items, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { int[] colors = {Color.RED, Color.GREEN, Color.BLUE, 0xFF99CCFF, Color.BLACK, Color.WHITE, Color.YELLOW, 0xFFFFCC99}; paint.setColor(colors[which]); } }).create(); dialog.show(); 


If you run the application, you will see that by double-tapping the color selection window appears.

Conclusion



Recognizing user gestures was not so difficult. We disassembled this on the example of the implementation of a graphical editor. A similar implementation, of course, can have not only applications, but also games.

Sources of the project here .

Used information from developer.android.com . On the basis used this article.
I would also like to thank the users of AndreyMI , LeoCcoder , silentnuke and vovkab

Source: https://habr.com/ru/post/154193/


All Articles