title: Pan, Zoom and Scale a custom View for Canvas drawing in Android
body: <p>I'm working on a drawing app, where the user can pan &amp; zoom to a specific portion of the screen and start drawing with zoom applied. To be more specific, I'm looking for a way to implement zoom, pinch &amp; pan gesture in Canvas (horizontal and vertical scrolling with moveable x, y coordinates).</p>
  
  <p>Right now, I've successfully developed the zoom only feature but it's not accurate and the pan option is not working. </p>
  
  <p>See my sample code here,</p>
  
  <pre><code>       public class DrawingView extends View {
  
          //canvas
          private Canvas drawCanvas;
          //canvas bitmap
          private Bitmap canvasBitmap;
  
          private boolean dragged = false;
          private float displayWidth;
          private float displayHeight;
  
          //These two constants specify the minimum and maximum zoom
          private static float MIN_ZOOM = 1f;
          private static float MAX_ZOOM = 5f;
  
          private float scaleFactor = 1.f;
          private ScaleGestureDetector detector;
  
          //These constants specify the mode that we're in
          private static int NONE = 0;
          private static int DRAG = 1;
          private static int ZOOM = 2;
  
          private int mode;
  
           public DrawingView(Context context, AttributeSet attrs) {
                  super(context, attrs);
                  ctx = context;
                  setupDrawing();
              }
  
           private void setupDrawing() {
                  detector = new ScaleGestureDetector(getContext(), new ScaleListener());
  
                  WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
                  Display display = wm.getDefaultDisplay();
                  DisplayMetrics metrics = new DisplayMetrics();
                  display.getMetrics(metrics);
  
                  displayWidth = metrics.widthPixels;
                  displayHeight = metrics.heightPixels;
  
                  setFocusable(true);
                  setFocusableInTouchMode(true);
          }
  
  class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
          @Override
          public boolean onScale(ScaleGestureDetector detector) {
              scaleFactor *= detector.getScaleFactor();
              scaleFactor = Math.max(MIN_ZOOM, Math.min(scaleFactor, MAX_ZOOM));
              return true;
          }
      }
  
          @Override
              protected void onSizeChanged(int w, int h, int oldw, int oldh) {
                  //view given size
                  super.onSizeChanged(w, h, oldw, oldh);
                  canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
                  drawCanvas = new Canvas(canvasBitmap);
              }
  
           @Override
              public boolean onTouchEvent(MotionEvent event) {
  
                      //detect user touch
                      float x = event.getX();
                      float y = event.getY();
  
                      switch (event.getAction() &amp; MotionEvent.ACTION_MASK) {
  
                          case MotionEvent.ACTION_DOWN:
                              //
                              downx = event.getX();
                              downy = event.getY();
  
                              mode = DRAG;
                              //We assign the current X and Y coordinate of the finger to startX and startY minus the previously translated
                              //amount for each coordinates This works even when we are translating the first time because the initial
                              //values for these two variables is zero.
                              startX = event.getX() - previousTranslateX;
                              startY = event.getY() - previousTranslateY;
                              break;
  
                              case MotionEvent.ACTION_MOVE:
                              translateX = event.getX() - startX;
                              translateY = event.getY() - startY;
                              //We cannot use startX and startY directly because we have adjusted their values using the previous translation values.
                              //This is why we need to add those values to startX and startY so that we can get the actual coordinates of the finger.
                              double distance = Math.sqrt(Math.pow(event.getX() - (startX + previousTranslateX), 2) + Math.pow(event.getY() - (startY + previousTranslateY), 2));
                              if (distance &gt; 0) {
                                  dragged = true;
                              }
                              break;
  
                              case MotionEvent.ACTION_POINTER_DOWN:
                              mode = ZOOM;
                              break;
  
  
                              ase MotionEvent.ACTION_UP:
                              //
                              upx = event.getX();
                              upy = event.getY();
  
                              mode = NONE;
                              dragged = false;
                              //All fingers went up, so let's save the value of translateX and translateY into previousTranslateX and
                              //previousTranslate
                              previousTranslateX = translateX;
                              previousTranslateY = translateY;
                              break;
  
                              case MotionEvent.ACTION_POINTER_UP:
                              mode = DRAG;
                              //This is not strictly necessary; we save the value of translateX and translateY into previousTranslateX
                              //and previousTranslateY when the second finger goes up
                              previousTranslateX = translateX;
                              previousTranslateY = translateY;
                              break;
  
                              detector.onTouchEvent(event);
  
                      //We redraw the canvas only in the following cases:
                      //
                      // o The mode is ZOOM
                      //        OR
                      // o The mode is DRAG and the scale factor is not equal to 1 (meaning we have zoomed) and dragged is
                      //   set to true (meaning the finger has actually moved)
                      if ((mode == DRAG &amp;&amp; scaleFactor != 1f &amp;&amp; dragged) || mode == ZOOM) {
                          invalidate();
                      }
          }
  
      @Override
          protected void onDraw(Canvas canvas) {
              canvas.save();
  
                  //If translateX times -1 is lesser than zero, let's set it to zero. This takes care of the left bound
                  if ((translateX * -1) &lt; 0) {
                      translateX = 0;
                  }
  
                  //This is where we take care of the right bound. We compare translateX times -1 to (scaleFactor - 1) * displayWidth.
                  //If translateX is greater than that value, then we know that we've gone over the bound. So we set the value of
                  //translateX to (1 - scaleFactor) times the display width. Notice that the terms are interchanged; it's the same
                  //as doing -1 * (scaleFactor - 1) * displayWidth
                  else if ((translateX * -1) &gt; (scaleFactor - 1) * displayWidth) {
                      translateX = (1 - scaleFactor) * displayWidth;
                  }
  
                  if (translateY * -1 &lt; 0) {
                      translateY = 0;
                  }
  
                  //We do the exact same thing for the bottom bound, except in this case we use the height of the display
                  else if ((translateY * -1) &gt; (scaleFactor - 1) * displayHeight) {
                      translateY = (1 - scaleFactor) * displayHeight;
                  }
  
                  //We need to divide by the scale factor here, otherwise we end up with excessive panning based on our zoom level
                  //because the translation amount also gets scaled according to how much we've zoomed into the canvas.
                  canvas.translate(translateX / scaleFactor, translateY / scaleFactor);
              }
  
              //We're going to scale the X and Y coordinates by the same amount
              //canvas.scale(scaleFactor, scaleFactor);
              canvas.scale(this.scaleFactor, this.scaleFactor, this.detector.getFocusX(), this.detector.getFocusY());
      }
  </code></pre>