Android多点触控之ZoomImageView完全解析
ZoomImageView是一個(gè)類(lèi)似photoview的圖片預(yù)覽控件,實(shí)現(xiàn)了對(duì)圖片的手勢(shì)放大縮小平移,以及雙擊放大縮小解決和viewpager滑動(dòng)沖突等功能,主要是通過(guò)GestureDetector,Matrix相關(guān)api以及對(duì)事件的ontouch處理實(shí)現(xiàn)的,這個(gè)代碼網(wǎng)上能搜索到很多,但是因?yàn)樽⑨屔偎砸耆x懂有些困難,于是本人整理了一段時(shí)間,寫(xiě)了一個(gè)完整的注釋版本,基本每行代碼都做到了講解,大家共同學(xué)習(xí)。
public class ZoomImageView extends ImageView implements ViewTreeObserver.OnGlobalLayoutListener,ScaleGestureDetector.OnScaleGestureListener,View.OnTouchListener{@SuppressWarnings("unused")private static final String TAG = "ZoomImageView";/*** 最大放大倍數(shù)*/public static final float mMaxScale = 4.0f;/*** 默認(rèn)縮放*/private float mInitScale = 1.0f;/*** 雙擊放大比例*/private float mMidScale=2.0f;/*** 檢測(cè)縮放手勢(shì) 多點(diǎn)觸控手勢(shì)識(shí)別 獨(dú)立的類(lèi)不是GestureDetector的子類(lèi)*/ScaleGestureDetector mScaleGestureDetector = null;//檢測(cè)縮放的手勢(shì)/***檢測(cè)類(lèi)似長(zhǎng)按啊 輕按啊 拖動(dòng) 快速滑動(dòng) 雙擊啊等等 OnTouch方法雖然也可以* 但是對(duì)于一些復(fù)雜的手勢(shì)需求自己去通過(guò)軌跡時(shí)間等等判斷很復(fù)雜,因此我們采用系統(tǒng)* 提供的手勢(shì)類(lèi)進(jìn)行處理*/private GestureDetector mGestureDetector;/*** 如果正在縮放中就不向下執(zhí)行,防止多次雙擊*/private boolean mIsAutoScaling;/*** Matrix的對(duì)圖像的處理* Translate 平移變換* Rotate 旋轉(zhuǎn)變換* Scale 縮放變換* Skew 錯(cuò)切變換*/Matrix mScaleMatrix = new Matrix();/*** 處理矩陣的9個(gè)值*/float[] mMartixValue = new float[9];public ZoomImageView(Context context) {this(context, null);}public ZoomImageView(Context context, AttributeSet attrs) {this(context, attrs, 0);}public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);setScaleType(ScaleType.MATRIX);mScaleGestureDetector = new ScaleGestureDetector(context, this);this.setOnTouchListener(this); //縮放的捕獲要建立在setOnTouchListener上//符合滑動(dòng)的距離 它獲得的是觸發(fā)移動(dòng)事件的最短距離,如果小于這個(gè)距離就不觸發(fā)移動(dòng)控件,//如viewpager就是用這個(gè)距離來(lái)判斷用戶(hù)是否翻頁(yè)mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();//監(jiān)聽(tīng)雙擊事件 SimpleOnGestureListener是OnGestureListener接口實(shí)現(xiàn)類(lèi),//使用這個(gè)復(fù)寫(xiě)需要的方法就可以不用復(fù)寫(xiě)所有的方法mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {@Overridepublic boolean onDoubleTap(MotionEvent e) {//如果正在縮放中就不向下執(zhí)行,防止多次雙擊if (mIsAutoScaling) {return true;}//縮放的中心點(diǎn)float x = e.getX();float y = e.getY();//如果當(dāng)前縮放值小于這個(gè)臨界值 則進(jìn)行放大if (getScale() < mMidScale) {mIsAutoScaling = true;//view中的方法 已x,y為坐標(biāo)點(diǎn)放大到mMidScale 延時(shí)10mspostDelayed(new AutoScaleRunble(mMidScale, x, y), 16);} else {//如果當(dāng)前縮放值大于這個(gè)臨界值 則進(jìn)行縮小操作 縮小到mInitScalemIsAutoScaling = true;postDelayed(new AutoScaleRunble(mInitScale, x, y), 16);}return true;}});}@Overrideprotected void onAttachedToWindow() {super.onAttachedToWindow();getViewTreeObserver().addOnGlobalLayoutListener(this);}//suppress deprecate warning because i have dealt with it@Override@SuppressWarnings("deprecation")protected void onDetachedFromWindow() {super.onDetachedFromWindow();if (Build.VERSION.SDK_INT > Build.VERSION_CODES.JELLY_BEAN) {getViewTreeObserver().removeOnGlobalLayoutListener(this);}getViewTreeObserver().removeGlobalOnLayoutListener(this);}//--------------------------implement OnTouchListener----------------------------//***處理現(xiàn)圖片放大后移動(dòng)查看*/private int mLastPointCount;//觸摸點(diǎn)發(fā)生移動(dòng)時(shí)的觸摸點(diǎn)個(gè)數(shù)private boolean isCanDrag;//判斷是否可以拖拽private float mLatX;//記錄移動(dòng)之前按下去的那個(gè)坐標(biāo)點(diǎn)private float mLastY;private int mTouchSlop;//系統(tǒng)默認(rèn)觸發(fā)移動(dòng)事件的最短距離private boolean isCheckTopAndBottom;//是否可以上下拖動(dòng)private boolean isCheckLeftAndRight;//是否可以左右拖動(dòng)@Overridepublic boolean onTouch(View v, MotionEvent event) {//雙擊事件進(jìn)行關(guān)聯(lián)if (mGestureDetector.onTouchEvent(event)) {//如果是雙擊的話(huà)就直接不向下執(zhí)行了return true;}//將事件傳遞給ScaleGestureDetectormScaleGestureDetector.onTouchEvent(event);float x = 0;float y = 0;//可能出現(xiàn)多手指觸摸的情況 ACTION_DOWN事件只能執(zhí)行一次所以多點(diǎn)觸控不能在down事件里面處理int pointerCount = event.getPointerCount();for (int i = 0; i < pointerCount; i++) {x += event.getX(i);y += event.getY(i);}//取平均值,得到的就是多點(diǎn)觸控后產(chǎn)生的那個(gè)點(diǎn)的坐標(biāo)x /= pointerCount;y /= pointerCount;//每當(dāng)觸摸點(diǎn)發(fā)生移動(dòng)時(shí)(從靜止到移動(dòng)),重置mLasX , mLastY mLastPointCount防止再次進(jìn)入if (mLastPointCount != pointerCount) {//這里加一個(gè)參數(shù)并且設(shè)置成false的目的是,要判斷位移的距離是否符合觸發(fā)移動(dòng)事件的最短距離isCanDrag = false;//記錄移動(dòng)之前按下去的那個(gè)坐標(biāo)點(diǎn),記錄的值類(lèi)似于斷點(diǎn)續(xù)移,下次移動(dòng)的時(shí)候從這個(gè)點(diǎn)開(kāi)始mLatX = x;mLastY = y;}//重新賦值 說(shuō)明如果是一些列連續(xù)滑動(dòng)的操作就不會(huì)再次進(jìn)入上面的判斷 否則會(huì)重新確定坐標(biāo)移動(dòng)原點(diǎn)mLastPointCount = pointerCount;RectF rectF = getMatrixRectF();switch (event.getAction()) {case MotionEvent.ACTION_DOWN://按下的時(shí)候如果發(fā)現(xiàn)圖片縮放寬或者高大于屏幕寬高則請(qǐng)求viewpager不攔截事件交給ZoomImageView處理//ZoomImageView可以進(jìn)行縮放操作if (rectF.width() > getWidth() || rectF.height() > getHeight()){getParent().requestDisallowInterceptTouchEvent(true);}break;case MotionEvent.ACTION_MOVE://按下的時(shí)候如果發(fā)現(xiàn)圖片縮放寬或者高大于屏幕寬高則請(qǐng)求viewpager不攔截事件交給ZoomImageView處理//ZoomImageView可以進(jìn)行縮放操作if (rectF.width() > getWidth() || rectF.height() > getHeight()){getParent().requestDisallowInterceptTouchEvent(true);}//x,y移動(dòng)的距離float dx = x - mLatX;float dy = y - mLastY;//如果是不能拖拽,可能是因?yàn)槭种缸兓?這時(shí)就去重新檢測(cè)看看是不是符合滑動(dòng)if (!isCanDrag) {//反正是根據(jù)勾股定理,調(diào)用系統(tǒng)APIisCanDrag = isMoveAction(dx, dy);Log.e(TAG, "移動(dòng)3---->" + pointerCount);}if (isCanDrag) {if (getDrawable() != null) {//判斷是寬或者高小于屏幕,就不在那個(gè)方向進(jìn)行拖拽isCheckLeftAndRight = isCheckTopAndBottom = true;if (rectF.width() < getWidth()) {//如果圖片寬度小于控件寬度isCheckLeftAndRight = false;dx = 0;}if (rectF.height() < getHeight()) { //如果圖片的高度小于控件的高度isCheckTopAndBottom = false;dy = 0;}mScaleMatrix.postTranslate(dx, dy);//解決拖拽的時(shí)候左右 上下都會(huì)出現(xiàn)留白的情況checkBorderAndCenterWhenTranslate();setImageMatrix(mScaleMatrix);}}mLatX = x;//記錄的值類(lèi)似于斷點(diǎn)續(xù)移,下次移動(dòng)的時(shí)候從這個(gè)點(diǎn)開(kāi)始mLastY = y;break;case MotionEvent.ACTION_UP:case MotionEvent.ACTION_CANCEL:mLastPointCount = 0;//抬起或者取消事件時(shí)候把這個(gè)置空break;}return true;}//----------------------手勢(shì)implement OnScaleGestureListener------------------------///***處理圖片縮放*/@Overridepublic boolean onScale(ScaleGestureDetector detector) {float scale = getScale();//當(dāng)前相對(duì)于初始尺寸的縮放(之前matrix中獲得)Log.e(TAG, "matrix scale---->" + scale);float scaleFactor = detector.getScaleFactor();//這個(gè)時(shí)刻縮放的/當(dāng)前縮放尺度 (現(xiàn)在手勢(shì)獲取)Log.e(TAG, "scaleFactor---->" + scaleFactor);if (getDrawable() == null)return true;if ((scale < mMaxScale && scaleFactor > 1.0f) //放大|| (scale > mInitScale && scaleFactor < 1.0f)) {//縮小//如果要縮放的值比初始化還要小的話(huà),就按照最小可以縮放的值進(jìn)行縮放if (scaleFactor * scale < mInitScale){scaleFactor = mInitScale / scale;Log.e(TAG, "進(jìn)來(lái)了1" + scaleFactor);}///如果要縮放的值比最大縮放值還要大,就按照最大可以縮放的值進(jìn)行縮放if (scaleFactor * scale > mMaxScale){scaleFactor = mMaxScale / scale;Log.e(TAG, "進(jìn)來(lái)了2---->" + scaleFactor);}Log.e(TAG, "scaleFactor2---->" + scaleFactor);//設(shè)置縮放比例mScaleMatrix.postScale(scaleFactor, scaleFactor,detector.getFocusX(), detector.getFocusY());//縮放中心是兩手指之間checkBorderAndCenterWhenScale();//解決這種縮放導(dǎo)致縮放到最小時(shí)圖片位置可能發(fā)生了變化// mScaleMatrix.postScale(scaleFactor, scaleFactor, // getWidth() / 2, getHeight() / 2);//縮放中心是屏幕中心點(diǎn)setImageMatrix(mScaleMatrix);//通過(guò)手勢(shì)給圖片設(shè)置縮放}//返回值代表本次縮放事件是否已被處理。如果已被處理,那么detector就會(huì)重置縮放事件;// 如果未被處理,detector會(huì)繼續(xù)進(jìn)行計(jì)算,修改getScaleFactor()的返回值,直到被處理為止。// 因此,它常用在判斷只有縮放值達(dá)到一定數(shù)值時(shí)才進(jìn)行縮放return true;}@Overridepublic boolean onScaleBegin(ScaleGestureDetector detector) {//縮放開(kāi)始一定要返回true該detector是否處理后繼的縮放事件。返回false時(shí),不會(huì)執(zhí)行onScale()return true;}@Overridepublic void onScaleEnd(ScaleGestureDetector detector) {//縮放結(jié)束時(shí)}boolean once = true;/***圖片初始化其大小 必須在onAttachedToWindow方法后才能獲取寬高*/@Overridepublic void onGlobalLayout() {if (!once)return;Drawable d = getDrawable();if (d == null)return;//獲取imageview寬高int width = getWidth();int height = getHeight();//獲取圖片寬高int imgWidth = d.getIntrinsicWidth();int imgHeight = d.getIntrinsicHeight();float scale = 1.0f;//如果圖片的寬或高大于屏幕,縮放至屏幕的寬或者高if (imgWidth > width && imgHeight <= height)scale = (float) width / imgWidth;if (imgHeight > height && imgWidth <= width)scale = (float) height / imgHeight;//如果圖片寬高都大于屏幕,按比例縮小if (imgWidth > width && imgHeight > height)scale = Math.min((float) imgWidth / width, (float) imgHeight / height);mInitScale = scale;//將圖片移動(dòng)至屏幕中心mScaleMatrix.postTranslate((width - imgWidth) / 2, (height - imgHeight) / 2);mScaleMatrix.postScale(scale, scale, getWidth() / 2, getHeight() / 2);setImageMatrix(mScaleMatrix);once = false;}/*** 獲取當(dāng)前縮放比例*/public float getScale() {//Matrix為一個(gè)3*3的矩陣,一共9個(gè)值,復(fù)制到這個(gè)數(shù)組當(dāng)中mScaleMatrix.getValues(mMartixValue);return mMartixValue[Matrix.MSCALE_X];//取出圖片寬度的縮放比例}/*** 在縮放時(shí),解決上下左右留白的情況*/private void checkBorderAndCenterWhenScale() {RectF rectF = getMatrixRectF();float deltaX = 0;float deltaY = 0;int width = getWidth();int height = getHeight();// 如果寬或高大于屏幕,則控制范圍if (rectF.width() >= width) {if (rectF.left > 0) {deltaX = -rectF.left;//獲取坐標(biāo)留白的距離Log.e(TAG, "寬有問(wèn)題1---->" +rectF.width()+"--"+rectF.left+"--"+width);}if (rectF.right < width) {//屏幕寬-屏幕已經(jīng)占據(jù)的大小 得到右邊留白的寬度deltaX = width - rectF.right;Log.e(TAG, "寬有問(wèn)題2---->" +rectF.width()+"--"+rectF.left+"--"+width);}}if (rectF.height() >= height) {if (rectF.top > 0) {deltaY = -rectF.top;//同上,獲取上面留白的距離}if (rectF.bottom < height) {//同上 獲取下面留白的距離deltaY = height - rectF.bottom;}}// 如果寬或高小于屏幕,則讓其居中if (rectF.width() < width) {//圖片的中心點(diǎn)距離屏幕的中心點(diǎn)距離計(jì)算(畫(huà)個(gè)圖很明了)deltaX = width * 0.5f - rectF.right + 0.5f * rectF.width();Log.e(TAG, "寬有問(wèn)題3---->" +rectF.width()+"--"+rectF.right+"結(jié)果"+deltaX);}if (rectF.height() < height) {deltaY = height * 0.5f - rectF.bottom + 0.5f * rectF.height();Log.e(TAG, "高有問(wèn)題4---->" +rectF.height()+"--"+rectF.bottom+"結(jié)果"+deltaY);}mScaleMatrix.postTranslate(deltaX, deltaY);}/*** 獲得圖片放大縮小以后的寬和高,以及l(fā),r,t,b*/private RectF getMatrixRectF() {Matrix rMatrix = mScaleMatrix;//獲得當(dāng)前圖片的矩陣RectF rectF = new RectF();//創(chuàng)建一個(gè)空矩形Drawable d = getDrawable();if (d != null) {//使這個(gè)矩形的寬和高同當(dāng)前圖片一致//設(shè)置坐標(biāo)位置(l和r是左邊矩形的坐標(biāo)點(diǎn) tb是右邊矩形的坐標(biāo)點(diǎn) lr設(shè)置為0就是設(shè)置為原寬高)rectF.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());//將矩陣映射到矩形上面,之后我們可以通過(guò)獲取到矩陣的上下左右坐標(biāo)以及寬高//來(lái)得到縮放后圖片的上下左右坐標(biāo)和寬高rMatrix.mapRect(rectF);//把坐標(biāo)位置放入矩陣}return rectF;}/***判斷是否可以拖動(dòng)*/private boolean isMoveAction(float dx, float dy) {return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;}/*** 放大移動(dòng)的過(guò)程中解決上下左右留白的情況*/private void checkBorderAndCenterWhenTranslate() {RectF rectF = getMatrixRectF();float deltax = 0;float deltay = 0;int width = getWidth();int height = getHeight();//可以上下拖動(dòng)且距離屏幕上方留白 根據(jù)Android系統(tǒng)坐標(biāo)系往上移動(dòng)的值要取負(fù)值if (rectF.top > 0 && isCheckTopAndBottom) {deltay = -rectF.top;Log.e(TAG, "上面留白距離---->" +rectF.top);}//可以上下拖動(dòng)且距離屏幕底部留白 根據(jù)Android系統(tǒng)坐標(biāo)系往下移動(dòng)的值要取正值if (rectF.bottom < height && isCheckTopAndBottom) {deltay = height - rectF.bottom;Log.e(TAG, "下面留白距離---->" +rectF.bottom);}//可以左右拖動(dòng)且左邊留白 根據(jù)Android系統(tǒng)坐標(biāo)系往左移動(dòng)的值要取負(fù)值if (rectF.left > 0 && isCheckLeftAndRight) {deltax = -rectF.left;Log.e(TAG, "左邊留白距離---->" +rectF.left);}//可以左右拖動(dòng)且右邊留白 根據(jù)Android系統(tǒng)坐標(biāo)系往右移動(dòng)的值要取正值if (rectF.right < width && isCheckLeftAndRight) {deltax = width - rectF.right;Log.e(TAG, "右邊留白距離---->" +rectF.right);}mScaleMatrix.postTranslate(deltax, deltay);//處理偏移量}/*** View.postDelay()方法延時(shí)執(zhí)行雙擊放大縮小 在主線(xiàn)程中運(yùn)行 沒(méi)隔16ms給用戶(hù)產(chǎn)生過(guò)渡的效果的*/private class AutoScaleRunble implements Runnable {private float mTrgetScale;//縮放目標(biāo)值private float x;//縮放中心點(diǎn)private float y;private float tempScale;//可能是BIGGER可能是SMALLERprivate float BIGGER = 1.07f;private float SMALLER = 0.93f;//構(gòu)造傳入縮放目標(biāo)值,縮放的中心點(diǎn)public AutoScaleRunble(float mTrgetScale, float x, float y) {this.mTrgetScale = mTrgetScale;this.x = x;this.y = y;if (getScale() < mTrgetScale) {//雙擊放大//這個(gè)縮放比1f大就行 隨便取個(gè)1.07tempScale = BIGGER;}if (getScale() > mTrgetScale) {//雙擊縮小//這個(gè)縮放比1f小就行 隨便取個(gè)0.93tempScale = SMALLER;}}@Overridepublic void run() {//執(zhí)行縮放mScaleMatrix.postScale(tempScale, tempScale, x, y);//在縮放時(shí),解決上下左右留白的情況checkBorderAndCenterWhenScale();setImageMatrix(mScaleMatrix);//獲取當(dāng)前的縮放值float currentScale = getScale();//如果當(dāng)前正在放大操作并且當(dāng)前的放大尺度小于縮放的目標(biāo)值,或者正在縮小并且縮小的尺度大于目標(biāo)值//則再次延時(shí)16ms遞歸調(diào)用直到縮放到目標(biāo)值if ((tempScale > 1.0f && currentScale < mTrgetScale) || (tempScale < 1.0f && currentScale > mTrgetScale)) {postDelayed(this, 16);} else {//代碼走到這兒來(lái)說(shuō)明不能再進(jìn)行縮放了,可能放大的尺寸超過(guò)了mTrgetScale,//也可能縮小的尺寸小于mTrgetScale//所以這里我們mTrgetScale / currentScale 用目標(biāo)縮放尺寸除以當(dāng)前的縮放尺寸//得到縮放比,重新執(zhí)行縮放到//mMidScale或者mInitScalefloat scale = mTrgetScale / currentScale;mScaleMatrix.postScale(scale, scale, x, y);checkBorderAndCenterWhenScale();setImageMatrix(mScaleMatrix);//執(zhí)行完成后重置mIsAutoScaling = false;}}} }看完估計(jì)你也一臉懵逼了,這么多從哪兒開(kāi)始入口看,那我們就從最基礎(chǔ)的功能開(kāi)始看,首先第一步我們要實(shí)現(xiàn)圖片的手勢(shì)放大縮小:我們寫(xiě)在構(gòu)造方法中初始化ScaleGestureDetector這是一個(gè)專(zhuān)門(mén)處理多點(diǎn)觸控點(diǎn)類(lèi),接著onAttachedToWindow()方法對(duì)view的布局初始化進(jìn)行監(jiān)聽(tīng)在其回調(diào)方法當(dāng)中我們才可以在onGlobalLayout()方法中獲取view的寬高并對(duì)圖片固定住初始大小。第二步我們OnScaleGestureListener的回調(diào)方法對(duì)圖片的寬高進(jìn)行處理,調(diào)用mScaleMatrix.postScale實(shí)現(xiàn)圖片的手勢(shì)放大縮小。上述兩步就實(shí)現(xiàn)了圖片的手勢(shì)放大縮小功能。
接著我們看進(jìn)階功能,同理第一步我們?cè)趏nTouch方法中拿到觸摸點(diǎn)的數(shù)量,計(jì)算觸摸點(diǎn)的平均值,然后在移動(dòng)的時(shí)候,得到dx ,dy 進(jìn)行范圍檢查以后,調(diào)用mScaleMatrix.postTranslate進(jìn)行設(shè)置偏移量完成圖片放大后自由的移動(dòng),第二步我們還是在構(gòu)造方法當(dāng)中初始化GestureDetector這是一個(gè)我們用來(lái)監(jiān)聽(tīng)雙擊手勢(shì)的類(lèi),設(shè)置雙擊手勢(shì)判斷的臨界值mMidScale,并利用postDelayed執(zhí)行一個(gè)Runnable給用戶(hù)一個(gè)過(guò)渡時(shí)間差,Runnable中再次根據(jù)的當(dāng)前的縮放值繼續(xù)執(zhí)行。
最后我們考慮到和viewpager一起使用會(huì)有滑動(dòng)沖突問(wèn)題,所以我們要解決這個(gè),主要看onTouch方法當(dāng)我們圖片的寬或高大于屏幕寬或高時(shí),因?yàn)榇藭r(shí)可以移動(dòng),我們不想被攔截。交給ZoomImageView處理:
getParent().requestDisallowInterceptTouchEvent(true);執(zhí)行了這段代碼viewpager便不攔截事件,直接交給ZoomImageView處理事件,便也不會(huì)走onInterceptTouchEvent方法對(duì)事件進(jìn)行攔截處理,比我們通過(guò)onInterceptTouchEvent方法來(lái)攔截事件處理的簡(jiǎn)單多了。
啰啰嗦嗦的一堆,我們看下效果圖:
好了,還有重要的一點(diǎn)就是使用的時(shí)候設(shè)置的是src而不是bac,上面提到的”留白”自己可以去實(shí)踐下才能理解,如果你對(duì)上述代碼還有什么疑問(wèn),請(qǐng)留言盡管問(wèn),哈哈。
總結(jié)
以上是生活随笔為你收集整理的Android多点触控之ZoomImageView完全解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: R语言特征提取与特征选择
- 下一篇: 【知识积累】腾讯云CentOS 7服务器