6

当我研究在图像上触摸平移/缩放的方法时,我通常会发现有效、简单的代码——但没有什么能完全满足我的需求。图像不需要在实际图像(位图)的边缘与其视图之间显示空白区域。如果位图是 200x100 而视图是 50x50,用户应该只能缩小到 100x50,允许他们水平滑动图像,但不能垂直滑动。

我的代码在移动(翻译)图像时做得很好——直到图像被缩放。然后有些东西被扔掉了;我可以将位图移动得足够远以看到它周围的间隙。这可能是与当前比例因子分解像素测量相关的简单而明显的事情,但我找不到它。我怀疑这与下面 onDraw() 中 maxX 和 maxY 的计算有关。有任何想法吗?

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;
    }

}
4

3 回答 3

7

这是我在经过大量痛苦的实验后最终自己想出的东西——在 Android 中如何处理位图的过程中学习了一些有趣的东西。这段代码远非完美,但它适合我的目的——希望它也能帮助其他人。

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
 */
public class PanZoomView extends View {

    public static final String TAG = PanZoomView.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 float viewHeight;
    private float viewWidth;
    float canvasWidth, canvasHeight;

    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 PanZoomView(Context context) {
        super(context);
        setup();
    }

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

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

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

    public void setBitmap(Bitmap bmp) {
        setImageBitmap(bmp);
    }

    public void setImageBitmap(Bitmap bmp) {
        bitmap = bmp;
        resetZoom();
        resetPan();
        firstDraw = true;
        invalidate();
    }

    public Bitmap getImageBitmap() {
        return bitmap;
    }

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

    public void resetZoom() {
        mScaleFactor = 1.0f;
    }

    public void resetPan() {
        mPosX = 0f;
        mPosY = 0f;
    }

    public void setImageDrawable(Drawable drawable) {
        setImageBitmap(((BitmapDrawable) drawable).getBitmap());
    }

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

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

    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)) {
            //Don't let the user zoom out so much that the image is smaller
            //than its containing frame
            float minXScaleFactor = (float) viewWidth / (float) bitmap.getWidth();
            float minYScaleFactor = (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 image
        if (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
    }

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

        if (panEnabled) {
            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 * 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: {
                    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;
    }

    /**
     * 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) {
        int origX = -1 * (int) mPosX * 2;
        int origY = -1 * (int) mPosY * 2;
        int width = (int) (viewWidth / mScaleFactor);
        int height = (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);
        Bitmap b = 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;
    }

      @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            viewHeight = h;
            viewWidth = w;
        }
}
于 2013-01-04T04:18:00.763 回答
1
mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));

这样,您最多可以缩放 5 倍,根据您想要的大小计算最大比例并添加到此,而不是一直使用 5。

此外,当我在一个项目上使用捏缩放时,我发现如果使用绝对值而不是乘法,它会更容易使用。只需在第一次触摸时获取手指的距离,在移动手指时,计算距离,然后根据第一次距离计算比例。这样,当您限制最小和最大比例时,它可以更好地跟随手指并且工作得更好。

于 2013-01-03T18:33:33.197 回答
0

这篇文章很旧并且已经得到了回答,但是我找到了另一个解决方案,它对我来说比接受的答案更好。

    protected override void OnDraw(Canvas canvas)
    {
        // Calculate the boundaries of the canvas
        var minX = (int)((_viewWidth / _scaleFactor) - canvas.Width);
        var minY = (int)((_viewHeight / _scaleFactor) - canvas.Height);

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

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

        base.OnDraw(canvas);
    }

    protected override void OnSizeChanged(int w, int h, int oldw, int oldh)
    {
        base.OnSizeChanged(w, h, oldw, oldh);

        _viewHeight = h;
        _viewWidth = w;
    }

请注意,此代码是用 编写的Xamarin.Android,但是将其转换为 Java 将很容易。

这使图像完美地保持在其边界内。

于 2018-06-21T09:22:18.687 回答