实现Android图片双击放大缩小查看大图+ViewPager+
本文是根据鸿洋,打造个性的图片预览与多点触控而来,主要是熟悉里面的效果
效果
效果可以看到上面的效果是可以根据多指缩放,双击放大缩小,同时嵌套ViewPager
关于这样的效果国外有个小伙Chris Banes写的很好,PhotoView
具体实现步骤:
图片加载时实现监听
自定义控件并且继承自ImageView,我们知道在oncreate中View.getWidth和View.getHeight无法获得一个view的高度和宽度,这是因为View组件布局要在onResume回调后完成。所以现在需要使用getViewTreeObserver().addOnGlobalLayoutListener()来获得宽度或者高度。这是获得一个view的宽度和高度的方法之一。重写onAttachedToWindow()方法
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
实现方法,并且获取图片宽高:
@Override
public void onGlobalLayout() {
//布尔值防止多次加载
if (!once) {
//获取屏幕的宽高
int width = getWidth();
int height = getHeight();
//获取加载到的图片资源
Drawable drawable = getDrawable();
//获取图片的宽高
int dWidth = drawable.getIntrinsicWidth();
int dHeight = drawable.getIntrinsicHeight();
//初始化的时候,我们要将图片居中显示
//缩放比例
float scale = 1.0f;
if (dWidth > width && dHeight < height) {
scale = width * 1.0f / dWidth;
}
if (dHeight > height && dWidth < width) {
scale = height * 1.0f / dHeight;
}
if ((dWidth > width && dHeight > height) || (dWidth < width && dHeight < height)) {
scale = Math.min(width * 1.0f / dWidth, height * 1.0f / dHeight);
}
//初始化缩放比例
mInitScale = scale;
//最大缩放比例
mMaxScale = mInitScale * 4;
//中等缩放比例
mMidScale = mInitScale * 2;
//图片移动到中心的距离
int dx = getWidth() / 2 - dWidth / 2;
int dy = getHeight() / 2 - dHeight / 2;
//进行平移
mScaleMatrix.postTranslate(dx, dy);
//进行缩放
mScaleMatrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
setImageMatrix(mScaleMatrix);
once = true;
}
}
通过上面的步骤可以设置图片居中显示,比例缩放到正确的位置!
接下来实现图片缩放
多手指缩放需要用到的一个类是ScaleGestureDetector,我们在构造初始化它
//初始化Matrix
mScaleMatrix = new Matrix();
//预防在布局里没有或者设置其他类型
super.setScaleType(ScaleType.MATRIX);
//缩放初始化
mScaleGestureDetector = new ScaleGestureDetector(context, this);
//同样,缩放的捕获要建立在setOnTouchListener上
setOnTouchListener(this);
这样实现其方法:
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
float scale = getScale();
if (getDrawable() == null) {
return true;
}
//这里是想放大和缩小
if ((scale < mMaxScale && scaleFactor > 1.0f) || (scale > mInitScale && scaleFactor < 1.0f)) {
//这里如果要缩放的值比初始化还要小的话,就按照最小可以缩放的值进行缩放
if (scale * scaleFactor < mInitScale) {
scaleFactor = mInitScale / scale;
}
//这个是放大的同理
if (scale * scaleFactor > mMaxScale) {
scaleFactor = mMaxScale / scale;
}
//detector.getFocusX(), detector.getFocusY(),是在缩放中心点进行缩放
mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
//在缩放的时候会出现图片漏出白边,位置出现移动,所以要另外做移动处理
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
}
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
//开始时设置为true
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
检查露出时用到的方法
private void checkBorderAndCenterWhenScale() {
RectF matrixRectF = getMatrixRectF();
float deltaX = 0;
float deltY = 0;
int width = getWidth();
int height = getHeight();
//缩放后的宽度大于屏幕
if (matrixRectF.width() >= width) {
if (matrixRectF.left > 0) {
//这就是说左边露出了一部分,怎么办,补上啊,补多少?
deltaX = -matrixRectF.left;
}
if (matrixRectF.right < width) {
//这就是右边露出了
deltaX = width - matrixRectF.right;
}
}
if (matrixRectF.height() >= height) {
if (matrixRectF.top > 0) {
deltY = -matrixRectF.top;
}
if (matrixRectF.bottom < height) {
deltY = -height - matrixRectF.bottom;
}
}
//如果宽或者是高,小于屏幕的话,那就没理由的居中就行
if (matrixRectF.width() < width) {
deltaX = width / 2f - matrixRectF.right + matrixRectF.width() / 2;
}
if (matrixRectF.height() < height) {
deltY = height / 2f - matrixRectF.bottom + matrixRectF.height() / 2;
}
mScaleMatrix.postTranslate(deltaX, deltY);
}
获取缩放值
/**
* 获取缩放
*
* @return
*/
private float getScale() {
float[] values = new float[9];
mScaleMatrix.getValues(values);
return values[Matrix.MSCALE_X];
}
实现图片放大后移动查看
移动需要在OnTouch里处理:
@Override
public boolean onTouch(View v, MotionEvent event) {
mScaleGestureDetector.onTouchEvent(event);
float x = 0;
float y = 0;
//可能出现多手指的情况
int pointerCount = event.getPointerCount();
for (int i = 0; i < pointerCount; i++) {
x += event.getX(i);
y += event.getY(i);
}
x /= pointerCount;
y /= pointerCount;
if (mLastPointCount != pointerCount) {
//手指变化后就不能继续拖拽
isCanDrag = false;
//记录最后的位置,重置
mLatX = x;
mLastY = y;
}
//记录最后一次手指的个数
mLastPointCount = pointerCount;
RectF rectF = getMatrixRectF();
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
//x,y移动的距离
float dx = x - mLatX;
float dy = y - mLastY;
//如果是不能拖拽,可能是因为手指变化,这时就去重新检测看看是不是符合滑动
if (!isCanDrag) {
//反正是根据勾股定理,调用系统API
isCanDrag = isMoveAction(dx, dy);
}
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;
}
补上空白
private void checkBorderAndCenterWhenTranslate() {
RectF rectF = getMatrixRectF();
float deltax = 0;
float deltay = 0;
int width = getWidth();
int height = getHeight();
if (rectF.top > 0 && isCheckTopAndBottom) {
deltay = -rectF.top;
}
if (rectF.bottom < height && isCheckTopAndBottom) {
deltay = height - rectF.bottom;
}
if (rectF.left > 0 && isCheckLeftAndRight) {
deltax = -rectF.left;
}
if (rectF.right < width && isCheckLeftAndRight) {
deltax = width - rectF.right;
}
mScaleMatrix.postTranslate(deltax, deltay);
}
判断是否是滑动
private boolean isMoveAction(float dx, float dy) {
return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
}
双击实现放大和缩小
双击需要用到系统的一个类,在构造里初始化,同样也需要在OnTouch里进行关联
@Override
public boolean onTouch(View v, MotionEvent event) {
//双击进行关联
if (mGestureDetector.onTouchEvent(event)) {
//如果是双击的话就直接不向下执行了
return true;
}
//缩放进行关联
mScaleGestureDetector.onTouchEvent(event);
...
}
在构造里进行处理双击监听
public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
//初始化Matrix
mScaleMatrix = new Matrix();
//预防在布局里没有或者设置其他类型
super.setScaleType(ScaleType.MATRIX);
//缩放初始化
mScaleGestureDetector = new ScaleGestureDetector(context, this);
//同样,缩放的捕获要建立在setOnTouchListener上
setOnTouchListener(this);
//符合滑动的距离
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
//自动缩放时需要有一个自动的过程
mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onDoubleTap(MotionEvent e) {
//如果再自动缩放中就不向下执行,防止多次双击
if (isAutoScaling) {
return true;
}
//缩放的中心点
float x = e.getX();
float y = e.getY();
if (getScale() < mMidScale) {
isAutoScaling = true;
postDelayed(new AutoScaleRunble(mMidScale, x, y), 16);
} else {
isAutoScaling = true;
postDelayed(new AutoScaleRunble(mInitScale, x, y), 16);
}
return true;
}
});
}
自动缩放处理类,实现Runnable
private class AutoScaleRunble implements Runnable {
private float mTrgetScale;
private float x;
private float y;
private float tempScale;
private float BIGGER = 1.07f;
private float SMALLER = 0.93f;
//构造传入缩放目标值,缩放的中心点
public AutoScaleRunble(float mTrgetScale, float x, float y) {
this.mTrgetScale = mTrgetScale;
this.x = x;
this.y = y;
if (getScale() < mTrgetScale) {
tempScale = BIGGER;
}
if (getScale() > mTrgetScale) {
tempScale = SMALLER;
}
}
@Override
public void run() {
mScaleMatrix.postScale(tempScale, tempScale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
float currentScale = getScale();
//如果你想放大并且当然值并没有到达目标值,可以继续放大,同理缩小也是一样
if ((tempScale > 1.0f && currentScale < mTrgetScale) || (tempScale < 1.0f && currentScale > mTrgetScale)) {
postDelayed(this, 16);
} else {//此时不能再进行放大或者缩小了,要放大为目标值
float scale = mTrgetScale / currentScale;
mScaleMatrix.postScale(scale, scale, x, y);
checkBorderAndCenterWhenScale();
setImageMatrix(mScaleMatrix);
isAutoScaling = false;
}
}
}
最后嵌入到ViewPager,这里要做一个处理,在OnTouch.因为ViewPager,滑动是需要拦截时间自己处理翻页的
case MotionEvent.ACTION_DOWN:
//当图片放大时,这个时候左右滑动查看图片,就请求ViewPager不拦截事件!
if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()) {
if (getParent() instanceof ViewPager) {
getParent().requestDisallowInterceptTouchEvent(true);
}
}
break;
case MotionEvent.ACTION_MOVE:
//x,y移动的距离
float dx = x - mLatX;
float dy = y - mLastY;
//这里的处理是,当图片移动到最边缘的时候,不能在移动了,此时是应该Viewpager去处理事件,翻页
if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
if (getParent() instanceof ViewPager) {
//让父类进行拦截处理
getParent().requestDisallowInterceptTouchEvent(false);
}
} else if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()){
if (getParent() instanceof ViewPager) {
//让父类进行拦截处理
getParent().requestDisallowInterceptTouchEvent(true);
}
}
\MainAvtivity和ZoomImageView的链接
总结
虽然以上代码可以实现我们需要的功能但是还有不完美的地方,下面看看效果就知道:
代码效果从效果可以看出,当我的图片放大后,处于边缘时,我如果向右滑动可以切换,确实是实现了这样的效果,但是我当切换手指不松开,然后向反方向滑动时,会切出另外一面的pager,而不是去继续移动大图查看隐藏的部分,为什么会这样呢,因为当我图片放大正好左边处于边缘时,如果向右切换,这个时候是可以切换的,并且这个时候让VIewPager接管了滑动事件处理
if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
if (getParent() instanceof ViewPager) {
//让父类进行拦截处理
getParent().requestDisallowInterceptTouchEvent(false);
}
}
那么问题就来了,这个时候VIewPager切换时手指并没有离开,事件处理依然掌握,当反方向切换时当然是会执行右边切换!!
最好的办法就是,当ViewPager左边切换时,如果放弃左边切换此时再把事件给子控件,这样图片又可以继续移动查看了!
例如微信就可以实现这个效果!
本代码中目前我还没想到好的解决办法,有谁知道请告诉我!!