自定义ImageView,实现图片的缩放

  • 多指的图片缩放,最大放大倍数是4倍
  • 双击的图片缩放,有两个缩放等级
  • 与ViewPager兼容,在图片放大的情况下可移动图片
  • 使用到的知识点有:Matrix矩阵的操作,GestureDetector手势监测器,Runnable等

大致思路(参照ZoomImageView代码,在下方):

  • 继承OnGlobalLayoutListener,实现onGlobalLayout()方法,在该方法中初始化参数
  • 获取控件宽高,设置各个缩放比例,设置图片的Matrix缩放、偏移等;
  • 在onAttachedToWindow()方法中添加监听:getViewTreeObserver().addOnGlobalLayoutListener(this);
  • 在onDetachedFromWindow()方法中移除监听:getViewTreeObserver().removeOnGlobalLayoutListener(this);
  • 这一步完成了图片的居中显示,以及图片的压缩。
  • 继承ScaleGestureDetector.OnScaleGestureListener,实现onScale()方法,设置缩放手势的监听,进行相应的操作;
  • 获得手势缩放的比例即可进行图片缩放,但是要进行极限缩放大小的逻辑判断
  • 注意onScaleBegin()这个方法的返回值要设置为true;
  • 继承OnTouchListener接口,在onTouch()方法中,将触摸事件交给scaleGestureDetector对象去消费;
  • 这一步完成了图片的多指缩放。
  • 图片的缩放中心设置为手势中心时,可能会出现边界留白的情况,所以要进行图片边界以及中心位置的控制(drawableOffsetControl()方法):
  • 当图片宽高有一项是大于控件大小,根据各种可能出现留白的情况进行图片偏移量的设置;
  • 当图片缩放到宽高都小于控件大小,则把图片中心移动到控件中心;
  • 这一步完成了图片多指缩放之后显示位置的优化,在很多地方都会用到这个方法,较重要。
  • 图片的自由移动:覆写onTouch()方法,根据手指个数和移动位置来判断是移动图片还是缩放图片
  • 获取最后一次的X、Y方向的手势中心,当前后两次偏移量超过一定的值,则移动图片;
  • 手指个数发生变化,则各个数值要重新设置;
  • 使用上一步的drawableOffsetControl()方法进行边界的处理,否则会导致图片移动到控件以外的地方;
  • 这一步完成了图片的移动,但是此时仍和ViewPager有冲突。
  • 双击时的缩放:产生一个GestureDetector对象,传入SimpleOnGestureListener接口,覆写onDoubleTap()方法
  • 获取双击位置,根据当前缩放比例,来对缩放等级进行逻辑判断;
  • 自定义一个Runnable,在runnable中每次缩放一个固定的值,然后将当前缩放比和目标值进行对比,不断postDelay()直到达到目标值,同时定义一个变量,防止用户重复地双击。
  • 兼容ViewPager:在放大情况下可移动图片而不是切换图片页面;
  • 当图片宽度大于控件宽度,则在onTouch()中调用getParent().requestDisallowInterceptTouchEvent(true);不许父控件ViewPager拦截ImageView的响应事件;
  • 当图片已经达到边界,则父控件拦截事件,requestDisallowInterceptTouchEvent(false),可以切换页面;
  • 在手指抬起后,缩放一个肉眼看不出的值,使得下次仍可以移动一个已经放大的图片,防止响应事件被屏蔽。

 

项目DEMO

ZoomImageView代码

package com.cyrus.zoomimageviewdemo;

import android.app.Activity;
import android.content.Context;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.ViewParent;
import android.view.ViewTreeObserver;
import android.widget.ImageView;

/**
 * 可缩放图片的ImageView,可双击缩放或多指缩放,与ViewPager兼容+
 * <p>
 * Created by Cyrus on 2016/11/7.
 */

public class ZoomImageView extends ImageView implements
        ViewTreeObserver.OnGlobalLayoutListener,
        ScaleGestureDetector.OnScaleGestureListener,
        View.OnTouchListener {

    /**
     * 该参数用于判断某次触摸是否算是一个滑动操作
     */
    private static final int TOUCH_SLOP = 8;

    /**
     * 控件的宽度
     */
    private int mViewWidth;
    /**
     * 控件的高度
     */
    private int mViewHeight;
    /**
     * 用于判断图片是否已经初始化
     */
    private boolean mHasInit;
    /**
     * 最小缩放比例
     */
    private float mMinScale;
    /**
     * 初始化缩放比例
     */
    private float mInitScale;
    /**
     * 中等缩放比例
     */
    private float mMidScale;
    /**
     * 最大缩放比例
     */
    private float mMaxScale;
    /**
     * 图片矩阵
     */
    private Matrix mMatrix;
    /**
     * 用户多指触控监测器
     */
    private ScaleGestureDetector mScaleGestureDetector;
    /**
     * 用于手势操作监测器,主要进行双击操作的检测
     */
    private GestureDetector mGestureDetector;
    /**
     * 最后一次触摸操作的手指数量
     */
    private int mLastPointerCount;
    /**
     * 最后一次缩放位置的X轴方向中心
     */
    private float mLastScaleX;
    /**
     * 最后一次缩放位置的Y轴方向中心
     */
    private float mLastScaleY;
    /**
     * 用于判断图片是否可以移动
     */
    private boolean mPicCanMove;
    /**
     * 判断是否正在自动缩放状态,防止用户多次双击缩放,导致错乱
     */
    private boolean mIsAutoScaling;

    public ZoomImageView(Context context) {
        this(context, null);
    }

    public ZoomImageView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public ZoomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

        mMatrix = new Matrix();// 图片的矩阵
        setScaleType(ScaleType.MATRIX);// 在这里设置是屏蔽掉xml文件中设置的缩放模式

        mScaleGestureDetector = new ScaleGestureDetector(context, this);// 监听多指缩放手势
        setOnTouchListener(this);// 监听触摸事件

        mGestureDetector = new GestureDetector(context, new GestureDetector
                .SimpleOnGestureListener() {// 监听双击缩放
            @Override
            public boolean onDoubleTap(MotionEvent e) {
                if (mIsAutoScaling) {
                    return true;
                }

                float scaleX = e.getX();
                float scaleY = e.getY();

                float currentScale = getScale();

                if (mMinScale <= currentScale && currentScale < mMidScale) {
                    postDelayed(new AutoScaleRunnable(mMidScale, scaleX, scaleY), 0);
                } else if (mMidScale <= currentScale && currentScale < mMaxScale) {
                    postDelayed(new AutoScaleRunnable(mMaxScale, scaleX, scaleY), 0);
                } else {
                    postDelayed(new AutoScaleRunnable(mInitScale, scaleX, scaleY), 0);
                }

                return true;
            }

            /*
             * 如果当前图片是在一个Activity的ViewPager中打开的,单击可以关闭图片
             */
            @Override
            public boolean onSingleTapConfirmed(MotionEvent e) {
                ViewParent parent = getParent();
                if (parent instanceof ViewPager) {
                    Context context = ((ViewPager) parent).getContext();
                    if (context instanceof Activity) {
                        ((Activity) context).finish();
                    }
                }

                return true;
            }
        });
    }

    @Override
    public void onGlobalLayout() {
        // 获取控件宽高
        mViewWidth = getWidth();
        mViewHeight = getHeight();

        // 初始化图片大小和位置
        if (!mHasInit) {
            // 获取图片宽高
            Drawable drawable = getDrawable();
            if (drawable == null) {
                return;
            }
            int picWidth = drawable.getIntrinsicWidth();
            int picHeight = drawable.getIntrinsicHeight();

            // 设置初始、中等、最大缩放比例
            mInitScale = mViewWidth * 1.0f / picWidth;
            mMinScale = mInitScale * 0.8f;
            mMidScale = mInitScale * 2;
            mMaxScale = mInitScale * 4;

            // 设置图片移动到控件中心的偏移量
            int offsetX = mViewWidth / 2 - picWidth / 2;
            int offsetY = mViewHeight > picHeight * mInitScale
                    ? mViewHeight / 2 - picHeight / 2
                    : 0;// 长图片从头开始查看,所以偏移为0

            mMatrix.postTranslate(offsetX, offsetY);
            mMatrix.postScale(mInitScale, mInitScale, mViewWidth / 2,
                    mViewHeight > picHeight * mInitScale
                            ? mViewHeight / 2
                            : 0);// 长图片Y轴的缩放位置不在中心,而在顶部
            setImageMatrix(mMatrix);

            mHasInit = true;
        }
    }

    /**
     * 获得当前图片缩放比例
     *
     * @return 当前缩放比例
     */
    private float getScale() {
        float values[] = new float[9];
        mMatrix.getValues(values);
        return values[Matrix.MSCALE_X];
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float currentScale = getScale();// 当前图片缩放比例
        float scaleFactor = detector.getScaleFactor();// 手势缩放因素

        if (getDrawable() == null) {
            return true;
        }

        /*
         * 控制缩放范围:
         * 放大手势——scaleFactor > 1.0f;
         * 缩小手势——scaleFactor < 1.0f;
         *
         * matrix.postScale()方法是按照已经缩放过的图片,再去进行一次缩放的。
         * 之前如果已经调用了postScale(scale, scale),那么图片宽高就已经缩放了scale个系数,
         * 再调用postScale(scaleFactor, scaleFactor),就会在scale系数的基础上缩放scaleFactor个系数,
         * 除以currentScale这个参数,就是为了将之前已经缩放过的scale个系数给抵消掉。
         */
        if ((currentScale < mMaxScale && scaleFactor > 1.0f)
                || (currentScale > mMinScale && scaleFactor < 1.0f)) {
            if (currentScale * scaleFactor < mMinScale) {
                // 除以currentScale,抵消上一次postScale()的缩放比例
                scaleFactor = mMinScale / currentScale;
            } else if (currentScale * scaleFactor > mMaxScale) {
                scaleFactor = mMaxScale / currentScale;
            }

            mMatrix.postScale(scaleFactor, scaleFactor,
                    detector.getFocusX(), detector.getFocusY());

            drawableOffsetControl();// 控制图片位置

            setImageMatrix(mMatrix);
        }

        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        return true;// 这里return true才能识别缩放手势
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mGestureDetector.onTouchEvent(event)) {
            return true;
        }

        mScaleGestureDetector.onTouchEvent(event);

        float scaleX = 0;// 多点触控X中心
        float scaleY = 0;// 多点触控Y中心
        int pointerCount = event.getPointerCount();

        // 计算X、Y轴的手势中心
        for (int i = 0; i < pointerCount; i++) {
            scaleX += event.getX(i);
            scaleY += event.getY(i);
        }
        scaleX = scaleX / pointerCount;
        scaleY = scaleY / pointerCount;

        // 手指数量发生改变,重新设置参数
        if (mLastPointerCount != pointerCount) {
            mLastPointerCount = pointerCount;
            mLastScaleX = scaleX;
            mLastScaleY = scaleY;
            mPicCanMove = false;
        }

        RectF matrixRectF = getMatrixRectF();
        float picWidth = matrixRectF.width();

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                // 兼容与ViewPager的冲突
                ViewParent parent = getParent();
                if (picWidth > mViewWidth + 0.01) {
                    if (parent instanceof ViewPager) {
                        parent.requestDisallowInterceptTouchEvent(true);
                        if (matrixRectF.right == mViewWidth
                                || matrixRectF.left == 0) {
                            parent.requestDisallowInterceptTouchEvent(false);
                        }
                    }
                }

                // 获取当前缩放中心和上一次缩放中心的差值
                float offsetX = scaleX - mLastScaleX;
                float offsetY = scaleY - mLastScaleY;

                if (!mPicCanMove) {
                    mPicCanMove = isMoveAction(offsetX, offsetY);
                } else {
                    if (getDrawable() != null) {
                        // 图片宽度小于控件宽度,X轴方向不移动
                        if (matrixRectF.width() < mViewWidth) {
                            offsetX = 0;
                        }
                        // 图片高度小于控件高度,Y轴方向不移动
                        if (matrixRectF.height() < mViewHeight) {
                            offsetY = 0;
                        }
                    }

                    mMatrix.postTranslate(offsetX, offsetY);
                    drawableOffsetControl();// 移动图片时的边界控制

                    setImageMatrix(mMatrix);
                }

                // 更新最后一次手势的中心
                mLastScaleX = scaleX;
                mLastScaleY = scaleY;
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                mLastPointerCount = 0;
                // 这里放大了一个肉眼看不出的数值:
                //
                // 在ACTION_MOVE中,如果图片的left或者right与控件边界相等,则不能移动图片,
                // 而是响应ViewPager的操作,这不是下一次操作所希望的;
                //
                // 缩放一个极小的数值,让图片的left、right与控件边界不等,使得可以重新进行移动图片。
                mMatrix.postScale(1.00001f, 1.000001f, mViewWidth / 2, mViewHeight / 2);
                setImageMatrix(mMatrix);
                break;
        }

        return true;
    }

    /**
     * 判断一次手势中心的偏移是否已经满足MotionEvent.ACTION_MOVE的操作
     *
     * @param offsetX 手势中心的X轴方向偏移量
     * @param offsetY 手势中心的Y轴方向偏移量
     * @return 如果算是一次MOVE操作,返回true,否则返回false
     */
    private boolean isMoveAction(float offsetX, float offsetY) {
        return Math.sqrt(offsetX * offsetX + offsetY * offsetY) > TOUCH_SLOP;
    }

    /**
     * 设置图片的偏移量,用于图片边界的控制,防止图片和控件之间出现白边
     */
    private void drawableOffsetControl() {
        RectF matrixRectF = getMatrixRectF();

        // 图片宽高
        float picWidth = matrixRectF.width();
        float picHeight = matrixRectF.height();

        // 图片偏移量
        float offsetX = 0;
        float offsetY = 0;

        // 图片X轴方向上的偏移量
        if (picWidth >= mViewWidth) {// 图片宽度大于控件宽度
            if (matrixRectF.left > 0) {// 左边留有空白
                offsetX = -matrixRectF.left;
            }

            if (matrixRectF.right < mViewWidth) {// 右边留有空白
                offsetX = mViewWidth - matrixRectF.right;
            }
        } else {// 图片宽度小于控件宽度,居中显示
            offsetX = mViewWidth / 2 - matrixRectF.right + picWidth / 2;
        }

        if (picHeight >= mViewHeight) {// 图片高度大于控件高度
            if (matrixRectF.top > 0) {// 上边留有空白
                offsetY = -matrixRectF.top;
            }

            if (matrixRectF.bottom < mViewHeight) {// 下边留有空白
                offsetY = mViewHeight - matrixRectF.bottom;
            }
        } else {// 图片高度小于控件高度,居中显示
            offsetY = mViewHeight / 2 - matrixRectF.bottom + picHeight / 2;
        }

        mMatrix.postTranslate(offsetX, offsetY);
    }

    /**
     * 获得图片缩放后的宽高,以及left, right, top, bottom
     *
     * @return 包含图片大小和位置的矩形
     */
    private RectF getMatrixRectF() {
        Matrix matrix = mMatrix;
        RectF rectF = new RectF();

        Drawable drawable = getDrawable();
        if (drawable != null) {
            rectF.set(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
            matrix.mapRect(rectF);
        }

        return rectF;
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        getViewTreeObserver().addOnGlobalLayoutListener(this);
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        getViewTreeObserver().removeOnGlobalLayoutListener(this);
    }

    /**
     * 双击缩放图片时,使图片缩放产生动画效果的内部类
     */
    private class AutoScaleRunnable implements Runnable {

        /**
         * 每次缩小图片的固定比例
         */
        private static final float SMALLER = 0.9f;
        /**
         * 每次放大图片的固定比例
         */
        private static final float LARGER = 1.1f;

        /**
         * 最终放大的目标值
         */
        private float mTargetScale;
        /**
         * 每次缩放图片的比例值
         */
        private float mScaleValue = 1.0f;
        /**
         * 缩放图片X轴位置
         */
        private float mScaleX;
        /**
         * 缩放图片Y轴位置
         */
        private float mScaleY;

        AutoScaleRunnable(float targetScale, float scaleX, float scaleY) {
            mTargetScale = targetScale;
            mScaleX = scaleX;
            mScaleY = scaleY;
            mScaleValue = getScale() < mTargetScale ? LARGER : SMALLER;
        }

        @Override
        public void run() {
            mIsAutoScaling = true;

            mMatrix.postScale(mScaleValue, mScaleValue, mScaleX, mScaleY);
            drawableOffsetControl();
            setImageMatrix(mMatrix);

            // 缩放之后判断当前图片是否已达到最终需要缩放的大小
            float currentScale = getScale();
            if ((mScaleValue > 1.0f && mTargetScale > currentScale)// 正在放大,且未达到目标
                    || (mScaleValue < 1.0f && mTargetScale < currentScale)) {// 正在缩小且未达到目标
                postDelayed(this, 0);// 再次缩放
            } else {
                float finalScaleValue = mTargetScale / currentScale;
                mMatrix.postScale(finalScaleValue, finalScaleValue, mScaleX, mScaleY);
                drawableOffsetControl();
                setImageMatrix(mMatrix);

                mIsAutoScaling = false;
            }
        }
    }

}