Перевести / масштабировать растровое изображение в границах?

Когда я исследую подходы к сенсорному панорамированию / масштабированию изображения, я обычно нахожу эффективный, простой код - но ничего, что делает то, что я хочу. Изображение никогда не должно показывать пустое пространство между краем фактического изображения (растровое изображение) и его видом. Если растровое изображение имеет размер 200x100, а представление - 50x50, пользователь должен иметь возможность только уменьшить изображение до 100x50, что позволяет ему перемещать изображение по горизонтали, но не по вертикали.

Мой код делает это хорошо при перемещении (переводе) изображения - пока изображение не будет увеличено. Тогда что-то сбрасывается; Я могу переместить растровое изображение достаточно далеко, чтобы увидеть пробелы вокруг него. Это'Возможно, что-то простое и очевидное связано с факторизацией измерений пикселей по текущему масштабному коэффициенту, но я могуне могу найти это. Я подозреваю, что это связано с расчетами maxX и maxY в onDraw () ниже. Есть идеи?

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.widget.ImageView;

/**
 * Most code from
 * http://android-developers.blogspot.com/2010/06/making-sense-of-multitouch.html
 * 
 * @author Chad Schultz
 * 
 */
public class PanZoomImageView extends ImageView {

    public static final String TAG = PanZoomImageView.class.getName();

    private static final int INVALID_POINTER_ID = -1;

    // The ‘active pointer’ is the one currently moving our object.
    private int mActivePointerId = INVALID_POINTER_ID;

    private Bitmap bitmap;

    private ScaleGestureDetector mScaleDetector;
    private float mScaleFactor = 1.f;
    private float minScaleFactor;

    private float mPosX;
    private float mPosY;

    private float mLastTouchX, mLastTouchY;

    private boolean firstDraw = true;

    private boolean panEnabled = true;
    private boolean zoomEnabled = true;

    public PanZoomImageView(Context context) {
        super(context);
        setup();
    }

    public PanZoomImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setup();
    }

    public PanZoomImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setup();
    }

    private void setup() {
        mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
    }

    @Override
    public void setImageBitmap(Bitmap bmp) {
        super.setImageBitmap(bmp);
        bitmap = bmp;
        firstDraw = true;
    }

    @Override
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        bitmap = ((BitmapDrawable) drawable).getBitmap();
        firstDraw = true;
    }

    public void onDraw(Canvas canvas) {
        Log.v(TAG, "onDraw()");
        if (bitmap == null) {
            Log.w(TAG, "nothing to draw - bitmap is null");
            super.onDraw(canvas);
            return;
        }

        if (firstDraw 
                && (bitmap.getHeight() > 0) 
                && (bitmap.getWidth() > 0) 
                && (canvas.getHeight() > 0) 
                && (canvas.getWidth() > 0)) {
            //Don't let the user zoom out so much that the image is smaller
            //than its containing frame
            float minXScaleFactor = (float) canvas.getWidth() / (float) bitmap.getWidth();
            float minYScaleFactor = (float) canvas.getHeight() / (float) bitmap.getHeight();
            minScaleFactor = Math.max(minXScaleFactor, minYScaleFactor);
            Log.d(TAG, "minScaleFactor: " + minScaleFactor);
            firstDraw = false;
        }
        mScaleFactor = Math.max(mScaleFactor, minScaleFactor);
        Log.d(TAG, "mScaleFactor: " + mScaleFactor);

        //Save the canvas without translating (panning) or scaling (zooming)
        //After each change, restore to this state, instead of compounding
        //changes upon changes
        canvas.save();

        int maxX, minX, maxY, minY;
        //How far can we move the image horizontally without having a gap between image and frame?
        maxX = (int) (mScaleFactor * (bitmap.getWidth() / 2) - (canvas.getWidth() / 2));
        minX = -1 * maxX;
        //How far can we move the image vertically without having a gap between image and frame?
        maxY = (int) (mScaleFactor * (bitmap.getHeight() / 2) - (canvas.getWidth() / 2));
        minY = -1 * maxY;
        //Do not go beyond the boundaries of the image
        if (mPosX > maxX) {
            mPosX = maxX;
        }
        if (mPosX < minX) {
            mPosX = minX;
        }
        if (mPosY > maxY) {
            mPosY = maxY;
        }
        if (mPosY < minY) {
            mPosY = minY;
        }

        Log.d(TAG, "canvas width: " + canvas.getWidth() + " canvas height: "
                + canvas.getHeight());
        Log.d(TAG, "bitmap width: " + bitmap.getWidth() + " height: " + bitmap.getHeight());
        Log.d(TAG, "translating mPosX: " + mPosX + " mPosY: " + mPosY);

        if (zoomEnabled) {
            Log.d(TAG, "zooming to scale factor of " + mScaleFactor);
            canvas.scale(mScaleFactor, mScaleFactor);
        } else {
            Log.d(TAG, "zooming disabled");
        }

        if (panEnabled) {
            Log.d(TAG, "panning to " + mPosX + "," + mPosY); 
            canvas.translate(mPosX, mPosY);
        } else {
            Log.d(TAG, "panning disabled");
        }

        super.onDraw(canvas);
        canvas.restore(); //clear translation/scaling
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();

                mLastTouchX = x;
                mLastTouchY = y;
                mActivePointerId = ev.getPointerId(0);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(pointerIndex);
                final float y = ev.getY(pointerIndex);

                // Only move if the ScaleGestureDetector isn't processing a gesture.
                if (!mScaleDetector.isInProgress()) {
                    float dx = x - mLastTouchX;
                    float dy = y - mLastTouchY;

                    //Adjust for zoom factor. Otherwise, the user's finger moving 10 pixels
                    //at 200% zoom causes the image to slide 20 pixels instead of perfectly
                    //following the user's touch
                    dx /= mScaleFactor;
                    dy /= mScaleFactor;

                    mPosX += dx;
                    mPosY += dy;

                    invalidate();
                }

                mLastTouchX = x;
                mLastTouchY = y;

                break;
            }

            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                break;
            }
        }

        return true;
    }

    private class ScaleListener extends
            ScaleGestureDetector.SimpleOnScaleGestureListener {
        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();
            // Don't let the object get too small or too large.
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
            Log.d(TAG, "detector scale factor: " + detector.getScaleFactor() + " mscalefactor: " + mScaleFactor);

            invalidate();
            return true;
        }
    }

    //Currently zoomEnabled/panEnabled can only be set programmatically, not in XML

    public boolean isPanEnabled() {
        return panEnabled;
    }

    public void setPanEnabled(boolean panEnabled) {
        this.panEnabled = panEnabled;
    }

    public boolean isZoomEnabled() {
        return zoomEnabled;
    }

    public void setZoomEnabled(boolean zoomEnabled) {
        this.zoomEnabled = zoomEnabled;
    }

}

Ответы на вопрос(3)

Ваш ответ на вопрос