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 传送门