上一章,我們學習了手勢 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 | 是否截獲父控件觸摸事件,放大時,需要截取,不然無法移動 |