title: Android Canvas - Scale and Translate
body: <p>I have an SVG that I want to zoom and pan using gestures in Android. It works, but not smoothly enough for my tastes. So how can I improve this class to have smooth panning that works at all zoom levels and stays under your finger, scales using a pinch gesture zooms from the area of the gesture (AKA focus) and as a bonus double tapping should result in snapping the image back to 1.0? I'm using this <a href="http://code.google.com/p/svg-android/" rel="nofollow">SVG library</a>. An acceptable answer should meet these functional requirements, not jump around unexpectedly and stick to GestureDetectors unless a case can be made that they just won't work.</p>
  
  <pre><code>public class MapView extends ViewGroup {
      private static final float MINIMUM_SCALE_FACTOR = 1.0f;
      private static final float MAXIMUM_SCALE_FACTOR = 10.0f;
  
      private Picture mPicture;
      private Paint mPaint;
  
      private ScaleGestureDetector mScaleDetector;
      private GestureDetector mMoveDetector;
  
      private float mScaleFactor;
      private float mScaleFocusX;
      private float mScaleFocusY;
      private float mFocusX = 0.0f;
      private float mFocusY = 0.0f;
  
      private float mImageHeight;
      private float mImageWidth;
      private int mViewHeight;
      private int mViewWidth;
      private RectF mDrawingRect;
  
  
      public MapView(Context context) {
          super(context);
  
          init(context);
      }
  
      public MapView(Context context, AttributeSet attrs) {
          super(context, attrs);
  
          init(context);
  
      }
  
      public MapView(Context context, AttributeSet attrs, int defStyle) {
          super(context, attrs, defStyle);
  
          init(context);
      }
  
      public void init(Context context) {
          Log.d(getClass().getCanonicalName(), "Initialized.");
  
          this.setBackgroundColor(Color.WHITE);
  
          this.mPaint = new Paint();
          mPaint.setStyle(Paint.Style.STROKE);
          mPaint.setColor(Color.BLUE);
          mPaint.setStrokeWidth(2);
          mPaint.setAntiAlias(true);
  
          this.mScaleFactor = MINIMUM_SCALE_FACTOR;
  
          // Setup Gesture Detectors
          this.mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
          this.mMoveDetector = new GestureDetector(context, new MoveListener());
      }
  
      @Override
      protected void onLayout(boolean changed, int l, int t, int r, int b) {
          Log.d(getClass().getCanonicalName(), "onLayout");
  
          this.mDrawingRect = getDrawingSquare();
      }
  
      @Override
      protected void onSizeChanged(int w, int h, int oldw, int oldh) {
          super.onSizeChanged(w, h, oldw, oldh);
  
          this.mViewWidth = w;
          this.mViewHeight = h;
      }
  
      @Override
      protected void onDraw(Canvas canvas) {
          super.onDraw(canvas);
  
          Log.d(getClass().getCanonicalName(), "onDraw()");
  
          canvas.save();
          canvas.scale(mScaleFactor, mScaleFactor, mScaleFocusX, mScaleFocusY);
  
          canvas.translate(mFocusX, mFocusY);
  
          canvas.drawRect((int) mDrawingRect.left, (int) mDrawingRect.top, (int) mDrawingRect.right, (int) mDrawingRect.bottom, mPaint);
          canvas.drawPicture(mPicture, mDrawingRect);
  
          canvas.restore();
      }
  
      @Override
      public boolean onTouchEvent(MotionEvent ev) {
          mScaleDetector.onTouchEvent(ev);
          mMoveDetector.onTouchEvent(ev);
  
          return true;
      }
  
      public RectF getDrawingSquare() {
          float width = getWidth();
          float height = getHeight();
          Log.d(getClass().getCanonicalName(), "Picture width " + width + " - height " + height);
  
          float side = Math.min(width, height);
  
          return new RectF(0, 0, side, side);
      }
  
      public void setSVG(SVG svg) {
          Log.d(getClass().getCanonicalName(), "SVG to Picture.");
  
          this.mPicture = svg.getPicture();
          this.mImageHeight = mPicture.getHeight();
          this.mImageWidth = mPicture.getWidth();
  
          invalidate();
      }
  
      private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
          @Override
          public boolean onScale(ScaleGestureDetector detector) {
              mScaleFactor *= detector.getScaleFactor(); // scale change since previous event
              mScaleFocusX = detector.getFocusX();
              mScaleFocusY = detector.getFocusY();
  
              // Don't let the object get too small or too large.
              mScaleFactor = Math.max(MINIMUM_SCALE_FACTOR, Math.min(mScaleFactor, MAXIMUM_SCALE_FACTOR));
  
              invalidate();
  
              return true;
          }
      }
  
      private class MoveListener extends GestureDetector.SimpleOnGestureListener {
  //        @Override
  //        public boolean onDown(MotionEvent e) {
  //            return true;
  //        }
  
          @Override
          public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
              scrollBy((int)distanceX, (int)distanceY);
  //            mFocusX += distanceX;
  //            mFocusY += distanceY;
  //            return true;
              return true;
          }
      }
  }
  </code></pre>