How does pinch zoom work with panning for image in

2019-07-24 14:09发布

问题:


Goal

An activity is made to view image, we can pinch zoom or pan the image. The image is centered in the screen in the beginning. Pinch zoom is centered at the center of the image, even after the image is panned somewhere else in the screen.

The image for displaying is downloaded from a given URL, and the URL is passed from extra of an intent to start the image viewing activity.

Pinch zoom is implemented by postScale(), pan by postTranslate().


Problem

After panning the image somewhere, the pinch-zoom center is still at the center of the screen. Tried to follow the center of the image when it's been moved to a new place but my code doesn't work that way. Please give some idea.

The image downloading and panning work well.


Code

activity_image_viewer_layout.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" >

    <LinearLayout
        android:orientation="vertical" 
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:gravity="center"
        android:background="@color/MyPureBlack" >

        <LinearLayout
          android:id="@+id/progressbar_wrapper"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical" >

            <ProgressBar 
                android:id="@+id/progressbar"
                style="?android:attr/progressBarStyleHorizontal" 
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:max="100"
                android:progress="0"
                android:layout_marginLeft="20dp"
                android:layout_marginRight="20dp"
                android:layout_gravity="center" >
            </ProgressBar>
        </LinearLayout>

        <ImageView
            android:id="@+id/image_viewer" 
            android:visibility="gone"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent" 
            android:background="@color/MyPureBlack"
            android:scaleType="matrix" >
        </ImageView>
    </LinearLayout>
</FrameLayout>

ActivityImageViewer.java

package com.com2us.hubapp.android;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLConnection;

import org.apache.http.util.ByteArrayBuffer;

import android.app.Activity;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.FloatMath;
import android.util.Log;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.animation.AlphaAnimation;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;

public class ActivityImageViewer extends Activity {
    File imageFile = null;

    // Matrices for pinch zoom and pan
    Matrix matrix = new Matrix();
    Matrix savedMatrix = new Matrix();
    Matrix savedMatrixZoom = new Matrix();

    // State of motion event
    static final int NONE = 0;
    static final int PAN = 1;
    static final int PINCH_ZOOM = 2;
    int mode = NONE;

    // The first pointer down
    PointF start = new PointF();

    // The center of the image (Failed to track it when the image has been moved)
    PointF centerOfImage = new PointF();

    // oldest is the Cartesian distance between first two pointers when the second pointer is down
    float oldDist = 1f;

    // MIN_SCALE/MAX_SCALE is the min/max scale factor
    private final float MIN_SCALE = 0.5f;
    private final float MAX_SCALE = 3.0f;

    // TOUCH_SENSITIVE is the minimum Cartesian distance between the first two pointers that triggers the pinch zoom
    private final float TOUCH_SENSITIVE = 10.0f;
    private final float SPACING_LEFT_AND_RIGHT = 30.0f;
    private final float SPACING_TOP_AND_BOTTOM = 30.0f;

    // The ImageView widget
    private ImageView image_viewer;

    // The progress bar shows what current progress is before the image downloading is completed
    private ProgressBar progressbar;
    private LinearLayout progressbar_wrapper;

    // An async task that downloads the image from a given URL
    private DownloadFilesTask downloadFilesTask;

    private class DownloadFilesTask extends AsyncTask<String, Integer, Bitmap> {
        protected Bitmap doInBackground(String... urls) {
            InputStream input = null;
            OutputStream output = null;
            try {
                URL url = new URL(urls[0]);
                URLConnection connection = url.openConnection();
                connection.connect();
                int lenghtOfFile = connection.getContentLength();
                // download the file
                InputStream is = connection.getInputStream();
                BufferedInputStream bis = new BufferedInputStream(is, 8190);

                ByteArrayBuffer baf = new ByteArrayBuffer(50);
                int current = 0;
                while ((current = bis.read()) != -1) {
                    baf.append((byte)current);
                }
                byte[] imageData = baf.toByteArray();
                Bitmap bmp = BitmapFactory.decodeByteArray(imageData, 0, imageData.length);

                //final int percent = (int) (total * 100 / lenghtOfFile);
                //publishProgress(percent);
                //lenghtOfFile

                return bmp;
            } catch (Exception e) {

            } finally {
                try {
                    if (output != null)
                        output.close();
                    output = null;
                } catch (IOException e) {

                }
                try {
                    if (input != null)
                        input.close();
                    input = null;
                } catch (IOException e) {

                }

            }
            return null;
        } // protected Bitmap doInBackground(String... urls) {}

        protected void onProgressUpdate(Integer... progress) {
            progressbar.setProgress(progress[0]);
        }

        protected void onPostExecute(Bitmap bmp) {
            if (bmp != null) {
                final AlphaAnimation animationAfter = new AlphaAnimation(0.0f, 1.0f);
                animationAfter.setDuration(300);
                animationAfter.setFillEnabled(true);
                animationAfter.setFillAfter(true);
                image_viewer.setAnimation(animationAfter);
                image_viewer.setImageBitmap(bmp);
                ViewTreeObserver viewTreeObserver = image_viewer.getViewTreeObserver();
                viewTreeObserver.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        Drawable drawable = image_viewer.getDrawable();
                        int dx = (image_viewer.getWidth() - drawable.getIntrinsicWidth()) / 2;
                        int dy = (image_viewer.getHeight() - drawable.getIntrinsicHeight()) / 2;
                        matrix.postTranslate(dx, dy);
                        image_viewer.setImageMatrix(matrix);
                    }
                });
                progressbar_wrapper.setVisibility(View.GONE);
                image_viewer.setVisibility(View.VISIBLE);
            } else {
                android.os.Handler handler = new android.os.Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        finish();
                    }
                }, 2000);
            }
        } // End of protected void onPostExecute(Bitmap bmp) {}
    } // End of private class DownloadFilesTask extends AsyncTask<String, Integer, Bitmap> {}

    // These are activity life cycle handling
    // onCreate
    @Override
    public void onCreate(Bundle savedInstanceState) {
        //setTheme(R.style.HubTheme);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_image_viewer);
        progressbar_wrapper = (LinearLayout) findViewById(R.id.progressbar_wrapper);
        image_viewer = (ImageView) findViewById(R.id.image_viewer);
        progressbar = (ProgressBar) findViewById(R.id.progressbar);
        image_viewer.setOnTouchListener(new MyOnTouchListener());
        final String uriForImage = getIntent().getStringExtra("url");
        downloadFilesTask = new DownloadFilesTask();
        downloadFilesTask.execute(uriForImage);
    }

    // onStart
    @Override
    protected void onStart() {
        super.onStart();
    }

    // onResume
    @Override
    protected void onResume() {
        super.onResume();
    }

    // onPause
    @Override
    protected void onPause() {
        super.onPause();
    }

    // onStop
    @Override
    protected void onStop() {
        super.onStop();
    }

    // onRestart
    @Override
    protected void onRestart() {
        super.onRestart();
    }

    // onDestroy
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (imageFile != null) {
            try {
                Drawable drawable = image_viewer.getDrawable();
                BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
                Bitmap bitmap = bitmapDrawable.getBitmap();
                bitmap.recycle();

                drawable = null;
                bitmapDrawable = null;
                bitmap = null;

            } catch (NullPointerException e) {
            }
        }
    }

    // onKeyDown
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            this.onBackPressed();
        }
        return true;
    }

    // onBackPressed
    public void onBackPressed() {
        finish();
    }

    // onConfigurationChanged
    @Override
    public void onConfigurationChanged(Configuration newConfig) {

        super.onConfigurationChanged(newConfig);
        if (newConfig.equals(Configuration.ORIENTATION_LANDSCAPE)) {

        } else if (newConfig.equals(Configuration.ORIENTATION_PORTRAIT)) {

        }
    }

    // onLowMemory
    @Override
    public void onLowMemory() {
        super.onLowMemory();
        finish();
    }

    // Get the Cartesian distance between the first two pointers
    private float spacing(MotionEvent event) {
        float x = 0;
        float y = 0;
        try {
            Method getX = MotionEvent.class.getMethod("getX", Integer.TYPE);
            Method getY = MotionEvent.class.getMethod("getX", Integer.TYPE);

            // x = event.getX(0) - event.getX(1);
            // y = event.getY(0) - event.getY(1);
            float x1 = (Float) getX.invoke(event, 0);
            float x2 = (Float) getX.invoke(event, 1);
            x = x1 - x2;
            float y1 = (Float) getY.invoke(event, 0);
            float y2 = (Float) getY.invoke(event, 1);
            y = y1 - y2;

        } catch (SecurityException e) {
        } catch (NoSuchMethodException e) {
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        } catch (InvocationTargetException e) {
        }
        return FloatMath.sqrt(x * x + y * y);
    }

    // Some flags set manually for convenience
    private final int MotionEvent_ACTION_MASK         = 255; // that is 0xFF or 11111111
    private final int MotionEvent_ACTION_POINTER_DOWN = 5;   // that is 101
    private final int MotionEvent_ACTION_POINTER_UP   = 6;   // that is 110

    private class MyOnTouchListener implements OnTouchListener {

        // onTouch
        @Override
        public boolean onTouch(View v, MotionEvent event) {
            ImageView view = (ImageView) v;
            Drawable drawable = view.getDrawable();
            if (drawable == null)
                return true;
            switch (event.getAction() & MotionEvent_ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                savedMatrix.set(matrix);
                start.set(event.getX(), event.getY());
                mode = PAN;
                break;
            case MotionEvent_ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                if (oldDist > TOUCH_SENSITIVE) {
                    savedMatrix.set(matrix);
                    mode = PINCH_ZOOM;
                }
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent_ACTION_POINTER_UP:
                mode = NONE;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mode == PAN) {
                    // /////////////////////////////////////////
                    matrix.set(savedMatrix);
                    float[] matrixValues = new float[9];
                    Rect viewRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
                    matrix.getValues(matrixValues);
                    float currentY = matrixValues[Matrix.MTRANS_Y];
                    float currentX = matrixValues[Matrix.MTRANS_X];
                    float currentScale = matrixValues[Matrix.MSCALE_X];
                    float currentHeight = drawable.getIntrinsicHeight() * currentScale;
                    float currentWidth = drawable.getIntrinsicWidth() * currentScale;
                    float dx = event.getX() - start.x;
                    float dy = event.getY() - start.y;
                    float newX = currentX + dx;
                    float newY = currentY + dy;

                    RectF drawingRect = new RectF(newX, newY, newX + currentWidth, newY + currentHeight);
                    float diffUp = Math.min(viewRect.bottom - drawingRect.bottom, viewRect.top - drawingRect.top) - SPACING_TOP_AND_BOTTOM;
                    float diffDown = Math.max(viewRect.bottom - drawingRect.bottom, viewRect.top - drawingRect.top) + SPACING_TOP_AND_BOTTOM;
                    float diffLeft = Math.min(viewRect.left - drawingRect.left, viewRect.right - drawingRect.right) - SPACING_LEFT_AND_RIGHT;
                    float diffRight = Math.max(viewRect.left - drawingRect.left, viewRect.right - drawingRect.right) + SPACING_LEFT_AND_RIGHT;
                    if (diffUp > 0) {
                        dy += diffUp;
                    }
                    if (diffDown < 0) {
                        dy += diffDown;
                    }
                    if (diffLeft > 0) {
                        dx += diffLeft;
                    }
                    if (diffRight < 0) {
                        dx += diffRight;
                    }
                    matrix.postTranslate(dx, dy);
                } else if (mode == PINCH_ZOOM) {
                    float newDist = spacing(event);
                    if (newDist > TOUCH_SENSITIVE) {
                        matrix.set(savedMatrix);
                        float scale = newDist / oldDist;

                        // Get the center of the image. (Failed to get it when image has been moved)
                        Rect viewRect = new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
                        centerOfImage.x = viewRect.centerX();
                        centerOfImage.y = viewRect.centerY();

                        float[] f = new float[9];
                        Matrix tmp = new Matrix(matrix);
                        tmp.postScale(scale, scale, centerOfImage.x, centerOfImage.y);
                        tmp.getValues(f);
                        float scaleX = f[Matrix.MSCALE_X];
                        if (scaleX < MIN_SCALE || scaleX > MAX_SCALE) {
                            matrix.set(savedMatrixZoom);
                        } else {
                            matrix.postScale(scale, scale, centerOfImage.x, centerOfImage.y);
                            savedMatrixZoom.set(matrix);
                        }


                    }
                }
                break;
            }
            view.setImageMatrix(matrix);
            return true;
        } // End of public boolean onTouch(View v, MotionEvent event) {}

    } // End of private class MyOnTouchListener implements OnTouchListener {}

} // End of public class ActivityImageViewer extends Activity {}

回答1:

You can use the Scale Gesture Detector for pinch to zoom. Instead of creating pinch to zoom from scratch you can do something like following,

public class MyCustomView extends View {

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

public MyCustomView(Context mContext){
    ...
    // View code goes here
    ...
    mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
}

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

@Override
public void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    canvas.save();
    canvas.scale(mScaleFactor, mScaleFactor);
    ...
    // onDraw() code goes here
    ...
    canvas.restore();
}

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

        invalidate();
        return true;
    }
}
}

Note : Your translation will reside in onDraw method to scale an image.