尊重劳动成果,转载请注明出处:https://blog.csdn.net/Emmanuel__/article/details/81264924
仓库地址:https://github.com/yongjianx/ScaleImageView
(使用方法详见github,源码附有详细注释,可供学习)
图片查看器支持单击、双击、长按、拖拽、多点触控缩放
效果:
多点触控缩放在模拟器上难以控制,多点触控效果大家可以到github仓库fork到本地编译看效果。
实现原理
ScaleImageView主要参考了PhotoView的实现,并在PhotoView
的基础上做了改进,比如效果图1中允许图片偏离x方向边界,手指抬起时回弹。
ScaleImageView继承ImageView,并初始化缩放类型:
setScaleType(ScaleType.MATRIX);
关于Matrix类的使用,这篇文章写得挺好android matrix 最全方法详解与进阶(完整篇)
单击、双击、长按
使用GestureDetector类,首先创建一个GestureDetector对象并实现OnGestureListener接口
//长按、单击、双击
mGestureDetector = new GestureDetector(mImageView.getContext(), new GestureDetector.SimpleOnGestureListener(){
@Override
public void onLongPress(MotionEvent e) {
Log.e(TAG, "onLongPress()...");
if (mOnLongClickListener != null)
mOnLongClickListener.onLongClick(mImageView);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.e(TAG, "onSingleTapConfirmed()...");
if (mOnClickListener != null)
mOnClickListener.onClick(mImageView);
return false;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.e(TAG, "onDoubleTap()...");
//获取点击的当前坐标
float x = e.getX();
float y = e.getY();
//获取当前缩放值
float scale = getScale();
/*
* 由于计算机储存float类型时会采用四舍五入方法,导致最终的放大缩小倍数不是完全精确到指定的值,
* 所以如果当前的放大缩小倍数与初始值的绝对值误差小于0.02时即认为本次的放大缩小倍数就是初始的倍数
*/
if (Math.abs(scale - mInitScale) < 0.02)
scale = mInitScale;
//当前缩放值大于初使的缩放值,对其进行缩小操作
if (scale > mInitScale){
new Thread(new AutoScaleRunnable(x, y, getScale(), mInitScale)).start();
}else {
//当前的缩放值小于等于初始的缩放值,对其进行放大的操作
new Thread(new AutoScaleRunnable(x, y, getScale(), mMaxScale)).start();
}
return true;
}
});
接着,接管目标View的onTouchEvent()方法,在待监听View的onTouchEvent()方法中添加如下实现
//将事件传递给单击,双击,长按手势检测的onTouchEvent处理
if (mGestureDetector.onTouchEvent(event)){
return true;
}
拖动、快速滑动、多点触控
首先,定义拖动、快速滑动、多点触控缩放监听器接口
/**
* 拖动、快速滑动、多点触控缩放监听器
*/
public interface OnGestureListener {
void onDrag(float dx, float dy);
void onFling(float startX, float startY, float velocityX, float velocityY);
void onScale(float scaleFactor, float focusX, float focusY);
}
接着,实现监听器接口
private OnGestureListener mOnGestureListner = new OnGestureListener() {
@Override
public void onDrag(float dx, float dy) {
Log.e(TAG,"onDrag()...");
boolean flag = true;//true,表示checkBorder(flag)只检查y方向的边界
if (mIsViewPager){//表示当前父容器为ViewPager
if (mScrollEdge == EDGE_BOTH || (mScrollEdge == EDGE_LEFT && dx>1f) ||
(mScrollEdge == EDGE_RIGHT && dx<-1f)){
mImageView.getParent().requestDisallowInterceptTouchEvent(false);
return;
}
else {
mImageView.getParent().requestDisallowInterceptTouchEvent(true);
}
flag = false;//false,当前父容器为ViewPager,表示checkBorder(flag)同时检查x,y方向的边界
}
mMatrix.postTranslate(dx, dy);
if (checkBorder(flag))//边界检查
mImageView.setImageMatrix(mMatrix);
// checkBorderAndCenter();
}
@Override
public void onFling(float startX, float startY, float velocityX, float velocityY) {
Log.e(TAG, "onFling()...");
if (mAutoFlingRunnable == null)
mAutoFlingRunnable = new AutoFlingRunnable(mImageView.getContext());
mAutoFlingRunnable.fling(getImageViewWidth(), getImageViewHeight(), (int) velocityX, (int) velocityY);
new Thread(mAutoFlingRunnable).start();
}
@Override
public void onScale(float scaleFactor, float focusX, float focusY) {
Log.e(TAG, "onScale()...");
if (mImageView.getDrawable() == null)
return;
final float scale = getScale();
//多点触控缩放时,条件一:还能继续放大;条件二:还能继续缩小
if ((scale< mMaxOVerstep && scaleFactor>1.0f) || (scale>mMinOverstep && scaleFactor<1.0f)){
mMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY);
checkBorderAndCenter();
}
}
};
实现监听
//拖动,多点触控缩放
mOnGeatureDetector = new OnGestureDetector(mImageView.getContext(), mOnGestureListner);
最后,接管目标View的onTouchEvent()方法,在待监听View的onTouchEvent()方法中添加如下实现
mOnGestureDetector.onTouchEvent(event);
OnGestureDetector是自定义的拖动、快速滑动、多点触控缩放手势检测类,内部处理onTouchEvent()事件逻辑如下:
private boolean processTouchEvent(MotionEvent event){
//getAction获得的int值是由pointer的index值和事件类型值组合而成的
//getActionWithMasked则只返回事件的类型值
//getAction() & ACTION_POINTER_INDEX_MASK就获得了pointer的id,等同于getActionIndex函数;
//getAction()& ACTION_MASK就获得了pointer的事件类型,等同于getActionMasked函数
switch (event.getActionMasked()){
case MotionEvent.ACTION_DOWN:
//获取第一根手指的id
mActivePointerId = event.getPointerId(0);
//手势速度追踪
if (mVelocityTracker == null)
mVelocityTracker = VelocityTracker.obtain();
//单击情况
if (mVelocityTracker != null)
mVelocityTracker.addMovement(event);
mLastTouchX = getActiveX(event);
mLastTouchY = getActiveY(event);
mIsDragging = false;
break;
case MotionEvent.ACTION_MOVE://move处理是否拖动
final float x = getActiveX(event);
final float y = getActiveY(event);
final float dx = x - mLastTouchX;
final float dy = y - mLastTouchY;
//判断手势滑动距离是否足以触发ACTION_MOVE事件
if (!mIsDragging)
mIsDragging = isMoveAction(dx, dy);
if (mIsDragging){
if (event.getPointerCount() == 1)//手指数为1时才能拖动
mOnGestureListener.onDrag(dx, dy);
mLastTouchX = x;
mLastTouchY = y;
if (mVelocityTracker != null)
mVelocityTracker.addMovement(event);
}
break;
case MotionEvent.ACTION_UP://手指抬起时处理是否快速滑动
//当前活动的手指设置为无效状态
mActivePointerId = INVALID_POINTER_ID;
if (mIsDragging){
if (mVelocityTracker != null){
mLastTouchX = getActiveX(event);
mLastTouchY = getActiveY(event);
//设置时间单位为1s
mVelocityTracker.computeCurrentVelocity(1000);
//x,y轴的速度,单位:像素/s
final float vX = mVelocityTracker.getXVelocity();
final float vY = mVelocityTracker.getYVelocity();
//判断是否触发快速滑动事件
if (isFlingAction(vX, vY))
mOnGestureListener.onFling(mLastTouchX, mLastTouchY, -vX, -vY);
}
}
//回收mVelocityTracker
if (mVelocityTracker != null){
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_CANCEL:
mActivePointerId = INVALID_POINTER_ID;
//回收mVelocityTracker
if (mVelocityTracker != null){
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
case MotionEvent.ACTION_POINTER_UP://多点触控中,手指抬起处理手指id的切换问题
//获取某一根手指抬起时的索引
int pointerIndex = event.getActionIndex();
//根据索引获取id
int pointerId = event.getPointerId(pointerIndex);
//如果是抬起的是第一根手指,即正在滑动的手指
if (pointerId == mActivePointerId){
//那么对应获取第二点
final int newPointerIndex = pointerId == 0? 1 : 0;
//将id指向第二根手指
mActivePointerId = event.getPointerId(newPointerIndex);
//获取第二根手指的当前坐标
mLastTouchX = event.getX(newPointerIndex);
mLastTouchY = event.getY(newPointerIndex);
}
break;
}
//根据id将索引指向后抬起的手指
mActivePointerIndex = event.
findPointerIndex(mActivePointerId != INVALID_POINTER_ID? mActivePointerId : 0);
// Log.e(TAG, "mActivePointerIndex= "+mActivePointerIndex);
return true;
}
关于图片加载
当使用像imageView.setBackgroundResource()
, imageView.setImageResource()
, 或者 BitmapFactory.decodeResource()
这样的方法来设置一张大图片的时候,这些函数在完成decode后,最终都是通过java层的createBitmap()
来完成的,需要消耗更多内存,图片滑动时会出现卡顿现象,甚至可能OOM。
所以为防止OOM, 改用先通过BitmapFactory.decodeStream()
方法,创建出一个bitmap,再将其设为ImageView的 source。这是因为,decodeStream()
直接调用JNI>>nativeDecodeAsset()
来完成decode,无需再使用java层的createBitmap()
,从而节省了java层的空间。如果在读取时加上图片的Config参数,可以跟有效减少加载的内存,从而跟有效阻止抛out of Memory异常。
值得注意的是,decodeStream()
是直接读取图片资料的字节码了, 不会根据机器的各种分辨率来自动适应,使用了decodeStream()
之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。
/**
* 大图片处理机制
* 利用Bitmap 转存 R图片
* @param mContext
* @param imgId
* @param mImageView
* @throws IOException
*/
public static void getBitmapForImgResourse(Context mContext, int imgId, ImageView mImageView) throws IOException {
InputStream is = mContext.getResources().openRawResource(imgId);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
options.inSampleSize = 1;
final Bitmap btp = BitmapFactory.decodeStream(is, null, options);
mImageView.setImageBitmap(btp);
is.close();
}
另外,也可以使用图片加载框架Glide(Github地址:https://github.com/bumptech/glide/releases), 关于Glide的用法网上资源很多,这里不再详述。
ps : 限于篇幅,详细代码参考github仓库:https://github.com/yongjianx/ScaleImageView,不足之处,欢迎交流学习!