Android ImageView手勢縮放完整的實現
已經有很多開源的縮放控制元件了,實際做專案沒有必要重複造輪子,但對於學習來說自己親自實現一個縮放的ImageView是大有益處的。所以這裡分享一下自己學習的心得。
1、建立一個類繼承ImageView。
public class GestureImageView extends ImageView { public GestureImageView(Context context) { super(context); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } }
2、既然要實現手勢縮放,那麼首先應該取得控制元件的觸控事件,包括多點觸控。這裡在控制元件直接通過View的OnTouchListener來獲取所需事件。
注意:如果想要實現用ViewPager和手勢縮放控制元件做相簿應用的話,最好將事件封裝在控制元件外部,否則會跟父控制元件事件衝突出現莫名其妙的Bug。
public void GestureImageViewInit(){ this.setOnTouchListener(this); } public GestureImageView(Context context) { super(context); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); GestureImageViewInit(); } @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: //手指按下事件 Log.e("TouchEvent","ActionDown"); break; case MotionEvent.ACTION_POINTER_DOWN: //螢幕上已經有一個點按住 再按下一點時觸發該事件 Log.e("TouchEvent","ActionPointerDown"); break; case MotionEvent.ACTION_POINTER_UP: //螢幕上已經有兩個點按住 再鬆開一點時觸發該事件 Log.e("TouchEvent","ActionPointerUp"); break; case MotionEvent.ACTION_MOVE: //手指移動時觸發事件 Log.e("TouchEvent","ActionMove"); break; case MotionEvent.ACTION_UP: //手指鬆開時觸發事件 Log.e("TouchEvent","ActionUp"); break; } //注意這裡return 的一定要是true 否則只會觸發按下事件 return true; }
3、實現ImageView縮放和位移基本操作
public voidsetImageMatrix(Matrix matrix);
最基本的操作就是通過Matrix來實現ImageView的縮放和位移。由於後面會有大量的座標操作,座標變數統一為PointF型別。而且要實現Matrix操作,ScaleType也需要設定成Matrix。宣告一個變數matrix用於記錄當前的matrix操作。
為了便於初始化,將初始化的東西統一放在GestureImageViewInit()中,後面會持續將東西放進ImageView中。
private Matrix matrix; public void GestureImageViewInit(){ this.setOnTouchListener(this); this.setScaleType(ScaleType.MATRIX); matrix=new Matrix(); } public GestureImageView(Context context) { super(context); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs) { super(context, attrs); GestureImageViewInit(); } public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); GestureImageViewInit(); }
①縮放操作
/**
* 根據縮放因子縮放圖片
* @param scale
*/
public void setImageScale(PointF scale){
matrix.setScale(scale.x, scale.y);
this.setImageMatrix(matrix);
}
②位移操作
/**
* 根據偏移量改變圖片位置
* @param offset
*/
public void setImageTranslation(PointF offset){
matrix.postTranslate(offset.x, offset.y);
this.setImageMatrix(matrix);
}
定義了上述操作之後就可以完成後面複雜的功能了。
先上一張無任何操作設定的Demo,我們會發現圖片顯示不正常,因為我們沒有對圖片進行任何操作,所以圖片為原圖大小,並且在安卓中,座標系原點是左上角,因此我們看到的是原圖的左上部分,一般相簿瀏覽都會對圖片進行自適應顯示,這裡我們先實現圖片最開始的自適應顯示。
要獲取view的寬高,對onMeasure函式重寫即可。同時,由於放大仍然是以左上角為座標原點的,所以放大之後需要進行唯一操作將圖片移動至view的中心。這裡需要儲存放大前原始影象的大小imageSize和縮放操作後的scaleSize,所以對setImageScale進行修改。
private PointF viewSize;
private PointF imageSize;
private PointF scaleSize;
private PointF originScale;
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width=MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
viewSize=new PointF(width,height);
Log.e("view size",viewSize.toString());
//獲取當前Drawable的大小
Drawable drawable=getDrawable();
if(drawable==null){
Log.e("no drawable","drawable is nullPtr");
}else {
imageSize=new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
Log.e("drawable size",imageSize.toString());
}
FitCenter();
}
/**
* 使圖片儲存在中央
*/
public void FitCenter(){
float scaleH=viewSize.y/imageSize.y;
float scaleW=viewSize.x/imageSize.x;
//選擇小的縮放因子確保圖片全部顯示在視野內
float scale =scaleH<scaleW?scaleH:scaleW;
//根據view適應大小
setImageScale(new PointF(scale, scale));
originScale.set(scale,scale);
//根據縮放因子大小來將圖片中心調整到view 中心
if(scaleH<scaleW)
setImageTranslation(new PointF(viewSize.x/2-scaleSize.x/2,0));
else
setImageTranslation(new PointF(0,viewSize.y/2-scaleSize.y/2));
}
/**
* 根據縮放因子縮放圖片
* @param scale
*/
public void setImageScale(PointF scale){
matrix.setScale(scale.x, scale.y);
scaleSize.set(scale.x*imageSize.x,scale.y*imageSize.y);
this.setImageMatrix(matrix);
}
可以發現,圖片已經在中間正確顯示了。
4、實現雙擊放大
完成了前面的準備工作之後,先來實現第一個小功能,雙擊放大,這裡放大倍數為2倍。雙擊間隔為280ms,可以感覺自己的感覺調。
long doubleClickTimeSpan=280;
long lastClickTime=0;
int rationZoomIn=2;
將ActionDown事件處理修改成下面這樣。雙擊放大和復原這一步就完成了。 case MotionEvent.ACTION_DOWN:
//手指按下事件
if(event.getPointerCount()==1){
if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){
//雙擊事件觸發
Log.e("TouchEvent","DoubleClick");
if(curMode==0) {
curMode=1;
setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn));
}else {
curMode=0;
FitCenter();
}
}else {
lastClickTime=event.getEventTime();
}
}
5、實現根據點選位置放大。(這裡暫且不考慮邊界檢測問題)
要根據點選位置為中心進行放大,那麼首先就要記錄雙擊位置。
PointF start;
假設點選點座標為(x1,y1) 在圖片上歸一化座標即以圖片左上角為原點的座標為
((x1-curPoint.x)/scaleSize.x,(y1-curPoint.y)/scaleSize.y),記錄為
relativePoint(x2,y2)。(如果點選位置超出了圖片範圍,那麼結果需要另行處理。)
那麼經過縮放操作之後這一點在圖片上的歸一化座標是不變的,但絕對座標變成了(x2*scaleSize.x,y2*scaleSize.y)。
只要將絕對座標移動至(x1,y1)處就可以實現以點選中心放大了。
修改如下
case MotionEvent.ACTION_DOWN:
start.set(event.getX(),event.getY());
//手指按下事件
if(event.getPointerCount()==1){
if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){
//雙擊事件觸發
Log.e("TouchEvent", "DoubleClick");
if(curMode==ZoomMode.Ordinary) {
curMode=ZoomMode.ZoomIn;
relativePoint=new PointF();
//計算歸一化座標
relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y);
setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn));
setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y));
}else {
curMode=ZoomMode.Ordinary;
FitCenter();
}
}else {
lastClickTime=event.getEventTime();
}
}
break;
curPoint變數在setImageTranslation中獲取。
/**
* 根據偏移量改變圖片位置
* @param offset
*/
public void setImageTranslation(PointF offset){
matrix.postTranslate(offset.x, offset.y);
curPoint.set(offset);
this.setImageMatrix(matrix);
}
6、實現雙指縮放和拖動操作
雙指縮放就需要用到ActionPointer這個事件。
首先新增變數用於記錄雙指中心點,雙指距離。記錄
private PointF center;
private float doubleFingerDistance=0;
當雙指距離大於一定距離時進入雙指縮放模式,此時根據雙指距離的相對變化修改matrix,同時校正圖片位置到雙指中心(這裡和上面關於中心縮放的原理是一樣的)。因為雙指縮放時會在之前的基礎上再次放大,因此需要一個變數來儲存當前的縮放比例,在FitCenter初始化為初始的scale。
public void FitCenter(){
float scaleH=viewSize.y/imageSize.y;
float scaleW=viewSize.x/imageSize.x;
//選擇小的縮放因子確保圖片全部顯示在視野內
float scale =scaleH<scaleW?scaleH:scaleW;
//根據view適應大小
setImageScale(new PointF(scale, scale));
originScale.set(scale, scale);
//根據縮放因子大小來將圖片中心調整到view 中心
if(scaleH<scaleW) {
setImageTranslation(new PointF(viewSize.x / 2 - scaleSize.x / 2, 0));
fitMode=1;
}
else {
fitMode=0;
setImageTranslation(new PointF(0, viewSize.y / 2 - scaleSize.y / 2));
}
//記錄縮放因子 下次繼續從這個比例縮放
scaleDoubleZoom=originScale.x;
}
而拖動則在ActionMove裡判斷偏移量,將偏移量附加到ImageView上即可。
程式碼如下
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
if(event.getPointerCount()==1){
if(curMode==ZoomMode.ZoomIn){
setImageTranslation(new PointF(event.getX() - start.x, event.getY() - start.y));
start.set(event.getX(),event.getY());
}
}else {
//雙指縮放時判斷是否滿足一定距離
if (Math.abs(getDoubleFingerDistance(event) - doubleFingerDistance) > 50 && curMode != ZoomMode.DoubleZoomIn) {
//獲取雙指中點
center.set((event.getX(0) + event.getX(1)) / 2, (event.getY(0) + event.getY(1)) / 2);
//設定起點
start.set(center);
curMode = ZoomMode.DoubleZoomIn;
doubleFingerDistance = getDoubleFingerDistance(event);
relativePoint = new PointF();
//根據圖片當前座標值計算歸一化座標
relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y);
}
if(curMode==ZoomMode.DoubleZoomIn)
{
float scale =scaleDoubleZoom*getDoubleFingerDistance(event)/doubleFingerDistance;
setImageScale(new PointF(scale, scale));
setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y));
}
}
break;
這裡已經可以縮放並且跟隨手指移動了。還有很多事情需要做,比如雙指縮放時也可以進行移動等等這類細節。
7、實現邊界檢測寫著寫著,發現這個控制元件其實並不難,但是需要注意處理的細節特別多,尤其是邊界條件這一塊。
原來的專案中已經實現了,貼上一部分作為參考。就是判斷上下左右邊界進行偏移量調整。
/**
*邊界修正處理函式 使圖片一直在可視範圍內,根據margin可以適當將黑邊顯示出來
* @param offset 偏移量
* @param margin 超出圖片邊界的餘量
*/
public void boundaryCorrect(Vector2 offset,float margin){
Vector2 XandY=getMatrixTranslation(matrix);
float xOver;
float yOver;
//設定上下左右的邊界
if(currentBitmapSize.x>=viewSize.x){
xLeft=0;
xRight=-currentBitmapSize.x+viewSize.x;
}else {
//圖片的寬度比檢視小時,則應處在中間位置
xLeft=(viewSize.x-currentBitmapSize.x)/2;
xRight=viewSize.x-xLeft-currentBitmapSize.x;
}
if(currentBitmapSize.y>=viewSize.y){
yTop=0;
yBottom=-currentBitmapSize.y+viewSize.y;
} else {
//圖片的高度比檢視小時,則應處在中間位置
yTop=(viewSize.y-currentBitmapSize.y)/2;
yBottom=viewSize.y-yTop-currentBitmapSize.y;
}
//修正offset
//左邊界
xOver=XandY.x+offset.x-xLeft;
if(XandY.x+offset.x>xLeft)
offset.setX((float) Math.pow((margin-xOver) / margin, 2) * offset.x);
//右邊界
xOver=xRight-XandY.x-offset.x;
if(XandY.x+offset.x<xRight)
offset.setX((float) Math.pow((margin-xOver) / margin, 2) * offset.x);
//上邊界
yOver=XandY.y+offset.y-yTop;
if(XandY.y+offset.y>=yTop)
offset.setY((float) Math.pow((margin-yOver) / margin, 2) * offset.y);
//下邊界
yOver=yBottom-XandY.y-offset.y;
if(XandY.y+offset.y<= yBottom)
offset.setY((float) Math.pow((margin-yOver) / margin, 2) * offset.y);
}
8、完善控制元件,加上邊界回彈、縮放、復位等動畫。
同樣這裡也是非常繁雜的步驟,不再繼續寫了。
關於控制元件動畫,有很多方式可以實現,這裡我用了子執行緒定時回撥重新整理位置,大概意思就是將一步操作細分為多步操作,細分的程度可以自己選擇,不過不推薦我這種實現方式......
貼下完整的Demo程式碼吧,有時間會繼續優化完成的= =
package com.qtree.gestureimageview;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
/**
* Created by John on 2016/5/9.
*/
public class GestureImageView extends ImageView implements View.OnTouchListener {
public class ZoomMode{
public final static int Ordinary=0;
public final static int ZoomIn=1;
public final static int DoubleZoomIn=2;
}
private int curMode=0;
private Matrix matrix;
private PointF viewSize;
private PointF imageSize;
private PointF scaleSize;
//記錄圖片當前座標
private PointF curPoint;
private PointF originScale;
//0:寬度適應 1:高度適應
private int fitMode=0;
private PointF start;
private PointF center;
private float scaleDoubleZoom=0;
private PointF relativePoint;
private float doubleFingerDistance=0;
long doubleClickTimeSpan=280;
long lastClickTime=0;
int rationZoomIn=2;
public void GestureImageViewInit(){
this.setOnTouchListener(this);
this.setScaleType(ScaleType.MATRIX);
matrix=new Matrix();
originScale=new PointF();
scaleSize=new PointF();
start=new PointF();
center=new PointF();
curPoint=new PointF();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width=MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
viewSize=new PointF(width,height);
//獲取當前Drawable的大小
Drawable drawable=getDrawable();
if(drawable==null){
Log.e("no drawable","drawable is nullPtr");
}else {
imageSize=new PointF(drawable.getMinimumWidth(),drawable.getMinimumHeight());
}
FitCenter();
}
/**
* 使圖片儲存在中央
*/
public void FitCenter(){
float scaleH=viewSize.y/imageSize.y;
float scaleW=viewSize.x/imageSize.x;
//選擇小的縮放因子確保圖片全部顯示在視野內
float scale =scaleH<scaleW?scaleH:scaleW;
//根據view適應大小
setImageScale(new PointF(scale, scale));
originScale.set(scale, scale);
//根據縮放因子大小來將圖片中心調整到view 中心
if(scaleH<scaleW) {
setImageTranslation(new PointF(viewSize.x / 2 - scaleSize.x / 2, 0));
fitMode=1;
}
else {
fitMode=0;
setImageTranslation(new PointF(0, viewSize.y / 2 - scaleSize.y / 2));
}
//記錄縮放因子 下次繼續從這個比例縮放
scaleDoubleZoom=originScale.x;
}
public GestureImageView(Context context) {
super(context);
GestureImageViewInit();
}
public GestureImageView(Context context, AttributeSet attrs) {
super(context, attrs);
GestureImageViewInit();
}
public GestureImageView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
GestureImageViewInit();
}
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
start.set(event.getX(),event.getY());
//手指按下事件
if(event.getPointerCount()==1){
if(event.getEventTime()-lastClickTime<=doubleClickTimeSpan){
//雙擊事件觸發
Log.e("TouchEvent", "DoubleClick");
if(curMode==ZoomMode.Ordinary) {
curMode=ZoomMode.ZoomIn;
relativePoint=new PointF();
//計算歸一化座標
relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y);
setImageScale(new PointF(originScale.x * rationZoomIn, originScale.y * rationZoomIn));
setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y));
}else {
curMode=ZoomMode.Ordinary;
FitCenter();
}
}else {
lastClickTime=event.getEventTime();
}
}
break;
case MotionEvent.ACTION_POINTER_DOWN:
//螢幕上已經有一個點按住 再按下一點時觸發該事件
doubleFingerDistance=getDoubleFingerDistance(event);
break;
case MotionEvent.ACTION_POINTER_UP:
//螢幕上已經有兩個點按住 再鬆開一點時觸發該事件
curMode=ZoomMode.ZoomIn;
scaleDoubleZoom=scaleSize.x/imageSize.x;
if(scaleSize.x<viewSize.x&&scaleSize.y<viewSize.y){
curMode=ZoomMode.Ordinary;
FitCenter();
}
break;
case MotionEvent.ACTION_MOVE:
//手指移動時觸發事件
if(event.getPointerCount()==1){
if(curMode==ZoomMode.ZoomIn){
setImageTranslation(new PointF(event.getX() - start.x, event.getY() - start.y));
start.set(event.getX(),event.getY());
}
}else {
//雙指縮放時判斷是否滿足一定距離
if (Math.abs(getDoubleFingerDistance(event) - doubleFingerDistance) > 50 && curMode != ZoomMode.DoubleZoomIn) {
//獲取雙指中點
center.set((event.getX(0) + event.getX(1)) / 2, (event.getY(0) + event.getY(1)) / 2);
//設定起點
start.set(center);
curMode = ZoomMode.DoubleZoomIn;
doubleFingerDistance = getDoubleFingerDistance(event);
relativePoint = new PointF();
//根據圖片當前座標值計算歸一化座標
relativePoint.set(( start.x-curPoint.x )/ scaleSize.x,(start.y-curPoint.y)/scaleSize.y);
}
if(curMode==ZoomMode.DoubleZoomIn)
{
float scale =scaleDoubleZoom*getDoubleFingerDistance(event)/doubleFingerDistance;
setImageScale(new PointF(scale, scale));
setImageTranslation(new PointF(start.x - relativePoint.x * scaleSize.x, start.y - relativePoint.y * scaleSize.y));
}
}
break;
case MotionEvent.ACTION_UP:
//手指鬆開時觸發事件
break;
}
//注意這裡return 的一定要是true 否則只會觸發按下事件
return true;
}
/**
* 根據縮放因子縮放圖片
* @param scale
*/
public void setImageScale(PointF scale){
matrix.setScale(scale.x, scale.y);
scaleSize.set(scale.x*imageSize.x,scale.y*imageSize.y);
this.setImageMatrix(matrix);
}
/**
* 根據偏移量改變圖片位置
* @param offset
*/
public void setImageTranslation(PointF offset){
matrix.postTranslate(offset.x, offset.y);
curPoint.set(offset);
this.setImageMatrix(matrix);
}
public static float getDoubleFingerDistance(MotionEvent event){
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float)Math.sqrt(x * x + y * y) ;
}
}