最近有个需求是做一个点击图片放大放小功能,然后参考了自定义ZoomImageView这个类来做,但是做到后面发现缩小之后,滑动的时候会与ViewPager冲突,他会滑到下一页,我肯定是希望缩小滑动的时候,应该靠近边界了再滑动到下一页,网上找了很多方法,有说用getParent().requestDisallowInterceptTouchEvent(true);这个拦截,但我感觉不是明智之举。
我先展示我做的效果:
那怎么去做这个ViewPager图片放大缩小功能了,代码如下:
首先参考https://blog.csdn.net/qq_35983486/article/details/125314603他的ZoomImageView类,写的还可以,我运行了,挺丝滑的,因为有惯性滑动,还有一些缩到最小的时候,弹一下的细节功能,他的代码如下:
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateInterpolator;
import android.widget.ImageView;
import android.widget.OverScroller;
@SuppressLint("AppCompatCustomView")
public class ZoomImageView extends ImageView implements ViewTreeObserver.OnGlobalLayoutListener {
private boolean mIsOneLoad = true;
//初始化的比例,也就是最小比例
private float mInitScale;
//图片最大比例
private float mMaxScale;
//双击能达到的最大比例
private float mMidScale;
private Matrix mScaleMatrix;
//捕获用户多点触控
private ScaleGestureDetector mScaleGestureDetector;
//移动
private GestureDetector gestureDetector;
//双击
private boolean isEnlarge = false;//是否放大
private ValueAnimator mAnimator; //双击缩放动画
//滚动
private OverScroller scroller;
private int mCurrentX, mCurrentY;
private ValueAnimator translationAnimation; //惯性移动动画
//单击
private OnClickListener onClickListener;//单击监听
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);
//记住,一定要把ScaleType设置成ScaleType.MATRIX,否则无法缩放
setScaleType(ScaleType.MATRIX);
scroller = new OverScroller(context);
mScaleMatrix = new Matrix();
//手势缩放
mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
@Override
public boolean onScale(ScaleGestureDetector detector) {
scale(detector);
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
scaleEnd(detector);
}
});
//滑动和双击监听
gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, final float distanceX, final float distanceY) {
//滑动监听
onTranslationImage(-distanceX, -distanceY);
return true;
}
@Override
public boolean onDoubleTap(MotionEvent e) {
//双击监听
onDoubleDrowScale(e.getX(), e.getY());
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//移动之后,松开手指产生的惯性,会触发以下方法
mCurrentX = (int) e2.getX();
mCurrentY = (int) e2.getY();
RectF rectF = getMatrixRectF();
if (rectF == null) {
return false;
}
//startX为当前图片左边界的x坐标
int startX = mCurrentX;
int startY = mCurrentY;
int minX = 0, maxX = 0, minY = 0, maxY = 0;
int vX = Math.round(velocityX);
int vY = Math.round(velocityY);
maxX = Math.round(rectF.width());
maxY = Math.round(rectF.height());
if (startX != maxX || startY != maxY) {
//调用fling方法,然后我们可以通过调用getCurX和getCurY来获得当前的x和y坐标
//这个坐标的计算是模拟一个惯性滑动来计算出来的,我们根据这个x和y的变化可以模拟
//出图片的惯性滑动
scroller.fling(startX, startY, vX, vY, 0, maxX, 0, maxY, maxX, maxY);
}
if (translationAnimation != null && translationAnimation.isStarted())
translationAnimation.end();
translationAnimation = ObjectAnimator.ofFloat(0, 1);
translationAnimation.setDuration(500);
translationAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
if (scroller.computeScrollOffset()) {
//获得当前的x坐标
int newX = scroller.getCurrX();
int dx = newX - mCurrentX;
mCurrentX = newX;
//获得当前的y坐标
int newY = scroller.getCurrY();
int dy = newY - mCurrentY;
mCurrentY = newY;
//进行平移操作
if (dx != 0 && dy != 0)
onTranslationImage(dx, dy);
}
}
});
translationAnimation.start();
return super.onFling(e1, e2, velocityX, velocityY);
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
//单击事件
if(onClickListener != null)
onClickListener.onClick(ZoomImageView.this);
return true;
}
});
}
@Override
public void setOnClickListener(OnClickListener onClickListener) {
this.onClickListener = onClickListener;
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
getViewTreeObserver().addOnGlobalLayoutListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
/**
* imageView加载完成后调用,获取imageView加载完成后的图片大小
*/
@Override
public void onGlobalLayout() {
if (mIsOneLoad) {
//得到控件的宽和高
int width = getWidth();
int height = getHeight();
//获取图片,如果没有图片则直接退出
Drawable d = getDrawable();
if (d == null)
return;
//获取图片的宽和高
int dw = d.getIntrinsicWidth();
int dh = d.getIntrinsicHeight();
float scale = 1.0f;
if (dw > width && dh <= height) {
scale = width * 1.0f / dw;
}
if (dw <= width && dh > height) {
scale = height * 1.0f / dh;
}
if ((dw <= width && dh <= height) || (dw >= width && dh >= height)) {
scale = Math.min(width * 1.0f / dw, height * 1.0f / dh);
}
//图片原始比例,图片回复原始大小时使用
mInitScale = scale;
//图片双击后放大的比例
mMidScale = mInitScale * 2;
//手势放大时最大比例
mMaxScale = mInitScale * 4;
//设置移动数据,把改变比例后的图片移到中心点
float translationX = width * 1.0f / 2 - dw / 2;
float translationY = height * 1.0f / 2 - dh / 2;
mScaleMatrix.postTranslate(translationX, translationY);
mScaleMatrix.postScale(mInitScale, mInitScale, width * 1.0f / 2, height * 1.0f / 2);
setImageMatrix(mScaleMatrix);
mIsOneLoad = false;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return mScaleGestureDetector.onTouchEvent(event)|
gestureDetector.onTouchEvent(event);
}
//手势操作(缩放)
public void scale(ScaleGestureDetector detector) {
Drawable drawable = getDrawable();
if (drawable == null)
return;
float scale = getScale();
//获取手势操作的值,scaleFactor>1说明放大,<1则说明缩小
float scaleFactor = detector.getScaleFactor();
//获取手势操作后的比例,当放操作后比例在[mInitScale,mMaxScale]区间时允许放大
mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
setImageMatrix(mScaleMatrix);
removeBorderAndTranslationCenter();
}
//手势操作结束
public void scaleEnd(ScaleGestureDetector detector) {
float scale = getScale();
scale = detector.getScaleFactor() * scale;
if (scale < mInitScale) {
scaleAnimation(mInitScale, getWidth() / 2, getHeight() / 2);
} else if (scale > mMaxScale) {
scaleAnimation(mMaxScale, getWidth() / 2, getHeight() / 2);
}
}
//手势操作(移动)
private void onTranslationImage(float dx, float dy) {
if (getDrawable() == null)
return;
RectF rect = getMatrixRectF();
//图片宽度小于控件宽度时不允许左右移动
if (rect.width() <= getWidth())
dx = 0.0f;
//图片高度小于控件宽度时,不允许上下移动
if (rect.height() <= getHeight())
dy = 0.0f;
//移动距离等于0,那就不需要移动了
if (dx == 0.0f && dy == 0.0f)
return;
//是一个矩阵操作,用于在当前的缩放矩阵基础上进行平移操作。它将图像沿着 x 和 y 轴分别平移 dx 和 dy 的距离。
//在给定的代码中,mScaleMatrix 是一个用于缩放和平移图像的矩阵对象。通过调用 postTranslate(dx, dy) 方法,将图像沿着 x 和 y 轴平移指定的距离。
//这个平移操作会影响到 setImageMatrix(mScaleMatrix),将更新后的矩阵应用到图像视图上,从而实现图像的平移效果。
//总结起来,mScaleMatrix.postTranslate(dx, dy) 的作用是在当前的缩放矩阵基础上对图像进行平移操作,使图像在屏幕上移动指定的距离。
mScaleMatrix.postTranslate(dx, dy);
setImageMatrix(mScaleMatrix);
//去除移动边界
removeBorderAndTranslationCenter();
}
//消除控件边界和把图片移动到中间
private void removeBorderAndTranslationCenter() {
RectF rectF = getMatrixRectF();
if (rectF == null)
return;
int width = getWidth();
int height = getHeight();
float widthF = rectF.width();
float heightF = rectF.height();
float left = rectF.left;
float right = rectF.right;
float top = rectF.top;
float bottom = rectF.bottom;
float translationX = 0.0f, translationY = 0.0f;
if (left > 0) {
//左边有边界
if (widthF > width) {
//图片宽度大于控件宽度,移动到左边贴边
translationX = -left;
} else {
//图片宽度小于控件宽度,移动到中间
translationX = width * 1.0f / 2f - (widthF * 1.0f / 2f + left);
}
} else if (right < width) {
//右边有边界
if (widthF > width) {
//图片宽度大于控件宽度,移动到右边贴边
translationX = width - right;
} else {
//图片宽度小于控件宽度,移动到中间
translationX = width * 1.0f / 2f - (widthF * 1.0f / 2f + left);
}
}
if (top > 0) {
//顶部有边界
if (heightF > height) {
//图片高度大于控件高度,去除顶部边界
translationY = -top;
} else {
//图片高度小于控件宽度,移动到中间
translationY = height * 1.0f / 2f - (top + heightF * 1.0f / 2f);
}
} else if (bottom < height) {
//底部有边界
if (heightF > height) {
//图片高度大于控件高度,去除顶部边界
translationY = height - bottom;
} else {
//图片高度小于控件宽度,移动到中间
translationY = height * 1.0f / 2f - (top + heightF * 1.0f / 2f);
}
}
mScaleMatrix.postTranslate(translationX, translationY);
setImageMatrix(mScaleMatrix);
}
/**
* 双击改变大小
*
* @param x 点击的中心点
* @param y 点击的中心点
*/
private void onDoubleDrowScale(float x, float y) {
//如果缩放动画已经在执行,那就不执行任何事件
if (mAnimator != null && mAnimator.isRunning())
return;
float drowScale = getDoubleDrowScale();
//执行动画缩放,不然太难看了
scaleAnimation(drowScale, x, y);
}
/**
* 缩放动画
*
* @param drowScale 缩放的比例
* @param x 中心点
* @param y 中心点
*/
private void scaleAnimation(final float drowScale, final float x, final float y) {
if (mAnimator != null && mAnimator.isRunning())
return;
mAnimator = ObjectAnimator.ofFloat(getScale(), drowScale);
mAnimator.setDuration(500);
mAnimator.setInterpolator(new AccelerateInterpolator());
mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float value = ((float) animation.getAnimatedValue()) / getScale();
mScaleMatrix.postScale(value, value, x, y);
setImageMatrix(mScaleMatrix);
removeBorderAndTranslationCenter();
}
});
mAnimator.start();
}
//返回双击后改变的大小比例(我们希望缩放误差在deviation范围内)
private float getDoubleDrowScale() {
float deviation = 0.05f;
float drowScale = 1.0f;
float scale = getScale();
if (Math.abs(mInitScale - scale) < deviation)
scale = mInitScale;
if (Math.abs(mMidScale - scale) < deviation)
scale = mMidScale;
if (Math.abs(mMaxScale - scale) < deviation)
scale = mMaxScale;
if (scale != mMidScale) {
//当前大小不等于mMidScale,则调整到mMidScale
drowScale = mMidScale;
isEnlarge = scale < mMidScale;
} else {
//如果等于mMidScale,则判断放大或者缩小
//判断是放大或者缩小,如果上次是放大,则继续放大,缩小则继续缩小
if (isEnlarge) {
//放大
drowScale = mMaxScale;
} else {
//缩小
drowScale = mInitScale;
}
}
return drowScale;
}
//获取图片宽高以及左右上下边界
private RectF getMatrixRectF() {
Drawable drawable = getDrawable();
if (drawable == null) {
return null;
}
RectF rectF = new RectF(0, 0, drawable.getMinimumWidth(), drawable.getMinimumHeight());
Matrix matrix = getImageMatrix();
matrix.mapRect(rectF);
return rectF;
}
/**
* 获取当前图片的缩放值
*
* @return
*/
private float getScale() {
float[] values = new float[9];
mScaleMatrix.getValues(values);
return values[Matrix.MSCALE_X];
}
/**
* 解决和父控件滑动冲突 只要图片边界超过控件边界,返回true
*
* @param direction
* @return true 禁止父控件滑动
*/
@Override
public boolean canScrollHorizontally(int direction) {
RectF rect = getMatrixRectF();
if (rect == null || rect.isEmpty())
return false;
if (direction > 0) {
return rect.right >= getWidth() + 1;
} else {
return rect.left <= 0 - 1;
}
}
/**
*
*
* @param direction
* @return
*/
@Override
public boolean canScrollVertically(int direction) {
RectF rect = getMatrixRectF();
if (rect == null || rect.isEmpty())
return false;
if (direction > 0) {
return rect.bottom >= getHeight() + 1;
} else {
return rect.top <= -1;
}
}
}
然后就是写viewpager的适配器,我参考的是原作者的代码,然后我自己改进了一下,因为作者的代码是10年前的了,原作者代码参考链接https://github.com/tenthbitinc/ZoomImageView,他用的还是以前的线程在加载图片,我自己用glide加载图片,代码如下:
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.viewpager.widget.PagerAdapter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.util.List;
public class ImageAdapter extends PagerAdapter {
/**
* 传递过来的数据
*/
private final List<WallPaperResponse.ResBean.VerticalBean> verticalBean;
public ImageAdapter(List<WallPaperResponse.ResBean.VerticalBean> verticalBean) {
this.verticalBean=verticalBean;
}
@Override
public int getCount() {
return verticalBean.size();
}
@Override
public View instantiateItem(ViewGroup container, int position) {
final ZoomImageView zoomImageView = new ZoomImageView(container.getContext());
final String bitmapResource = verticalBean.get(position).getImg();
//.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)来启用Glide的默认磁盘缓存策略。这样可以确保在需要时从缓存加载位图,并在不再需要时自动释放资源。
Glide.with(zoomImageView)
.asBitmap()
.load(bitmapResource)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(zoomImageView);
container.addView(zoomImageView, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
return zoomImageView;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
@Override
public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
}
我自己用的是一MVVM的架构,设置好viewpager适配器就行,代码如下:
public class PictureViewActivity extends AppCompatActivity{
private ViewPager viewPager;
private PictureViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_picture_view);
viewModel = new ViewModelProvider(this).get(PictureViewModel.class);
int img = getIntent().getIntExtra("img",0);
initView();
//热门壁纸 网络请求
viewModel.getWallPaper();
viewModel.wallPaper.observe(this,wallPapers -> {
viewPager.setAdapter(new ImageAdapter(wallPapers));
viewPager.setCurrentItem(img,false);
});
}
/**
* 初始化
*/
private void initView() {
viewPager=findViewById(R.id.vp);
//在页面之间添加边距(可选)
viewPager.setPageMargin((int) getResources().getDisplayMetrics().density * 10);
}
}