前言
最近在学习视频的时候看到大神讲解如何缩放和拖动图片,没想到平时很常见的一个功能真的实现起来还是挺麻烦的,这里就记录一下自定义控件实现缩放拖动图片的实现过程。
实现结果
缩放实现
首先需要把图片加载到屏幕上去,控件的大小正好是屏幕的宽高,而图片则是随机大小的,最开始的时候希望图片能够等比缩放并居中展示,这样就需要考虑四种情况。
第一种情况:
首先将其中心点移动到屏幕中心点,再把宽高里缩放较小的放到和屏幕一样大,另外一边按照等比进行缩放。
第二种情况:
将中心点移动到屏幕中心,再将宽高所方法较小的缩小到和屏幕一样大,另外一边按照等比缩放,可以看出来第一种和第二种其实是同一种情况。
第三种情况:
将中心点移动到屏幕中心,再把高度拉伸到屏幕高度,宽度等比拉伸。
第四种情况:
将中心点移动到屏幕中心,再把宽度拉伸到屏幕宽度,高度等比拉伸。
分析完了前面四种情况,这种等比缩放并居中展示就可以放在第一次展示的时候处理,实现代码如下:
@Override
public void onGlobalLayout() {
Log.d(TAG, "onGlobalLayout");
if (mIsFirstInit) {
int originWidth = bitmapDrawable.getIntrinsicWidth();
int originHeight = bitmapDrawable.getIntrinsicHeight();
int viewWidth = getWidth();
int viewHeight = getHeight();
float scale = 0;
// 第一二种情况
if (originWidth > viewWidth && originHeight > viewHeight ||
originWidth < viewWidth && originHeight < viewHeight) {
scale = Math.min((float) viewWidth / originWidth, (float) viewHeight / originHeight);
} else if (originWidth > viewWidth) { // 第三种情况
scale = viewWidth * 1.0f / originWidth;
} else { // 第四种情况
scale = viewHeight * 1.0f / originHeight;
}
matrix.setScale(scale, scale, viewWidth / 2, viewHeight / 2);
matrix.preTranslate((viewWidth - originWidth) / 2, (viewHeight - originHeight) / 2);
setImageMatrix(matrix);
mIsFirstInit = false;
}
}
接下来考虑缩放的处理,缩放操作通常都需要两根手指,程序根据用户手指捏合的程度得到需要缩放的比例,Android环境已经为用户提供了ScaleGestureDetecter工具类帮助用户得到缩放中心点和比例,需要注意的时候onBeginScale方法一定要返回true,之后的回调才会继续调用。
private void init() {
setScaleType(ScaleType.MATRIX);
setImageResource(R.drawable.dog);
bitmapDrawable = (BitmapDrawable) getDrawable();
scaleDetector = new ScaleGestureDetector(getContext(), this);
matrix = new Matrix();
originRect.left = 0;
originRect.top = 0;
originRect.right = bitmapDrawable.getIntrinsicWidth();
originRect.bottom = bitmapDrawable.getIntrinsicHeight();
mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
float scaleFactor = detector.getScaleFactor();
Log.d(TAG, "onScale scaleFactor = " + scaleFactor);
float scale = getScaleValue();
matrix.setScale(scaleFactor * scale,
scaleFactor * scale, detector.getFocusX(), detector.getFocusY());
adjustBorder(matrix);
setImageMatrix(matrix);
return true;
}
对图形图像处理有一定了解的开发者都知道一个3*3的矩阵能够完成图形的缩放移动,这里就通过操作ImageView的setImageMatrix来实现图形的缩放处理,不过处理过程中会出现白边的情况,为了避免不合适的用户体验需要去掉导致白边的缩放增加自动移动处理。仅考虑横向的白边,可以分为下面三种情况。
第一种情况:
白边出现在左边并且图像当前的宽度大于屏幕,很显然是因为图片过于靠右,将图片向左边移动白边的宽度就可以了。
第二种情况:
白边出现在右边并且图像当前宽度大于屏幕,这是因为图像过于靠左,只要把图片向右移动白边宽度就可以了。
第三种情况:
白边出现在左边并且图像当前的宽度小于屏幕,这是因为图像太小无法充满屏幕,这时需要将两边的白边处理成一样的宽度,避免左右不对称的情况。
private void adjustBorder(Matrix matrix) {
RectF rectF = getMatrixRect();
float dx = 0, dy = 0;
// 如果当前图片大于屏幕出现白边
if (rectF.width() >= getWidth()) {
// 左边出现白边
if (rectF.left > 0) {
dx = -rectF.left;
}
// 右边出现白边
if (rectF.right < getWidth()) {
dx = getWidth() - rectF.right;
}
}
// 如果图片小于屏幕宽度,左右都有白边
if (rectF.width() < getWidth()) {
dx = rectF.width() / 2 + getWidth() / 2 - rectF.right;
}
if (rectF.height() >= getHeight()) {
if (rectF.top > 0) {
dy = -rectF.top;
}
if (rectF.bottom < getHeight()) {
dy = getHeight() - rectF.bottom;
}
}
if (rectF.height() < getHeight()) {
dy = getHeight() / 2f - rectF.bottom + rectF.height() / 2f;
}
matrix.postTranslate(dx, dy);
}
上下出现白边的情况和左右分析类似,这里不再赘述。
移动实现
前面的缩放需要两根手指,移动只需要一根手指就可以了,在onTouchEvent方法中只要监控到用户一只手指接触屏幕就需要判断是否在做移动操作,如果是就记录下用户移动的距离并且将这个距离放入到矩阵中,在真正设置给ImageView之前先要检查移动后是否会出现白边问题,需要注意的是如果图片尺寸小于屏幕高度不允许用户移动,所以只存在图片当前尺寸大于屏幕的情况。
// 记录用户上一次手指所在位置
private int mLastX;
private int mLastY;
// 记录用户手指按下的位置
private int mMotionX;
private int mMotionY;
// 用户是否在拖动
private boolean mIsDragging = false;
private int mTouchSlop;
private boolean isDragging(int dx, int dy) {
return dx * dx + dy * dy > mTouchSlop * mTouchSlop;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
scaleDetector.onTouchEvent(event);
if (event.getPointerCount() == 1) { // 用户只用一只手指视为移动
int x = (int) event.getX(), y = (int) event.getY();
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
// 记录按下位置
mLastX = mMotionX = x;
mLastY = mMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
// 如果用户刚按下并且移动距离超出了点击范围,确认用户在拖动
if (!mIsDragging && isDragging(x - mMotionX, y - mMotionY)) {
mIsDragging = true;
}
if (mIsDragging) {
int dx = x - mLastX;
int dy = y - mLastY;
matrix.postTranslate(dx, dy);
checkBorder(matrix, dx, dy);
setImageMatrix(matrix);
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
mIsDragging = false;
break;
}
}
return true;
}
private void checkBorder(Matrix matrix, int deltaX, int deltaY) {
float dx = 0, dy = 0;
RectF rectF = getMatrixRect();
// 如果图片小于屏幕,直接不允许移动
if (rectF.width() < getWidth()) {
dx = -deltaX;
} else {
// 图片大于屏幕出现左边白边
if (rectF.left > 0) {
dx = -rectF.left;
}
// 图像大于屏幕出现右边白边
if (rectF.right < getWidth()) {
dx = getWidth() - rectF.right;
}
}
if (rectF.height() < getHeight()) {
dy = -deltaY;
} else {
if (rectF.top > 0) {
dy = -rectF.top;
}
if (rectF.bottom < getHeight()) {
dy = getHeight() - rectF.bottom;
}
}
matrix.postTranslate(dx, dy);
}