上一章,我们学习了手势 GestureDecetor 的基本使用 Android 手势学习 GestureDetector,这一次,我们来学习使用 ScaleGestureDetector 来实现一个图片缩放的效果,如下:
文章参考鸿洋大大的图片缩放
使用
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
然后在你的 module 中添加:
implementation 'com.github.LillteZheng:ScaleImageview:1.0'
然后添加控件即可:
<com.zhengsr.mylibrary.ScaleImageView
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_margin="10dp"
app:scale_limit_board="false"
app:scale_max_factor="5"
app:scale_auto_time="5"
app:scale_double_factor="2"
android:src="@mipmap/a1"/>
一、分析:
- 通过 ScaleGestureDetector 拿到缩放中心和缩放因子
- 通过 matrix 进行图片的缩放和平移
- 双击进行放大和缩小
- 结合 GestureDetector 进行平移
- 使用 getParent().requestDisallowInterceptTouchEvent(true); 进行事件发发截取
二、讲解
2.1 平移与缩放
首先,当加载一张图片的时候,并不清楚图片的大小,如果想要它居中且全部显示出来,则需要通过平移和缩放的形式,这里可以通过 Imageview 的 scaleTyle 设置成 matrix 的形式;比如,在初始化中:
setScaleType(ScaleType.MATRIX);
这样,则保证了图片的缩放模式是以 matrix 的。假设需要加载的图片比较小,在左上角,这时,需要移动到屏幕中心,如下图:
此时,需要将它先移动到屏幕中心,只需要屏幕的中心点减去图片的中心点坐标即可,dw 和dh 为图片的宽高:
mMatrix = new Matrix();
float dx = width * 1.0f / 2 - dw * 1.0f / 2;
float dy = height * 1.0f / 2 - dh * 1.0f / 2;
mMatrix.postTranslate(dx,dy);
接着进行比例缩放,目前来说,有两种方式,假如整个屏幕只填放一张图片,那么,缩放只需要保证宽或者高一个比例缩放即可,只要保证了其中一个,以适配最小宽高来,那么另一个宽或者高也能适配,如下:
//控件宽高都比图片大,即放大了多少倍,拿到宽高的最小放大比例
if (width > dw && height > dh){
scale = Math.min((width * 1.0f / dw),(height * 1.0f / dh));
}
//图片宽高比控件大,即缩小了多少倍,,拿到宽高的最大放小比例
if (dw > width && dh > height){
scale = Math.max((width * 1.0f / dw),(height * 1.0f / dh));
}
//图片高比控件的高大,即应该缩放多少
if (dw < width && dh > height){
scale = height * 1.0f / dh;
}
//图片宽比控件的宽大,即应该缩放多少
if (dw > width && dh < height){
scale = width * 1.0f / dw;
}
这样拿到缩放比例后,直接用 matrix 即可:
mMatrix.postScale(scale,scale,width * 1.0f / 2, height * 1.0f / 2);
但如果你想效果图中,指定了宽高呢?这样如果只缩放其中一个宽或高,则另一个,就有可能存在不能完全适配的问题,所以,需要拿到宽和高的缩放比例,如:
scale = width * 1.0f / dw;
scaley = height * 1.0f / dh;
然后调用 matrix:
mMatrix.postScale(scale, scaley, width * 1.0f / 2, height * 1.0f / 2);
接着调用 imageview 自带的方法,即可完成移动中心点和缩放问题:
setImageMatrix(mMatrix);
可以使用
app:scale_autofit="true"
来使用哪种比例,一个随便指定高度为200dp 的张方形图片不同效果如下:
所以,如果你是指定高度,且图片是长方形的,建议 scale_autofit true,如果是 match_parent 且正方形的则 false 即可。
2.2 使用 ScaleGestureDetector 进行缩放
在进行平移缩放之后,可以使用 ScaleGestureDetector 进行缩放即可,它的三个方法如下:
@Override
public boolean onScale(ScaleGestureDetector detector) {
//拿到缩放比例
float scale = getScale();
float scaleFactor = detector.getScaleFactor();
//拿到缩放中心
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
return true;
}
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
}
需要注意的是 onScaleBegin return true,这样事件才能继续传递下去。接着在 onScale 拿到缩放比例中心和当前的缩放比例因子;整体代码如下:
//拿到缩放比例
float scale = getScale();
float scaleFactor = detector.getScaleFactor();
//拿到缩放中心
float focusX = detector.getFocusX();
float focusY = detector.getFocusY();
//默认不能缩放至屏幕控件大小
if (mIsLimitBroad) {
//重新复制,让它逼近于最小值
if (scale * scaleFactor < mDeflautScale) {
scaleFactor = mDeflautScale / scale;
}
if (scale * scaleFactor > mMaxScale) {
scaleFactor = mMaxScale / scale;
}
}
mMatrix.postScale(scaleFactor,scaleFactor,focusX,focusY);
checkBroad();
setImageMatrix(mMatrix);
mIsLimitBroad 即 app:scale_limit_board 表示是否缩放能否能小于控件,如果为 true,则表示不能缩小到控件大小,可以通过比例换算去是实现,如果 scale * scaleFactor < mDeflautScale ,则让 scaleFactor = mDeflautScale / scale 即可。
其中 getScale()方法表示拿到当前的缩放比例:
/**
* 拿到缩放比例,默认拿到 x 即可
* @return
*/
private float getScale(){
float[] floats = new float[9];
mMatrix.getValues(floats);
return floats[Matrix.MSCALE_X];
}
checkBroad() 则是,在缩放时,如果不对中心点进行校正,不对图片进行平移校正,则会出现缩小时,图片跑到屏幕外侧或者中心点不对的问题,如下图这样:
所以,需要修正一下:
/**
* 检查边界,让它不能有空白和让它一直保在屏幕中心
*/
private void checkBroad() {
RectF rectF = getMatrixRectF();
float dx = 0;
float dy = 0;
//检测边界,不能让它留空白
if (rectF.width() >= getWidth()) {
if (rectF.right < getWidth()) {
dx = getWidth() - rectF.right;
}
if (rectF.left > 0) {
dx = -rectF.left;
}
}else {
//保证在屏幕中心
dx = getWidth() * 1.0f /2 - rectF.right + rectF.width() * 1.0f / 2;
}
//检测边界,不能让它留空白
if (rectF.height() >= getHeight()){
if (rectF.top > 0){
dy = -rectF.top;
}
if (rectF.bottom < getHeight()){
dy = getHeight() - rectF.bottom;
}
}else{
//保证在屏幕中心
dy = getHeight() * 1.0f /2 - rectF.bottom + rectF.height() * 1.0f / 2;
}
mMatrix.postTranslate(dx,dy);
}
getMatrixRectF() 为拿到当前缩放后的图片大小:
/**
* 拿到缩放后的图片大小
* @return
*/
private RectF getMatrixRectF(){
Matrix matrix = new Matrix(mMatrix);
RectF rectF = new RectF();
Drawable d = getDrawable();
if (d != null){
rectF.set(0,0,d.getIntrinsicWidth(),d.getIntrinsicHeight());
matrix.mapRect(rectF);
}
return rectF;
}
2.3 移动放大的图片
图片移动,在 onTouchEvent 中进行处理,需要考虑的是,当用两个手指拖动,然后其中一个手指抬起,此时中心点是有变动的,这时需要改变中心点的坐标,不然会出现一下子跳动的现象,具体代码如下:
//防止多指变动,中心点改变,而出现拖动问题
int pointerCount = event.getPointerCount();
float x = 0;
float y = 0;
for (int i = 0; i < pointerCount; i++) {
x += event.getX(i);
y += event.getY(i);
}
x = x / pointerCount;
y = y / pointerCount;
if (mLastPointCount != pointerCount){
mLastx = x;
mLasty = y;
}
//记录上一次的手指个数
mLastPointCount = pointerCount;
switch (event.getActionMasked()){
case MotionEvent.ACTION_MOVE:
requestFocusViewpager();
float dx = x - mLastx;
float dy = y - mLasty;
mMatrix.postTranslate(dx,dy);
checkBroad();
setImageMatrix(mMatrix);
mLastx = x;
mLasty = y;
break;
case MotionEvent.ACTION_UP:
mLastPointCount = 0;
break;
default:break;
}
三、双击放大
双击的方法,我们也可以通过 GestureDetector 手势这个类拿到双击的方法:
/**
* 指监听 双击的方法
*/
class SimpleGesture extends GestureDetector.SimpleOnGestureListener{
@Override
public boolean onDoubleTap(MotionEvent e) {
float x = e.getX();
float y = e.getY();
if (isCanDoubleScale) {
//放大
if (getScale() < mDoubleScale) {
post(new AutoScale(mDoubleScale, x, y));
} else { //缩放到自身大小
post(new AutoScale(mDeflautScale, x, y));
}
}
return true;
}
}
而AutoScale就是表示 缩放的类:
/**
* 双击缩放比例
*/
class AutoScale implements Runnable {
private final float LARGE = 1.07f;
private final float SMALL = 0.93f;
float targetScale;
float x;
float y;
float tempScale;
public AutoScale(float targetScale,float x, float y) {
this.targetScale = targetScale;
this.x = x;
this.y = y;
isCanDoubleScale = false;
//放大
if (targetScale > getScale()){
tempScale = LARGE;
}else{
tempScale = SMALL;
}
}
@Override
public void run() {
mMatrix.postScale(tempScale, tempScale , x, y);
checkBroad();
setImageMatrix(mMatrix);
float currentScale = getScale();
boolean isCanLarge = currentScale < targetScale && tempScale > 1.0f;
boolean isCanSmall = currentScale > targetScale && tempScale < 1.0f;
if (isCanLarge || isCanSmall) {
//若还未到达缩放的目标值,则继续缩放
postDelayed(this, mDoubleAutoTime);
}else{
//达到表示了,但可能有偏差,还需要调整一下
mMatrix.postScale(targetScale/currentScale, targetScale/currentScale , x, y);
checkBroad();
setImageMatrix(mMatrix);
isCanDoubleScale = true;
}
}
}
这样,关键知识点就讲解完了,自定义属性如下:
缩放控件 ScaleImageView
名称 | 类型 | 说明 |
---|---|---|
scale_auto_time | reference,integer | 双击时,达到放大的时间 |
scale_limit_board | boolean | 是否限制边界,即不能缩放到比控件小 |
scale_autofit | boolean | 自动适配缩放值,有些图片是正方形,如果你的高度没设定好,建议设置为false,不能会变形 |
scale_double_factor | integer | 双击时放大倍数 |
scale_max_factor | integer | 可放大的最大倍数 |
scale_interrupt_parent_touch | boolean | 是否截获父控件触摸事件,放大时,需要截取,不然无法移动 |