一、概述
現在app中,圖片預覽功能肯定是少不了的,用戶基本已經形成條件反射,看到小圖,點擊看大圖,看到大圖兩個手指開始進行放大,放大后,開始移動到指定部位~~~
我相信看圖的整個步驟,大家或者說用戶應該不希望被打斷把~~~“我擦,竟然不能放大,什么玩意,卸了~~“ , "我擦,竟然不能移動,留有何用,卸了~~"。
哈~所以對于圖片的預覽,一來,我們要讓用戶爽;二來,我們作為開發者,也得知道如何實現~~~
想要做到圖片支持多點觸控,自由的進行縮放、平移,需要了解幾個知識點:Matrix , GestureDetector , ScaleGestureDetector 以及事件分發機制,ps:不會咋辦,不會你懂的。
二、Matrix
矩陣,看深入了都是3維矩陣的乘啊什么的,怪麻煩的~~
其實這么了解下就行了:
Matrix
數據結構:3維矩陣;
內部存儲:new Float[9] ; 內部就是個一維數組,內部9個元素;可以進行setValues(float[] values)進行初始化
每個元素代表的意思:
{     MSCALE_X, MSKEW_X, MTRANS_X,          MSKEW_Y, MSCALE_Y, MTRANS_Y,          MPERSP_0, MPERSP_1, MPERSP_2  };  字面上,應該能看出來哪個代表x方向縮放,哪個代表垂直方向的偏移量吧~~有不認識的3個,沒事,請無視。
操作
比如你想要設置matrix的偏移量為200,100
你可以這么寫:
Matrix transMatrix = new Matrix();     float[] values = new float[] { 1.0, 0, 200, 0, 1.0, 100, 0, 0, 1.0 };     transMatrix.setValues(values); 如果需要在旋轉30度,放大兩倍~~
這么寫其實怪麻煩的~~
Matrix提供了一些常用的API:例如我們可以這么寫:
Matrix transMatrix = new Matrix(); transMatrix.postTranslate(200, 100);

如何獲取值:
當然了,我們對一個Matrix進行了各種操作,一會postScale,一會postTranslate;那么現在如何獲得當前的縮放比例:
前面說setValues可以初始化,那么getValues就能拿到當前矩陣的值,拿到的是個一維數組,9個元素;再通過下標取對應值就可以。
比如我想知道現在x方向縮放比例:
public final float getScale()   {     scaleMatrix.getValues(matrixValues);     return matrixValues[Matrix.MSCALE_X];   } 好了,知道這些就夠了~~
GestureDetector :
嗯,自己看API,能夠捕捉到長按、雙擊什么的;用法會在例子中
ScaleGestureDetector:
嗯,有點像繼承來的,其實不是的,獨立的一個類~用于檢測縮放的手勢~~~用法會在例子中
三、實戰
為了大家更好的理解,我會獨立出每個功能,最后再整合到一起~~也方面大家對每個API的使用的學習。
1、自由的縮放
需求:當圖片加載時,將圖片在屏幕中居中;圖片寬或高大于屏幕的,縮小至屏幕大小;自由對圖片進行方法或縮小;
代碼不是很長,直接貼代碼了:
package com.zhy.view;  import android.content.Context; import android.graphics.Matrix; import android.graphics.drawable.Drawable; import android.util.AttributeSet; import android.util.Log; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.ScaleGestureDetector.OnScaleGestureListener; import android.view.View; import android.view.View.OnTouchListener; import android.view.ViewTreeObserver; import android.widget.ImageView;  public class ZoomImageView extends ImageView implements OnScaleGestureListener,     OnTouchListener, ViewTreeObserver.OnGlobalLayoutListener  {   private static final String TAG = ZoomImageView.class.getSimpleName();      public static final float SCALE_MAX = 4.0f;   /**    * 初始化時的縮放比例,如果圖片寬或高大于屏幕,此值將小于0    */   private float initScale = 1.0f;    /**    * 用于存放矩陣的9個值    */   private final float[] matrixValues = new float[9];    private boolean once = true;    /**    * 縮放的手勢檢測    */   private ScaleGestureDetector mScaleGestureDetector = null;    private final Matrix mScaleMatrix = new Matrix();    public ZoomImageView(Context context)   {     this(context, null);   }    public ZoomImageView(Context context, AttributeSet attrs)   {     super(context, attrs);     super.setScaleType(ScaleType.MATRIX);     mScaleGestureDetector = new ScaleGestureDetector(context, this);     this.setOnTouchListener(this);   }    @Override   public boolean onScale(ScaleGestureDetector detector)   {     float scale = getScale();     float scaleFactor = detector.getScaleFactor();      if (getDrawable() == null)       return true;      /**      * 縮放的范圍控制      */     if ((scale < SCALE_MAX && scaleFactor > 1.0f)         || (scale > initScale && scaleFactor < 1.0f))     {       /**        * 最大值最小值判斷        */       if (scaleFactor * scale < initScale)       {         scaleFactor = initScale / scale;       }       if (scaleFactor * scale > SCALE_MAX)       {         scaleFactor = SCALE_MAX / scale;       }       /**        * 設置縮放比例        */       mScaleMatrix.postScale(scaleFactor, scaleFactor, getWidth() / 2,           getHeight() / 2);       setImageMatrix(mScaleMatrix);     }     return true;    }    @Override   public boolean onScaleBegin(ScaleGestureDetector detector)   {     return true;   }    @Override   public void onScaleEnd(ScaleGestureDetector detector)   {   }    @Override   public boolean onTouch(View v, MotionEvent event)   {     return mScaleGestureDetector.onTouchEvent(event);    }       /**    * 獲得當前的縮放比例    *    * @return    */   public final float getScale()   {     mScaleMatrix.getValues(matrixValues);     return matrixValues[Matrix.MSCALE_X];   }    @Override   protected void onAttachedToWindow()   {     super.onAttachedToWindow();     getViewTreeObserver().addOnGlobalLayoutListener(this);   }    @SuppressWarnings("deprecation")   @Override   protected void onDetachedFromWindow()   {     super.onDetachedFromWindow();     getViewTreeObserver().removeGlobalOnLayoutListener(this);   }    @Override   public void onGlobalLayout()   {     if (once)     {       Drawable d = getDrawable();       if (d == null)         return;       Log.e(TAG, d.getIntrinsicWidth() + " , " + d.getIntrinsicHeight());       int width = getWidth();       int height = getHeight();       // 拿到圖片的寬和高       int dw = d.getIntrinsicWidth();       int dh = d.getIntrinsicHeight();       float scale = 1.0f;       // 如果圖片的寬或者高大于屏幕,則縮放至屏幕的寬或者高       if (dw > width && dh <= height)       {         scale = width * 1.0f / dw;       }       if (dh > height && dw <= width)       {         scale = height * 1.0f / dh;       }       // 如果寬和高都大于屏幕,則讓其按按比例適應屏幕大小       if (dw > width && dh > height)       {         scale = Math.min(dw * 1.0f / width, dh * 1.0f / height);       }       initScale = scale;       // 圖片移動至屏幕中心             mScaleMatrix.postTranslate((width - dw) / 2, (height - dh) / 2);       mScaleMatrix           .postScale(scale, scale, getWidth() / 2, getHeight() / 2);       setImageMatrix(mScaleMatrix);       once = false;     }    }  } 我們在onGlobalLayout的回調中,根據圖片的寬和高以及屏幕的寬和高,對圖片進行縮放以及移動至屏幕的中心。如果圖片很小,那就正常顯示,不放大了~
我們讓OnTouchListener的MotionEvent交給ScaleGestureDetector進行處理
@Override   public boolean onTouch(View v, MotionEvent event)   {     return mScaleGestureDetector.onTouchEvent(event);    } 2、在onScale的回調中對圖片進行縮放的控制,首先進行縮放范圍的判斷,然后設置mScaleMatrix的scale值
現在的效果:
小于屏幕的寬和高

大于屏幕的寬和高

真機錄的效果不太好~~湊合看~
現在已經能夠~~~隨意的放大縮小了~~~
可是,可是,存在問題:
(1)縮放的中心點,我們設置是固定的,屏幕中間
(2)放大后,無法移動~
下面,我們先解決縮放的中心點問題,不能一直按屏幕中心么,像我這樣的,我比較關注妹子的眼睛,我要放大那一塊~~~
(1)設置縮放中心
(2)單純的設置縮放中心
僅僅是設置中心很簡單,直接修改下中心點 :
/** * 設置縮放比例 */ mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusX()); setImageMatrix(mScaleMatrix);
但是,隨意的中心點放大、縮小,會導致圖片的位置的變化,最終導致,圖片寬高大于屏幕時,圖片與屏幕間出現白邊;圖片小于屏幕,但是不居中。
3、控制縮放時圖片顯示的范圍
所以我們在縮放的時候需要手動控制下范圍:
/**    * 在縮放時,進行圖片顯示范圍的控制    */   private void checkBorderAndCenterWhenScale()   {      RectF rect = getMatrixRectF();     float deltaX = 0;     float deltaY = 0;      int width = getWidth();     int height = getHeight();      // 如果寬或高大于屏幕,則控制范圍     if (rect.width() >= width)     {       if (rect.left > 0)       {         deltaX = -rect.left;       }       if (rect.right < width)       {         deltaX = width - rect.right;       }     }     if (rect.height() >= height)     {       if (rect.top > 0)       {         deltaY = -rect.top;       }       if (rect.bottom < height)       {         deltaY = height - rect.bottom;       }     }     // 如果寬或高小于屏幕,則讓其居中     if (rect.width() < width)     {       deltaX = width * 0.5f - rect.right + 0.5f * rect.width();     }     if (rect.height() < height)     {       deltaY = height * 0.5f - rect.bottom + 0.5f * rect.height();     }     Log.e(TAG, "deltaX = " + deltaX + " , deltaY = " + deltaY);      mScaleMatrix.postTranslate(deltaX, deltaY);    }    /**    * 根據當前圖片的Matrix獲得圖片的范圍    *    * @return    */   private RectF getMatrixRectF()   {     Matrix matrix = mScaleMatrix;     RectF rect = new RectF();     Drawable d = getDrawable();     if (null != d)     {       rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());       matrix.mapRect(rect);     }     return rect;   } 在onScale里面記得調用:
/** * 設置縮放比例 */ mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY()); checkBorderAndCenterWhenScale(); setImageMatrix(mScaleMatrix);
這樣就好了,可以自由的放大任何地方,并且不會出現邊界出現白邊,也能很好的讓圖片顯示在屏幕中間(當圖片寬或高小于屏幕);
貼下布局文件:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <com.zhy.view.ZoomImageView android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="matrix" android:src="@drawable/xx" /> </RelativeLayout>
眼睛是心靈的窗戶,咱們來放大看看,效果圖:

四、進階
1、自由的進行移動
下面繼續完善我們的ImageView~~
首先加入放大后的移動~~
我們在onTouchEvent里面,加上移動的代碼,當然了,必須長或寬大于屏幕才可以移動~~~
@Override   public boolean onTouch(View v, MotionEvent event)   {     mScaleGestureDetector.onTouchEvent(event);      float x = 0, y = 0;     // 拿到觸摸點的個數     final int pointerCount = event.getPointerCount();     // 得到多個觸摸點的x與y均值     for (int i = 0; i < pointerCount; i++)     {       x += event.getX(i);       y += event.getY(i);     }     x = x / pointerCount;     y = y / pointerCount;      /**      * 每當觸摸點發生變化時,重置mLasX , mLastY      */     if (pointerCount != lastPointerCount)     {       isCanDrag = false;       mLastX = x;       mLastY = y;     }           lastPointerCount = pointerCount;      switch (event.getAction())     {     case MotionEvent.ACTION_MOVE:       Log.e(TAG, "ACTION_MOVE");       float dx = x - mLastX;       float dy = y - mLastY;              if (!isCanDrag)       {         isCanDrag = isCanDrag(dx, dy);       }       if (isCanDrag)       {         RectF rectF = getMatrixRectF();         if (getDrawable() != null)         {           isCheckLeftAndRight = isCheckTopAndBottom = true;           // 如果寬度小于屏幕寬度,則禁止左右移動           if (rectF.width() < getWidth())           {             dx = 0;             isCheckLeftAndRight = false;           }           // 如果高度小雨屏幕高度,則禁止上下移動           if (rectF.height() < getHeight())           {             dy = 0;             isCheckTopAndBottom = false;           }           mScaleMatrix.postTranslate(dx, dy);           checkMatrixBounds();           setImageMatrix(mScaleMatrix);         }       }       mLastX = x;       mLastY = y;       break;      case MotionEvent.ACTION_UP:     case MotionEvent.ACTION_CANCEL:       Log.e(TAG, "ACTION_UP");       lastPointerCount = 0;       break;     }      return true;   } 首先我們拿到觸摸點的數量,然后求出多個觸摸點的平均值,設置給我們的mLastX , mLastY , 然后在移動的時候,得到dx ,dy 進行范圍檢查以后,調用mScaleMatrix.postTranslate進行設置偏移量,當然了,設置完成以后,還需要再次校驗一下,不能把圖片移動的與屏幕邊界出現白邊,校驗完成后,調用setImageMatrix.
這里:需要注意一下,我們沒有復寫ACTION_DOWM,是因為,ACTION_DOWN在多點觸控的情況下,只要有一個手指按下狀態,其他手指按下不會再次觸發ACTION_DOWN,但是多個手指以后,觸摸點的平均值會發生很大變化,所以我們沒有用到ACTION_DOWN。每當觸摸點的數量變化,我們就會跟新當前的mLastX,mLastY.
下面是上面用到的兩個私有方法,一個用于檢查邊界,一個用于判斷是否是拖動的操作:
/**    * 移動時,進行邊界判斷,主要判斷寬或高大于屏幕的    */   private void checkMatrixBounds()   {     RectF rect = getMatrixRectF();      float deltaX = 0, deltaY = 0;     final float viewWidth = getWidth();     final float viewHeight = getHeight();     // 判斷移動或縮放后,圖片顯示是否超出屏幕邊界     if (rect.top > 0 && isCheckTopAndBottom)     {       deltaY = -rect.top;     }     if (rect.bottom < viewHeight && isCheckTopAndBottom)     {       deltaY = viewHeight - rect.bottom;     }     if (rect.left > 0 && isCheckLeftAndRight)     {       deltaX = -rect.left;     }     if (rect.right < viewWidth && isCheckLeftAndRight)     {       deltaX = viewWidth - rect.right;     }     mScaleMatrix.postTranslate(deltaX, deltaY);   }    /**    * 是否是推動行為    *    * @param dx    * @param dy    * @return    */   private boolean isCanDrag(float dx, float dy)   {     return Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop;   } 這樣,我們就可以快樂的放大、縮小加移動了~~~
效果圖:這次換個男人的圖片,我們越獄的主角之一,TBug~

我們的縮放+移動搞定~~
2、雙擊放大與縮小
談到雙擊事件,我們的GestureDetector終于要登場了,這哥們可以捕獲雙擊事件~~
(1)GestureDetector的使用
因為GestureDetector設置監聽器的話,方法一大串,而我們只需要onDoubleTap這個回調,所以我們準備使用它的一個內部類SimpleOnGestureListener,對接口的其他方法實現了空實現。
不過還有幾個問題需要討論下,才能開始我們的代碼:
(1)我們雙擊尺寸如何變化?
我是這樣的,根據當前的縮放值,如果是小于2的,我們雙擊直接到變為原圖的2倍;如果是2,4之間的,我們雙擊直接為原圖的4倍;其他狀態也就是4倍,雙擊后還原到最初的尺寸。
如果你覺得這樣不合適,可以根據自己的愛好調整。
我們雙擊變化,需要一個動畫~~比如我們上例的演示圖,圖片很大,全屏顯示的時候initScale=0.5左后,如果雙擊后變為2,也就是瞬間大了四倍,沒有一個過渡的效果的話,給用戶的感覺會特別差。所以,我們準備使用postDelay執行一個Runnable,Runnable中再次根據的當然的縮放值繼續執行。
首先我們在構造方法中,完成對GestureDetector的初始化,以及設置onDoubleTap監聽
public ZoomImageView(Context context, AttributeSet attrs)   {     super(context, attrs);     mScaleGestureDetector = new ScaleGestureDetector(context, this);     mGestureDetector = new GestureDetector(context,         new SimpleOnGestureListener()         {           @Override           public boolean onDoubleTap(MotionEvent e)           {             if (isAutoScale == true)               return true;              float x = e.getX();             float y = e.getY();             Log.e("DoubleTap", getScale() + " , " + initScale);             if (getScale() < SCALE_MID)             {               ZoomImageView.this.postDelayed(                   new AutoScaleRunnable(SCALE_MID, x, y), 16);               isAutoScale = true;             } else if (getScale() >= SCALE_MID                 && getScale() < SCALE_MAX)             {               ZoomImageView.this.postDelayed(                   new AutoScaleRunnable(SCALE_MAX, x, y), 16);               isAutoScale = true;             } else             {               ZoomImageView.this.postDelayed(                   new AutoScaleRunnable(initScale, x, y), 16);               isAutoScale = true;             }              return true;           }         });     mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();     super.setScaleType(ScaleType.MATRIX);     this.setOnTouchListener(this);   } 當雙擊的時候,首先判斷是否正在自動縮放,如果在,直接retrun ; 
然后就進入了我們的if,如果當然是scale小于2,則通過view.發送一個Runnable進行執行;其他類似;
/**    * 自動縮放的任務    *    * @author zhy    *    */   private class AutoScaleRunnable implements Runnable   {     static final float BIGGER = 1.07f;     static final float SMALLER = 0.93f;     private float mTargetScale;     private float tmpScale;      /**      * 縮放的中心      */     private float x;     private float y;      /**      * 傳入目標縮放值,根據目標值與當前值,判斷應該放大還是縮小      *      * @param targetScale      */     public AutoScaleRunnable(float targetScale, float x, float y)     {       this.mTargetScale = targetScale;       this.x = x;       this.y = y;       if (getScale() < mTargetScale)       {         tmpScale = BIGGER;       } else       {         tmpScale = SMALLER;       }      }      @Override     public void run()     {       // 進行縮放       mScaleMatrix.postScale(tmpScale, tmpScale, x, y);       checkBorderAndCenterWhenScale();       setImageMatrix(mScaleMatrix);        final float currentScale = getScale();       //如果值在合法范圍內,繼續縮放       if (((tmpScale > 1f) && (currentScale < mTargetScale))           || ((tmpScale < 1f) && (mTargetScale < currentScale)))       {         ZoomImageView.this.postDelayed(this, 16);       } else//設置為目標的縮放比例       {         final float deltaScale = mTargetScale / currentScale;         mScaleMatrix.postScale(deltaScale, deltaScale, x, y);         checkBorderAndCenterWhenScale();         setImageMatrix(mScaleMatrix);         isAutoScale = false;       }      }   } 代碼寫完了,我們依然需要把我們的event傳給它,依然是在onTouch方法:
@Override   public boolean onTouch(View v, MotionEvent event)   {     if (mGestureDetector.onTouchEvent(event))       return true; 好了,雙擊放大與縮小的功能就搞定了,下面測試下~~~
效果圖,終于可以用模擬器了~~

3、處理與ViewPager的沖突
直接把我們的圖片作為ViewPager的Item,可想而知,肯定有沖突~~
(1)布局文件
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" > <android.support.v4.view.ViewPager android:id="@+id/id_viewpager" android:layout_width="fill_parent" android:layout_height="fill_parent" > </android.support.v4.view.ViewPager> </RelativeLayout>
(2)Activity代碼
 
package com.zhy.zhy_scalegesturedetector02;  import android.app.Activity; import android.os.Bundle; import android.support.v4.view.PagerAdapter; import android.support.v4.view.ViewPager; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView;  import com.zhy.view.ZoomImageView;  public class MainActivity extends Activity {   private ViewPager mViewPager;   private int[] mImgs = new int[] { R.drawable.tbug, R.drawable.a,       R.drawable.xx };   private ImageView[] mImageViews = new ImageView[mImgs.length];    @Override   protected void onCreate(Bundle savedInstanceState)   {     super.onCreate(savedInstanceState);     setContentView(R.layout.vp);          mViewPager = (ViewPager) findViewById(R.id.id_viewpager);     mViewPager.setAdapter(new PagerAdapter()     {        @Override       public Object instantiateItem(ViewGroup container, int position)       {         ZoomImageView imageView = new ZoomImageView(             getApplicationContext());         imageView.setImageResource(mImgs[position]);         container.addView(imageView);         mImageViews[position] = imageView;         return imageView;       }        @Override       public void destroyItem(ViewGroup container, int position,           Object object)       {         container.removeView(mImageViews[position]);       }        @Override       public boolean isViewFromObject(View arg0, Object arg1)       {         return arg0 == arg1;       }        @Override       public int getCount()       {         return mImgs.length;       }     });    } } 現在直接運行,發現ViewPager好著呢,但是我們的圖片放大以后,移動和ViewPager沖突了,又不能移動了~。。。擦擦擦。。。
(3)處理沖突
現在我們迅速的想一想,記得之前學習過事件分發機制,我們的ZoomImageView在ViewPager中,如果我們不想被攔截,那么如何做呢?
首先不想被攔截的條件是:我們的寬或高大于屏幕寬或高時,因為此時可以移動,我們不想被攔截。接下來,不想被攔截:
getParent().requestDisallowInterceptTouchEvent(true);
一行代碼足以,如果你對事件分發中,不被攔截不清晰,可以參考:如何不被攔截 。
放在一起我們的代碼就是:
switch (event.getAction())     {     case MotionEvent.ACTION_DOWN:       if (rectF.width() > getWidth() || rectF.height() > getHeight())       {         getParent().requestDisallowInterceptTouchEvent(true);       }       break;     case MotionEvent.ACTION_MOVE:       if (rectF.width() > getWidth() || rectF.height() > getHeight())       {         getParent().requestDisallowInterceptTouchEvent(true);       } ~當寬或高大于屏幕寬或高時,拖動效果認為是移動圖片,反之則讓ViewPager去處理
此時的效果:

ok,現在已經解決了和ViewPager的沖突,ps:尼瑪不應該雙擊還能放大兩次到4倍,,,,,好惡心。。。
(4)到達邊界事件交給ViewPager處理
可能有些用戶還希望,當圖片到達邊界時,不能再拖動的時候,能夠把事件給ViewPager
那就在ACTION_MOVE中,判斷當前已經到達邊界,且還在拉的時候,事件交給ViewPager
if (isCanDrag)       {          if (getDrawable() != null)         {           if (getMatrixRectF().left == 0 && dx > 0)           {             getParent().requestDisallowInterceptTouchEvent(false);           }            if (getMatrixRectF().right == getWidth() && dx < 0)           {             getParent().requestDisallowInterceptTouchEvent(false);           } 此時的效果:

好了,其實添加了這個之后,體驗一般哈~~~
終于寫完了,代碼中可能存在BUG,發現問題,或者解決了發現的BUG時,希望可以直接在博客下面留言,也能夠方便他人~~
到此,我們的Android 手勢檢測實戰 打造支持縮放平移的圖片預覽效果 結束~~!
建議把雙擊放大到4倍的地方,注釋掉一個If
//           else if (getScale() >= SCALE_MID //               && getScale() < SCALE_MAX) //           { //             ZoomImageView.this.postDelayed( //                 new AutoScaleRunnable(SCALE_MAX, x, y), 16); //             isAutoScale = true; //           }  連續雙擊放大,感覺不爽,代碼已經上傳,我就不重傳了,如果你也覺得不爽,可以自行注釋。
新聞熱點
疑難解答