(一)自定義ImageView,初步實現多點觸控、自由縮放


      真心佩服那些一直專注於技術共享的大神們,正是因為他們無私的分享精神,我才能每天都有進步。近日又算是仔細學了android的自定義控件技術,跟着大神的腳步實現了一個自定義的ImageView。里面涉及到常用的多點觸控技術。在此十分感謝那些默默奉獻的大神們,同時向他們學習,也把自己的學習過程以及收獲到的知識分享給大家。這個自定義的ImgaeView實現了圖片的自由縮放,自由移動,並解決了與ViewPager的兼容性問題。開發完成后,可以直接代替普通的ImageView了。廢話不多說了,現在就跟我一起進入到自定義ImageView實現多點觸控的開發之旅中吧。

     本項目所涉及到的源碼以及圖片素材,可點擊下面的鏈接下載:
http://download.csdn.net/detail/fuyk12/9243417

 

一、效果展示以及前言說明

      由於多點觸控在模擬器上無法演示,因此我也用真機錄制了一個gif,展示給大家看,但是錄制的效果不是很流暢,不過足以說明問題了。效果展示如下,左圖為真機效果,右圖為模擬上的效果(無法演示自由縮放):

                    

       效果說明:  項目中展示的是一個ViewPager,里面放置了三張圖片(如上圖所示的三張圖片)。而盛載圖片的就是自定義的ImageView,由於在其中實現了多點觸控技術,因此我們從上圖中可以看到,每一張圖片都可以自由縮放和移動。因為左圖展示的是我在手機上的操作,所以你看不到我觸控的地方,這很正常。小伙伴們做的時候,就可以在自己的手機上看到了。那么上圖展示的都包括什么效果呢?其中包括:圖片的自由縮放,圖片的自由移動,圖片的雙擊放大和縮小的效果。

     所用到的知識點: 基本的android知識不必多說。此外還需要用到android下的縮放手勢監控類ScaleGestureDetector,其他多樣的手勢監控類(例如監控雙擊)GestureDetector,以及基本的OnTouchListener和OnGlobalLayoutListener。還有控制圖片變換的Matrix。這些API如果大家不會用,可以網上學習一下。因為如果再講這些基礎的API,文章顯得超級冗余。因此本系列文章的主旨不是講解使用到的android基礎知識,而是實戰,主要分析實現這個自定義ImageView的各種邏輯以及碰到的困難。那些API,很容易就學會的,或者讀者也可以一般跟着做這個項目的代碼,一邊學。

 

     限於篇幅, 這一篇文章將初步實現圖片的自由縮放,在本篇文章的基礎上,更多內容在后續的文章里面。

 

二、進入項目實戰

      下面就讓開始寫代碼,一步一步來實現上面所展示的效果吧。

(1)在控件上圖片的顯示控制

      新建項目,新建類ZoomImageView繼承自ImageView。此時它里面的代碼如下:

 

package com.example.view;

import android.widget.ImageView;

public class ZoomImageView extends ImageView
{
    public ZoomImageView(Context context)
    {
        this(context,null);
    }
    public ZoomImageView(Context context, AttributeSet attrs) 
    {
        this(context, attrs,0);

    }
    public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
             
    }
    
}

 

    代碼很簡單,不再解釋。 然后將需要的圖片素材都拷貝進來(文章開頭可以下載)。首先要考慮的是,有的圖片很大,有的圖片很小,我們需要將這些圖片合適的顯示在ZoomImageView上。在這里,我們讓所有的圖片都顯示在屏幕中央,也就是說,大的圖片讓它縮小,小的圖片讓它放大。為了詳細的說明這個問題。看下面的一張分析圖:

 

      上面分析的是一張比較小的圖片,因此縮放是放大,如果圖片比較大,那么縮放就應該是縮小了。想想為什么無論是放大還是縮小,都要以小的縮放比例為標准?這是因為,比如寬度需要方法2倍才能與屏幕一樣寬,高度需要放大3倍才能與屏幕一樣高,如果我們選擇放大3倍,那么寬度就超出了屏幕。按照這樣的道理,想要達到圖示B的那樣的展示效果,無論放大還是縮小都應該以小的比例為標准。從上面圖示的分析,我們就拿到了將圖片顯示在ZoomImageView上的標准,即:

平移:
x方向:屏幕寬度/2 - 圖片原始寬度/2
y方向:屏幕高度/2 - 圖片原始高度/2
縮放:通過圖片原始寬高與屏幕寬高的比較,以小的縮放比例為標准進行縮放。

         好了,圖片顯示的原理已經分析完畢,下面將其用代碼表達出來。修改ZoomImageView的代碼如下:

  1 package com.example.view;
  2 
  3 import android.annotation.SuppressLint;
  4 import android.content.Context;
  5 import android.graphics.Matrix;
  6 import android.graphics.drawable.Drawable;
  7 import android.util.AttributeSet;
  8 import android.util.Log;
  9 import android.view.MotionEvent;
 10 import android.view.ScaleGestureDetector;
 11 import android.view.ScaleGestureDetector.OnScaleGestureListener;
 12 import android.view.View;
 13 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 14 import android.view.View.OnTouchListener;
 15 import android.widget.ImageView;
 16 
 17 public class ZoomImageView extends ImageView implements OnGlobalLayoutListener 
 18 {
 19     private boolean mOnce = false;//是否執行了一次
 20     
 21     /**
 22      * 初始縮放的比例
 23      */
 24     private float initScale;
 25     /**
 26      * 縮放比例
 27      */
 28     private float midScale;
 29     /**
 30      * 可放大的最大比例
 31      */
 32     private float maxScale;
 33     /**
 34      * 縮放矩陣
 35      */
 36     private Matrix scaleMatrix;
 37     
 38     /**
 39      * 縮放的手勢監控類
 40      */
 41     private ScaleGestureDetector mScaleGestureDetector;
 42 
 43     public ZoomImageView(Context context)
 44     {
 45         this(context,null);
 46     }
 47     public ZoomImageView(Context context, AttributeSet attrs) 
 48     {
 49         this(context, attrs,0);
 50 
 51     }
 52     public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
 53     {
 54         super(context, attrs, defStyle);
 55         
 56         scaleMatrix = new Matrix();
 57         
 58         setScaleType(ScaleType.MATRIX);        
 59     }
 60     
 61     /**
 62      * 該方法在view與window綁定時被調用,且只會被調用一次,其在view的onDraw方法之前調用
 63      */
 64     protected void onAttachedToWindow()
 65     {
 66         super.onAttachedToWindow();
 67         //注冊監聽器
 68         getViewTreeObserver().addOnGlobalLayoutListener(this);
 69     }
 70     
 71     /**
 72      * 該方法在view被銷毀時被調用
 73      */
 74     @SuppressLint("NewApi") protected void onDetachedFromWindow() 
 75     {
 76         super.onDetachedFromWindow();
 77         //取消監聽器
 78         getViewTreeObserver().removeOnGlobalLayoutListener(this);
 79     }
 80     
 81     /**
 82      * 當一個view的布局加載完成或者布局發生改變時,OnGlobalLayoutListener會監聽到,調用該方法
 83      * 因此該方法可能會被多次調用,需要在合適的地方注冊和取消監聽器
 84      */
 85     public void onGlobalLayout() 
 86     {
 87         if(!mOnce)
 88         {
 89             //獲得當前view的Drawable
 90             Drawable d = getDrawable();
 91             
 92             if(d == null)
 93             {
 94                 return;
 95             }
 96             
 97             //獲得Drawable的寬和高
 98             int dw = d.getIntrinsicWidth();
 99             int dh = d.getIntrinsicHeight();
100             
101             //獲取當前view的寬和高
102             int width = getWidth();
103             int height = getHeight();
104             
105             //縮放的比例,scale可能是縮小的比例也可能是放大的比例,看它的值是大於1還是小於1
106             float scale = 1.0f;
107             
108             //如果僅僅是圖片寬度比view寬度大,則應該將圖片按寬度縮小
109             if(dw>width&&dh<height)
110             {
111                 scale = width*1.0f/dw;
112             }
113             //如果圖片和高度都比view的大,則應該按最小的比例縮小圖片
114             if(dw>width&&dh>height)
115             {
116                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
117             }
118             //如果圖片寬度和高度都比view的要小,則應該按最小的比例放大圖片
119             if(dw<width&&dh<height)
120             {
121                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
122             }
123             //如果僅僅是高度比view的大,則按照高度縮小圖片即可
124             if(dw<width&&dh>height)
125             {
126                 scale = height*1.0f/dh;
127             }
128             
129             //初始化縮放的比例
130             initScale = scale;
131             midScale = initScale*2;
132             maxScale = initScale*4;
133             
134             //移動圖片到達view的中心
135             int dx = width/2 - dw/2;
136             int dy = height/2 - dh/2;
137             scaleMatrix.postTranslate(dx, dy);
138             
139             //縮放圖片
140             scaleMatrix.postScale(initScale, initScale, width/2, height/2);    
141             
142             setImageMatrix(scaleMatrix);
143             mOnce = true;
144         }
145         
146     }
147 
148 }

 

      代碼解釋:多了些成員變量,大家先敲上即可,以后自會知道他們什么作用。在這里,使用OnGlobalLayoutListener來監聽ZoomImageView的狀態改變,,注意在ZoomImageView的onDraw方法執行之前,會調用onGlobalLayout方法。因此保證了在圖片被畫出來之前,就已經實現了對其顯示位置的控制。因為OnGlobalLayoutListener監聽的是view狀態的改變,因此可能會被多出調用,所以需要一個布爾型變量mOnce來控制,onGlobalLayout中的代碼只能執行一次,而且在onDetachedFromWindow的時候要取消監聽。而onGrobalLayout中的代碼,就是根據前面的分析所寫出來的。注意在縮放的時候,有多種情況需要考慮。關於第129行到132行,是保存縮放比例。因為縮放圖片不可能無限放大,應該有一個縮放的范圍。這里,取的是initScale~maxScale。而midScale算是中間的一個臨界點,后面會用到。代碼的注釋很詳細了,其他的就不再多解釋了。

 

      下面我們就來看一看圖片的位置是否按照代碼中預設的擺正了。修改activity_main.xml,如下:

 1 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:layout_width="match_parent"
 4     android:layout_height="match_parent"
 5    >
 6    
 7     <com.example.view.ZoomImageView
 8         android:id="@+id/img_ziv"
 9         android:layout_width="match_parent"
10         android:layout_height="match_parent"
11         android:scaleType="matrix"
12         android:src="@drawable/mingxing0403">"
13         
14     </com.example.view.ZoomImageView>
15 
16 
17 </RelativeLayout>

       在布局中,放了一張圖片在ZoomImageView中,MainActivity中默認已經加載 了這個布局,不必修改。現在運行下程序,效果如下:

     ok,圖片的顯示這部分已經完成了。下面開始編寫自由縮放的代碼。

(2)圖片的自由縮放

      自由縮放是通過ScaleGestureDetector這個類來實現的。需要將手指觸摸的事件傳遞給它,然后對手指移動進行監控,獲取縮放因子才能實現縮放功能。大體邏輯就是:首先獲取到圖片心當前的縮放比例,然后根據縮放因子,判斷是否超出了我們允許的縮放范圍,如果不在允許的縮放范圍內,就禁止縮放,否則就允許縮放。先看代碼,再詳細的解釋吧。修改ZoomImageView,如下:

  1 package com.example.view;
  2 
  3 import android.annotation.SuppressLint;
  4 import android.content.Context;
  5 import android.graphics.Matrix;
  6 import android.graphics.drawable.Drawable;
  7 import android.util.AttributeSet;
  8 import android.util.Log;
  9 import android.view.MotionEvent;
 10 import android.view.ScaleGestureDetector;
 11 import android.view.ScaleGestureDetector.OnScaleGestureListener;
 12 import android.view.View;
 13 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
 14 import android.view.View.OnTouchListener;
 15 import android.widget.ImageView;
 16 
 17 public class ZoomImageView extends ImageView implements OnGlobalLayoutListener, 
 18 OnScaleGestureListener, OnTouchListener
 19 {
 20     private boolean mOnce = false;//是否執行了一次
 21     
 22     /**
 23      * 初始縮放的比例
 24      */
 25     private float initScale;
 26     /**
 27      * 縮放比例
 28      */
 29     private float midScale;
 30     /**
 31      * 可放大的最大比例
 32      */
 33     private float maxScale;
 34     /**
 35      * 縮放矩陣
 36      */
 37     private Matrix scaleMatrix;
 38     
 39     /**
 40      * 縮放的手勢監控類
 41      */
 42     private ScaleGestureDetector mScaleGestureDetector;
 43 
 44     public ZoomImageView(Context context)
 45     {
 46         this(context,null);
 47     }
 48     public ZoomImageView(Context context, AttributeSet attrs) 
 49     {
 50         this(context, attrs,0);
 51 
 52     }
 53     public ZoomImageView(Context context, AttributeSet attrs, int defStyle)
 54     {
 55         super(context, attrs, defStyle);
 56         
 57         scaleMatrix = new Matrix();
 58         
 59         setScaleType(ScaleType.MATRIX);
 60         
 61         mScaleGestureDetector = new ScaleGestureDetector(context, this);
 62         //觸摸回調
 63         setOnTouchListener(this);
 64         
 65     }
 66     
 67     /**
 68      * 該方法在view與window綁定時被調用,且只會被調用一次,其在view的onDraw方法之前調用
 69      */
 70     protected void onAttachedToWindow()
 71     {
 72         super.onAttachedToWindow();
 73         //注冊監聽器
 74         getViewTreeObserver().addOnGlobalLayoutListener(this);
 75     }
 76     
 77     /**
 78      * 該方法在view被銷毀時被調用
 79      */
 80     @SuppressLint("NewApi") protected void onDetachedFromWindow() 
 81     {
 82         super.onDetachedFromWindow();
 83         //取消監聽器
 84         getViewTreeObserver().removeOnGlobalLayoutListener(this);
 85     }
 86     
 87     /**
 88      * 當一個view的布局加載完成或者布局發生改變時,OnGlobalLayoutListener會監聽到,調用該方法
 89      * 因此該方法可能會被多次調用,需要在合適的地方注冊和取消監聽器
 90      */
 91     public void onGlobalLayout() 
 92     {
 93         if(!mOnce)
 94         {
 95             //獲得當前view的Drawable
 96             Drawable d = getDrawable();
 97             
 98             if(d == null)
 99             {
100                 return;
101             }
102             
103             //獲得Drawable的寬和高
104             int dw = d.getIntrinsicWidth();
105             int dh = d.getIntrinsicHeight();
106             
107             //獲取當前view的寬和高
108             int width = getWidth();
109             int height = getHeight();
110             
111             //縮放的比例,scale可能是縮小的比例也可能是放大的比例,看它的值是大於1還是小於1
112             float scale = 1.0f;
113             
114             //如果僅僅是圖片寬度比view寬度大,則應該將圖片按寬度縮小
115             if(dw>width&&dh<height)
116             {
117                 scale = width*1.0f/dw;
118             }
119             //如果圖片和高度都比view的大,則應該按最小的比例縮小圖片
120             if(dw>width&&dh>height)
121             {
122                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
123             }
124             //如果圖片寬度和高度都比view的要小,則應該按最小的比例放大圖片
125             if(dw<width&&dh<height)
126             {
127                 scale = Math.min(width*1.0f/dw, height*1.0f/dh);
128             }
129             //如果僅僅是高度比view的大,則按照高度縮小圖片即可
130             if(dw<width&&dh>height)
131             {
132                 scale = height*1.0f/dh;
133             }
134             
135             //初始化縮放的比例
136             initScale = scale;
137             midScale = initScale*2;
138             maxScale = initScale*4;
139             
140             //移動圖片到達view的中心
141             int dx = width/2 - dw/2;
142             int dy = height/2 - dh/2;
143             scaleMatrix.postTranslate(dx, dy);
144             
145             //縮放圖片
146             scaleMatrix.postScale(initScale, initScale, width/2, height/2);    
147             
148             setImageMatrix(scaleMatrix);
149             mOnce = true;
150         }
151         
152     }
153     /**
154      * 獲取當前已經縮放的比例
155      * @return  因為x方向和y方向比例相同,所以只返回x方向的縮放比例即可
156      */
157     private float getDrawableScale()
158     {
159         
160         float[] values = new float[9];
161         scaleMatrix.getValues(values);
162         
163         return values[Matrix.MSCALE_X];
164         
165     }
166 
167     /**
168      * 縮放手勢進行時調用該方法
169      * 
170      * 縮放范圍:initScale~maxScale
171      */
172     public boolean onScale(ScaleGestureDetector detector)
173     {
174         
175         if(getDrawable() == null)
176         {
177             return true;//如果沒有圖片,下面的代碼沒有必要運行
178         }
179         
180         float scale = getDrawableScale();
181         //獲取當前縮放因子
182         float scaleFactor = detector.getScaleFactor();
183         
184         if((scale<maxScale&&scaleFactor>1.0f)||(scale>initScale&&scaleFactor<1.0f))
185         {
186             //如果縮小的范圍比允許的最小范圍還要小,就重置縮放因子為當前的狀態的因子
187             if(scale*scaleFactor<initScale&&scaleFactor<1.0f)
188             {
189                 scaleFactor = initScale/scale;
190             }
191             //如果縮小的范圍比允許的最小范圍還要小,就重置縮放因子為當前的狀態的因子
192             if(scale*scaleFactor>maxScale&&scaleFactor>1.0f)
193             {
194                 scaleFactor = maxScale/scale;
195             }
196             
197 //            scaleMatrix.postScale(scaleFactor, scaleFactor, getWidth()/2, getHeight()/2);
198             scaleMatrix.postScale(scaleFactor, scaleFactor,detector.getFocusX(), 
199                     detector.getFocusY());
200             
201             
202             setImageMatrix(scaleMatrix);//千萬不要忘記設置這個,我總是忘記
203         }
204         
205         
206     
207         return true;
208     }
209     /**
210      * 縮放手勢開始時調用該方法
211      */
212     public boolean onScaleBegin(ScaleGestureDetector detector) 
213     {    
214         //返回為true,則縮放手勢事件往下進行,否則到此為止,即不會執行onScale和onScaleEnd方法
215         return true;
216     }
217     /**
218      * 縮放手勢完成后調用該方法
219      */
220     public void onScaleEnd(ScaleGestureDetector detector)
221     {
222         
223         
224     }
225 
226     /**
227      * 監聽觸摸事件
228      */
229     public boolean onTouch(View v, MotionEvent event)
230     {
231     
232         if(mScaleGestureDetector != null)
233         {
234             //將觸摸事件傳遞給手勢縮放這個類
235             mScaleGestureDetector.onTouchEvent(event);
236         }
237         return true;
238     }
239 
240 }

 

       紅色部分是我們增加的代碼。從代碼中可以看到,ZoomImageView實現了OnScaleGestureListener和OnTouchListener接口。在第229行onTouch方法中,將觸摸事件傳遞給了mScaleGestureDetector 。在mScaleGestureDetector 的onScale方法中,對縮放邏輯進行了編寫。首先使用getDrawableScale獲取到當前縮放比例,然后再獲取到縮放因子。然后進行判斷:如果當前縮放比例比最大比例小,且縮放因子大於1,說明想放大,這是被允許的,因為還還可以再放大。如果當前縮放比例比最小比例大,且縮放因子小於1,說明想縮小,這也是被允許的。利用Matrix的postScale方法設置縮放即可。其中的邏輯還是比較簡單的。其他就沒什么好解釋的了,注釋很清晰。那么效果如何呢?達到預期了嗎?運行程序,效果如下:

 

      由於多點觸控在模擬上沒法演示,因此zheli仍舊是一個手機上錄制的gif。在真機上,我是用兩根手指進行縮放的。可以看到,縮放的的效果是實現了。但是問題也出現了。什么問題呢?即縮放完成后,圖片的位置不再居中了,與屏幕之間有了空隙。我們想要的顯然不是這樣子的效果。我們需要縮放完成后,圖片位置不移動,而且縮放過程中,不允許與屏幕有空白間隙。那么怎么解決這個問題呢??限於篇幅,就放在下一篇文章中吧。如果你還接着往下做的話,請保存好代碼,看下一篇文章《(二)彌補圖片自由縮放出現的間隙》。點擊下面的鏈接即可:
http://www.cnblogs.com/fuly550871915/p/4939954.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM