Android viewpager + 可縮放的imageview


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作為測試容器做進一步優化。

實現功能

 

  1. 當進行縮放操作時,手勢不會同時觸發ViewPager的滑動切換Item事件。
  2. 當進行拖動操作時,除非圖片已經到達左右邊界,否則不觸發ViewPager的滑動切換Item事件。
  3. 當進行拖動操作時,若圖片左邊緣到達左邊界,則可以向左滑動觸發ViewPager切換至前一個Item;當圖片右邊緣到達右邊界,則可以向右滑動觸發ViewPager的切換至后一個Item。
  4. 每個down-up(cancel)事件周期內只執行一種類型的事務操作(縮放、拖動或者ViewPager切換Item),防止多重事務互相干擾。
  5. 將事務處理封裝到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如果有問題的可以在這篇文章下留言或私信我。


免責聲明!

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



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