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