Skip to content

Instantly share code, notes, and snippets.

@showsky
Created October 19, 2022 11:09
Show Gist options
  • Save showsky/03c390bd5e27429e9fe1f511ce6d2038 to your computer and use it in GitHub Desktop.
Save showsky/03c390bd5e27429e9fe1f511ce6d2038 to your computer and use it in GitHub Desktop.
ZoomImageView
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewTreeObserver;
import androidx.annotation.NonNull;
import tw.com.feebee.ConfigFlag;
import tw.com.feebee.utils.Logger;
/**
* Created by showsky on 2022/10/12
*/
public class ZoomImageView extends androidx.appcompat.widget.AppCompatImageView
implements ViewTreeObserver.OnGlobalLayoutListener,ScaleGestureDetector.OnScaleGestureListener,View.OnTouchListener{
@SuppressWarnings("unused")
private static final String TAG = "ZoomImageView";
/**
* 最大放大倍数
*/
public static final float mMaxScale = 4.0f;
/**
* 默认缩放
*/
private float mInitScale = 1.0f;
/**
* 双击放大比例
*/
private float mMidScale = 2.0f;
/**
* 检测缩放手势 多点触控手势识别 独立的类不是GestureDetector的子类
*/
private ScaleGestureDetector mScaleGestureDetector = null;//检测缩放的手势
/**
*检测类似长按啊 轻按啊 拖动 快速滑动 双击啊等等 OnTouch方法虽然也可以
* 但是对于一些复杂的手势需求自己去通过轨迹时间等等判断很复杂,因此我们采用系统
* 提供的手势类进行处理
*/
private GestureDetector mGestureDetector;
/**
* 如果正在缩放中就不向下执行,防止多次双击
*/
private boolean mIsAutoScaling;
/**
* Matrix的对图像的处理
* Translate 平移变换
* Rotate 旋转变换
* Scale 缩放变换
* Skew 错切变换
*/
private Matrix mScaleMatrix = new Matrix();
/**
* 处理矩阵的9个值
*/
private float[] mMatrixValue = new float[9];
//
private View.OnClickListener mOnClickListener;
public ZoomImageView(Context context) {
this(context, null);
}
public ZoomImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}
private void initView(Context context) {
setScaleType(ScaleType.MATRIX);
mScaleGestureDetector = new ScaleGestureDetector(context, this);
this.setOnTouchListener(this); //缩放的捕获要建立在setOnTouchListener上
//符合滑动的距离 它获得的是触发移动事件的最短距离,如果小于这个距离就不触发移动控件,
//如viewpager就是用这个距离来判断用户是否翻页
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//监听双击事件 SimpleOnGestureListener是OnGestureListener接口实现类,
//使用这个复写需要的方法就可以不用复写所有的方法
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
//如果正在缩放中就不向下执行,防止多次双击
if (mIsAutoScaling) {
return true;
}
//缩放的中心点
float x = e.getX();
float y = e.getY();
//如果当前缩放值小于这个临界值 则进行放大
if (getScale() < mMidScale) {
mIsAutoScaling = true;
//view中的方法 已x,y为坐标点放大到mMidScale 延时10ms
postDelayed(new AutoScaleRunnable(mMidScale, x, y), 16);
} else {
//如果当前缩放值大于这个临界值 则进行缩小操作 缩小到mInitScale
mIsAutoScaling = true;
postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
}
return true;
}
@Override
public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (mOnClickListener != null) {
mOnClickListener.onClick(ZoomImageView.this);
return true;
}
return super.onSingleTapUp(e);
}
});
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
//suppress deprecate warning because i have dealt with it
@Override
@SuppressWarnings("deprecation")
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
//--------------------------implement OnTouchListener----------------------------/
/**
*处理现图片放大后移动查看
*/
private int mLastPointCount;//触摸点发生移动时的触摸点个数
private boolean isCanDrag;//判断是否可以拖拽
private float mLatX;//记录移动之前按下去的那个坐标点
private float mLastY;
private int mTouchSlop;//系统默认触发移动事件的最短距离
private boolean isCheckTopAndBottom;//是否可以上下拖动
private boolean isCheckLeftAndRight;//是否可以左右拖动
@Override
public boolean onTouch(View v, MotionEvent event) {
//双击事件进行关联
if (mGestureDetector.onTouchEvent(event)) {
//如果是双击的话就直接不向下执行了
return true;
}
//将事件传递给ScaleGestureDetector
mScaleGestureDetector.onTouchEvent(event);
float x = 0;
float y = 0;
//可能出现多手指触摸的情况 ACTION_DOWN事件只能执行一次所以多点触控不能在down事件里面处理
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
x += event.getX(i);
y += event.getY(i);
}
//取平均值,得到的就是多点触控后产生的那个点的坐标
x /= pointerCount;
y /= pointerCount;
//每当触摸点发生移动时(从静止到移动),重置mLasX , mLastY mLastPointCount防止再次进入
if (mLastPointCount != pointerCount) {
//这里加一个参数并且设置成false的目的是,要判断位移的距离是否符合触发移动事件的最短距离
isCanDrag = false;
//记录移动之前按下去的那个坐标点,记录的值类似于断点续移,下次移动的时候从这个点开始
mLatX = x;
mLastY = y;
}
//重新赋值 说明如果是一些列连续滑动的操作就不会再次进入上面的判断 否则会重新确定坐标移动原点
mLastPointCount = pointerCount;
RectF rectF = getMatrixRectF();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下的时候如果发现图片缩放宽或者高大于屏幕宽高则请求viewpager不拦截事件交给ZoomImageView处理
//ZoomImageView可以进行缩放操作
if (Math.floor(rectF.width()) > getWidth() || Math.floor(rectF.height()) > getHeight()) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_MOVE:
//按下的时候如果发现图片缩放宽或者高大于屏幕宽高则请求viewpager不拦截事件交给ZoomImageView处理
//ZoomImageView可以进行缩放操作
if (Math.floor(rectF.width()) > getWidth() || Math.floor(rectF.height()) > getHeight()) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
//x,y移动的距离
float dx = x - mLatX;
float dy = y - mLastY;
//如果是不能拖拽,可能是因为手指变化,这时就去重新检测看看是不是符合滑动
if (!isCanDrag) {
//反正是根据勾股定理,调用系统API
isCanDrag = isMoveAction(dx, dy);
Logger.e(TAG, "移动3---->" + pointerCount);
}
if (isCanDrag) {
if (getDrawable() != null) {
//判断是宽或者高小于屏幕,就不在那个方向进行拖拽
isCheckLeftAndRight = isCheckTopAndBottom = true;
if (rectF.width() < getWidth()) {//如果图片宽度小于控件宽度
isCheckLeftAndRight = false;
dx = 0;
}
if (rectF.height() < getHeight()) { //如果图片的高度小于控件的高度
isCheckTopAndBottom = false;
dy = 0;
}
mScaleMatrix.postTranslate(dx, dy);
//解决拖拽的时候左右 上下都会出现留白的情况
checkBorderAndCenterWhenTranslate();
setImageMatrix(mScaleMatrix);
}
}
mLatX = x;//记录的值类似于断点续移,下次移动的时候从这个点开始
mLastY = y;
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mLastPointCount = 0;//抬起或者取消事件时候把这个置空
break;
}
return true;
}
//----------------------手势implement OnScaleGestureListener------------------------//
/**
*处理图片缩放
*/
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scale = getScale();//当前相对于初始尺寸的缩放(之前matrix中获得)
Logger.e(TAG, "matrix scale---->" + scale);
float scaleFactor = detector.getScaleFactor();//这个时刻缩放的/当前缩放尺度 (现在手势获取)
Logger.e(TAG, "scaleFactor---->" + scaleFactor);
if (getDrawable() == null) {
return true;
}
if ((scale < mMaxScale && scaleFactor > 1.0f) //放大
|| (scale > mInitScale && scaleFactor < 1.0f)) {//缩小
//如果要缩放的值比初始化还要小的话,就按照最小可以缩放的值进行缩放
if (scaleFactor * scale < mInitScale){
scaleFactor = mInitScale / scale;
Logger.e(TAG, "进来了1" + scaleFactor);
}
///如果要缩放的值比最大缩放值还要大,就按照最大可以缩放的值进行缩放
if (scaleFactor * scale > mMaxScale){
scaleFactor = mMaxScale / scale;
Logger.e(TAG, "进来了2---->" + scaleFactor);
}
Logger.e(TAG, "scaleFactor2---->" + scaleFactor);
//设置缩放比例
mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());//缩放中心是两手指之间
checkBorderAndCenterWhenScale();//解决这种缩放导致缩放到最小时图片位置可能发生了变化
// mScaleMatrix.postScale(scaleFactor, scaleFactor,
// getWidth() / 2, getHeight() / 2);//缩放中心是屏幕中心点
setImageMatrix(mScaleMatrix);//通过手势给图片设置缩放
}
//返回值代表本次缩放事件是否已被处理。如果已被处理,那么detector就会重置缩放事件;
// 如果未被处理,detector会继续进行计算,修改getScaleFactor()的返回值,直到被处理为止。
// 因此,它常用在判断只有缩放值达到一定数值时才进行缩放
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//缩放开始一定要返回true该detector是否处理后继的缩放事件。返回false时,不会执行onScale()
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
//缩放结束时
}
boolean once = true;
/**
*图片初始化其大小 必须在onAttachedToWindow方法后才能获取宽高
*/
@Override
public void onGlobalLayout() {
if (!once) {
return;
}
Drawable d = getDrawable();
if (d == null) {
return;
}
//获取imageview宽高
int width = getWidth();
int height = getHeight();
//获取图片宽高
int imgWidth = d.getIntrinsicWidth();
int imgHeight = d.getIntrinsicHeight();
float scale = 1.0f;
//如果图片的宽或高大于屏幕,缩放至屏幕的宽或者高
if (imgWidth > width && imgHeight <= height) {
scale = (float) width / imgWidth;
}
if (imgHeight > height && imgWidth <= width) {
scale = (float) height / imgHeight;
}
//如果图片宽高都大于屏幕,按比例缩小
if (imgWidth > width && imgHeight > height) {
scale = Math.min((float) imgWidth / width, (float) imgHeight / height);
}
// small image
if (imgWidth < width && imgHeight < height) {
scale = Math.min((float) width / imgWidth, (float) height / imgHeight);
mMidScale = 3.0f;
}
mInitScale = scale;
//将图片移动至屏幕中心
mScaleMatrix.postTranslate((width - imgWidth) / 2, (height - imgHeight) / 2);
mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);
setImageMatrix(mScaleMatrix);
once = false;
// add image size
if (ConfigFlag.isBeta()) {
Bitmap bitmap =((BitmapDrawable) getDrawable()).getBitmap().copy(Bitmap.Config.RGB_565, true);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
paint.setColor(Color.RED);
paint.setTypeface(Typeface.DEFAULT_BOLD);
String debugInfo = String.format(
"%s:%s %sx%s s:%s",
imgWidth,
imgHeight,
width,
height,
Math.round(mInitScale * 100.0) / 100.0
);
canvas.drawText(debugInfo, 10 ,20, paint);
canvas.save();
setImageBitmap(bitmap);
}
}
/**
* 获取当前缩放比例
*/
public float getScale() {
//Matrix为一个3*3的矩阵,一共9个值,复制到这个数组当中
mScaleMatrix.getValues(mMatrixValue);
return mMatrixValue[Matrix.MSCALE_X];//取出图片宽度的缩放比例
}
/**
* 在缩放时,解决上下左右留白的情况
*/
private void checkBorderAndCenterWhenScale() {
RectF rectF = getMatrixRectF();
float deltaX = 0;
float deltaY = 0;
int width = getWidth();
int height = getHeight();
// 如果宽或高大于屏幕,则控制范围
if (rectF.width() >= width) {
if (rectF.left > 0) {
deltaX = -rectF.left;//获取坐标留白的距离
Logger.e(TAG, "宽有问题1---->" +rectF.width()+"--"+rectF.left+"--"+width);
}
if (rectF.right < width) {
//屏幕宽-屏幕已经占据的大小 得到右边留白的宽度
deltaX = width - rectF.right;
Logger.e(TAG, "宽有问题2---->" +rectF.width()+"--"+rectF.left+"--"+width);
}
}
if (rectF.height() >= height) {
if (rectF.top > 0) {
deltaY = -rectF.top;//同上,获取上面留白的距离
}
if (rectF.bottom < height) {//同上 获取下面留白的距离
deltaY = height - rectF.bottom;
}
}
// 如果宽或高小于屏幕,则让其居中
if (rectF.width() < width) {
//图片的中心点距离屏幕的中心点距离计算(画个图很明了)
deltaX = width * 0.5f - rectF.right + 0.5f * rectF.width();
Logger.e(TAG, "宽有问题3---->" +rectF.width()+"--"+rectF.right+"结果"+deltaX);
}
if (rectF.height() < height) {
deltaY = height * 0.5f - rectF.bottom + 0.5f * rectF.height();
Logger.e(TAG, "高有问题4---->" +rectF.height()+"--"+rectF.bottom+"结果"+deltaY);
}
mScaleMatrix.postTranslate(deltaX, deltaY);
}
/**
* 获得图片放大缩小以后的宽和高,以及l,r,t,b
*/
private RectF getMatrixRectF() {
Matrix rMatrix = mScaleMatrix;//获得当前图片的矩阵
RectF rectF = new RectF();//创建一个空矩形
Drawable d = getDrawable();
if (d != null) {
//使这个矩形的宽和高同当前图片一致
//设置坐标位置(l和r是左边矩形的坐标点 tb是右边矩形的坐标点 lr设置为0就是设置为原宽高)
rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
//将矩阵映射到矩形上面,之后我们可以通过获取到矩阵的上下左右坐标以及宽高
//来得到缩放后图片的上下左右坐标和宽高
rMatrix.mapRect(rectF);//把坐标位置放入矩阵
}
return rectF;
}
/**
*判断是否可以拖动
*/
private boolean isMoveAction(float dx, float dy) {
return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
}
/**
* 放大移动的过程中解决上下左右留白的情况
*/
private void checkBorderAndCenterWhenTranslate() {
RectF rectF = getMatrixRectF();
float deltax = 0;
float deltay = 0;
int width = getWidth();
int height = getHeight();
//可以上下拖动且距离屏幕上方留白 根据Android系统坐标系往上移动的值要取负值
if (rectF.top > 0 && isCheckTopAndBottom) {
deltay = -rectF.top;
Logger.e(TAG, "上面留白距离---->" +rectF.top);
}
//可以上下拖动且距离屏幕底部留白 根据Android系统坐标系往下移动的值要取正值
if (rectF.bottom < height && isCheckTopAndBottom) {
deltay = height - rectF.bottom;
Logger.e(TAG, "下面留白距离---->" +rectF.bottom);
}
//可以左右拖动且左边留白 根据Android系统坐标系往左移动的值要取负值
if (rectF.left > 0 && isCheckLeftAndRight) {
deltax = -rectF.left;
Logger.e(TAG, "左边留白距离---->" +rectF.left);
if (deltax < 100) {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
//可以左右拖动且右边留白 根据Android系统坐标系往右移动的值要取正值
if (rectF.right < width && isCheckLeftAndRight) {
deltax = width - rectF.right;
Logger.e(TAG, "右边留白距离---->" +rectF.right);
if (deltax < 100) {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
mScaleMatrix.postTranslate(deltax, deltay);//处理偏移量
}
public void setOnClickListener(View.OnClickListener onClickListener) {
mOnClickListener = onClickListener;
}
/**
* View.postDelay()方法延时执行双击放大缩小 在主线程中运行 没隔16ms给用户产生过渡的效果的
*/
private class AutoScaleRunnable implements Runnable {
private float mTargetScale;//缩放目标值
private float x;//缩放中心点
private float y;
private float tempScale;//可能是BIGGER可能是SMALLER
private float BIGGER = 1.07f;
private float SMALLER = 0.93f;
//构造传入缩放目标值,缩放的中心点
public AutoScaleRunnable(float targetScale, float x, float y) {
tempScale = mTargetScale = targetScale;
this.x = x;
this.y = y;
if (getScale() < mTargetScale) {//双击放大
//这个缩放比1f大就行 随便取个1.07
tempScale = BIGGER;
}
if (getScale() > mTargetScale) {//双击缩小
//这个缩放比1f小就行 随便取个0.93
tempScale = SMALLER;
}
}
@Override
public void run() {
//执行缩放
mScaleMatrix.postScale(tempScale, tempScale, x, y);
//在缩放时,解决上下左右留白的情况
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
//获取当前的缩放值
float currentScale = getScale();
//如果当前正在放大操作并且当前的放大尺度小于缩放的目标值,或者正在缩小并且缩小的尺度大于目标值
//则再次延时16ms递归调用直到缩放到目标值
if ((tempScale > 1.0f && currentScale < mTargetScale) || (tempScale <
1.0f && currentScale > mTargetScale)) {
postDelayed(this, 16);
} else {
//代码走到这儿来说明不能再进行缩放了,可能放大的尺寸超过了mTrgetScale,
//也可能缩小的尺寸小于mTrgetScale
//所以这里我们mTrgetScale / currentScale 用目标缩放尺寸除以当前的缩放尺寸
//得到缩放比,重新执行缩放到
//mMidScale或者mInitScale
float scale = mTargetScale / currentScale;
mScaleMatrix.postScale(scale, scale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
//执行完成后重置
mIsAutoScaling = false;
}
}
}
}
@showsky
Copy link
Author

showsky commented Oct 19, 2022

@showsky
Copy link
Author

showsky commented Oct 19, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment