Touch

Duration

15 minutes

Tip: If you are doing this exercise live in a session, make sure to make good use of the instructor, they are online to answer any questions you have!

Goals

In this lab, you will code a multi-touch drawing application that will let you draw several lines at the same time. Each line will use a randomly-generated color. The screenshot below shows the finished app with a simple user-created drawing.

Required assets

The Resources folder for this part contains a Start solution you will use as starter code and a Completed solution you can use to check your work.

Challenge

Use the high-level guidelines here to complete the exercise. Detailed instructions are provided below if you would like more information.

The starter project contains a single Activity that displays a custom View. You'll add code to the custom View to track and visualize simultaneous touch events.

  1. Open the XamPaint solution in the Start folder from the Resources for this part.
  2. Override the OnTouchEvent method. All your code will be added in this method.
  3. Create a switch statement based on ActionMasked.
  4. Add cases to the switch for each of the 5 Actions you need to handle.
  5. At the end of each case, you will need to return true to indicate you have handled the event.
  6. Add a default case that returns false.
  7. When a finger touches the screen: create a new Path, move the path to the initial X/Y point, and add the path to dictionary of current lines and the list of all lines. Use the pointer ID as the dictionary key. You will also need to create a new Paint object using the supplied helper method and add it to the List of Paints.
  8. When you receive a Move action, update all current lines so they include their new X/Y point.
  9. When a finger leaves the screen, remove the associated Path from the dictionary.
  10. Run the app to test your work.

Steps

You can either use the above guidelines or follow the step-by-step instructions shown here.

Open the XamPaintMultiTouch start solution

  1. Open the Start solution in the Resources folder for this part.
  2. Open PaintView.cs, all your work will be done in this file.
  3. Locate the Dictionary field. It will store the lines that are currently "in process" (i.e. the ones the user is currently drawing). You will add a path to this dictionary when the user starts drawing a new line and remove it when they lift that finger off the screen. Notice that the dictionary maps from int to Path. You will use the pointer ID as the dictionary key and the path as the value. This will let you easily lookup a path using the pointer ID.
  4. Locate the List field that stores Path objects. This will hold all the lines the user has drawn during the current session. You will add a path to it whenever a new finger contacts the screen. You will not need to remove lines from the list - they only need to be removed when the user clicks the Clear button and that code has been provided for you. Feel free to examine the implementation of the Clear method to see this.
  5. Locate the List field that stores Paint objects. This will hold the paint colors used for each line. You will use the supplied GenerateRandomColorPaint method to create a new Paint whenever a new finger contacts the screen. You will then add the Paint to this list. You will not need to remove Paints from the list - they only need to be removed when the user clicks the Clear button and that code has been provided for you. Feel free to examine the implementation of the Clear method to see this.
  6. Locate the OnDraw method. Notice that it redraws all lines when anything changes. This technique was selected intentionally even though is not the most efficient solution. It is simple to code and keeps us focused on the touch events rather than adding extra code to optimize the drawing performance.

Override OnTouchEvent

  1. Override the OnTouchEvent method in the PaintView class. All your work will done inside this method.
  2. Add a switch statement based on the ActionMasked property of the MotionEvent parameter.
  3. Add cases for the MotionEventActions values Down, PointerDown, Move, PointerUp and Up.
  4. At the end of each case, you will need to return true to indicate you have handled the event.
  5. Add a default case that returns false.

Show Code

Create a new Path and Paint when a finger touches the screen

When a finger touches the screen, you will create a new Path object for the finger, add the starting point, and store it in both the dictionary and the list. You will also need to create and store a Paint object for this line. These operations apply to both the Down and PointerDown cases.

  1. Begin in the MotionEventActions.Down case. In this case, you know there is just one finger on the screen and information about it is at index 0.
  2. Get the current pointer ID using the GetPointerId method on the MotionEvent, passing in 0.
  3. Get the X and Y coordinates of the finger using the GetX and GetY methods, passing in 0.
  4. Create a new Path object.
  5. Call MoveTo on the path and pass it the X/Y coordinates.
  6. Add the path to the Dictionary using the pointer ID as the key.
  7. Add the path to the List of Path objects.
  8. Use the GenerateRandomColorPaint method to create a Paint object.
  9. Add the Paint to the List of Paint objects.
  10. Repeat the above steps for the MotionEventActions.PointerDown case. This case is used when a new finger contacts the screen while there are other fingers present. The only difference between the this case and the last one is that here you must use ActionIndex in your calls to GetPointerId, GetX, and GetY. The code for the two cases is nearly identical. If you have time, feel free to refactor the code to remove the repeated parts. Note that the solution code does not do the refactoring in order to keep it simple.

Show Code

Update lines when pointers move

When a Move action occurs, you need to update all the current Paths with the new X/Y points. Remember that a Move action reports the updated position of all fingers currently touching the screen.

  1. Iterate through every active pointer using a for loop and the PointerCount property on the MotionEvent.
  2. Retrieve the pointer ID for the current finger using the GetPointerId method. Use the loop variable from your for loop as the parameter.
  3. Retrieve the X and Y coordinates for the current finger using the GetX and GetY methods. Use the loop variable from your for loop as the parameter.
  4. Retrieve a Path from the Dictionary using the pointer ID as the key.
  5. Call LineTo on the Path and pass it the X/Y coordinates.
  6. Outside the loop, call Invalidate to force a redraw of the view.

Show Code

Respond to the Up actions

When a finger leaves the screen, you need to remove its associated Path from the Dictionary. You will be working in the MotionEventActions.Up and MotionEventActions.PointerUp cases. The only difference between the two cases is the parameter you use to get the pointer ID: for MotionEventActions.Up you use 0 and for MotionEventActions.PointerUp you use the ActionIndex.

  1. Find the pointer ID using the MotionEvent's GetPointerId method, passing in 0 or the ActionIndex as appropriate.
  2. Use the ID to remove the Path from the Dictionary; remember the ID is the dictionary key.
  3. Run the app to test your work.

Show Code

Optimize drawing (Optional)

The steps below outline how to use a bitmap to reduce the workload in the OnDraw callback. This requires some knowledge of the Canvas and Bitmap classes as well as many changes to the current code. The guidance provided here is not complete in every detail; however, a full solution is available in the Completed_OptimizedDrawing folder. Due to time constraints, this part is intended to be an after-class exercise.

The current implementation redraws all the lines whenever any of them change. As a performance optimization, you could draw the lines into a bitmap as the user creates them and then copy the entire bitmap to the screen in OnDraw.

  1. Begin with the solution to the previous part. All your work here will be done in the PaintView class.
  2. Remove the Dictionary and List fields that stored the Path objects and all code that used the Paths throughout the PaintView class. The new drawing technique will not use Path.
  3. Add a Dictionary<int, Paint> field named paints. Initialize it to a new instance on the line where you declare it.
  4. A Bitmap is a grid of pixels. It has a width and a height. Each pixel can be addressed using an X/Y coordinate pair of integers. Each pixel has a color specified using an integer. Bitmap provides methods to get/set the values of individual pixels. There are also a few methods to work with a group of pixels simultaneously. Add a Bitmap field named drawingSurface to the PaintView class.
  5. A Canvas is a collection of drawing methods. Canvas does not have an intrinsic destination for the drawing; you need to supply a Bitmap when you create the Canvas. For example, Canvas has a DrawLine method that will calculate which pixels in the underlying Bitmap should be modified and set those pixels for you. Add a Canvas field named drawingCanvas to the PaintView class.
  6. A MotionEvent.PointerCoords object is a container for an X/Y coordinate pair. It gives you a convenient way to store the X/Y location of a touch event. This implementation will use a Dictionary of PointerCoords objects to remember the last X/Y coordinate pair for each finger currently on the screen. If we know the previous X/Y coordinates and the current X/Y coordinates, we will be able to draw a line between the two points. Add a Dictionary<int, MotionEvent.PointerCoords> field named coords to the PaintView class. Initialize it to a new instance on the same line where you declare it.
  7. To create a Bitmap, you use one of the static Bitmap.Create methods. To create a Canvas, you use new and pass a Bitmap to the constructor. Add the following method to your PaintView class. The Bitmap it creates will match the screen dimensions. The code is inside the OnSizeChanged method so it re-runs when the device is rotated. Note that our implementation will not preserve the user's drawing across orientation changes.
  8. protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
    {
      base.OnSizeChanged(w, h, oldw, oldh);
      drawingSurface = Bitmap.CreateBitmap(w, h, Bitmap.Config.Argb8888); // full-screen bitmap
      drawingCanvas  = new Canvas(drawingSurface); // the canvas will draw into the bitmap
      paints.Clear();
      coords.Clear();
    }
    
  9. Modify the Down and PointerDown cases in your OnTouchEvent method. The strategy is to locate the X/Y coordinate of the new touch point and add it to the coords Dictionary. The key will be the pointer ID. You should also create a new Paint object and add it to the paints dictionary. The code for Down is shown below. The code for PointerDown analogous so it is not shown here.
  10. case MotionEventActions.Down:
    {
      int id = e.GetPointerId(0);
      paints.Add(id, GenerateRandomColorPaint());
      var start = new MotionEvent.PointerCoords();
      e.GetPointerCoords(id, start);
      coords.Add(id, start);
      return true;
    }
    
  11. Modify the Move case in your OnTouchEvent method. The goal is to retrieve the new X/Y coordinates for each finger, grab the previous X/Y coordinates from the coords Dictionary and use the Canvas's DrawLine method to draw a line between those two points. Then update the X/Y coordinates in the coords Dictionary to the new values. The core of the code is shown below (this code will be the body of the loop).
  12. var id = e.GetPointerId(index);
    float x = e.GetX(index);
    float y = e.GetY(index);
    drawingCanvas.DrawLine(coords[id].X, coords[id].Y, x, y, paints[id]);
    coords[id].X = x;
    coords[id].Y = y;
    
  13. Modify the PointerUp and Up cases in your OnTouchEvent method. Remove the Paint object from its Dictionary. Remove the PointerCoords object from its Dictionary.
  14. Replace your implementation of the OnDraw method with the one given below. This copies your Bitmap into the Canvas supplied by Android. This should be more efficient than redrawing all the lines as we did in the previous implementation.
  15. protected override void OnDraw(Canvas canvas)
    {
      canvas.DrawBitmap(drawingSurface, 0, 0, null);
    }
    
  16. Replace your implementation of the Clear method with the one given below. This overwrites all the pixels in the Bitmap and then forces a redraw of the screen.
  17. public void Clear()
    {
      drawingCanvas.DrawColor(Color.Black, PorterDuff.Mode.Clear);
      Invalidate();
    }
    
  18. Run the app to test your work. It should behave identically to the previous implementation from the user's perspective.

Summary

During this lab, you applied what we have learned about multi-touch in Android to track and visualize touch interactions within a custom view.

Go Back