自定义view

实现Android图片双击放大缩小查看大图+ViewPager+

2017-05-06  本文已影响490人  HannyYeung

本文是根据鸿洋,打造个性的图片预览与多点触控而来,主要是熟悉里面的效果

效果

效果

可以看到上面的效果是可以根据多指缩放,双击放大缩小,同时嵌套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左边切换时,如果放弃左边切换此时再把事件给子控件,这样图片又可以继续移动查看了!
例如微信就可以实现这个效果!

微信效果

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

上一篇下一篇

猜你喜欢

热点阅读