美文网首页
Android 图片手势缩放,绘制标记

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

作者: 隐姓埋名的猫大叔 | 来源:发表于2020-07-20 22:22 被阅读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 传送门

    相关文章

      网友评论

          本文标题:Android 图片手势缩放,绘制标记

          本文链接:https://www.haomeiwen.com/subject/psdecktx.html