我分四部分介绍:

       1.Imageview利用Matrix和OnScaleGestureListener实现手势缩放;

       2.在第一部分的基础上实现图片跟随手指进行滑动;

       3.在一、二的基础上利用GestureDetector的OnDoubleTap回调实现图片双击还原;

       4.在一、二、三的基础上将ImageView扩展为FrameLayout,实现布局内所有子控件都能手势缩放、移动、双击还原。

一、Imageview利用Matrix和OnScaleGestureListener实现手势缩放

1.基础知识:

Matrix是一个3*3的矩阵


* MSCALE_X MSKEW_X MTRANS_X * MKEW_Y MSCALE_Y MTRANS_Y * MPERSP_0 MPERSP_1 MPERSP_2


Matrix.postScale(x, y, px, py)矩阵缩放,x是x方向上的缩放大小,y是y方向上的缩放大小,(px,py)是缩放中心

Matrix.postTranslate(dx, dy)矩阵平移

setImageMatrix(matrix)将自己的matrix赋值给ImageView的matrix从而实现ImageView中图片的缩放、平移等操作

OnScaleGestureListener中的onScale回调中的ScaleGestureDetector.getScaleFactor()可以获取当前手势缩放的大小

ImageView需要设置setScaleType(ScaleType.MATRIX);才能实现缩放

OnScaleGestureListener中的OnScaleBegin回调必须return true才有缩放效果

2.实现思路:

实现OnScaleGestureListener监听,在OnScale回调方法中,根据ScaleGestureDetector.getScaleFactor()获取当前手势缩放的大小,利用Matrix.postScale(x, y, px, py)方法将缩放大小保存在矩阵中,用setImageMatrix(matrix)方法,将矩阵赋值给ImageView的内部矩阵,从而实现图片的手势缩放,是不是很简单。

3.实现代码:


/**
 * 只支持手势缩放的ImageView
 */
public class ZoomOnlyImageView extends AppCompatImageView implements ScaleGestureDetector.OnScaleGestureListener {

    private ScaleGestureDetector scaleGestureDetector;//手势缩放
    /**
     * MSCALE_X  MSKEW_X    MTRANS_X
     * MKEW_Y    MSCALE_Y   MTRANS_Y
     * MPERSP_0  MPERSP_1   MPERSP_2
     */
    private Matrix mMatrix;//缩放矩阵
    private float maxScale = 4.0f;//最大缩放到原图的四倍
    private float minScale = 0.5f;//最小缩放到原图的0.5倍

    public ZoomOnlyImageView(Context context) {
        super(context);
        init(context);
    }

    public ZoomOnlyImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public ZoomOnlyImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    //初始化参数
    private void init(Context context) {
        setScaleType(ScaleType.MATRIX);//允许imageview缩放
        scaleGestureDetector = new ScaleGestureDetector(new WeakReference<Context>(context).get(),
                new WeakReference<ZoomOnlyImageView>(this).get());
        mMatrix = new Matrix();
    }

    @Override
    public boolean onScale(ScaleGestureDetector detector) {//OnScaleGestureListener里的方法
        if (getDrawable() == null) {
            return true;
        }
        //获取本次的缩放值
        float scale = detector.getScaleFactor();
        Log.i("zhangdi", "scaleFactor = "+scale);
        float preScale = getPreScale();
        Log.i("zhangdi", "preScale = "+preScale);
        if (preScale * scale < maxScale &&
                preScale * scale > minScale) {//preScale * scale可以计算出此次缩放执行的话,缩放值是多少

            //detector.getFocusX()缩放手势中心的x坐标,detector.getFocusY()y坐标
//            mMatrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());
            mMatrix.postScale(scale, scale, getWidth()/2, getHeight()/2);
            setImageMatrix(mMatrix);
            makeDrawableCenter();
        }
        return true;
    }

    @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {//OnScaleGestureListener里的方法,缩放开始
        return true;//必须返回true才有效果
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {//OnScaleGestureListener里的方法,缩放结束

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return scaleGestureDetector.onTouchEvent(event);
    }

    //获取目前一共缩放了多少
    private float getPreScale() {
        float[] matrix = new float[9];
        mMatrix.getValues(matrix);
        return matrix[Matrix.MSCALE_X];
    }

    //缩小的时候让图片居中
    private void makeDrawableCenter() {

        RectF rect = new RectF();
        Drawable d = getDrawable();
        if (d != null) {
            rect.set(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());//设置rect的初始四个角值是图片的四个顶点值
            Log.i("zhangdi", "bitmapWidth = "+d.getIntrinsicWidth()+", bitmapHeight = "+d.getIntrinsicHeight());
            mMatrix.mapRect(rect);//获取通过当前矩阵变换后的四个角值
            Log.i("zhangdi", "matrixWidth = "+rect.width()+", matrixHeight = "+rect.height());
            Log.i("zhangdi", "bmLeft: "+rect.left+" bmRight: "+rect.right+" bmTop: "+rect.top+" bmBottom: "+rect.bottom);
        }

        int width = getWidth();
        int height = getHeight();

        float dx=0, dy=0;

        // 如果宽或高大于屏幕,则控制范围
        if (rect.width() >= width)
        {
            if (rect.left > 0)
            {
                dx = -rect.left;
            }
            if (rect.right < width)
            {
                dx = width - rect.right;
            }
        }
        if (rect.height() >= height)
        {
            if (rect.top > 0)
            {
                dy = -rect.top;
            }
            if (rect.bottom < height)
            {
                dy = height - rect.bottom;
            }
        }

        if (rect.width() < width) {
            dx = width/2 - (rect.right - rect.width()/2);//控件中心点横坐标减去图片中心点横坐标为X方向应移动距离
        }
        if (rect.height() < height) {
            dy = height/2 - (rect.bottom - rect.height()/2);
        }
        Log.i("zhangdi", "dx = "+dx+", dy = "+dy);

        if (dx != 0 || dy != 0) {
            mMatrix.postTranslate(dx, dy);
            setImageMatrix(mMatrix);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        setImageDrawable(null);
        scaleGestureDetector = null;
    }
}


二、在第一部分的基础上实现图片跟随手指进行滑动

1.实现思路:把每次的手指移动距离赋值给矩阵,将矩阵赋值给ImageView实现图片的平移,注意控制移动范围不能超过图片范围。

2.代码实现:修改第一部分的onTouchEvent方法


@Override
public boolean onTouchEvent(MotionEvent event) {
    scaleGestureDetector.onTouchEvent(event);

    float x=0, y=0;
    final int pointerCount = event.getPointerCount();//获取手指个数
    for (int i=0; i<pointerCount; i++) {
        x += event.getX(i);
        y += event.getY(i);
    }
    x = x/pointerCount;//获取x平均值
    y = y/pointerCount;//获取y平均值
    if (pointerCount != lastPointerCount)
    {
        lastX = x;
        lastY = y;
    }
    Log.i("zhangdi", "pointCount: "+pointerCount+", lastPointCount: "+lastPointerCount);
    lastPointerCount = pointerCount;
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_MOVE:
            float delX = x - lastX;//x方向计算移动距离
            float delY = y - lastY;//计算y方向移动距离

            RectF rectF = getMatrixRectF();
            //控制移动边界不能超出图片范围
            if ((rectF.left >= 0 && delX > 0) || (rectF.right <= getWidth() && delX < 0)) {
                delX = 0;
            }
            if ((rectF.top >= 0 && delY > 0) || (rectF.bottom <= getHeight() && delY < 0)) {
                delY = 0;
            }

            mMatrix.postTranslate(delX, delY);
            setImageMatrix(mMatrix);
            lastX = x;
            lastY = y;
            break;
        case MotionEvent.ACTION_UP:
            lastPointerCount = 0;
            break;
    }
    return true;
}


三、在一、二的基础上利用GestureDetector的OnDoubleTap回调实现图片双击还原

1.实现思路:利用GestureDetector的OnDoubleTap监听回调,判断当前矩阵的缩放比例是不是1,不是的话则matrix.reset(),将matrix赋值给ImageView实现图片双击还原。

2.实现代码:

   1.修改init()方法,创建GestureDetector对象:


//初始化参数
private void init(Context context) {
    setScaleType(ScaleType.MATRIX);//允许imageview缩放
    scaleGestureDetector = new ScaleGestureDetector(new WeakReference<Context>(context).get(),
            new WeakReference<ZoomTranslateDoubleTapImageView>(this).get());
    mMatrix = new Matrix();
    gestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){
        @Override
        public boolean onDoubleTap(MotionEvent e) {//双击图片还原
            if (getPreScale() != 1.0f) {
                ZoomTranslateDoubleTapImageView.this.postDelayed(new Runnable() {
                    @Override
                    public void run() {

                        mMatrix.reset();
                        setImageMatrix(mMatrix);
                        makeDrawableCenter();
                    }
                },16);
            }
            return true;
        }
    });
}


2.在onTouchEvent方法中增加双击事件关联:


if (gestureDetector.onTouchEvent(event)) { return true; }


四、在一、二、三的基础上将ImageView扩展为FrameLayout,实现布局内所有子控件都能手势缩放、移动、双击还原:

1.实现思路:与上面的方法基本没什么区别,只需要把继承ImageView改为继承FrameLayout,但是FrameLayout没有setImageMatrix方法,我们进入ImageView的源码中可以看出setImageMatrix方法主要是将matrix赋值给了mDrawMatrix,而mDrawMatrix在onDraw方法中作用在了canvas上canvas.concat(mDrawMatrix),从而使图片实现矩阵中的相关操作,所以我们的FrameLayout虽然没有setImageMatrix这个方法,但是我们只需要在Matrix相关操作赋值后,调用invalidate()方法重绘界面,并且在它的onDraw方法中使canvas与矩阵关联就行了,需要注意的是ViewGroup出于效率的考虑有些是默认绕过onDraw方法的,所以我们需要重写dispatchDraw方法而不是onDraw方法。

2.实现代码:

1.将所有的setImageMatrix方法替换成invalidate方法;

2.重写dispatchDraw方法:


@Override protected void dispatchDraw(Canvas canvas) { View view = getChildAt(0); canvas.save(); canvas.concat(mMatrix); view.draw(canvas); canvas.restore(); }


3.使用注意事项:

在自定义布局中只能包含一个ViewGroup控件,然后所有的子view都放到这个ViewGroup中才能实现预期效果,类似ScrollerView的使用。