Skip to content Skip to sidebar Skip to footer

Translate/scale Bitmap Within Boundaries?

When I research approaches for touch pan/zoom on an image, I generally find effective, simple code--but nothing that does quite what I want. The image needs to never show a blank s

Solution 1:

Here's what I eventually came up with on my own after a great deal of painful experimentation--learning some interesting things along the way about how Bitmaps are handled in Android. This code is far from perfect, but it suits my purposes--hopefully it will help others as well.

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.view.View;

/**
 * @author Chad Schultz
 * @version 1
 */publicclassPanZoomViewextendsView {

    publicstaticfinalStringTAG= PanZoomView.class.getName();

    privatestaticfinalintINVALID_POINTER_ID= -1;

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

    private Bitmap bitmap;
    privatefloat viewHeight;
    privatefloat viewWidth;
    float canvasWidth, canvasHeight;

    private ScaleGestureDetector mScaleDetector;
    privatefloatmScaleFactor=1.f;
    privatefloat minScaleFactor;

    privatefloat mPosX;
    privatefloat mPosY;

    privatefloat mLastTouchX, mLastTouchY;

    privatebooleanfirstDraw=true;

    privatebooleanpanEnabled=true;
    privatebooleanzoomEnabled=true;

    publicPanZoomView(Context context) {
        super(context);
        setup();
    }

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

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

    privatevoidsetup() {
        mScaleDetector = newScaleGestureDetector(getContext(), newScaleListener());
    }

    publicvoidsetBitmap(Bitmap bmp) {
        setImageBitmap(bmp);
    }

    publicvoidsetImageBitmap(Bitmap bmp) {
        bitmap = bmp;
        resetZoom();
        resetPan();
        firstDraw = true;
        invalidate();
    }

    public Bitmap getImageBitmap() {
        return bitmap;
    }

    public Bitmap getBitmap() {
        return getImageBitmap();
    }

    publicvoidresetZoom() {
        mScaleFactor = 1.0f;
    }

    publicvoidresetPan() {
        mPosX = 0f;
        mPosY = 0f;
    }

    publicvoidsetImageDrawable(Drawable drawable) {
        setImageBitmap(((BitmapDrawable) drawable).getBitmap());
    }

    public BitmapDrawable getImageDrawable() {
        BitmapDrawablebd=newBitmapDrawable(getContext().getResources(), bitmap);
        return bd;
    }

    public BitmapDrawable getDrawable() {
        return getImageDrawable();
    }

    publicvoidonDraw(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)) {
            //Don't let the user zoom out so much that the image is smaller//than its containing framefloatminXScaleFactor= (float) viewWidth / (float) bitmap.getWidth();
            floatminYScaleFactor= (float) viewHeight / (float) bitmap.getHeight();
            minScaleFactor = Math.max(minXScaleFactor, minYScaleFactor);
            Log.d(TAG, "minScaleFactor: " + minScaleFactor);
            mScaleFactor = minScaleFactor; //start out "zoomed out" all the way

            mPosX = mPosY = 0;
            firstDraw = false;

        }
        mScaleFactor = Math.max(mScaleFactor, minScaleFactor);

        canvasHeight = canvas.getHeight();
        canvasWidth = canvas.getWidth();
//      Log.d(TAG, "canvas density: " + canvas.getDensity() + " bitmap density: " + bitmap.getDensity());//      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;
        //Regardless of the screen density (HDPI, MDPI) or the scale factor, //The image always consists of bitmap width divided by 2 pixels. If an image//is 200 pixels wide and you scroll right 100 pixels, you just scrolled the image//off the screen to the left.
        minX = (int) (((viewWidth / mScaleFactor) - bitmap.getWidth()) / 2);
        maxX = 0;
        //How far can we move the image vertically without having a gap between image and frame?
        minY = (int) (((viewHeight / mScaleFactor) - bitmap.getHeight()) / 2);
        maxY = 0;
        Log.d(TAG, "minX: " + minX + " maxX: " + maxX + " minY: " + minY + " maxY: " + maxY);
        //Do not go beyond the boundaries of the imageif (mPosX > maxX) {
            mPosX = maxX;
        }
        if (mPosX < minX) {
            mPosX = minX;
        }
        if (mPosY > maxY) {
            mPosY = maxY;
        }
        if (mPosY < minY) {
            mPosY = minY;
        }

//      Log.d(TAG, "view width: " + viewWidth + " view height: "//              + viewHeight);//      Log.d(TAG, "bitmap width: " + bitmap.getWidth() + " height: " + bitmap.getHeight());//      Log.d(TAG, "translating mPosX: " + mPosX + " mPosY: " + mPosY);//      Log.d(TAG, "zooming to scale factor of " + mScaleFactor);
        canvas.scale(mScaleFactor, mScaleFactor);

//      Log.d(TAG, "panning to " + mPosX + "," + mPosY); 
        canvas.translate(mPosX, mPosY);

        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, mPosX, mPosY, null);
        canvas.restore(); //clear translation/scaling
    }

    @OverridepublicbooleanonTouchEvent(MotionEvent ev) {
        // Let the ScaleGestureDetector inspect all events.if (zoomEnabled) {
            mScaleDetector.onTouchEvent(ev);
        }

        if (panEnabled) {
            finalintaction= ev.getAction();
            switch (action & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN: {
                    finalfloatx= ev.getX();
                    finalfloaty= ev.getY();

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

                case MotionEvent.ACTION_MOVE: {
                    finalintpointerIndex= ev.findPointerIndex(mActivePointerId);
                    finalfloatx= ev.getX(pointerIndex);
                    finalfloaty= ev.getY(pointerIndex);

                    // Only move if the ScaleGestureDetector isn't processing a gesture.if (!mScaleDetector.isInProgress()) {
                        floatdx= x - mLastTouchX;
                        floatdy= 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 * 2);
                        dy /= (mScaleFactor * 2);

                        mPosX += dx;
                        mPosY += dy;

                        Log.v(TAG, "moving by " + dx + "," + dy + " mScaleFactor: " + mScaleFactor);

                        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: {
                    finalintpointerIndex= (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                    finalintpointerId= ev.getPointerId(pointerIndex);
                    if (pointerId == mActivePointerId) {
                        // This was our active pointer going up. Choose a new// active pointer and adjust accordingly.finalintnewPointerIndex= pointerIndex == 0 ? 1 : 0;
                        mLastTouchX = ev.getX(newPointerIndex);
                        mLastTouchY = ev.getY(newPointerIndex);
                        mActivePointerId = ev.getPointerId(newPointerIndex);
                    }
                    break;
                }
            }
        }
        returntrue;
    }

    privateclassScaleListenerextendsScaleGestureDetector.SimpleOnScaleGestureListener {
        @OverridepublicbooleanonScale(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();
            returntrue;
        }
    }

    //Currently zoomEnabled/panEnabled can only be set programmatically, not in XMLpublicbooleanisPanEnabled() {
        return panEnabled;
    }

    publicvoidsetPanEnabled(boolean panEnabled) {
        this.panEnabled = panEnabled;
    }

    publicbooleanisZoomEnabled() {
        return zoomEnabled;
    }

    publicvoidsetZoomEnabled(boolean zoomEnabled) {
        this.zoomEnabled = zoomEnabled;
    }

    /**
     * Calls getCroppedBitmap(int outputWidth, int outputHeight) without
     * scaling the resulting bitmap to any specific size.
     * @return
     */public Bitmap getCroppedBitmap() {
        return getCroppedBitmap(0, 0);
    }

    /**
     * Takes the section of the bitmap visible in its View object
     * and exports that to a Bitmap object, taking into account both
     * the translation (panning) and zoom (scaling).
     * WARNING: run this in a separate thread, not on the UI thread!
     * If you specify that a 200x200 image should have an outputWidth
     * of 400 and an outputHeight of 50, the image will be squished
     * and stretched to those dimensions.
     * @param outputWidth desired width of output Bitmap in pixels
     * @param outputHeight desired height of output Bitmap in pixels
     * @return the visible portion of the image in the PanZoomImageView
     */public Bitmap getCroppedBitmap(int outputWidth, int outputHeight) {
        intorigX= -1 * (int) mPosX * 2;
        intorigY= -1 * (int) mPosY * 2;
        intwidth= (int) (viewWidth / mScaleFactor);
        intheight= (int) (viewHeight / mScaleFactor);
        Log.e(TAG, "origX: " + origX + " origY: " + origY + " width: " + width + " height: " + height + " outputWidth: " + outputWidth + " outputHeight: " + outputHeight + "getLayoutParams().width: " + getLayoutParams().width + " getLayoutParams().height: " + getLayoutParams().height);
        Bitmapb= Bitmap.createBitmap(bitmap, origX, origY, width, height);

        if (outputWidth > 0 && outputWidth > 0) {
            //Use the exact dimensions given--chance this won't match the aspect ratio
            b = Bitmap.createScaledBitmap(b, outputWidth, outputHeight, true);
        }

        return b;
    }

      @OverrideprotectedvoidonSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            viewHeight = h;
            viewWidth = w;
        }
}

Solution 2:

mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

This way you can scale up to 5 times, calculate the maximum scale based on the size you want and add to this instead of using 5 all the time.

Also when I worked with pinch zoom on a project I find it easier to use if you use absolute values instead of multiplying. Just get the distance of the fingers on the first touch and when moving the fingers, calculate the distance and then the scale based on the first distance. This way it follows the fingers better and works better when you limit the minimum and maximum scale.

Solution 3:

This post is quite old and already answered, however I found another solution which worked even better for me then the accepted answer.

protectedoverridevoidOnDraw(Canvas canvas)
    {
        // Calculate the boundaries of the canvasvar minX = (int)((_viewWidth / _scaleFactor) - canvas.Width);
        var minY = (int)((_viewHeight / _scaleFactor) - canvas.Height);

        if (_posX > 0)
            _posX = 0;
        elseif (_posX < minX)
            _posX = minX;
        if (_posY > 0)
            _posY = 0;
        elseif (_posY < minY)
            _posY = minY;

        // Change image position
        canvas.Scale(_scaleFactor, _scaleFactor);
        canvas.Translate(_posX, _posY);

        base.OnDraw(canvas);
    }

    protectedoverridevoidOnSizeChanged(int w, int h, int oldw, int oldh)
    {
        base.OnSizeChanged(w, h, oldw, oldh);

        _viewHeight = h;
        _viewWidth = w;
    }

Note that this code is written in Xamarin.Android, however converting this to Java will be easy.

This keeps the image perfectly within it's boundaries.

Post a Comment for "Translate/scale Bitmap Within Boundaries?"