自定义可拖拽可放大图片控件学习

最近整理旧项目的一些图片功能,看到了一个可放大可拖拽的控件,仔细看了看涉及到的东西还挺多,特别里面点击事件的处理让我觉得很有意思,学到了挺多,下面看代码:

public class MatrixImageView extends ImageView {

	public final static String TAG = "MatrixImageView";

	//控件信息
	private float mStartScale;
	private float mImageWidth;
	private float mImageHeight;

	private final Matrix mMatrix = new Matrix();

	//手势
	private GestureDetector mGestureDetector;

	//单击回调
	private OnSingleTapListener singleTapListener;

	public MatrixImageView(Context context) {
		super(context);
		init();
	}

	public MatrixImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init();
	}

	private void init() {
		//是一个OnTouchListener,用来处理拖拽和放大效果
		MatrixTouchListener listener = new MatrixTouchListener();
		setOnTouchListener(listener);
		//在OnGestureListener里嵌一个OnTouchListener来实现双击和单击事件
		mGestureDetector = new GestureDetector(getContext(), new GestureListener(listener));

		//默认状态
		setBackgroundColor(Color.BLACK);
        //这里是关键,图片会对宽自适应
		setScaleType(ScaleType.FIT_CENTER);
	}

	public void setOnSingleTapListener(OnSingleTapListener onSingleTapListener) {
		this.singleTapListener = onSingleTapListener;
	}

	@Override
	public void setImageBitmap(Bitmap bm) {
		super.setImageBitmap(bm);
		if (getWidth() == 0) {
			ViewTreeObserver vto = getViewTreeObserver();
			vto.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
				public boolean onPreDraw() {
					initData();
					MatrixImageView.this.getViewTreeObserver().removeOnPreDrawListener(this);
					return true;
				}
			});
		}else {
			initData();
		}
	}

	private void initData() {
		mMatrix.set(getImageMatrix());
		float[] values = new float[9];
		mMatrix.getValues(values);
		mImageWidth = getWidth() / values[Matrix.MSCALE_X];
		mImageHeight = (getHeight() - values[Matrix.MTRANS_Y] * 2) / values[Matrix.MSCALE_Y];
		//初始缩放倍数
		mStartScale = values[Matrix.MSCALE_X];
	}

	public class MatrixTouchListener implements OnTouchListener {
		//拖拉照片模式
		private static final int MODE_DRAG = 1;
		//放大缩小照片模式
		private static final int MODE_ZOOM = 2;
		//不支持Matrix
		private static final int MODE_UNABLE = 3;

		private int mMode = 0;

		//最大缩放级别
		float mMaxScale = 6;
		//双击时的缩放级别
		float mDoubleClickScale = 2;


		//缩放开始时的手指间距
		private float mStartDis;
		//当前Matrix
		private final Matrix mCurrentMatrix = new Matrix();

		//用于记录开始时候的坐标位置
		private final PointF mStartPoint = new PointF();

		boolean isLeftDraggable;
		boolean isRightDraggable;
		boolean isFirstMove = false;

		@SuppressLint("ClickableViewAccessibility")
		@Override
		public boolean onTouch(View v, MotionEvent event) {
			switch (event.getActionMasked()) {
				case MotionEvent.ACTION_DOWN:
					mMode = MODE_DRAG;
					mStartPoint.set(event.getX(), event.getY());

					//居中不进行操作
					checkMatrixEnable();
					startDrag();
					break;
				case MotionEvent.ACTION_UP:
				case MotionEvent.ACTION_CANCEL:
					resetMatrix();
					stopDrag();
					break;
				case MotionEvent.ACTION_MOVE:
					if (mMode == MODE_ZOOM) {
						setZoomMatrix(event);
					}else if (mMode == MODE_DRAG) {
						setDragMatrix(event);
					}else {
						stopDrag();
					}
					break;
				case MotionEvent.ACTION_POINTER_DOWN:
					if (mMode == MODE_UNABLE)
						return true;
					mMode = MODE_ZOOM;
					mStartDis = distance(event);
					break;
				case MotionEvent.ACTION_POINTER_UP:
				default:
					break;
			}
			return mGestureDetector.onTouchEvent(event);
		}

		//思考一下,拖动只有在图片放大的时候才能进行
		private void startDrag() {
			isFirstMove = true;

			float[] values = new float[9];
			getImageMatrix().getValues(values);

			//出左边界
			if (values[Matrix.MTRANS_X] < 0) {
				isRightDraggable = true;
			}

			//出右边界
			if ((mImageWidth) * values[Matrix.MSCALE_X] + values[Matrix.MTRANS_X] > getWidth()) {
				isLeftDraggable = true;
			}

			//出界才允许拖拽
			if (isLeftDraggable || isRightDraggable) {
				//禁止父容器拦截事件,注意会复位
				getParent().requestDisallowInterceptTouchEvent(true);
			}
		}

		private void stopDrag() {
			//放开父层的View截获touch事件
			getParent().requestDisallowInterceptTouchEvent(false);
		}

		public void setDragMatrix(MotionEvent event) {
			//缩小时被重置了,这里只有放大状态
			if (isZoomChanged()) {
				float dx = event.getX() - mStartPoint.x;// 得到x轴的移动距离
				float dy = event.getY() - mStartPoint.y;// 得到y轴的移动距离

				//避免和双击冲突,大于10f才算是拖动
				if (Math.sqrt(dx * dx + dy * dy) > 10f) {
					mStartPoint.set(event.getX(), event.getY());
					mCurrentMatrix.set(getImageMatrix());

					float[] values = new float[9];
					mCurrentMatrix.getValues(values);
					dy = checkDyBound(values, dy);
					dx = checkDxBound(values, dx, dy);
					mCurrentMatrix.postTranslate(dx, dy);
					setImageMatrix(mCurrentMatrix);
				}
			}else {
				stopDrag();
			}
		}

		/**
		 *  判断缩放级别是否是改变过
		 *  @return   true表示非初始值,false表示初始值
		 */
		private boolean isZoomChanged() {
			float[] values = new float[9];
			getImageMatrix().getValues(values);
			//获取当前X轴缩放级别
			float scale = values[Matrix.MSCALE_X];
			return scale != mStartScale;
		}

		/**
		 *  和当前矩阵对比,检验dy,使图像移动后不会超出ImageView边界
		 *  @param values Matrix values
		 *  @param dy dy
		 *  @return 检验dy
		 */
		private float checkDyBound(float[] values, float dy) {
			float height = getHeight();
			//在画面内,Y轴未铺满
			if (mImageHeight * values[Matrix.MSCALE_Y] < height) {
				return 0;
			}
			//上面到顶了,坐标系以图片左上角为原点???
			if (values[Matrix.MTRANS_Y] + dy > 0) {
				dy = -values[Matrix.MTRANS_Y];
			}
			//下面到顶,图片高度减去屏幕高度是最大拖拽高度
			else if (values[Matrix.MTRANS_Y] + dy < -(mImageHeight * values[Matrix.MSCALE_Y] - height))
				dy = -(mImageHeight * values[Matrix.MSCALE_Y] - height) - values[Matrix.MTRANS_Y];
			return dy;
		}

		private float checkDxBound(float[] values, float dx, float dy) {
			float width = getWidth();
			//宽高比大于5:2???
			boolean b = Math.abs(dx) * 0.4f > Math.abs(dy);
			if (!isLeftDraggable && dx < 0) {
				if (b && isFirstMove) {
					stopDrag();
				}
				return 0;
			}

			if (!isRightDraggable && dx > 0) {
				if (b && isFirstMove) {
					stopDrag();
				}
				return 0;
			}
			isLeftDraggable = true;
			isRightDraggable = true;
			if (isFirstMove) {
				isFirstMove = false;
			}

			//画面内
			if (mImageWidth * values[Matrix.MSCALE_X] < width) {
				return 0;
			}

			//左边到顶
			if (values[Matrix.MTRANS_X] + dx > 0) {
				dx = -values[Matrix.MTRANS_X];
			}

			//右边到顶
			else if (values[Matrix.MTRANS_X] + dx < -(mImageWidth * values[Matrix.MSCALE_X] - width)) {
				dx = -(mImageWidth * values[Matrix.MSCALE_X] - width) - values[Matrix.MTRANS_X];
			}
			return dx;
		}

		//设置缩放Matrix
		private void setZoomMatrix(MotionEvent event) {
			//只有同时触屏两个点的时候才执行
			if (event.getPointerCount() < 2)
				return;
			// 结束距离
			float endDis = distance(event);
			// 两个手指并拢在一起的时候像素大于10
			if (endDis > 10f) {
				float scale = endDis / mStartDis;
				//重置距离
				mStartDis = endDis;
				mCurrentMatrix.set(getImageMatrix());

				float[] values = new float[9];
				mCurrentMatrix.getValues(values);
				scale = checkMaxScale(scale, values);

				//中心点
				PointF centerF = getCenter(scale, values);
				mCurrentMatrix.postScale(scale, scale, centerF.x, centerF.y);
				setImageMatrix(mCurrentMatrix);
			}
		}

		@SuppressWarnings("IntegerDivisionInFloatingPointContext")
		private PointF getCenter(float scale, float[] values) {
			//比原尺寸缩小的时候,或向放大方向时,使用图形中心点
			if (scale * values[Matrix.MSCALE_X] < mStartScale || scale >= 1) {
				return new PointF(getWidth() / 2, getHeight() / 2);
			}
			//比原尺寸大,且缩小时
			float cx = getWidth() / 2;
			float cy = getHeight() / 2;
			//拖拽到左边时,应该以左边界为准缩小,scale < 1比较前面一项?(简化)
			if ((getWidth() / 2 - values[Matrix.MTRANS_X]) * scale < getWidth() / 2)
				cx = 0;
			//拖拽到右边时,应该以右边界为准缩小
			if ((mImageWidth * values[Matrix.MSCALE_X] + values[Matrix.MTRANS_X]) * scale
					< getWidth())
				cx = getWidth();
			return new PointF(cx, cy);
		}

		private float checkMaxScale(float scale, float[] values) {
			if (scale * values[Matrix.MSCALE_X] > mMaxScale) {
				scale = mMaxScale / values[Matrix.MSCALE_X];
			}
			return scale;
		}

		private void resetMatrix() {
			if (checkRest()) {
				//缩小了需要重置
				mCurrentMatrix.set(mMatrix);
				setImageMatrix(mCurrentMatrix);
			}else {
				float[] values = new float[9];
				getImageMatrix().getValues(values);
				float height = mImageHeight * values[Matrix.MSCALE_Y];
				//放大但是Y轴未占满
				if (height < getHeight()) {
					//保证上下边距一致,再按之前y轴移动量移动???
					float topMargin = (getHeight() - height) / 2;
					if (topMargin != values[Matrix.MTRANS_Y]) {
						mCurrentMatrix.set(getImageMatrix());
						mCurrentMatrix.postTranslate(0, topMargin - values[Matrix.MTRANS_Y]);
						setImageMatrix(mCurrentMatrix);
					}
				}
			}
		}

		private boolean checkRest() {
			float[] values = new float[9];
			getImageMatrix().getValues(values);
			float scale = values[Matrix.MSCALE_X];
			return scale < mStartScale;
		}

		private void checkMatrixEnable() {
			if (getScaleType() != ScaleType.CENTER_INSIDE) {
				setScaleType(ScaleType.MATRIX);
			}else {
				//图片居中不支持拖拽
				mMode = MODE_UNABLE;
			}
		}

		private float distance(MotionEvent event) {
			float dx = event.getX(1) - event.getX(0);
			float dy = event.getY(1) - event.getY(0);
			return (float) Math.sqrt(dx * dx + dy * dy);
		}

		@SuppressWarnings("IntegerDivisionInFloatingPointContext")
		public void onDoubleClick() {
			float scale = isZoomChanged() ? 1 : mDoubleClickScale;
			mCurrentMatrix.set(mMatrix);
			mCurrentMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
			setImageMatrix(mCurrentMatrix);
		}
	}

	private class GestureListener extends SimpleOnGestureListener {
		private final MatrixTouchListener listener;

		public GestureListener(MatrixTouchListener listener) {
			this.listener = listener;
		}

		@Override
		public boolean onDown(MotionEvent e) {
			return true;
		}

		@Override
		public boolean onDoubleTap(MotionEvent e) {
			listener.onDoubleClick();
			return true;
		}

		@Override
		public boolean onSingleTapUp(MotionEvent e) {
			return super.onSingleTapUp(e);
		}

		@Override
		public void onLongPress(MotionEvent e) {
			super.onLongPress(e);
		}

		@Override
		public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
			return super.onScroll(e1, e2, distanceX, distanceY);
		}

		@Override
		public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
			return super.onFling(e1, e2, velocityX, velocityY);
		}

		@Override
		public void onShowPress(MotionEvent e) {
			super.onShowPress(e);
		}

		@Override
		public boolean onDoubleTapEvent(MotionEvent e) {
			return super.onDoubleTapEvent(e);
		}

		@Override
		public boolean onSingleTapConfirmed(MotionEvent e) {
			if (singleTapListener != null) {
				singleTapListener.onSingleTap();
			}
			return super.onSingleTapConfirmed(e);
		}
	}

	public interface OnSingleTapListener {
		void onSingleTap();
	}
}

代码有点长,但是我在看得过程中也加了很多注释,下面简单讲几点,其他一切看代码。

	private void init() {
		//是一个OnTouchListener,用来处理拖拽和放大效果
		MatrixTouchListener listener = new MatrixTouchListener();
		setOnTouchListener(listener);
		//在OnGestureListener里嵌一个OnTouchListener来实现双击和单击事件
		mGestureDetector = new GestureDetector(getContext(), new GestureListener(listener));

		//默认状态
		setBackgroundColor(Color.BLACK);
		//这里是关键,图片会对宽自适应
		setScaleType(ScaleType.FIT_CENTER);
	}

这里有两点需要注意,一个是 OnTouchListener和 OnGestureListener,这两个接口用来实现了两个不同的功能,特别的是我们在 OnGestureListener 里嵌一个 OnTouchListener。

另一点就是设置图片缩放类型,设置 FIT_CENTER 可以让图片宽度适应,后面一些计算都依赖于这个前提条件。

//禁止父容器拦截事件,注意会复位
getParent().requestDisallowInterceptTouchEvent(true);

这个在滑动冲突总经常用到,禁止父容器拦截事件,在拖拽过程总不应该让父容器触发事件。

mCurrentMatrix.set(getImageMatrix());
float[] values = new float[9];
mCurrentMatrix.getValues(values);
dy = checkDyBound(values, dy);
dx = checkDxBound(values, dx, dy);
mCurrentMatrix.postTranslate(dx, dy);
setImageMatrix(mCurrentMatrix);


float scale = isZoomChanged() ? 1 : mDoubleClickScale;
mCurrentMatrix.set(mMatrix);
mCurrentMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
setImageMatrix(mCurrentMatrix);

这里注意 getImageMatrix() 获取图片的 Matrix,更新完数据之后,要用 setImageMatrix 再设置一次,其中 Matrix 的修改,具体看下面官方文档吧!

https://developer.android.google.cn/reference/kotlin/androidx/compose/ui/graphics/Matrix?hl=en

其他各种判断界限的逻辑,说实话我也搞得有点懵,如果注释有误,可以再评论中指出,共同学习。

结语

这样一个控件写下来还是挺有难度的,看完也挺有收获。

end

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值