Android 图片手势缩放,绘制标记

2020-07-20  本文已影响0人  隐姓埋名的猫大叔

在开发项目过程中,曾经遇到这么个功能需求:某运动员身体不适,要在一张人体骨骼图上圈出相应不适的地方,可以手势缩放查看细节,点击重复的地点则取消选择,提交伤病说明
在浏览相关github与博客后,发现类似但不符合的,故此站在巨人的肩膀上写出此篇文章(很久前借鉴大佬的但是再去找的时候找不到链接,知道的小伙伴提供下),给一些小伙伴们借鉴。

效果图如下(文末附上github地址):


录制动态图.gif

首先将一些接口与抽象类介绍下:
IPhotoViewZoom ,用于监听图片缩放与返回缩放比例的回调

public interface IPhotoViewZoom {
    /**
     * Returns true if the PhotoView is set to allow zooming of Photos.
     *
     * @return true if the PhotoView allows zooming.
     */
    boolean canZoom();

    /**
     * Gets the Display Rectangle of the currently displayed Drawable. The
     * Rectangle is relative to this View and includes all scaling and
     * translations.
     *
     * @return - RectF of Displayed Drawable
     */
    RectF getDisplayRect();

    /**
     * @return The current minimum scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    float getMinScale();

    /**
     * @return The current middle scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    float getMidScale();

    /**
     * @return The current maximum scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    float getMaxScale();

    /**
     * Returns the current scale value
     *
     * @return float - current scale value
     */
    float getScale();

    /**
     * Return the current scale type in use by the ImageView.
     */
    ImageView.ScaleType getScaleType();

    /**
     * Whether to allow the ImageView's parent to intercept the touch event when the photo is scroll to it's horizontal edge.
     */
    void setAllowParentInterceptOnEdge(boolean allow);

    /**
     * Sets the minimum scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    void setMinScale(float minScale);

    /**
     * Sets the middle scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    void setMidScale(float midScale);

    /**
     * Sets the maximum scale level. What this value represents depends on the current {@link ImageView.ScaleType}.
     */
    void setMaxScale(float maxScale);

    /**
     * Register a callback to be invoked when the Photo displayed by this view is long-pressed.
     *
     * @param listener - Listener to be registered.
     */
    void setOnLongClickListener(View.OnLongClickListener listener);

    /**
     * Register a callback to be invoked when the Matrix has changed for this
     * View. An example would be the user panning or scaling the Photo.
     *
     * @param listener - Listener to be registered.
     */
    void setOnMatrixChangeListener(PhotoViewAttacherZoom.OnMatrixChangedListener listener);

    /**
     * Register a callback to be invoked when the Photo displayed by this View
     * is tapped with a single tap.
     *
     * @param listener - Listener to be registered.
     */
    void setOnPhotoTapListener(PhotoViewAttacherZoom.OnPhotoTapListener listener);

    /**
     * Register a callback to be invoked when the View is tapped with a single
     * tap.
     *
     * @param listener - Listener to be registered.
     */
    void setOnViewTapListener(PhotoViewAttacherZoom.OnViewTapListener listener);

    /**
     * Controls how the image should be resized or moved to match the size of
     * the ImageView. Any scaling or panning will happen within the confines of
     * this {@link ImageView.ScaleType}.
     *
     * @param scaleType - The desired scaling mode.
     */
    void setScaleType(ImageView.ScaleType scaleType);

    /**
     * Allows you to enable/disable the zoom functionality on the ImageView.
     * When disable the ImageView reverts to using the FIT_CENTER matrix.
     *
     * @param zoomable - Whether the zoom functionality is enabled.
     */
    void setZoomable(boolean zoomable);

    /**
     * Zooms to the specified scale, around the focal point given.
     *
     * @param scale  - Scale to zoom to
     * @param focalX - X Focus Point
     * @param focalY - Y Focus Point
     */
    void zoomTo(float scale, float focalX, float focalY);
}


抽象类VersionedGestureDetectorZoom,定义一些手势如单指滑动,抬起,按下,双手指触摸等属性方法

public abstract class VersionedGestureDetectorZoom {
    OnGestureListener mListener;

    public static VersionedGestureDetectorZoom newInstance(Context context, OnGestureListener listener) {
        final int sdkVersion = Build.VERSION.SDK_INT;
        VersionedGestureDetectorZoom detector = null;

        if (sdkVersion < Build.VERSION_CODES.ECLAIR) {
            detector = new CupcakeDetector(context);
        } else if (sdkVersion < Build.VERSION_CODES.FROYO) {
            detector = new EclairDetector(context);
        } else {
            detector = new FroyoDetector(context);
        }
        detector.mListener = listener;
        return detector;
    }

    public boolean onTouchEvent(MotionEvent ev) {
        return true;
    }

    public abstract boolean isScaling();

    public interface OnGestureListener {
        public void onDrag(float dx, float dy);

        public void onScale(float scaleFactor, float focusX, float focusY);

        public void setUpXY(float x, float y);

        public void setDownXY(float x, float y);

        public void setMoveXY(float x, float y);

        public void setTwofingerTonch(boolean b);

        public void setpostInvalidate();

        public void extendedImg();//放大图片

    }

    private static class CupcakeDetector extends VersionedGestureDetectorZoom {
        float mLastTouchX;
        float mLastTouchY;
        final float mTouchSlop;
        final float mMinimumVelocity;

        public CupcakeDetector(Context context) {
            final ViewConfiguration configuration = ViewConfiguration.get(context);
            mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
            mTouchSlop = configuration.getScaledTouchSlop();
        }

        private VelocityTracker mVelocityTracker;
        private boolean mIsDragging;

        float getActiveX(MotionEvent ev) {
            return ev.getX();
        }

        float getActiveY(MotionEvent ev) {
            return ev.getY();
        }

        public boolean isScaling() {
            return false;
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN: {
                    mListener.setDownXY(ev.getX(), ev.getY());
                    mVelocityTracker = VelocityTracker.obtain();
                    mVelocityTracker.addMovement(ev);

                    mLastTouchX = getActiveX(ev);
                    mLastTouchY = getActiveY(ev);
                    mIsDragging = false;
                    break;
                }

                case MotionEvent.ACTION_MOVE: {
                    mListener.setMoveXY(ev.getX(), ev.getY());
                    final float x = getActiveX(ev);
                    final float y = getActiveY(ev);
                    final float dx = x - mLastTouchX, dy = y - mLastTouchY;

                    if (!mIsDragging) {
                        // Use Pythagoras to see if drag length is larger than
                        // touch slop
                        mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;
                    }

                    if (mIsDragging) {
                        //LogUtils.w("3699**" + dx + "<-->" + dy);
                        mListener.onDrag(dx, dy);
                        mLastTouchX = x;
                        mLastTouchY = y;

                        if (null != mVelocityTracker) {
                            mVelocityTracker.addMovement(ev);
                        }
                    }
                    break;
                }

                case MotionEvent.ACTION_CANCEL: {
                    // Recycle Velocity Tracker
                    if (null != mVelocityTracker) {
                        mVelocityTracker.recycle();
                        mVelocityTracker = null;
                    }
                    break;
                }

                case MotionEvent.ACTION_UP: {
                    mListener.setUpXY(ev.getX(), ev.getY());
                    if (mIsDragging) {
                        if (null != mVelocityTracker) {
                            mLastTouchX = getActiveX(ev);
                            mLastTouchY = getActiveY(ev);

                            // Compute velocity within the last 1000ms
                            mVelocityTracker.addMovement(ev);
                            mVelocityTracker.computeCurrentVelocity(1000);

                            final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker.getYVelocity();

                            // If the velocity is greater than minVelocity, call
                            // listener
                            if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) {
                                //mListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);
                            }
                        }
                    }

                    // Recycle Velocity Tracker
                    if (null != mVelocityTracker) {
                        mVelocityTracker.recycle();
                        mVelocityTracker = null;
                    }
                    break;
                }
            }

            return true;
        }
    }


    @TargetApi(5)
    private static class EclairDetector extends CupcakeDetector {
        private static final int INVALID_POINTER_ID = -1;
        private int mActivePointerId = INVALID_POINTER_ID;
        private int mActivePointerIndex = 0;

        public EclairDetector(Context context) {
            super(context);
        }

        @Override
        float getActiveX(MotionEvent ev) {
            try {
                return ev.getX(mActivePointerIndex);
            } catch (Exception e) {
                return ev.getX();
            }
        }

        @Override
        float getActiveY(MotionEvent ev) {
            try {
                return ev.getY(mActivePointerIndex);
            } catch (Exception e) {
                return ev.getY();
            }
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            final int action = ev.getAction();
            switch (action & MotionEvent.ACTION_MASK) {
                case MotionEvent.ACTION_DOWN:
                    mActivePointerId = ev.getPointerId(0);
                    break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    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;
                        mActivePointerId = ev.getPointerId(newPointerIndex);
                        mLastTouchX = ev.getX(newPointerIndex);
                        mLastTouchY = ev.getY(newPointerIndex);
                    }
                    break;
            }

            mActivePointerIndex = ev.findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId : 0);
            return super.onTouchEvent(ev);
        }
    }

    @TargetApi(8)
    private static class FroyoDetector extends EclairDetector {

        private final ScaleGestureDetector mDetector;

        // Needs to be an inner class so that we don't hit
        // VerifyError's on API 4.
        private final OnScaleGestureListener mScaleListener = new OnScaleGestureListener() {

            @Override
            public boolean onScale(ScaleGestureDetector detector) {
                mListener.onScale(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY());
                mListener.setTwofingerTonch(true);
                Log.w("3699双手向外侧", detector.getFocusX() + "onScale" + detector.getFocusY());
                return true;
            }

            @Override
            public boolean onScaleBegin(ScaleGestureDetector detector) {
                Log.w("3699双手向外侧", "onScaleBegin");
                mListener.extendedImg();
                mListener.setTwofingerTonch(true);
                return true;
            }

            @Override
            public void onScaleEnd(ScaleGestureDetector detector) {
                Log.w("3699双手向外侧", "onScaleEnd");
                mListener.setTwofingerTonch(false);
                // NO-OP
                //mListener.setTwofingerTonch(false);
            }
        };

        public FroyoDetector(Context context) {
            super(context);
            mDetector = new ScaleGestureDetector(context, mScaleListener);
        }

        @Override
        public boolean isScaling() {
            return mDetector.isInProgress();
        }

        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            mDetector.onTouchEvent(ev);
            return super.onTouchEvent(ev);
        }

    }
}

抽象类ScrollerProxy 滚动代理类,通过坐标用来记录View滚动的位置为水平或者垂直

public abstract class ScrollerProxy {

    public static ScrollerProxy getScroller(Context context) {
        if (VERSION.SDK_INT < VERSION_CODES.GINGERBREAD) {
            return new PreGingerScroller(context);
        } else {
            return new GingerScroller(context);
        }
    }

    public abstract boolean computeScrollOffset();

    public abstract void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY,
            int maxY, int overX, int overY);

    public abstract void forceFinished(boolean finished);

    public abstract int getCurrX();

    public abstract int getCurrY();

    @TargetApi(9)
    private static class GingerScroller extends ScrollerProxy {

        private OverScroller mScroller;

        public GingerScroller(Context context) {
            mScroller = new OverScroller(context);
        }

        @Override
        public boolean computeScrollOffset() {
            return mScroller.computeScrollOffset();
        }

        @Override
        public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY,
                int overX, int overY) {
            mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, overX, overY);
        }

        @Override
        public void forceFinished(boolean finished) {
            mScroller.forceFinished(finished);
        }

        @Override
        public int getCurrX() {
            return mScroller.getCurrX();
        }

        @Override
        public int getCurrY() {
            return mScroller.getCurrY();
        }
    }

    private static class PreGingerScroller extends ScrollerProxy {

        private Scroller mScroller;

        public PreGingerScroller(Context context) {
            mScroller = new Scroller(context);
        }

        @Override
        public boolean computeScrollOffset() {
            return mScroller.computeScrollOffset();
        }

        @Override
        public void fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY,
                int overX, int overY) {
            mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY);
        }

        @Override
        public void forceFinished(boolean finished) {
            mScroller.forceFinished(finished);
        }

        @Override
        public int getCurrX() {
            return mScroller.getCurrX();
        }

        @Override
        public int getCurrY() {
            return mScroller.getCurrY();
        }
    }
}

再附上SDK不同版本中,View的动画操作调用SDK16,Compat

SDK16类

@TargetApi(16)
public class SDK16 {

    public static void postOnAnimation(View view, Runnable r) {
        view.postOnAnimation(r);
    }

}

Compat类

public class Compat {
    
    private static final int SIXTY_FPS_INTERVAL = 1000 / 60;
    
    public static void postOnAnimation(View view, Runnable runnable) {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            SDK16.postOnAnimation(view, runnable);
        } else {
            view.postDelayed(runnable, SIXTY_FPS_INTERVAL);
        }
    }

}

抽象类PhotoViewAttacherZoom 定义手势监听后,通过 Matrix 矩阵来控制视图的变换和滚动等属性方法

public abstract class PhotoViewAttacherZoom implements IPhotoViewZoom, View.OnTouchListener,
        VersionedGestureDetectorZoom.OnGestureListener,
        GestureDetector.OnDoubleTapListener,
        ViewTreeObserver.OnGlobalLayoutListener {

    static final String LOG_TAG = "PhotoViewAttacherZoom";

    // let debug flag be dynamic, but still Proguard can be used to remove from
    // release builds
    static final boolean DEBUG = Log.isLoggable(LOG_TAG, Log.DEBUG);

    static final int EDGE_NONE = -1;
    static final int EDGE_LEFT = 0;
    static final int EDGE_RIGHT = 1;
    static final int EDGE_BOTH = 2;

    public static final float DEFAULT_MAX_SCALE = 6.0f;
    public static final float DEFAULT_MID_SCALE = 1.75f;
    public static final float DEFAULT_MIN_SCALE = 1.0f;

    private float mMinScale = DEFAULT_MIN_SCALE;
    private float mMidScale = DEFAULT_MID_SCALE;
    private float mMaxScale = DEFAULT_MAX_SCALE;

    public float mGetX;
    public float mGetY;
    private boolean mAllowParentInterceptOnEdge = true;

    private static void checkZoomLevels(float minZoom, float midZoom,
                                        float maxZoom) {
        if (minZoom >= midZoom) {
            throw new IllegalArgumentException(
                    "MinZoom should be less than MidZoom");
        } else if (midZoom >= maxZoom) {
            throw new IllegalArgumentException(
                    "MidZoom should be less than MaxZoom");
        }
    }

    /**
     * @return true if the ImageView exists, and it's Drawable existss
     */
    private static boolean hasDrawable(ImageView imageView) {
        return null != imageView && null != imageView.getDrawable();
    }

    /**
     * @return true if the ScaleType is supported.
     */
    private static boolean isSupportedScaleType(final ScaleType scaleType) {
        if (null == scaleType) {
            return false;
        }

        switch (scaleType) {
            case MATRIX:
                throw new IllegalArgumentException(scaleType.name()
                        + " is not supported in PhotoView");

            default:
                return true;
        }
    }

    /**
     * Set's the ImageView's ScaleType to Matrix.
     */
    private static void setImageViewScaleTypeMatrix(ImageView imageView) {
        if (null != imageView) {
            if (imageView instanceof PhotoViewZoom) {
                /**
                 * PhotoView sets it's own ScaleType to Matrix, then diverts all
                 * calls setScaleType to this.setScaleType. Basically we don't
                 * need to do anything here
                 */
            } else {
                imageView.setScaleType(ScaleType.MATRIX);
            }
        }
    }

    private WeakReference<ImageView> mImageView;
    private ViewTreeObserver mViewTreeObserver;

    // Gesture Detectors
    private GestureDetector mGestureDetector;
    private VersionedGestureDetectorZoom mScaleDragDetector;

    // These are set so we don't keep allocating them on the heap
    private final Matrix mBaseMatrix = new Matrix();
    private final Matrix mDrawMatrix = new Matrix();
    private final Matrix mSuppMatrix = new Matrix();
    private final RectF mDisplayRect = new RectF();
    private final float[] mMatrixValues = new float[9];

    // Listeners
    private OnMatrixChangedListener mMatrixChangeListener;
    private OnPhotoTapListener mPhotoTapListener;
    private OnViewTapListener mViewTapListener;
    private OnLongClickListener mLongClickListener;

    private int mIvTop, mIvRight, mIvBottom, mIvLeft;
    private FlingRunnable mCurrentFlingRunnable;
    private int mScrollEdge = EDGE_BOTH;

    private boolean mZoomEnabled;
    private ScaleType mScaleType = ScaleType.FIT_CENTER;

    public PhotoViewAttacherZoom(ImageView imageView) {
        mImageView = new WeakReference<ImageView>(imageView);

        imageView.setOnTouchListener(this);

        mViewTreeObserver = imageView.getViewTreeObserver();
        mViewTreeObserver.addOnGlobalLayoutListener(this);

        // Make sure we using MATRIX Scale Type
        setImageViewScaleTypeMatrix(imageView);

        if (!imageView.isInEditMode()) {
            // Create Gesture Detectors...
            mScaleDragDetector = VersionedGestureDetectorZoom.newInstance(
                    imageView.getContext(), this);

            mGestureDetector = new GestureDetector(imageView.getContext(),
                    new GestureDetector.SimpleOnGestureListener() {

                        // forward long click listener
                        @Override
                        public void onLongPress(MotionEvent e) {
                            if (null != mLongClickListener) {
                                mLongClickListener.onLongClick(mImageView.get());
                            }
                        }
                    });

            mGestureDetector.setOnDoubleTapListener(this);

            // Finally, update the UI so that we're zoomable
            setZoomable(true);
        }
    }

    @Override
    public final boolean canZoom() {
        return mZoomEnabled;
    }

    /**
     * Clean-up the resources attached to this object. This needs to be called
     * when the ImageView is no longer used. A good example is from
     * {@link View#onDetachedFromWindow()} or from
     * {@link android.app.Activity#onDestroy()}. This is automatically called if
     */
    @SuppressLint("NewApi")
    // @SuppressWarnings("deprecation")
    // public final void cleanup() {
    // if (null != mImageView) {
    // mImageView.get().getViewTreeObserver().removeGlobalOnLayoutListener(this);
    // }
    // mViewTreeObserver = null;
    //
    // // Clear listeners too
    // mMatrixChangeListener = null;
    // mPhotoTapListener = null;
    // mViewTapListener = null;
    //
    // // Finally, clear ImageView
    // mImageView = null;
    // }
    @SuppressWarnings("deprecation")
    public final void cleanup() {
        if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
            if (null != mImageView) {
                if (mImageView.get() != null) {
                    mImageView.get().getViewTreeObserver()
                            .removeOnGlobalLayoutListener(this);
                }
            }

            if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) {
                mViewTreeObserver.removeOnGlobalLayoutListener(this);

                mViewTreeObserver = null;

                // Clear listeners too
                mMatrixChangeListener = null;
                mPhotoTapListener = null;
                mViewTapListener = null;
                // Finally, clear ImageView
                mImageView = null;
            }

        } else {
            if (null != mImageView) {
                mImageView.get().getViewTreeObserver()
                        .removeGlobalOnLayoutListener(this);
            }

            if (null != mViewTreeObserver && mViewTreeObserver.isAlive()) {
                mViewTreeObserver.removeGlobalOnLayoutListener(this);

                mViewTreeObserver = null;

                // Clear listeners too
                mMatrixChangeListener = null;
                mPhotoTapListener = null;
                mViewTapListener = null;
                // Finally, clear ImageView
                mImageView = null;
            }
        }
    }

    @Override
    public boolean onDoubleTap(MotionEvent motionEvent) {
        return false;
    }

    @Override
    public boolean onDoubleTapEvent(MotionEvent motionEvent) {
        return false;
    }

    @Override
    public final RectF getDisplayRect() {
        checkMatrixBounds();
        return getDisplayRect(getDisplayMatrix());
    }

    public final ImageView getImageView() {
        ImageView imageView = null;

        if (null != mImageView) {
            imageView = mImageView.get();
        }

        if (null == imageView) {
            cleanup();
            throw new IllegalStateException(
                    "ImageView no longer exists. You should not use this PhotoViewAttacherZoom any more.");
        }

        return imageView;
    }

    @Override
    public float getMinScale() {
        return mMinScale;
    }

    @Override
    public float getMidScale() {
        return mMidScale;
    }

    @Override
    public float getMaxScale() {
        return mMaxScale;
    }

    @Override
    public final float getScale() {
        return getValue(mSuppMatrix, Matrix.MSCALE_X);
    }

    @Override
    public final ScaleType getScaleType() {
        return mScaleType;
    }

    //获取图片的上坐标
    private PointF getLeftPointF(ImageView mImgPic) {
        Rect rectTemp = mImgPic.getDrawable().getBounds();
        float[] values = new float[9];
        mSuppMatrix.getValues(values);
        float leftX = values[2];
        float leftY = values[5];
        //Log.e("3699坐标", "左上角坐标:x" + leftX + "y" + leftY);
        return new PointF(leftX, leftY);
    }

    //获取图片的下坐标
    private PointF getRightPointF(ImageView mImgPic) {
        Rect rectTemp = mImgPic.getDrawable().getBounds();
        float[] values = new float[9];
        mSuppMatrix.getValues(values);
        float leftX = values[2] + rectTemp.width() * values[0];
        float leftY = values[5] + rectTemp.height() * values[4];

        setImgPointF(rectTemp.width() * values[0], rectTemp.height() * values[4], values[2], values[5], leftX, leftY);

        return new PointF(leftX, leftY);
    }

    protected abstract void setImgPointF(float scaleWidth, float scalehight, float leftX, float leftY, float rightX, float rightY);


    public final void onDrag(float dx, float dy) {
        //if (DEBUG)
        {
            Log.d(LOG_TAG, String.format("onDrag: dx: %.2f. dy: %.2f", dx, dy));
        }

        ImageView imageView = getImageView();
        getLeftPointF(imageView);
        getRightPointF(imageView);
        if (null != imageView && hasDrawable(imageView)) {
            mSuppMatrix.postTranslate(dx, dy);
            checkAndDisplayMatrix();

            /**
             * Here we decide whether to let the ImageView's parent to start
             * taking over the touch event.
             *
             * First we check whether this function is enabled. We never want
             * the parent to take over if we're scaling. We then check the edge
             * we're on, and the direction of the scroll (i.e. if we're pulling
             * against the edge, aka 'overscrolling', let the parent take over).
             */
            if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling()) {
                if (mScrollEdge == EDGE_BOTH
                        || (mScrollEdge == EDGE_LEFT && dx >= 1f)
                        || (mScrollEdge == EDGE_RIGHT && dx <= -1f)) {
                    imageView.getParent().requestDisallowInterceptTouchEvent(
                            false);
                }
            }
        }
    }

    //@Override
    public final void onFling(float startX, float startY, float velocityX,
                              float velocityY) {
       /* //if (DEBUG)
        {
            Log.d(LOG_TAG, "onFling. sX: " + startX + " sY: " + startY
                    + " Vx: " + velocityX + " Vy: " + velocityY);
        }
        setpostInvalidate();
        ImageView imageView = getImageView();
        if (hasDrawable(imageView))
        {
            mCurrentFlingRunnable = new FlingRunnable(imageView.getContext());
            Log.w("3699坐标88",mCurrentFlingRunnable.getCurrX()+"");
            mCurrentFlingRunnable.fling(imageView.getWidth(),
                    imageView.getHeight(), (int) velocityX, (int) velocityY);
            imageView.post(mCurrentFlingRunnable);
        }*/
    }

    @Override
    public final void onGlobalLayout() {
        ImageView imageView = getImageView();

        if (null != imageView && mZoomEnabled) {
            final int top = imageView.getTop();
            final int right = imageView.getRight();
            final int bottom = imageView.getBottom();
            final int left = imageView.getLeft();

            /**
             * We need to check whether the ImageView's bounds have changed.
             * This would be easier if we targeted API 11+ as we could just use
             * View.OnLayoutChangeListener. Instead we have to replicate the
             * work, keeping track of the ImageView's bounds and then checking
             * if the values change.
             */
            if (top != mIvTop || bottom != mIvBottom || left != mIvLeft
                    || right != mIvRight) {
                // Update our base matrix, as the bounds have changed
                updateBaseMatrix(imageView.getDrawable());

                // Update values as something has changed
                mIvTop = top;
                mIvRight = right;
                mIvBottom = bottom;
                mIvLeft = left;
            }
        }
    }

    public final void onScale(float scaleFactor, float focusX, float focusY) {
        //if (DEBUG)
        {
            Log.d(LOG_TAG, String.format(
                    "onScale: scale: %.2f. fX: %.2f. fY: %.2f", scaleFactor,
                    focusX, focusY));
        }

        if (hasDrawable(getImageView())
                && (getScale() < mMaxScale || scaleFactor < 1f)) {
            mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
            checkAndDisplayMatrix();
        }
    }

    public final boolean onSingleTapConfirmed(MotionEvent e) {
        ImageView imageView = getImageView();
        Log.w("3699tap", "``````````````");
        if (null != imageView) {
            if (null != mPhotoTapListener) {
                final RectF displayRect = getDisplayRect();

                if (null != displayRect) {
                    final float x = e.getX(), y = e.getY();


                    // Check to see if the user tapped on the photo
                    if (displayRect.contains(x, y)) {

                        float xResult = (x - displayRect.left)
                                / displayRect.width();
                        float yResult = (y - displayRect.top)
                                / displayRect.height();

                        mPhotoTapListener.onPhotoTap(imageView, xResult,
                                yResult,x,y);
                        return true;
                    }
                }
            }
            if (null != mViewTapListener) {
                mViewTapListener.onViewTap(imageView, e.getX(), e.getY());
            }
        }

        return false;
    }

    @Override
    public final boolean onTouch(View v, MotionEvent ev) {
        boolean handled = false;

        if (mZoomEnabled) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    // First, disable the Parent from intercepting the touch
                    // event
                    v.getParent().requestDisallowInterceptTouchEvent(true);

                    // If we're flinging, and the user presses down, cancel
                    // fling
                    cancelFling();
                    break;

                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    // If the user has zoomed less than min scale, zoom back
                    // to min scale
                    if (getScale() < mMinScale) {
                        RectF rect = getDisplayRect();
                        if (null != rect) {
                            v.post(new AnimatedZoomRunnable(getScale(), mMinScale,
                                    rect.centerX(), rect.centerY()));
                            handled = true;
                        }
                    }
                    break;
            }

            // Check to see if the user double tapped
            if (null != mGestureDetector && mGestureDetector.onTouchEvent(ev)) {
                handled = true;
            }

            // Finally, try the Scale/Drag detector
            if (null != mScaleDragDetector
                    && mScaleDragDetector.onTouchEvent(ev)) {
                handled = true;
            }
        }

        return handled;
    }

    @Override
    public void setAllowParentInterceptOnEdge(boolean allow) {
        mAllowParentInterceptOnEdge = allow;
    }

    @Override
    public void setMinScale(float minScale) {
        checkZoomLevels(minScale, mMidScale, mMaxScale);
        mMinScale = minScale;
    }

    @Override
    public void setMidScale(float midScale) {
        checkZoomLevels(mMinScale, midScale, mMaxScale);
        mMidScale = midScale;
    }

    @Override
    public void setMaxScale(float maxScale) {
        checkZoomLevels(mMinScale, mMidScale, maxScale);
        mMaxScale = maxScale;
    }

    @Override
    public final void setOnLongClickListener(OnLongClickListener listener) {
        mLongClickListener = listener;
    }

    @Override
    public final void setOnMatrixChangeListener(OnMatrixChangedListener listener) {
        mMatrixChangeListener = listener;
    }

    @Override
    public final void setOnPhotoTapListener(OnPhotoTapListener listener) {
        mPhotoTapListener = listener;
    }

    @Override
    public final void setOnViewTapListener(OnViewTapListener listener) {
        mViewTapListener = listener;
    }

    @Override
    public final void setScaleType(ScaleType scaleType) {
        if (isSupportedScaleType(scaleType) && scaleType != mScaleType) {
            mScaleType = scaleType;

            // Finally update
            update();
        }
    }

    @Override
    public final void setZoomable(boolean zoomable) {
        mZoomEnabled = zoomable;
        update();
    }

    public final void update() {
        ImageView imageView = getImageView();

        if (null != imageView) {
            if (mZoomEnabled) {
                // Make sure we using MATRIX Scale Type
                setImageViewScaleTypeMatrix(imageView);

                // Update the base matrix using the current drawable
                updateBaseMatrix(imageView.getDrawable());
            } else {
                // Reset the Matrix...
                resetMatrix();
            }
        }
    }

    @Override
    public final void zoomTo(float scale, float focalX, float focalY) {
        ImageView imageView = getImageView();

        if (null != imageView) {
            imageView.post(new AnimatedZoomRunnable(getScale(), scale, focalX,
                    focalY));
        }
    }

    protected Matrix getDisplayMatrix() {
        mDrawMatrix.set(mBaseMatrix);
        mDrawMatrix.postConcat(mSuppMatrix);
        return mDrawMatrix;
    }

    private void cancelFling() {
        if (null != mCurrentFlingRunnable) {
            mCurrentFlingRunnable.cancelFling();
            mCurrentFlingRunnable = null;
        }
    }

    /**
     * Helper method that simply checks the Matrix, and then displays the result
     */
    private void checkAndDisplayMatrix() {
        checkMatrixBounds();
        setImageViewMatrix(getDisplayMatrix());
        Log.w("checkAndDisplayMatrix", "***********************");
        setpostInvalidate();
        getRightPointF(getImageView());
    }

    private void checkImageViewScaleType() {
        ImageView imageView = getImageView();
        /**
         * PhotoView's getScaleType() will just divert to this.getScaleType() so
         * only call if we're not attached to a PhotoView.
         */
        if (null != imageView && !(imageView instanceof PhotoViewZoom)) {
            if (imageView.getScaleType() != ScaleType.MATRIX) {
                throw new IllegalStateException(
                        "The ImageView's ScaleType has been changed since attaching a PhotoViewAttacherZoom");
            }
        }
    }

    private void checkMatrixBounds() {
        final ImageView imageView = getImageView();
        if (null == imageView) {
            return;
        }

        final RectF rect = getDisplayRect(getDisplayMatrix());
        if (null == rect) {
            return;
        }

        final float height = rect.height(), width = rect.width();
        float deltaX = 0, deltaY = 0;

        final int viewHeight = imageView.getHeight();
        if (height <= viewHeight) {
            switch (mScaleType) {
                case FIT_START:
                    deltaY = -rect.top;
                    break;
                case FIT_END:
                    deltaY = viewHeight - height - rect.top;
                    break;
                default:
                    deltaY = (viewHeight - height) / 2 - rect.top;
                    break;
            }
        } else if (rect.top > 0) {
            deltaY = -rect.top;
        } else if (rect.bottom < viewHeight) {
            deltaY = viewHeight - rect.bottom;
        }

        final int viewWidth = imageView.getWidth();
        if (width <= viewWidth) {
            switch (mScaleType) {
                case FIT_START:
                    deltaX = -rect.left;
                    break;
                case FIT_END:
                    deltaX = viewWidth - width - rect.left;
                    break;
                default:
                    deltaX = (viewWidth - width) / 2 - rect.left;
                    break;
            }
            mScrollEdge = EDGE_BOTH;
        } else if (rect.left > 0) {
            mScrollEdge = EDGE_LEFT;
            deltaX = -rect.left;
        } else if (rect.right < viewWidth) {
            deltaX = viewWidth - rect.right;
            mScrollEdge = EDGE_RIGHT;
        } else {
            mScrollEdge = EDGE_NONE;
        }

        // Finally actually translate the matrix
        mSuppMatrix.postTranslate(deltaX, deltaY);
    }

    /**
     * Helper method that maps the supplied Matrix to the current Drawable
     *
     * @param matrix - Matrix to map Drawable against
     * @return RectF - Displayed Rectangle
     */
    private RectF getDisplayRect(Matrix matrix) {
        ImageView imageView = getImageView();

        if (null != imageView) {
            Drawable d = imageView.getDrawable();
            if (null != d) {
                mDisplayRect.set(0, 0, d.getIntrinsicWidth(),
                        d.getIntrinsicHeight());
                matrix.mapRect(mDisplayRect);
                return mDisplayRect;
            }
        }
        return null;
    }

    /**
     * Helper method that 'unpacks' a Matrix and returns the required value
     *
     * @param matrix     - Matrix to unpack
     * @param whichValue - Which value from Matrix.M* to return
     * @return float - returned value
     */
    private float getValue(Matrix matrix, int whichValue) {
        matrix.getValues(mMatrixValues);
        return mMatrixValues[whichValue];
    }

    /**
     * Resets the Matrix back to FIT_CENTER, and then displays it.s
     */
    private void resetMatrix() {
        mSuppMatrix.reset();
        setImageViewMatrix(getDisplayMatrix());
        checkMatrixBounds();
    }

    private void setImageViewMatrix(Matrix matrix) {
        ImageView imageView = getImageView();
        if (null != imageView) {
            checkImageViewScaleType();
            imageView.setImageMatrix(matrix);

            // Call MatrixChangedListener if needed
            if (null != mMatrixChangeListener) {
                RectF displayRect = getDisplayRect(matrix);
                if (null != displayRect) {
                    mMatrixChangeListener.onMatrixChanged(displayRect);
                }
            }
        }
    }

    /**
     * Calculate Matrix for FIT_CENTER
     *
     * @param d - Drawable being displayed
     */
    private void updateBaseMatrix(Drawable d) {
        ImageView imageView = getImageView();
        if (null == imageView || null == d) {
            return;
        }

        final float viewWidth = imageView.getWidth();
        final float viewHeight = imageView.getHeight();
        final int drawableWidth = d.getIntrinsicWidth();
        final int drawableHeight = d.getIntrinsicHeight();

        mBaseMatrix.reset();

        final float widthScale = viewWidth / drawableWidth;
        final float heightScale = viewHeight / drawableHeight;

        if (mScaleType == ScaleType.CENTER) {
            mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F,
                    (viewHeight - drawableHeight) / 2F);

        } else if (mScaleType == ScaleType.CENTER_CROP) {
            float scale = Math.max(widthScale, heightScale);
            mBaseMatrix.postScale(scale, scale);
            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
                    (viewHeight - drawableHeight * scale) / 2F);

        } else if (mScaleType == ScaleType.CENTER_INSIDE) {
            float scale = Math.min(1.0f, Math.min(widthScale, heightScale));
            mBaseMatrix.postScale(scale, scale);
            mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F,
                    (viewHeight - drawableHeight * scale) / 2F);
        } else {
            RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight);
            RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight);

            switch (mScaleType) {
                case FIT_CENTER:
                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER);
                    break;

                case FIT_START:
                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START);
                    break;

                case FIT_END:
                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END);
                    break;

                case FIT_XY:
                    mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL);
                    break;

                default:
                    break;
            }
        }

        resetMatrix();
    }

    /**
     * Interface definition for a callback to be invoked when the internal
     * Matrix has changed for this View.
     *
     * @author Chris Banes
     */
    public static interface OnMatrixChangedListener {
        /**
         * Callback for when the Matrix displaying the Drawable has changed.
         * This could be because the View's bounds have changed, or the user has
         * zoomed.
         *
         * @param rect - Rectangle displaying the Drawable's new bounds.
         */
        void onMatrixChanged(RectF rect);
    }

    /**
     * Interface definition for a callback to be invoked when the Photo is
     * tapped with a single tap.
     *
     * @author Chris Banes
     */
    public static interface OnPhotoTapListener {

        /**
         * A callback to receive where the user taps on a photo. You will only
         * receive a callback if the user taps on the actual photo, tapping on
         * 'whitespace' will be ignored.
         *
         * @param view - View the user tapped.
         * @param x    - where the user tapped from the of the Drawable, as
         *             percentage of the Drawable width.
         * @param y    - where the user tapped from the top of the Drawable, as
         *             percentage of the Drawable height.
         */
        void onPhotoTap(View view, float x, float y, float xLeft, float yTop);
    }

    /**
     * Interface definition for a callback to be invoked when the ImageView is
     * tapped with a single tap.
     *
     * @author Chris Banes
     */
    public static interface OnViewTapListener {

        /**
         * A callback to receive where the user taps on a ImageView. You will
         * receive a callback if the user taps anywhere on the view, tapping on
         * 'whitespace' will not be ignored.
         *
         * @param view - View the user tapped.
         * @param x    - where the user tapped from the left of the View.
         * @param y    - where the user tapped from the top of the View.
         */
        void onViewTap(View view, float x, float y);
    }

    private class AnimatedZoomRunnable implements Runnable {
        // These are 'postScale' values, means they're compounded each iteration
        static final float ANIMATION_SCALE_PER_ITERATION_IN = 1.07f;
        static final float ANIMATION_SCALE_PER_ITERATION_OUT = 0.93f;

        private final float mFocalX, mFocalY;
        private final float mTargetZoom;
        private final float mDeltaScale;

        public AnimatedZoomRunnable(final float currentZoom,
                                    final float targetZoom, final float focalX, final float focalY) {
            mTargetZoom = targetZoom;
            mFocalX = focalX;
            mFocalY = focalY;

            if (currentZoom < targetZoom) {
                mDeltaScale = ANIMATION_SCALE_PER_ITERATION_IN;
            } else {
                mDeltaScale = ANIMATION_SCALE_PER_ITERATION_OUT;
            }
        }

        public void run() {
            ImageView imageView = getImageView();

            if (null != imageView) {
                mSuppMatrix.postScale(mDeltaScale, mDeltaScale, mFocalX,
                        mFocalY);
                checkAndDisplayMatrix();

                final float currentScale = getScale();
                if ((mDeltaScale > 1f && currentScale < mTargetZoom)
                        || (mDeltaScale < 1f && mTargetZoom < currentScale)) {
                    // We haven't hit our target scale yet, so post ourselves
                    // again
                    Compat.postOnAnimation(imageView, this);

                } else {
                    // We've scaled past our target zoom, so calculate the
                    // necessary scale so we're back at target zoom
                    final float delta = mTargetZoom / currentScale;
                    mSuppMatrix.postScale(delta, delta, mFocalX, mFocalY);
                    checkAndDisplayMatrix();
                }
                setpostInvalidate();
            }
        }
    }

    private class FlingRunnable implements Runnable {

        private final ScrollerProxy mScroller;
        private int mCurrentX, mCurrentY;

        public FlingRunnable(Context context) {
            mScroller = ScrollerProxy.getScroller(context);

        }

        public int getCurrX() {
            return mScroller.getCurrX();
        }

        public int getCurrY() {
            return mScroller.getCurrY();
        }

        public void cancelFling() {
            if (DEBUG) {
                Log.d(LOG_TAG, "Cancel Fling");
            }
            mScroller.forceFinished(true);
        }

        public void fling(int viewWidth, int viewHeight, int velocityX,
                          int velocityY) {
            final RectF rect = getDisplayRect();
            if (null == rect) {
                return;
            }

            final int startX = Math.round(-rect.left);
            final int minX, maxX, minY, maxY;

            if (viewWidth < rect.width()) {
                minX = 0;
                maxX = Math.round(rect.width() - viewWidth);
            } else {
                minX = maxX = startX;
            }

            final int startY = Math.round(-rect.top);
            if (viewHeight < rect.height()) {
                minY = 0;
                maxY = Math.round(rect.height() - viewHeight);
            } else {
                minY = maxY = startY;
            }

            mCurrentX = startX;
            mCurrentY = startY;

            if (DEBUG) {
                Log.d(LOG_TAG, "fling. StartX:" + startX + " StartY:" + startY
                        + " MaxX:" + maxX + " MaxY:" + maxY);
            }

            // If we actually can move, fling the scroller
            if (startX != maxX || startY != maxY) {
                mScroller.fling(startX, startY, velocityX, velocityY, minX,
                        maxX, minY, maxY, 0, 0);
            }
        }

        @Override
        public void run() {
            ImageView imageView = getImageView();
            if (null != imageView && mScroller.computeScrollOffset()) {

                final int newX = mScroller.getCurrX();
                final int newY = mScroller.getCurrY();

                if (DEBUG) {
                    Log.d(LOG_TAG, "fling run(). CurrentX:" + mCurrentX
                            + " CurrentY:" + mCurrentY + " NewX:" + newX
                            + " NewY:" + newY);
                }

                mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY);
                setImageViewMatrix(getDisplayMatrix());

                mCurrentX = newX;
                mCurrentY = newY;

                // Post On animation
                Compat.postOnAnimation(imageView, this);
            }
        }
    }
}

再来看看我们的自定义View: PhotoViewZoom,在里面实现缩放滑动,点击绘制圆圈

public class PhotoViewZoom extends androidx.appcompat.widget.AppCompatImageView implements IPhotoViewZoom {

    //该图的真实宽高
    private static final float mOriginalWidth = 1600f;
    private static final float mOriginalHeight = 1200f;
    private Boolean isTouch = true;//是否可以点击
    private final PhotoViewAttacherZoom mAttacher;
    private Bitmap mainBitmap;

    static final String LOG_TAG = "PhotoViewZoom";
    //控件的宽高
    private int mMeasureWidth;
    private int mMeasureHeight;
    //原始图片宽高
    // 图片显示的长宽
    private int mDisplayHeight;
    private int mDisplayWidth;
    //原始图片宽高
    private ScaleType mPendingScaleType;
    private int mBitWidth;//
    private int mBitHeight;



    private BitmapFactory.Options options;
    private float currentX = 0;
    private float currentY = 0;
    private float pointX;//手指触摸起点的位置
    private float pointY;
    private float moveX;//当前手指位置
    private float moveY;
    private float mScaleWidth = 0;
    private float mScalehight;
    private float mLeftX;
    private float mLeftY;

    boolean isDraw = true;
    boolean isWidthMoreHeight = false;
    private boolean mTwofingerTonch = false;
    private float onSelectedDrawX = 0f;
    private float onSelectedDrawY = 0f;
    private float mScaleWMode = 1.0f;
    private int mMeasuredHeight;
    private List<CirclePoint> circlePoints = new ArrayList<>();//记录点击了哪几个坐标

    private Paint paint;





    public void setCirclePoints(List<CirclePoint> circlePoints) {
        this.circlePoints = circlePoints;


    }

    public PhotoViewZoom(Context context) {
        this(context, null);
    }

    public PhotoViewZoom(Context context, AttributeSet attr) {
        this(context, attr, 0);
    }


    public PhotoViewZoom(Context context, AttributeSet attr, int defStyle) {
        super(context, attr, defStyle);
        super.setScaleType(ScaleType.MATRIX);

        paint = new Paint();
        paint.setAntiAlias(true);//抗锯齿
        paint.setColor(Color.RED);
        paint.setStyle(Paint.Style.STROKE); //绘制空心圆
        paint.setStrokeWidth(PhoneUtil.dp2px(getContext(), 2));


        mainBitmap = getBitmap(R.mipmap.icon_illness);//人体图
        mAttacher = new PhotoViewAttacherZoom(this) {
            @Override
            protected void setImgPointF(float scaleWidth, float scalehight, float leftX, float leftY, float rightX, float rightY) {


                Log.w(LOG_TAG, mScaleWidth + " <--w*h--> " + mScalehight + "  mLeftX"
                        + mLeftX + "  mLeftY" + mLeftY + "  rightX" + rightX + "  righty" + rightY);
                mScaleWidth = scaleWidth;
                mScalehight = scalehight;
                mLeftX = leftX;
                mLeftY = leftY;

                postInvalidate();
            }

            @Override
            public void setDownXY(float x, float y) {
                //手点击屏幕

                pointX = x;
                pointY = y;
                moveX = pointX;
                moveY = pointY;
                currentX = pointX;
                currentY = pointY;
            }

            @Override
            public void setMoveXY(float x, float y) {
                //手拖动界面
                moveX = x;
                moveY = y;
                isDraw = false;
                if ((Math.sqrt(Math.abs(moveX - pointX) * Math.abs(moveX - pointX) +
                        Math.abs(moveY - pointY) * Math.abs(moveY - pointY))) > 2) {
                    isDraw = false;
                }
            }

            @Override
            public void setTwofingerTonch(boolean b) {
                //手拖动界面
                mTwofingerTonch = b;
            }

            @Override
            public void setpostInvalidate() {
                //刷新界面,刷新坐标点

                postInvalidate();
            }

            @Override
            public void extendedImg() {
                if (mBodyEnlargeListener != null) {
                    mBodyEnlargeListener.changeSelectBtn();
                }
            }

            @Override
            public void setUpXY(float x, float y) {
                //获取图片实际的长宽

                if ((Math.sqrt(Math.abs(x - pointX) * Math.abs(x - pointX) +
                        Math.abs(y - pointY) * Math.abs(y - pointY))) <= 2) {
                    isDraw = true;
                }
                currentX = currentX + (moveX - pointX);
                currentY = currentY + (moveY - pointY);
                pointX = currentX;
                pointY = currentY;
                if (null != mPendingScaleType) {
                    setScaleType(mPendingScaleType);
                    mPendingScaleType = null;
                }

                if (!isDraw) {
                    isDraw = true;
                    return;
                }

                options = new BitmapFactory.Options();
                options.inSampleSize = 1;

                Log.w(LOG_TAG, pointX + "*" + mLeftX + "*" + mBitWidth + "*" + mScaleWidth);
               // Log.w("3699xxyyyy", pointY + "*" + mLeftY + "*" + mBitHeight + "*" + mScalehight);
                boolean touchPointInTransparent;
                float touchWidth, touchHeight;
                //Log.v("3699isWidthMoreHeight", isWidthMoreHeight + "");
                CirclePoint circlePoint = new CirclePoint();
                circlePoint.setLeftX(mLeftX);
                circlePoint.setLeftY(mLeftY);

                if (!isWidthMoreHeight) {
                    //左边距
                    float dx = (mMeasureWidth * mScaleWidth * 1.00f / mBitWidth - (mScaleWidth * mScaleWMode)) / 2;
                    touchWidth = (pointX - mLeftX - dx) * mBitWidth / (mScaleWidth * mScaleWMode);
                    touchHeight = (pointY - mLeftY) * mBitHeight * 1.0000f / (mScalehight * mScaleWMode);

                } else {
                    //上边距
                    float dy = (mMeasureHeight * mScalehight * 1.00f / mBitHeight - (mScalehight * mScaleWMode)) / 2;
                    Log.v(LOG_TAG, dy + "");
                    touchWidth = (pointX - mLeftX) * mBitWidth * 1.0000f / (mScaleWidth * mScaleWMode);
                    touchHeight = (pointY - mLeftY - dy) * mBitHeight * 1.0000f / (mScalehight * mScaleWMode);

                }
                //判断是否超过边界,超过就不设置,不加入
                touchPointInTransparent = isTouchPointInTransparent(touchWidth, touchHeight);


                if (!touchPointInTransparent) {
                    if (isTouch && !mTwofingerTonch) {//如果是可以点击修改,则保存更新坐标


                        onSelectedDrawX = touchWidth * mOriginalWidth * 1.00f / mBitWidth;
                        onSelectedDrawY = touchHeight * mOriginalHeight * 1.00f / mBitHeight;





                        postInvalidate();
                    }
                }
            }
        };

        int paddingLeft = this.getPaddingLeft();
        Log.v(LOG_TAG,"图片距离左边距:"+paddingLeft + "");
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mMeasuredHeight = getMeasuredHeight();
        int measuredWidth = getMeasuredWidth();
        Log.v(LOG_TAG, getMeasuredWidth() + "*" + mMeasuredHeight);
        mDisplayHeight = mMeasuredHeight;
        mDisplayWidth = measuredWidth;
        //mDisplayWidth = (int) (mBitWidth * mDisplayHeight * 1.00f / mBitHeight + 0.5f);
        mMeasureWidth = getMeasureWidth();
        mMeasureHeight = getMeasureHeigh();
        Log.v(LOG_TAG, getMeasuredWidth() + "mMeasureWidth:" + mMeasureWidth);
        Log.v(LOG_TAG, mMeasuredHeight + "mMeasureHeight:" + mMeasureHeight);
        if (mBitHeight > mBitWidth) {
            mScaleWMode = mDisplayHeight * 1.000f / mBitHeight;
        } else {
            mScaleWMode = mDisplayWidth * 1.000f / mBitWidth;
        }
        resetBit();
        Log.w(LOG_TAG,  getHeight() + "***" + getMeasuredHeight());
        Log.w(LOG_TAG,  mScaleWMode + "***" + mDisplayWidth + "  " + mDisplayHeight);
    }


    public void resetBit() {
        RectF rect = getDisplayRect();
        if (null != rect) {
            mAttacher.zoomTo(1.0f, rect.centerX(), rect.centerY());
        }
    }

    public float getDisWidth() {
        return mScaleWidth * mScaleWMode;
    }

    public float getDisHight() {
        return mScalehight * mScaleWMode;
    }

    public float getWindowDisWidth() {
        return mMeasureWidth * mScaleWidth * 1.00f / mBitWidth;
    }

    public float getWindowDisHeight() {
        return mMeasureHeight * mScalehight * 1.00f / mBitHeight;
    }

    public float getLeftDx() {
        return (getWindowDisWidth() - getDisWidth()) * 1.00f / 2;
    }

    public float getTopDy() {
        return (getWindowDisHeight() - getDisHight()) * 1.00f / 2;
    }

    //图片当前缩放倍数
    public float getScaleMultiple() {
        return mScaleWidth * 1.00f / mBitWidth;
    }

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



        if (!isDraw || mTwofingerTonch) {
            return;
        }



        /***
         * circleRadius 圆的半径
         * */
        int circleRadius = (int) (PhoneUtil.dp2px(getContext(), 15) * getScaleMultiple());
            for (int i = 0; i < circlePoints.size(); i++) {
                final RectF displayRect = getDisplayRect();
                float left = circlePoints.get(i).getRealX() * displayRect.width() + displayRect.left;
                float top = circlePoints.get(i).getRealY() * displayRect.height() + displayRect.top;

                circlePoints.get(i).setDisWidth(left);
                circlePoints.get(i).setDisHight(top);

                Log.e(LOG_TAG, "原点坐标 x:距离图片左上角百分比" + circlePoints.get(i).getRealX() + "   y :距离图片顶部百分比" + circlePoints.get(i).getRealY());
                Log.e(LOG_TAG, "图片距离左边和上边的距离 left:" + left + "   top:" + top);

                canvas.drawCircle(left, top, circleRadius, paint);
            }


    }





    /**
     * @param x
     * @param y
     * @return 判断点击区域是否在透明区域
     */

    private boolean isTouchPointInTransparent(float x, float y) {

        Drawable drawable = this.getDrawable();
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        Log.w(LOG_TAG, x + "*" + y + "--->");
        int pixel = 0;
        Log.w(LOG_TAG, ((x > 0 && x < mBitWidth && y > 0 && y < mBitHeight) ? "" : "不") + "在范围内");

        if (y > 0 && y < mBitHeight  && x<bitmap.getWidth()) {
            pixel = bitmap.getPixel((int) x, (int) y);//获取像素值
            Log.v(LOG_TAG, pixel + "");
        }
        Log.v(LOG_TAG, (pixel == 0) + "" + (bitmap.getPixel(291, 53) == 0));
        return pixel == 0;
    }

    private Bitmap getBitmap(int resId) {
        Bitmap bitmap = null;
        try {
            InputStream ins = this.getResources().openRawResource(resId);
            BitmapFactory.Options options = new BitmapFactory.Options();
            //inJustDecodeBounds为true,不返回bitmap,只返回这个bitmap的尺寸
            options.inJustDecodeBounds = true;
            BitmapFactory.decodeResource(getResources(), resId, options);
            //利用返回的原图片的宽高,我们就可以计算出缩放比inSampleSize,获取指定宽度为300像素,等长宽比的缩略图,减少图片的像素
            //使用RGB_565减少图片大小
            options.inPreferredConfig = Bitmap.Config.RGB_565;
            //释放内存,共享引用(21版本后失效)
            options.inPurgeable = true;
            options.inInputShareable = true;
            //inJustDecodeBounds为false,返回bitmap
            options.inJustDecodeBounds = false;
            bitmap = BitmapFactory.decodeStream(ins, null, options);
            mBitWidth = bitmap.getWidth();
            mBitHeight = bitmap.getHeight();
            mScaleWidth = mBitWidth;
            mScalehight = mBitHeight;

            Log.i(LOG_TAG, bitmap.getWidth() + "--" + bitmap.getHeight());
            isWidthMoreHeight = (mBitWidth > mBitHeight);
            Log.i(LOG_TAG, mBitWidth + "--" + mBitHeight);
        } catch (OutOfMemoryError e) {
            e.printStackTrace();
        } catch (ArithmeticException e) {
            e.printStackTrace();
        }
        if (bitmap == null) {
            // 如果实例化失败 返回默认的Bitmap对象
            return mainBitmap;
        }
        return bitmap;
    }


    public int getMeasureWidth() {
        WindowManager wm = (WindowManager) getContext()
                .getSystemService(Context.WINDOW_SERVICE);

        return wm.getDefaultDisplay().getWidth();
    }

    public int getMeasureHeigh() {
        return mMeasuredHeight;
    }

    @Override
    public boolean canZoom() {
        return mAttacher.canZoom();
    }

    @Override
    public RectF getDisplayRect() {
        return mAttacher.getDisplayRect();
    }

    @Override
    public float getMinScale() {
        return mAttacher.getMinScale();
    }

    @Override
    public float getMidScale() {
        return mAttacher.getMidScale();
    }

    @Override
    public float getMaxScale() {
        return mAttacher.getMaxScale();
    }

    @Override
    public float getScale() {
        return mAttacher.getScale();
    }

    @Override
    public ScaleType getScaleType() {
        return mAttacher.getScaleType();
    }

    @Override
    public void setAllowParentInterceptOnEdge(boolean allow) {
        mAttacher.setAllowParentInterceptOnEdge(allow);
    }

    @Override
    public void setMinScale(float minScale) {
        mAttacher.setMinScale(minScale);
    }

    @Override
    public void setMidScale(float midScale) {
        mAttacher.setMidScale(midScale);
    }

    @Override
    public void setMaxScale(float maxScale) {
        mAttacher.setMaxScale(maxScale);
    }

    @Override
    // setImageBitmap calls through to this method
    public void setImageDrawable(Drawable drawable) {
        super.setImageDrawable(drawable);
        if (null != mAttacher) {
            mAttacher.update();
        }
    }

    @Override
    public void setImageResource(int resId) {
        super.setImageResource(resId);
        if (null != mAttacher) {
            mAttacher.update();
        }
    }

    @Override
    public void setImageURI(Uri uri) {
        super.setImageURI(uri);
        if (null != mAttacher) {
            mAttacher.update();
        }
    }

    @Override
    public void setOnMatrixChangeListener(PhotoViewAttacherZoom.OnMatrixChangedListener listener) {
        mAttacher.setOnMatrixChangeListener(listener);
    }

    @Override
    public void setOnLongClickListener(OnLongClickListener l) {
        mAttacher.setOnLongClickListener(l);
    }

    @Override
    public void setOnPhotoTapListener(PhotoViewAttacherZoom.OnPhotoTapListener listener) {
        mAttacher.setOnPhotoTapListener(listener);
    }

    @Override
    public void setOnViewTapListener(PhotoViewAttacherZoom.OnViewTapListener listener) {
        mAttacher.setOnViewTapListener(listener);
    }

    @Override
    public void setScaleType(ScaleType scaleType) {
        if (null != mAttacher) {
            mAttacher.setScaleType(scaleType);
        } else {
            mPendingScaleType = scaleType;
        }
    }


    @Override
    public void setZoomable(boolean zoomable) {
        mAttacher.setZoomable(zoomable);
    }

    @Override
    public void zoomTo(float scale, float focalX, float focalY) {
        mAttacher.zoomTo(scale, focalX, focalY);
    }

    @Override
    protected void onDetachedFromWindow() {
        mAttacher.cleanup();
        super.onDetachedFromWindow();
    }

    public interface IBodyEnlargeListener {
        void changeSelectBtn();
    }

    private IBodyEnlargeListener mBodyEnlargeListener;

    public void setBodyEnlargeListener(IBodyEnlargeListener listener) {
        this.mBodyEnlargeListener = listener;
    }

    public Boolean getTouch() {
        return isTouch;
    }

    public void setTouch(Boolean touch) {
        isTouch = touch;
    }

}

其中CirclePoint 是用于记录点击坐标相关信息

public class CirclePoint {
   /**
    * 用于android端设计本地图片
    * */
   private  float x;
   private  float y;

   /**
    * x,y 坐标左边距和上边距的百分比
    * */
   private float realX;
   private float realY;

   public float getRealX() {
       return realX;
   }

   public void setRealX(float realX) {
       this.realX = realX;
   }

   public float getRealY() {
       return realY;
   }

   public void setRealY(float realY) {
       this.realY = realY;
   }

   private boolean ifDouble;

   public boolean isIfDouble() {
       return ifDouble;
   }

   public void setIfDouble(boolean ifDouble) {
       this.ifDouble = ifDouble;
   }

   private float disWidth;
   private float disHight;
   private float windowDisWidth;
   private float windowDisHeight;
   private float leftX;
   private float leftY;

   private float topDy;

   private float touchWidth;
   private float touchHeight;


   public float getTouchWidth() {
       return touchWidth;
   }

   public void setTouchWidth(float touchWidth) {
       this.touchWidth = touchWidth;
   }

   public float getTouchHeight() {
       return touchHeight;
   }

   public void setTouchHeight(float touchHeight) {
       this.touchHeight = touchHeight;
   }

   public CirclePoint() {
   }

   public float getLeftX() {
       return leftX;
   }

   public void setLeftX(float leftX) {
       this.leftX = leftX;
   }

   public float getLeftY() {
       return leftY;
   }

   public void setLeftY(float leftY) {
       this.leftY = leftY;
   }

   public float getDisWidth() {
       return disWidth;
   }

   public void setDisWidth(float disWidth) {
       this.disWidth = disWidth;
   }

   public float getDisHight() {
       return disHight;
   }

   public void setDisHight(float disHight) {
       this.disHight = disHight;
   }

   public float getWindowDisWidth() {
       return windowDisWidth;
   }

   public void setWindowDisWidth(float windowDisWidth) {
       this.windowDisWidth = windowDisWidth;
   }

   public float getWindowDisHeight() {
       return windowDisHeight;
   }

   public void setWindowDisHeight(float windowDisHeight) {
       this.windowDisHeight = windowDisHeight;
   }



   public float getTopDy() {
       return topDy;
   }

   public void setTopDy(float topDy) {
       this.topDy = topDy;
   }

   public CirclePoint(float x, float y) {
       this.x = x;
       this.y = y;
   }

   public float getX() {
       return x;
   }

   public void setX(float x) {
       this.x = x;
   }

   public float getY() {
       return y;
   }

   public void setY(float y) {
       this.y = y;
   }


   @Override
   public String toString() {
       return "CirclePoint{" +
               "realX=" + realX +
               ", realY=" + realY +
               ", disWidth=" + disWidth +
               ", disHight=" + disHight +
               '}';
   }
}

在我们MainActivity中实现
xml 截图如下:


xml代码截图.png

主界面MainActivity 代码

public class MainActivity extends AppCompatActivity {

    private PhotoViewZoom zoomView;
    List<CirclePoint> circlePointList=new ArrayList<>();//标记手触摸点击的坐标点
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initZooView();
    }

    private void initZooView() {
        zoomView=findViewById(R.id.zoomView);
        zoomView.setOnPhotoTapListener(new PhotoViewAttacherZoom.OnPhotoTapListener() {
            @Override
            public void onPhotoTap(View view, float x, float y, float xWidth, float yTop) {

                CirclePoint circlePoint=new CirclePoint();
                circlePoint.setRealX(x);
                circlePoint.setRealY(y);
                circlePoint.setDisWidth(xWidth);
                circlePoint.setDisHight(yTop);
                circlePointList= reMoveRepeatCircle(circlePoint);

                zoomView.setCirclePoints(circlePointList);
                view.invalidate();
            }
        });
    }

    /**
     * 判断圆是否相交 半径和等于圆心距 相切 半径和 小于圆心距 相离 半径和大于圆心距 相交
     * 去除圆相交,相切 半径 15dp
     * */
    private List<CirclePoint> reMoveRepeatCircle(CirclePoint circlePoint) {
        circlePointList.add(circlePoint);
        int diameter= PhoneUtil.dp2px(MainActivity.this,30);
        for(int i=0;i<circlePointList.size()-1;i++){

            for (int j=i+1;j<circlePointList.size();j++){
                CirclePoint point1=circlePointList.get(i);
                CirclePoint point2=circlePointList.get(j);
                double d = Math.sqrt(Math.pow(point1.getDisWidth()-point2.getDisWidth(),2) + Math.pow(point1.getDisHight()-point2.getDisHight(),2));
                if(d<diameter){
                    //相交,相离
                    circlePointList.get(i).setIfDouble(true);
                    circlePointList.get(j).setIfDouble(true);
                }

            }

        }
        for(int i=0;i<circlePointList.size();i++){
            if(circlePointList.get(i).isIfDouble()){

                circlePointList.remove(i);
                i--;
            }

        }

        return circlePointList;

    }
}

reMoveRepeatCircle 方法用于判断过滤圆重合或相交的坐标点,将其消除(如果不用判断可以注释掉)
setOnPhotoTapListener 是对点击绘制图片后坐标的回调,我们将其记录到集合中,并且 调用view.invalidate()重新绘制图片点击坐标。
至此,即功能实现,希望能对小伙伴们起到借鉴。
github 传送门

上一篇下一篇

猜你喜欢

热点阅读