實現Android圖片雙擊放大縮小查看大圖+ViewPager+多點觸控

本文是根據鴻洋,打造個性的圖片預覽與多點觸控而來,主要是熟悉里面的效果

效果

效果

可以看到上面的效果是可以根據多指縮放,雙擊放大縮小,同時嵌套ViewPager

關于這樣的效果國外有個小伙Chris Banes寫的很好,PhotoView

具體實現步驟:

圖片加載時實現監聽

自定義控件并且繼承自ImageView,我們知道在oncreate中View.getWidth和View.getHeight無法獲得一個view的高度和寬度,這是因為View組件布局要在onResume回調后完成。所以現在需要使用getViewTreeObserver().addOnGlobalLayoutListener()來獲得寬度或者高度。這是獲得一個view的寬度和高度的方法之一。重寫onAttachedToWindow()方法

@Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();
        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

實現方法,并且獲取圖片寬高:

    @Override
    public void onGlobalLayout() {
        //布爾值防止多次加載
        if (!once) {
            //獲取屏幕的寬高
            int width = getWidth();
            int height = getHeight();
            //獲取加載到的圖片資源
            Drawable drawable = getDrawable();
            //獲取圖片的寬高
            int dWidth = drawable.getIntrinsicWidth();
            int dHeight = drawable.getIntrinsicHeight();
            //初始化的時候,我們要將圖片居中顯示
            //縮放比例
            float scale = 1.0f;
            if (dWidth > width && dHeight < height) {
                scale = width * 1.0f / dWidth;
            }
            if (dHeight > height && dWidth < width) {
                scale = height * 1.0f / dHeight;
            }
            if ((dWidth > width && dHeight > height) || (dWidth < width && dHeight < height)) {
                scale = Math.min(width * 1.0f / dWidth, height * 1.0f / dHeight);
            }
            //初始化縮放比例
            mInitScale = scale;
            //最大縮放比例
            mMaxScale = mInitScale * 4;
            //中等縮放比例
            mMidScale = mInitScale * 2;
            //圖片移動到中心的距離
            int dx = getWidth() / 2 - dWidth / 2;
            int dy = getHeight() / 2 - dHeight / 2;
            //進行平移
            mScaleMatrix.postTranslate(dx, dy);
            //進行縮放
            mScaleMatrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
            setImageMatrix(mScaleMatrix);
            once = true;
        }
    }

通過上面的步驟可以設置圖片居中顯示,比例縮放到正確的位置!

接下來實現圖片縮放

多手指縮放需要用到的一個類是ScaleGestureDetector,我們在構造初始化它

       //初始化Matrix
        mScaleMatrix = new Matrix();
        //預防在布局里沒有或者設置其他類型
        super.setScaleType(ScaleType.MATRIX);
        //縮放初始化
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        //同樣,縮放的捕獲要建立在setOnTouchListener上
        setOnTouchListener(this);

這樣實現其方法:

  @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scaleFactor = detector.getScaleFactor();
        float scale = getScale();
        if (getDrawable() == null) {
            return true;
        }
        //這里是想放大和縮小
        if ((scale < mMaxScale && scaleFactor > 1.0f) || (scale > mInitScale && scaleFactor < 1.0f)) {
            //這里如果要縮放的值比初始化還要小的話,就按照最小可以縮放的值進行縮放
            if (scale * scaleFactor < mInitScale) {
                scaleFactor = mInitScale / scale;
            }
            //這個是放大的同理
            if (scale * scaleFactor > mMaxScale) {
                scaleFactor = mMaxScale / scale;
            }
            //detector.getFocusX(), detector.getFocusY(),是在縮放中心點進行縮放
            mScaleMatrix.postScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
            //在縮放的時候會出現圖片漏出白邊,位置出現移動,所以要另外做移動處理
            checkBorderAndCenterWhenScale();
            setImageMatrix(mScaleMatrix);
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        //開始時設置為true
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {

    }

檢查露出時用到的方法

 private void checkBorderAndCenterWhenScale() {
        RectF matrixRectF = getMatrixRectF();
        float deltaX = 0;
        float deltY = 0;
        int width = getWidth();
        int height = getHeight();
        //縮放后的寬度大于屏幕
        if (matrixRectF.width() >= width) {

            if (matrixRectF.left > 0) {
                //這就是說左邊露出了一部分,怎么辦,補上啊,補多少?
                deltaX = -matrixRectF.left;
            }
            if (matrixRectF.right < width) {
                //這就是右邊露出了
                deltaX = width - matrixRectF.right;
            }
        }
        if (matrixRectF.height() >= height) {
            if (matrixRectF.top > 0) {
                deltY = -matrixRectF.top;
            }
            if (matrixRectF.bottom < height) {
                deltY = -height - matrixRectF.bottom;
            }
        }
        //如果寬或者是高,小于屏幕的話,那就沒理由的居中就行
        if (matrixRectF.width() < width) {
            deltaX = width / 2f - matrixRectF.right + matrixRectF.width() / 2;
        }
        if (matrixRectF.height() < height) {
            deltY = height / 2f - matrixRectF.bottom + matrixRectF.height() / 2;
        }
        mScaleMatrix.postTranslate(deltaX, deltY);
    }

獲取縮放值

  /**
     * 獲取縮放
     *
     * @return
     */
    private float getScale() {
        float[] values = new float[9];
        mScaleMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }

實現圖片放大后移動查看

移動需要在OnTouch里處理:

   @Override
    public boolean onTouch(View v, MotionEvent event) {
        mScaleGestureDetector.onTouchEvent(event);
        float x = 0;
        float y = 0;
        //可能出現多手指的情況
        int pointerCount = event.getPointerCount();
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x /= pointerCount;
        y /= pointerCount;
        if (mLastPointCount != pointerCount) {
            //手指變化后就不能繼續拖拽
            isCanDrag = false;
            //記錄最后的位置,重置
            mLatX = x;
            mLastY = y;
        }
        //記錄最后一次手指的個數
        mLastPointCount = pointerCount;
        RectF rectF = getMatrixRectF();
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                //x,y移動的距離
                float dx = x - mLatX;
                float dy = y - mLastY;
                //如果是不能拖拽,可能是因為手指變化,這時就去重新檢測看看是不是符合滑動
                if (!isCanDrag) {
                    //反正是根據勾股定理,調用系統API
                    isCanDrag = isMoveAction(dx, dy);
                }
                if (isCanDrag) {
                    if (getDrawable() != null) {
                        //判斷是寬或者高小于屏幕,就不在那個方向進行拖拽
                        isCheckLeftAndRight = isCheckTopAndBottom = true;
                        if (rectF.width() < getWidth()) {
                            isCheckLeftAndRight = false;
                            dx = 0;
                        }
                        if (rectF.height() < getHeight()) {
                            isCheckTopAndBottom = false;
                            dy = 0;
                        }
                        mScaleMatrix.postTranslate(dx, dy);
                        //拖拽的時候會露出一部分空白,要補上
                        checkBorderAndCenterWhenTranslate();
                        setImageMatrix(mScaleMatrix);
                    }
                }
                mLatX = x;
                mLastY = y;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mLastPointCount = 0;
                break;
        }
        return true;
    }

補上空白

 private void checkBorderAndCenterWhenTranslate() {
        RectF rectF = getMatrixRectF();
        float deltax = 0;
        float deltay = 0;
        int width = getWidth();
        int height = getHeight();
        if (rectF.top > 0 && isCheckTopAndBottom) {
            deltay = -rectF.top;
        }
        if (rectF.bottom < height && isCheckTopAndBottom) {
            deltay = height - rectF.bottom;
        }
        if (rectF.left > 0 && isCheckLeftAndRight) {
            deltax = -rectF.left;
        }
        if (rectF.right < width && isCheckLeftAndRight) {
            deltax = width - rectF.right;
        }
        mScaleMatrix.postTranslate(deltax, deltay);
    }

判斷是否是滑動

 private boolean isMoveAction(float dx, float dy) {
        return Math.sqrt(dx * dx + dy * dy) > mTouchSlop;
    }

雙擊實現放大和縮小

雙擊需要用到系統的一個類,在構造里初始化,同樣也需要在OnTouch里進行關聯


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        //雙擊進行關聯
        if (mGestureDetector.onTouchEvent(event)) {
            //如果是雙擊的話就直接不向下執行了
            return true;
        }
        //縮放進行關聯
        mScaleGestureDetector.onTouchEvent(event);
        ...
    }

在構造里進行處理雙擊監聽

  public ZoomImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //初始化Matrix
        mScaleMatrix = new Matrix();
        //預防在布局里沒有或者設置其他類型
        super.setScaleType(ScaleType.MATRIX);
        //縮放初始化
        mScaleGestureDetector = new ScaleGestureDetector(context, this);
        //同樣,縮放的捕獲要建立在setOnTouchListener上
        setOnTouchListener(this);
        //符合滑動的距離
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        //自動縮放時需要有一個自動的過程
        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() {
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                //如果再自動縮放中就不向下執行,防止多次雙擊
                if (isAutoScaling) {
                    return true;
                }
                //縮放的中心點
                float x = e.getX();
                float y = e.getY();
                if (getScale() < mMidScale) {
                    isAutoScaling = true;
                    postDelayed(new AutoScaleRunble(mMidScale, x, y), 16);
                } else {
                    isAutoScaling = true;
                    postDelayed(new AutoScaleRunble(mInitScale, x, y), 16);
                }
                return true;
            }

        });
    }

自動縮放處理類,實現Runnable


    private class AutoScaleRunble implements Runnable {
        private float mTrgetScale;
        private float x;
        private float y;
        private float tempScale;
        private float BIGGER = 1.07f;
        private float SMALLER = 0.93f;

        //構造傳入縮放目標值,縮放的中心點
        public AutoScaleRunble(float mTrgetScale, float x, float y) {
            this.mTrgetScale = mTrgetScale;
            this.x = x;
            this.y = y;
            if (getScale() < mTrgetScale) {
                tempScale = BIGGER;
            }
            if (getScale() > mTrgetScale) {
                tempScale = SMALLER;
            }
        }

        @Override
        public void run() {
            mScaleMatrix.postScale(tempScale, tempScale, x, y);
            checkBorderAndCenterWhenScale();
            setImageMatrix(mScaleMatrix);
            float currentScale = getScale();
            //如果你想放大并且當然值并沒有到達目標值,可以繼續放大,同理縮小也是一樣
            if ((tempScale > 1.0f && currentScale < mTrgetScale) || (tempScale < 1.0f && currentScale > mTrgetScale)) {
                postDelayed(this, 16);
            } else {//此時不能再進行放大或者縮小了,要放大為目標值
                float scale = mTrgetScale / currentScale;
                mScaleMatrix.postScale(scale, scale, x, y);
                checkBorderAndCenterWhenScale();
                setImageMatrix(mScaleMatrix);
                isAutoScaling = false;
            }
        }
    }

最后嵌入到ViewPager,這里要做一個處理,在OnTouch.因為ViewPager,滑動是需要攔截時間自己處理翻頁的

    case MotionEvent.ACTION_DOWN:
                //當圖片放大時,這個時候左右滑動查看圖片,就請求ViewPager不攔截事件!
                if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()) {
                    if (getParent() instanceof ViewPager) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }
                break;
            case MotionEvent.ACTION_MOVE:
                //x,y移動的距離
                float dx = x - mLatX;
                float dy = y - mLastY;
                //這里的處理是,當圖片移動到最邊緣的時候,不能在移動了,此時是應該Viewpager去處理事件,翻頁
                if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
                    if (getParent() instanceof ViewPager) {
                        //讓父類進行攔截處理
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                } else if (rectF.width() > getWidth() + 0.01 || rectF.height() > getHeight()){
                    if (getParent() instanceof ViewPager) {
                        //讓父類進行攔截處理
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                }

\MainAvtivity和ZoomImageView的鏈接

總結

雖然以上代碼可以實現我們需要的功能但是還有不完美的地方,下面看看效果就知道:

代碼效果

從效果可以看出,當我的圖片放大后,處于邊緣時,我如果向右滑動可以切換,確實是實現了這樣的效果,但是我當切換手指不松開,然后向反方向滑動時,會切出另外一面的pager,而不是去繼續移動大圖查看隱藏的部分,為什么會這樣呢,因為當我圖片放大正好左邊處于邊緣時,如果向右切換,這個時候是可以切換的,并且這個時候讓VIewPager接管了滑動事件處理

 if ((dx < 0 && rectF.right <= getWidth()) || (dx > 0 && rectF.left >= 0)) {
                    if (getParent() instanceof ViewPager) {
                        //讓父類進行攔截處理
                        getParent().requestDisallowInterceptTouchEvent(false);
                    }
                }

那么問題就來了,這個時候VIewPager切換時手指并沒有離開,事件處理依然掌握,當反方向切換時當然是會執行右邊切換!!
最好的辦法就是,當ViewPager左邊切換時,如果放棄左邊切換此時再把事件給子控件,這樣圖片又可以繼續移動查看了!
例如微信就可以實現這個效果!

微信效果

本代碼中目前我還沒想到好的解決辦法,有誰知道請告訴我!!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容