http://files.cnblogs.com/files/liaolandemengxiang/PhotoWallFallsDemo.rar
http://files.cnblogs.com/files/liaolandemengxiang/ViewPager_imageview%E7%9A%84%E7%BC%A9%E6%94%BE%E4%BC%98%E5%8C%96%E5%90%8E.rar
第一個地址是中心縮放的demo,值得注意的是,這里面的位移是相對於(0,0)位置的偏移量,每次都是畫布重新畫圖片
第二個是優化了以后的viewpager 和zoomimageview的組合的demo,是通過imageview的回調函數還控制viewpager是否可以縮放。並對原來的bug進行修改。bug主要的問題是存在於沒有考慮高度大於屏幕高度的長圖片的縮放,已經中心位置的計算錯誤。還有就是對於最大縮放倍數的理解,初始化縮放比例是X,可是視為1,最大縮放倍數應該是X的倍數。
引自http://www.cnblogs.com/linjzong/p/4212474.html
在上一篇Android:手把手教你打造可縮放移動的ImageView最后提出了一個注意點:當自定義的MatrixImageView如ViewPager、ListView等帶有滑動效果的ViewGroup中時,ImageView自定義的拖動事件會和ViewGroup的滑動事件沖突,並且指出了沖突原因是由於ViewGroup攔截了Move事件的緣故。如果對於Touch事件的分發機制不甚了解的話,可以參考下這篇Android:30分鍾弄明白Touch事件分發機制。
這篇文章將會在MatrixImageView的基礎上,以ViewPager作為測試容器做進一步優化。
實現功能
- 當進行縮放操作時,手勢不會同時觸發ViewPager的滑動切換Item事件。
- 當進行拖動操作時,除非圖片已經到達左右邊界,否則不觸發ViewPager的滑動切換Item事件。
- 當進行拖動操作時,若圖片左邊緣到達左邊界,則可以向左滑動觸發ViewPager切換至前一個Item;當圖片右邊緣到達右邊界,則可以向右滑動觸發ViewPager的切換至后一個Item。
- 每個down-up(cancel)事件周期內只執行一種類型的事務操作(縮放、拖動或者ViewPager切換Item),防止多重事務互相干擾。
- 將事務處理封裝到MatrixImageView類內,提供狀態接口給ViewPager使用,方便適配多種ViewGroup。
實現原理
當ViewPager內嵌MatrixImageView時,由於MatrixImgaeView在Down事件中返回了true,表明ViewPager將捕獲本次完整的Touch事件(Move-Ponit_Down-UP等等),其中最重要的一個事件便是Move事件,因為ViewPager自身需要捕獲Move事件在onTouch中進行切換Item操作,MatrixImageView的捕獲意味着它將無法響應。不過,ViewPager本身控制着Touch事件的下發操作,每個Touch事件的下發都遵從從上至下層層遞歸,在MatrixImageView真正獲得Move事件前,Move事件必須經過ViewPager的onInterceptTouchEvent和dispatchTouchEvent事件,前者執行攔截操作后者執行下發操作。ViewPager便是在onInterceptTouchEvent中對Move事件進行了過濾,當移動距離超過一定值時,它會攔截掉Move事件,阻止MatrixImageView繼續處理Touch事件的權利,轉而讓自身的onTouch事件處理。於是,我們要做的便是重寫onInterceptTouchEvent事件,通過判斷MatrixImageView的狀態決定是否攔截。
具體實現
由於容器ViewPager在滿足條件的時候會攔截掉子View的touch事件,因此需要自定義個ViewPager修改攔截邏輯。當MatriImageView進行縮放和拖動時,我們不希望ViewPager攔截。具體代碼如下:
public class AlbumViewPager extends ViewPager implements OnChildMovingListener { /** 當前子控件是否處理拖動狀態 */ private boolean mChildIsBeingDragged=false; @Override public boolean onInterceptTouchEvent(MotionEvent arg0) { if(mChildIsBeingDragged) return false; return super.onInterceptTouchEvent(arg0); } @Override public void startDrag() { // TODO Auto-generated method stub mChildIsBeingDragged=true; } @Override public void stopDrag() { // TODO Auto-generated method stub mChildIsBeingDragged=false; } }
public interface OnChildMovingListener{ public void startDrag(); public void stopDrag(); }
通過判斷變量mChildIsBeingDragged的值決定是否攔截,而mChildIsBeingDragged的值通過OnChildMovingListener接口由MatriImageView進行設置。別忘了在PagerAdapter的instantiateItem中給MatriImageView設置監聽接口
MatrixImageView imageView = (MatrixImageView) imageLayout.findViewById(R.id.image); imageView.setOnMovingListener(AlbumViewPager.this);
ViewPager的改造便完成了,只需要新增一個變量和實現一個接口,之后對於事件的攔截操作都轉到了MatrixImageView中。
接下去看下改造后的MatrixImageView的onTouch方法。
/** 和ViewPager交互相關,判斷當前是否可以左移、右移 */ boolean mLeftDragable; boolean mRightDragable; /** 是否第一次移動 */ boolean mFirstMove=false; private PointF mStartPoint = new PointF(); @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: //設置拖動模式 mMode=MODE_DRAG; mStartPoint.set(event.getX(), event.getY()); isMatrixEnable(); startMove(); checkDragable(); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: reSetMatrix(); stopMove(); break; case MotionEvent.ACTION_MOVE: if (mMode == MODE_ZOOM) { setZoomMatrix(event); }else if (mMode==MODE_DRAG) { setDragMatrix(event); }else { stopMove(); } break; case MotionEvent.ACTION_POINTER_DOWN: if(mMode==MODE_UNABLE) return true; mMode=MODE_ZOOM; mStartDis = distance(event); break; case MotionEvent.ACTION_POINTER_UP: break; default: break; } return mGestureDetector.onTouchEvent(event); }
其中加紅部分的代碼都是修改后的代碼,逐一分析。
/** * 子控件開始進入移動狀態,令ViewPager無法攔截對子控件的Touch事件 */ private void startDrag(){ if(moveListener!=null) moveListener.startDrag(); } /** * 子控件開始停止移動狀態,ViewPager將攔截對子控件的Touch事件 */ private void stopDrag(){ if(moveListener!=null) moveListener.stopDrag(); }
startDrag和stopDrag方法很簡單,就是調用ViewPager傳遞進來的OnChildMovingListener接口進行mChildIsBeingDragged的設置。當監聽到down事件時,表示開始拖動,當接收到up和cancel事件時,表示結束拖動,以這個邏輯來說,ViewGroup將永遠無法攔截touch事件,所以我們還需要在其他地方設置stopDrag事件,后面說明。
接下去是在down事件中執行checkDragable方法:
/** * 根據當前圖片左右邊緣設置可拖拽狀態 */ private void checkDragable() { mLeftDragable=true; mRightDragable=true; mFirstMove=true; float[] values=new float[9]; getImageMatrix().getValues(values); //圖片左邊緣離開左邊界,表示不可右移 if(values[Matrix.MTRANS_X]>=0) mRightDragable=false; //圖片右邊緣離開右邊界,表示不可左移 if((mImageWidth)*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X]<=getWidth()){ mLeftDragable=false; } }
該方法將會重置mLeftDragable、mRightDragable、mFirstMove三個參數的狀態。mLeftDragable表示當前狀態下的Matrix可以向左拖動,mRightDragable表示當前狀態下的Matrix可以向右拖動,mFirstMove為每次完整touch事件(從down到up或cancel)中的第一次拖動操作標志。其中mLeftDragable和mRightDragable都是根據Matrix矩陣的數值計算出來的。
由於前面功能需求的時候說過"每個down-up(cancel)事件周期內只執行一種類型的事務操作(縮放、拖動或者ViewPager切換Item)",因此當進行縮放操作時,就不會再執行切換Item操作了,可以等縮放結束后執行up操作時stopDrag。而Move操作重點就是要識別是切換item還是拖動圖片了。查看修改后的setDragMatrix代碼
/** * 設置拖拽狀態下的Matrix * @param event */ public void setDragMatrix(MotionEvent event) { if(isZoomChanged()){ float dx = event.getX() - mStartPoint.x; // 得到x軸的移動距離 float dy = event.getY() - mStartPoint.y; // 得到x軸的移動距離 //避免和雙擊沖突,大於10f才算是拖動 if(Math.sqrt(dx*dx+dy*dy)>10f){ mStartPoint.set(event.getX(), event.getY()); //在當前基礎上移動 mCurrentMatrix.set(getImageMatrix()); float[] values=new float[9]; mCurrentMatrix.getValues(values); dy=checkDyBound(values,dy); dx=checkDxBound(values,dx,dy); mCurrentMatrix.postTranslate(dx, dy); setImageMatrix(mCurrentMatrix); } }else { stopDrag(); } } /** * 和當前矩陣對比,檢驗dx,使圖像移動后不會超出ImageView邊界 * @param values * @param dx * @return */ private float checkDxBound(float[] values,float dx,float dy) { float width=getWidth(); if(!mLeftDragable&&dx<0){ //加入和y軸的對比,表示在監聽到垂直方向的手勢時不切換Item if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){ stopDrag(); } return 0; } if(!mRightDragable&&dx>0){ //加入和y軸的對比,表示在監聽到垂直方向的手勢時不切換Item if(Math.abs(dx)*0.4f>Math.abs(dy)&&mFirstMove){ stopDrag(); } return 0; } mLeftDragable=true; mRightDragable=true; if(mFirstMove) mFirstMove=false; if(mImageWidth*values[Matrix.MSCALE_X]<width){ return 0; } if(values[Matrix.MTRANS_X]+dx>0){ dx=-values[Matrix.MTRANS_X]; } else if(values[Matrix.MTRANS_X]+dx<-(mImageWidth*values[Matrix.MSCALE_X]-width)){ dx=-(mImageWidth*values[Matrix.MSCALE_X]-width)-values[Matrix.MTRANS_X]; } return dx; }
處理邏輯是這樣的:
1.判斷當前縮放級別是否是原始縮放級別(isZoomChanged()),如果未縮放過那將可以直接切換Item,在這直接stopDrag。
2.若進行了縮放,那判斷是否累移動了10f,當移動了10f之后計算出x軸和y軸的移動量,並且通過checkDyBound方法計算出y軸的真實移動量
3.進入checkDxBound方法,首先判斷當前是否能夠左移,如果不能左移而實際的x軸偏移量是向左的,那就返回x的偏移量為0,防止左移。同時,如果當前是第一次移動,那就表示本次不是左移操作,而是向前切換item,於是執行stopDrag方法,令ViewPager攔截掉對MatrixImageView的事件分發。另外在這里加入和Y軸偏移量的對比,是為了防止執行的是垂直方向的滑動而導致stopDrag,ViewPager自身對於X軸偏移量/2小於Y軸偏移量的情況是不當成切換Item意圖的,這里設置為*0.4可以保證不沖突。
4.右移同理。
5.當第一次左移和右移判斷結果都不是切換Item后,將mLeftDragable和mRightDragable都設置為true,表示可以正常移動了。之后就和單個MatrixImageView的拖動處理一樣了。
到此便完成了內嵌到ViewGroup內的MatriImageView的改造。下面還有兩點顯示優化。
首先在reSetMatrix中加入了新的功能:當縮放后的圖片高度未達到ImageView高度時,在up和cancel之后會將其Y軸居中,防止“放大圖片-Y軸移動圖片-縮小圖片”導致圖片位置不對稱。異常圖效果如下:
/** * 重置Matrix */ private void reSetMatrix() { if(checkRest()){ mCurrentMatrix.set(mMatrix); setImageMatrix(mCurrentMatrix); }else { //判斷Y軸是否需要更正 float[] values=new float[9]; getImageMatrix().getValues(values); float height=mImageHeight*values[Matrix.MSCALE_Y]; if(height<getHeight()){ //在圖片真實高度小於容器高度時,Y軸居中,Y軸理想偏移量為兩者高度差/2, float topMargin=(getHeight()-height)/2; if(topMargin!=values[Matrix.MTRANS_Y]){ mCurrentMatrix.set(getImageMatrix()); mCurrentMatrix.postTranslate(0, topMargin-values[Matrix.MTRANS_Y]); setImageMatrix(mCurrentMatrix); } } } }
優化了縮放操作的縮放x軸對稱軸選擇問題。在"圖片放大-移動X軸-縮小圖片"時,若直接以ImageView中心點為縮放原點,可能會導致縮放后的圖片邊緣離開ImageView邊界。
出錯圖效果如下:
/** * 設置縮放Matrix * @param event */ private void setZoomMatrix(MotionEvent event) { //只有同時觸屏兩個點的時候才執行 if(event.getPointerCount()<2) return; float endDis = distance(event);// 結束距離 if (endDis > 10f) { // 兩個手指並攏在一起的時候像素大於10 float scale = endDis / mStartDis;// 得到縮放倍數 mStartDis=endDis;//重置距離 mCurrentMatrix.set(getImageMatrix());//初始化Matrix float[] values=new float[9]; mCurrentMatrix.getValues(values); scale = checkMaxScale(scale, values); PointF centerF=getCenter(scale,values); mCurrentMatrix.postScale(scale, scale,centerF.x,centerF.y); setImageMatrix(mCurrentMatrix); } } /** * 獲取縮放的中心點。 * @param scale * @param values * @return */ private PointF getCenter(float scale,float[] values) { //縮放級別小於原始縮放級別時或者為放大狀態時,返回ImageView中心點作為縮放中心點 if(scale*values[Matrix.MSCALE_X]<mScale||scale>=1){ return new PointF(getWidth()/2,getHeight()/2); } float cx=getWidth()/2; float cy=getHeight()/2; //以ImageView中心點為縮放中心,判斷縮放后的圖片左邊緣是否會離開ImageView左邊緣,是的話以左邊緣為X軸中心 if((getWidth()/2-values[Matrix.MTRANS_X])*scale<getWidth()/2) cx=0; //判斷縮放后的右邊緣是否會離開ImageView右邊緣,是的話以右邊緣為X軸中心 if((mImageWidth*values[Matrix.MSCALE_X]+values[Matrix.MTRANS_X])*scale<getWidth()) cx=getWidth(); return new PointF(cx,cy); }
通過判斷圖片寬度,決定是以ImageView中點為X軸縮放原點,還是以左右邊緣為縮放原點。
目前為止MatrixImageView的功能基本完善了,具體代碼還是放在我的Github上的照相機Demo。該View如果有問題的可以在這篇文章下留言或私信我。