实现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左边切换时,如果放弃左边切换此时再把事件给子控件,这样图片又可以继续移动查看了!
例如微信就可以实现这个效果!

微信效果

本代码中目前我还没想到好的解决办法,有谁知道请告诉我!!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容