Touch-sensitive graphics

In Japan we drew a circle in a big white View. Now let’s make the view touch-sensitive. When we touch it, the circle will jump to the point where we touched. See Input Events.

A Button should have a View.OnClickListener containing an onClick method; see Button. Any View can have a View.OnTouchListener containing an onTouch method. The onClick method of the View.onClickListener took only one argument, because we don’t care about the x, y coördinates within a button where the button was clicked. But the onTouch method of the View.OnTouchListener takes two arguments, because we do care about the coördinates.

Source code in Touch.zip

  1. MainActivity.java. The MainActivity creates a big white TouchView.
  2. TouchView.java. We plug a View.OnTouchListener into the TouchView to make the TouchView touch-sensitive.
  3. activity_main.xml. Unused and ignored.
  4. AndroidManifest.xml
  5. build.gradle (Module: app)

Create the project

Create a subclass of class View named TouchView. See Japan for creating a subclass of class View.

Things to try

  1. In place of the field
        private PointF point = new PointF();   //holds 2 floats
    
    in class TouchView, would it be simpler to have the following two fields?
        private float x, y;	//coordinates of pixel where finger touched
    
    Try it, but change it back when you’re done. I want to keep the data structure as high-level as possible.

  2. Let the circle’s initial position be at the center of the big white view. Add the onLayout method to class TouchView. This method is called automatically by we-don’t-know-who whenever the view is resized. This includes the time when the view is given its initial size, sometime between the view’s construction and the first call to onDraw. The methods getWidth and getHeight return zero before the first call to onLayout, so we can’t call them in the constructor.

    Open the file TouchView.java in Android Studio.
    Code → Generate… → Override Methods
    Press the Sort Alphabetically (↓a–z) button and select onLayout.

        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            point.set(getWidth() / 2f, getHeight() / 2f);
        }
    

  3. Let’s create a new circle each time we tap, without removing the old circle(s).
    1. In class TouchView, change the field
          private PointF point = new PointF();
      
      to the following. An ArrayList<PointF> is like an expandable array of PointFs.
          private ArrayList<PointF> points = new ArrayList<PointF>();
      
    2. Remove the onLayout method you added in the previous exercise.
    3. In the onTouch method of the View.OnTouchListener, change
                          point.set(event.getX(), event.getY());
      
      to
                          points.add(new PointF(event.getX(), event.getY()));
      
    4. In the onDraw method of the TouchView, change
              canvas.drawCircle(point.x, point.y, radius, paint);
      
      to
              for (int i = 0; i < points.size(); ++i) {
                  PointF point = points.get(i);
                  canvas.drawCircle(point.x, point.y, radius, paint);
              }
      
    Run the app and tap on the screen in several places. Then change it back.

  4. The circle jumps when the finger touches down. Let’s make it jump whe the finger lifts off. In the onTouch method of the View.OnTouchListener, change the switch statement from
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            //Put finger's x, y into point.
                            point.set(event.getX(), event.getY());
                            invalidate();	//call onDraw method of TouchView
                            return true;
    
                        default:
                            return false;
                    }
    
    to
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            return true;	//do nothing
    
                        case MotionEvent.ACTION_UP:
                            //Put finger's x, y into point.
                            point.set(event.getX(), event.getY());
                            invalidate();	//call onDraw method of TouchView
                            return true;	//do nothing else
    
                        default:
                            return false;
                    }
    
    If the MotionEvent.ACTION_DOWN case had returned false, we would have ignored all subsequent stimulus from this finger, including the ACTION_UP. See Event Listeners.

  5. Change the switch statement to the following. Then drag the circle with your finger.
  6.                 switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            return true;	//do nothing
    
                        case MotionEvent.ACTION_UP:
                            return true;	//do nothing
    
                        case MotionEvent.ACTION_MOVE:
                            //Put finger's x, y into point.
                            point.set(event.getX(), event.getY());
                            invalidate();	//call onDraw method of TouchView
                            return true;	//do nothing else
    
                        default:
                            return false;
                    }
    

  7. Make the circle blue when you start dragging it, and red when you release it. Add the following field to class TouchView.
        private int circleColor = Color.RED;
    
    Remove the call to setColor in the constructor for class TouchView. In the onDraw method of TouchView, insert the following statement before the call to drawCircle.
            paint.setColor(circleColor);
    
    Change the switch statement to the following.
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
    			//Put finger's x, y into point.
                            point.set(event.getX(), event.getY());
                            circleColor = Color.BLUE;
                            invalidate();	//call onDraw method of TouchView
                            return true;
    
                        case MotionEvent.ACTION_UP:
                            circleColor = Color.RED;
                            invalidate();	//call onDraw method of TouchView
                            return true;
    
                        case MotionEvent.ACTION_MOVE:
    			//Put finger's x, y into point.
                            point.set(event.getX(), event.getY());
                            invalidate();	//call onDraw method of TouchView
                            return true;
    
                        default:
                            return false;
                    }
    

  8. The center of the circle always goes to where the finger touched. And the circle moves even if we touch outside its circumference. Change the TouchView.java file to the following. The white area will no longer be touch sensitive, and the point in the circle that was initially touched will remain under the finger. For example, if we touch the lower edge of the circle, our finger will stick to that point. The View.OnTouchListener can have a field (the object previousCenter) even though View.OnTouchListener is an anonymous inner class. The radius is now a field of class TouchView.
    package edu.nyu.sps.touch;
    
    import android.content.Context;
    import android.graphics.Canvas;
    import android.graphics.Color;
    import android.graphics.Paint;
    import android.graphics.PointF;
    import android.view.MotionEvent;
    import android.view.View;
    
    public class TouchView extends View {
        private PointF center = new PointF(); //of circle
        private float radius;                 //of circle
        private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    
        public TouchView(Context context) {
            super(context);
            paint.setColor(Color.RED);
            paint.setStyle(Paint.Style.FILL);
    
            setOnTouchListener(new OnTouchListener() {
                PointF previousCenter = new PointF();   //where previous MotionEvent occurred
    
                @Override
                public boolean onTouch(View v, MotionEvent event) {
                    float dx = event.getX() - center.x;
                    float dy = event.getY() - center.y;
                    if (Math.hypot(dx, dy) > radius) {
                        return false;	//touched outside the circle
                	}
    
                    switch (event.getAction()) {
                        case MotionEvent.ACTION_DOWN:
                            previousCenter.set(event.getX(), event.getY());
                            return true;
    
                        case MotionEvent.ACTION_UP:
                            return true;
    
                        case MotionEvent.ACTION_MOVE:
                            //How far have we dragged since previous MotionEvent?
                            dx = event.getX() - previousCenter.x;
                            dy = event.getY() - previousCenter.y;
                            center.offset(dx, dy);
                            previousCenter.set(event.getX(), event.getY());
                            invalidate();
                            return true;
    
                        default:
                            return false;
                    }
                }
            });
        }
    
        @Override
        protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
            super.onLayout(changed, left, top, right, bottom);
            center.set(getWidth() / 2f, getHeight() / 2f);
            radius = .2f * Math.min(getWidth(), getHeight());
        };
    
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawColor(Color.WHITE);	//background
            canvas.drawCircle(center.x, center.y, radius, paint);
        }
    
    }
    

  9. The MotionEvent passed to the onTouch method of the listener can tell you the size and pressure of the touch, at least on some hardware. (I’ve never seen it work.) Make the circle bigger if user presses harder.

  10. Our circle is not an object, but we could have made it a Drawable object like the triangle here. Another possibility would be to let the circle be a (subclass of class) View, and put the circle object in a big white ViewGroup object such as a RelativeLayout.