具备水平和垂直拖动以及双指缩放的视图

20
是否有一种视图支持水平和垂直的拖动,同时还能双指捏合缩放和双击放大?此外,我希望能够在该视图上添加其他视图(例如按钮、文本框、视频视图等)。当父视图进行缩放或移动时,子视图(按钮)也需要随之移动。我已经尝试了多种解决方案,但没有一个符合我要求的所有选项。你们是否知道这样的项目存在于安卓平台?以下是两个我曾经尝试过的解决方案链接:

你好,你得到这个问题的最终解决方案了吗?能否请你分享一下? - ThaiPD
如果您能分享您的解决方案,那就太好了。 - pawpaw
9个回答

27
我认为你想要实现的是可能可行的,但是据我所知,没有内置的解决方案。从你问题的后半部分来看,我猜测你不需要一个可缩放的View,而是一个ViewGroup,它是所有可以包含其他视图的视图(例如布局)的超类。以下是一些代码,你可以从中开始构建自己的ViewGroup,大部分代码都来自于这篇博客文章。
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.*;

public class ZoomableViewGroup extends ViewGroup {

    private static final int INVALID_POINTER_ID = 1;
    private int mActivePointerId = INVALID_POINTER_ID;

    private float mScaleFactor = 1;
    private ScaleGestureDetector mScaleDetector;
    private Matrix mScaleMatrix = new Matrix();
    private Matrix mScaleMatrixInverse = new Matrix();

    private float mPosX;
    private float mPosY;
    private Matrix mTranslateMatrix = new Matrix();
    private Matrix mTranslateMatrixInverse = new Matrix();

    private float mLastTouchX;
    private float mLastTouchY;

    private float mFocusY;

    private float mFocusX;

    private float[] mInvalidateWorkingArray = new float[6];
    private float[] mDispatchTouchEventWorkingArray = new float[2];
    private float[] mOnTouchEventWorkingArray = new float[2];


    public ZoomableViewGroup(Context context) {
        super(context);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mTranslateMatrix.setTranslate(0, 0);
        mScaleMatrix.setScale(1, 1);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight());
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.save();
        canvas.translate(mPosX, mPosY);
        canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDispatchTouchEventWorkingArray[0] = ev.getX();
        mDispatchTouchEventWorkingArray[1] = ev.getY();
        mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
        ev.setLocation(mDispatchTouchEventWorkingArray[0],
                mDispatchTouchEventWorkingArray[1]);
        return super.dispatchTouchEvent(ev);
    }

    /**
     * Although the docs say that you shouldn't override this, I decided to do
     * so because it offers me an easy way to change the invalidated area to my
     * likening.
     */
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

        mInvalidateWorkingArray[0] = dirty.left;
        mInvalidateWorkingArray[1] = dirty.top;
        mInvalidateWorkingArray[2] = dirty.right;
        mInvalidateWorkingArray[3] = dirty.bottom;


        mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray);
        dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]),
                Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3]));

        location[0] *= mScaleFactor;
        location[1] *= mScaleFactor;
        return super.invalidateChildInParent(location, dirty);
    }

    private float[] scaledPointsToScreenPoints(float[] a) {
        mScaleMatrix.mapPoints(a);
        mTranslateMatrix.mapPoints(a);
        return a;
    }

    private float[] screenPointsToScaledPoints(float[] a){
        mTranslateMatrixInverse.mapPoints(a);
        mScaleMatrixInverse.mapPoints(a);
        return a;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mOnTouchEventWorkingArray[0] = ev.getX();
        mOnTouchEventWorkingArray[1] = ev.getY();

        mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

        ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();

                mLastTouchX = x;
                mLastTouchY = y;

                // Save the ID of this pointer
                mActivePointerId = ev.getPointerId(0);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                // Find the index of the active pointer and fetch its position
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(pointerIndex);
                final float y = ev.getY(pointerIndex);

                final float dx = x - mLastTouchX;
                final float dy = y - mLastTouchY;

                mPosX += dx;
                mPosY += dy;
                mTranslateMatrix.preTranslate(dx, dy);
                mTranslateMatrix.invert(mTranslateMatrixInverse);

                mLastTouchX = x;
                mLastTouchY = y;

                invalidate();
                break;
            }

            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                // Extract the index of the pointer that left the touch sensor
                final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                break;
            }
        }
        return true;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();
            if (detector.isInProgress()) {
                mFocusX = detector.getFocusX();
                mFocusY = detector.getFocusY();
            }
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
            mScaleMatrix.setScale(mScaleFactor, mScaleFactor,
                    mFocusX, mFocusY);
            mScaleMatrix.invert(mScaleMatrixInverse);
            invalidate();
            requestLayout();


            return true;
        }
    }
}

这个类应该可以将内容拖动并允许捏合缩放,双击缩放目前不可能,但应该很容易在 onTouchEvent 方法中实现。
如果您对在 ViewGroup 中布局子项有疑问,我发现这个视频非常有用;如果您对单个方法的工作或其他任何问题有进一步疑问,请在评论中随时提问。

这段代码有几个错误,包括需要扩展ViewGroup并且你漏掉了几个“}”。 - Marche101
@Artjom,我在想,为什么不直接在ViewGroup上使用setScaleX/Y,而要自己转换剪辑、触摸点等呢? - numan salati
当视图达到1:1比例时,是否有可能停止缩小?这是否容易实现? - edoardotognoni
不得不进行一些修改才能使其正常工作,但它确实完成了任务。最终使用了一个矩阵和基于this的onTouchEvent方法。 - Shalmezad
如何在翻译时限制 ZoomableViewGroup? - Cuong Nguyen
显示剩余3条评论

17

以下是已修正部分错误的Artjom回答的转载,主要包括花括号、导入和扩展ViewGroup。

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.view.*;

public class ZoomableViewGroup extends ViewGroup {

    private static final int INVALID_POINTER_ID = 1;
    private int mActivePointerId = INVALID_POINTER_ID;

    private float mScaleFactor = 1;
    private ScaleGestureDetector mScaleDetector;
    private Matrix mScaleMatrix = new Matrix();
    private Matrix mScaleMatrixInverse = new Matrix();

    private float mPosX;
    private float mPosY;
    private Matrix mTranslateMatrix = new Matrix();
    private Matrix mTranslateMatrixInverse = new Matrix();

    private float mLastTouchX;
    private float mLastTouchY;

    private float mFocusY;

    private float mFocusX;

    private float[] mInvalidateWorkingArray = new float[6];
    private float[] mDispatchTouchEventWorkingArray = new float[2];
    private float[] mOnTouchEventWorkingArray = new float[2];


    public ZoomableViewGroup(Context context) {
        super(context);
        mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
        mTranslateMatrix.setTranslate(0, 0);
        mScaleMatrix.setScale(1, 1);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight());
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.save();
        canvas.translate(mPosX, mPosY);
        canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDispatchTouchEventWorkingArray[0] = ev.getX();
        mDispatchTouchEventWorkingArray[1] = ev.getY();
        mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
        ev.setLocation(mDispatchTouchEventWorkingArray[0],
                mDispatchTouchEventWorkingArray[1]);
        return super.dispatchTouchEvent(ev);
    }

    /**
     * Although the docs say that you shouldn't override this, I decided to do
     * so because it offers me an easy way to change the invalidated area to my
     * likening.
     */
    @Override
    public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

        mInvalidateWorkingArray[0] = dirty.left;
        mInvalidateWorkingArray[1] = dirty.top;
        mInvalidateWorkingArray[2] = dirty.right;
        mInvalidateWorkingArray[3] = dirty.bottom;


        mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray);
        dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]),
                Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3]));

        location[0] *= mScaleFactor;
        location[1] *= mScaleFactor;
        return super.invalidateChildInParent(location, dirty);
    }

    private float[] scaledPointsToScreenPoints(float[] a) {
        mScaleMatrix.mapPoints(a);
        mTranslateMatrix.mapPoints(a);
        return a;
    }

    private float[] screenPointsToScaledPoints(float[] a){
        mTranslateMatrixInverse.mapPoints(a);
        mScaleMatrixInverse.mapPoints(a);
        return a;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        mOnTouchEventWorkingArray[0] = ev.getX();
        mOnTouchEventWorkingArray[1] = ev.getY();

        mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

        ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
        mScaleDetector.onTouchEvent(ev);

        final int action = ev.getAction();
        switch (action & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN: {
                final float x = ev.getX();
                final float y = ev.getY();

                mLastTouchX = x;
                mLastTouchY = y;

                // Save the ID of this pointer
                mActivePointerId = ev.getPointerId(0);
                break;
            }

            case MotionEvent.ACTION_MOVE: {
                // Find the index of the active pointer and fetch its position
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(pointerIndex);
                final float y = ev.getY(pointerIndex);

                final float dx = x - mLastTouchX;
                final float dy = y - mLastTouchY;

                mPosX += dx;
                mPosY += dy;
                mTranslateMatrix.preTranslate(dx, dy);
                mTranslateMatrix.invert(mTranslateMatrixInverse);

                mLastTouchX = x;
                mLastTouchY = y;

                invalidate();
                break;
            }

            case MotionEvent.ACTION_UP: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_CANCEL: {
                mActivePointerId = INVALID_POINTER_ID;
                break;
            }

            case MotionEvent.ACTION_POINTER_UP: {
                // Extract the index of the pointer that left the touch sensor
                final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
                final int pointerId = ev.getPointerId(pointerIndex);
                if (pointerId == mActivePointerId) {
                    // This was our active pointer going up. Choose a new
                    // active pointer and adjust accordingly.
                    final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
                    mLastTouchX = ev.getX(newPointerIndex);
                    mLastTouchY = ev.getY(newPointerIndex);
                    mActivePointerId = ev.getPointerId(newPointerIndex);
                }
                break;
            }
        }
        return true;
    }

    private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

        @Override
        public boolean onScale(ScaleGestureDetector detector) {
            mScaleFactor *= detector.getScaleFactor();
            if (detector.isInProgress()) {
                mFocusX = detector.getFocusX();
                mFocusY = detector.getFocusY();
            }
            mScaleFactor = Math.max(0.1f, Math.min(mScaleFactor, 5.0f));
            mScaleMatrix.setScale(mScaleFactor, mScaleFactor,
                    mFocusX, mFocusY);
            mScaleMatrix.invert(mScaleMatrixInverse);
            invalidate();
            requestLayout();


            return true;
        }
    }
}

我在ZoomableViewGroup中添加了一个子视图,缩放后,子视图的触摸坐标变得错误。我该如何修复它? - Cuong Nguyen
2
对于像我一样遇到这个问题的人:将方法的顺序更改为:<br/> private float[] screenPointsToScaledPoints(float[] a) { mScaleMatrixInverse.mapPoints(a); mTranslateMatrixInverse.mapPoints(a); return a; }<br/> - Cuong Nguyen

14

根据给出的答案,我使用了这段代码来使平移和缩放功能正常工作。一开始在枢轴点方面遇到了问题。

public class ZoomableViewGroup extends ViewGroup {

    // these matrices will be used to move and zoom image
    private Matrix matrix = new Matrix();
    private Matrix matrixInverse = new Matrix();
    private Matrix savedMatrix = new Matrix();
    // we can be in one of these 3 states
    private static final int NONE = 0;
    private static final int DRAG = 1;
    private static final int ZOOM = 2;
    private int mode = NONE;
    // remember some things for zooming
    private PointF start = new PointF();
    private PointF mid = new PointF();
    private float oldDist = 1f;
    private float[] lastEvent = null;

    private boolean initZoomApplied=false;

    private float[] mDispatchTouchEventWorkingArray = new float[2];
    private float[] mOnTouchEventWorkingArray = new float[2];

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDispatchTouchEventWorkingArray[0] = ev.getX();
        mDispatchTouchEventWorkingArray[1] = ev.getY();
        mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
        ev.setLocation(mDispatchTouchEventWorkingArray[0],
                mDispatchTouchEventWorkingArray[1]);
        return super.dispatchTouchEvent(ev);
    }

    private float[] scaledPointsToScreenPoints(float[] a) {
        matrix.mapPoints(a);
        return a;
    }

    private float[] screenPointsToScaledPoints(float[] a){
        matrixInverse.mapPoints(a);
        return a;
    }

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

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

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

    /**
     * Determine the space between the first two fingers
     */
    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float)Math.sqrt(x * x + y * y);
    }

    /**
     * Calculate the mid point of the first two fingers
     */
    private void midPoint(PointF point, MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        point.set(x / 2, y / 2);
    }


    private void init(Context context){

    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(l, t, l+child.getMeasuredWidth(), t + child.getMeasuredHeight());
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        float[] values = new float[9];
        matrix.getValues(values);
        float container_width = values[Matrix.MSCALE_X]*widthSize;
        float container_height = values[Matrix.MSCALE_Y]*heightSize;

        //Log.d("zoomToFit", "m width: "+container_width+" m height: "+container_height);
        //Log.d("zoomToFit", "m x: "+pan_x+" m y: "+pan_y);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);

                if(i==0 && !initZoomApplied && child.getWidth()>0){
                    int c_w = child.getWidth();
                    int c_h = child.getHeight();

                    //zoomToFit(c_w, c_h, container_width, container_height);
                }
            }
        }        

    }

    private void zoomToFit(int c_w, int c_h, float container_width, float container_height){
        float proportion_firstChild = (float)c_w/(float)c_h;
        float proportion_container = container_width/container_height;

        //Log.d("zoomToFit", "firstChildW: "+c_w+" firstChildH: "+c_h);
        //Log.d("zoomToFit", "proportion-container: "+proportion_container);
        //Log.d("zoomToFit", "proportion_firstChild: "+proportion_firstChild);

        if(proportion_container<proportion_firstChild){
            float initZoom = container_height/c_h;
            //Log.d("zoomToFit", "adjust height with initZoom: "+initZoom);
            matrix.postScale(initZoom, initZoom);
            matrix.postTranslate(-1*(c_w*initZoom-container_width)/2, 0);
            matrix.invert(matrixInverse);
        }else {
            float initZoom = container_width/c_w;
            //Log.d("zoomToFit", "adjust width with initZoom: "+initZoom);
            matrix.postScale(initZoom, initZoom);
            matrix.postTranslate(0, -1*(c_h*initZoom-container_height)/2);
            matrix.invert(matrixInverse);
        }
        initZoomApplied=true;
        invalidate();
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        canvas.save();
        canvas.setMatrix(matrix);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // handle touch events here
        mOnTouchEventWorkingArray[0] = event.getX();
        mOnTouchEventWorkingArray[1] = event.getY();

        mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

        event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                savedMatrix.set(matrix);
                start.set(event.getX(), event.getY());
                mode = DRAG;
                lastEvent = null;
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                if (oldDist > 10f) {
                    savedMatrix.set(matrix);
                    midPoint(mid, event);
                    mode = ZOOM;
                }
                lastEvent = new float[4];
                lastEvent[0] = event.getX(0);
                lastEvent[1] = event.getX(1);
                lastEvent[2] = event.getY(0);
                lastEvent[3] = event.getY(1);
                //d = rotation(event);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                lastEvent = null;
                break;
            case MotionEvent.ACTION_MOVE:
                if (mode == DRAG) {
                    matrix.set(savedMatrix);
                    float dx = event.getX() - start.x;
                    float dy = event.getY() - start.y;
                    matrix.postTranslate(dx, dy);
                    matrix.invert(matrixInverse);
                } else if (mode == ZOOM) {
                    float newDist = spacing(event);
                    if (newDist > 10f) {
                        matrix.set(savedMatrix);
                        float scale = (newDist / oldDist);
                        matrix.postScale(scale, scale, mid.x, mid.y);
                        matrix.invert(matrixInverse);
                    }
                }
                break;
        }

        invalidate();
        return true;
    }

}

感谢 onTouch 函数的贡献者:http://judepereira.com/blog/multi-touch-in-android-translate-scale-and-rotate/,同时也感谢 Artjom 对触摸事件分派方法的贡献。
我添加了一个 zoomToFit 方法,但在此时被注释掉了,因为大多数人不需要它。它会将子元素适应容器的大小,并以第一个子元素作为缩放因子的参考。

如何限制缩放因子?例如,我希望缩放范围在0.75和4之间。 - techtinkerer
你可以定义一个MAX_ZOOM:int MAX_ZOOM = 4;,然后在发布比例(matrix.postScale)之前添加以下行:float[] values = new float[9]; matrix.getValues(values); if(scale*values[Matrix.MSCALE_X] >= MAX_ZOOM){ scale = MAX_ZOOM/values[Matrix.MSCALE_X]; } - Thomas
好的代码!缩放功能更好(立即开始,这在Alex的代码中不是这样),而且没有枢轴点的问题。然而,它不允许使用双击拖动操作进行缩放。 - Donkey
在我的手机上(Nexus 4,CyanogenMod13,Android 6.0.1),子视图从状态栏下方开始,而ZoomableViewGroup则从状态栏正下方开始。如果在dispatchDraw中将canvas.setMatrix(matrix);替换为:float[] values = new float[9]; matrix.getValues(values); canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]); canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]);,它就可以正常工作... - Donkey
默认情况下,似乎忽略了边距。为了支持它们,在 onLayout 中更改行为 child.layout(child.getLeft(), child.getTop(), child.getLeft() + child.getMeasuredWidth(), child.getTop() + child.getMeasuredHeight());。此外,在该方法的开头添加 super.onLayout(changed, l, t, r, b) - Jake Lee
要更改最小和最大缩放因子,您应该实际更改行mScaleFactor = Math.max(1.0f, Math.min(mScaleFactor, 2.0f));为您的最小和最大浮点数。 - Jake Lee

14

Thomas的回答几乎是最好的(在我的手机上有一个位置错误):缩放立即开始(这在Alex的代码中不是这样),并且缩放在正确的支点处进行。

然而,与Alex的代码相反,不能使用“双击拖动”手势进行缩放(这不是一个广为人知的手势,但像在Google Chrome或Google Maps应用程序中一样只需一个手指就可以缩放非常有用)。因此,这里对Thomas的代码进行了修改,以使其成为可能(并修��子视图位置错误):

public class ZoomableView extends ViewGroup {

    // States.
    private static final byte NONE = 0;
    private static final byte DRAG = 1;
    private static final byte ZOOM = 2;

    private byte mode = NONE;

    // Matrices used to move and zoom image.
    private Matrix matrix = new Matrix();
    private Matrix matrixInverse = new Matrix();
    private Matrix savedMatrix = new Matrix();

    // Parameters for zooming.
    private PointF start = new PointF();
    private PointF mid = new PointF();
    private float oldDist = 1f;
    private float[] lastEvent = null;
    private long lastDownTime = 0l;

    private float[] mDispatchTouchEventWorkingArray = new float[2];
    private float[] mOnTouchEventWorkingArray = new float[2];


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDispatchTouchEventWorkingArray[0] = ev.getX();
        mDispatchTouchEventWorkingArray[1] = ev.getY();
        mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
        ev.setLocation(mDispatchTouchEventWorkingArray[0], mDispatchTouchEventWorkingArray[1]);
        return super.dispatchTouchEvent(ev);
    }

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

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

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


    private void init(Context context) {

    }


    /**
     * Determine the space between the first two fingers
     */
    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    /**
     * Calculate the mid point of the first two fingers
     */
    private void midPoint(PointF point, MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        point.set(x / 2, y / 2);
    }

    private float[] scaledPointsToScreenPoints(float[] a) {
        matrix.mapPoints(a);
        return a;
    }

    private float[] screenPointsToScaledPoints(float[] a) {
        matrixInverse.mapPoints(a);
        return a;
    }


    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
            }
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        float[] values = new float[9];
        matrix.getValues(values);
        canvas.save();
        canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]);
        canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]);
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // handle touch events here
        mOnTouchEventWorkingArray[0] = event.getX();
        mOnTouchEventWorkingArray[1] = event.getY();

        mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

        event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                savedMatrix.set(matrix);
                mode = DRAG;
                lastEvent = null;
                long downTime = event.getDownTime();
                if (downTime - lastDownTime < 300l) {
                    float density = getResources().getDisplayMetrics().density;
                    if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) < 40.f * density) {
                        savedMatrix.set(matrix);
                        mid.set(event.getX(), event.getY());
                        mode = ZOOM;
                        lastEvent = new float[4];
                        lastEvent[0] = lastEvent[1] = event.getX();
                        lastEvent[2] = lastEvent[3] = event.getY();
                    }
                    lastDownTime = 0l;
                } else {
                    lastDownTime = downTime;
                }
                start.set(event.getX(), event.getY());
                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                if (oldDist > 10f) {
                    savedMatrix.set(matrix);
                    midPoint(mid, event);
                    mode = ZOOM;
                }
                lastEvent = new float[4];
                lastEvent[0] = event.getX(0);
                lastEvent[1] = event.getX(1);
                lastEvent[2] = event.getY(0);
                lastEvent[3] = event.getY(1);
                break;
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                lastEvent = null;
                break;
            case MotionEvent.ACTION_MOVE:
                final float density = getResources().getDisplayMetrics().density;
                if (mode == DRAG) {
                    matrix.set(savedMatrix);
                    float dx = event.getX() - start.x;
                    float dy = event.getY() - start.y;
                    matrix.postTranslate(dx, dy);
                    matrix.invert(matrixInverse);
                    if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) > 20.f * density) {
                        lastDownTime = 0l;
                    }
                } else if (mode == ZOOM) {
                    if (event.getPointerCount() > 1) {
                        float newDist = spacing(event);
                        if (newDist > 10f * density) {
                            matrix.set(savedMatrix);
                            float scale = (newDist / oldDist);
                            matrix.postScale(scale, scale, mid.x, mid.y);
                            matrix.invert(matrixInverse);
                        }
                    } else {
                        matrix.set(savedMatrix);
                        float scale = event.getY() / start.y;
                        matrix.postScale(scale, scale, mid.x, mid.y);
                        matrix.invert(matrixInverse);
                    }
                }
                break;
        }

        invalidate();
        return true;
    }

}

如何在 onTouchEvent 中获取转换后的 x 和 y 坐标? - isuru
1
我对Thomas的回答有些疑问。我的视图的点击区域在实际视图下方(适用于Android < 25 API)。我发现问题出在dispatchDraw方法上。由于我对矩阵一无所知,因此我非常沮丧。你的dispatchDraw解决了这个问题。我真的想了解这种魔法是如何工作的。我想学习矩阵和ViewPort技术以及Rect对象。你能推荐一些好的学习资源吗?当我无法理解它的工作原理时,我真的不喜欢去做某件事情。感谢您提供的代码,也感谢@Thomas。 - Kotsu
这是最佳解决方案。 - Chandru
我对这个解决方案有问题。原本这个解决方案是完美的,但只有在这个ViewGroup里面不包含可点击的视图时才有效,因为子视图的点击会接收到 onTouchEvent()。你能否看看是否可以添加支持带有可点击视图的拖动?我认为使用 onInterceptTouchEvent() 可能行得通,但我不是处理触摸事件的专家。谢谢。 - ezefire
@Donkey!非常感谢您!!我使用了Thomas的答案并进行了一些修改,但是在低级API方面遇到了很多问题。您的答案救了我 :) - Quima
显示剩余2条评论

1
我正在使用这里发布的一些修改版本的代码。这个ZoomLayout使用Android手势识别器来进行滚动和缩放。它还可以在缩放或平移时保留中心点和边界。

https://github.com/maxtower/ZoomLayout/blob/master/app/src/main/java/com/maxtower/testzoomlayout/ZoomLayout.java

保留平移边界:

if (contentSize != null)
    {
        float[] values = new float[9];
        matrix.getValues(values);
        float totX = values[Matrix.MTRANS_X] + distanceX;
        float totY = values[Matrix.MTRANS_Y] + distanceY;
        float sx = values[Matrix.MSCALE_X];

        Rect viewableRect = new Rect();
        ZoomLayout.this.getDrawingRect(viewableRect);
        float offscreenWidth = contentSize.width() - (viewableRect.right - viewableRect.left);
        float offscreenHeight = contentSize.height() - (viewableRect.bottom - viewableRect.top);
        float maxDx = (contentSize.width() - (contentSize.width() / sx)) * sx;
        float maxDy = (contentSize.height() - (contentSize.height() / sx)) * sx;
        if (totX > 0 && distanceX > 0)
        {
            distanceX = 0;
        }
        if (totY > 0 && distanceY > 0)
        {
            distanceY = 0;
        }

        if(totX*-1 > offscreenWidth+maxDx && distanceX < 0)
        {
            distanceX = 0;
        }
        if(totY*-1 > offscreenHeight+maxDy && distanceY < 0)
        {
            distanceY = 0;
        }

    }

1

这个自定义视图是 Android 标准 imageView 的子类,并在其基础上添加了(多点)触摸平移和缩放功能(以及双击缩放功能):

https://github.com/sephiroth74/ImageViewZoom

http://blog.sephiroth.it/2011/04/04/imageview-zoom-and-scroll/

它类似于您已经了解的MikeOrtiz的TouchImageView,但添加了一些更多的功能。

您可以将其与其他所需的textView一起放在视图“堆栈”(Android FrameLayout或类似物)中使用。(我的意思是一堆视图,就像一堆盘子或一叠卡片。换句话说,一堆视图在Z轴上一个接一个地堆叠。)

将所有视图一起移动需要您控制Android手势(多点触控)机制并编写所需的代码。对于您(相当复杂的)要求,没有任何现成的解决方案。请查看本文:

http://android-developers.blogspot.it/2010/06/making-sense-of-multitouch.html


1
我提供了以下解决方案(结合了您的代码和我的一些想法):
  • 双击缩放和取消缩放
  • 单指双击缩放和取消缩放
  • 平移功能正常
  • 捏合缩放正常,它会在您指向的位置进行缩放
  • 子视图是可触摸的
  • 带有边框的布局!(无法通过取消缩放或平移来离开布局)

它没有动画效果,但完全可以使用。享受吧。

用法:

<com.yourapppath.ZoomableViewGroup
    android:id="@+id/zoomControl"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <FrameLayout
        android:id="@+id/frameLayoutZoom"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/planImageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:src="@drawable/yourdrawable"
            android:scaleType="center" />

    </FrameLayout>
</com.yourapppath.ZoomableViewGroup>

这里是zoomableViewGroup的Java文件,只需复制并使用:

public class ZoomableViewGroup extends ViewGroup {

    private boolean doubleTap = false;


    private float MIN_ZOOM = 1f;
    private float MAX_ZOOM = 2.5f;
    private float[] topLeftCorner = {0, 0};
    private float scaleFactor;

    // States.
    private static final byte NONE = 0;
    private static final byte DRAG = 1;
    private static final byte ZOOM = 2;

    private byte mode = NONE;

    // Matrices used to move and zoom image.
    private Matrix matrix = new Matrix();
    private Matrix matrixInverse = new Matrix();
    private Matrix savedMatrix = new Matrix();

    // Parameters for zooming.
    private PointF start = new PointF();
    private PointF mid = new PointF();
    private float oldDist = 1f;
    private float[] lastEvent = null;
    private long lastDownTime = 0l;
    private long downTime = 0l;

    private float[] mDispatchTouchEventWorkingArray = new float[2];
    private float[] mOnTouchEventWorkingArray = new float[2];


    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        mDispatchTouchEventWorkingArray[0] = ev.getX();
        mDispatchTouchEventWorkingArray[1] = ev.getY();
        mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
        ev.setLocation(mDispatchTouchEventWorkingArray[0], mDispatchTouchEventWorkingArray[1]);
        return super.dispatchTouchEvent(ev);
    }

    public ZoomableViewGroup(Context context) {
        super(context);
    }

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

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


    /**
     * Determine the space between the first two fingers
     */
    private float spacing(MotionEvent event) {
        float x = event.getX(0) - event.getX(1);
        float y = event.getY(0) - event.getY(1);
        return (float) Math.sqrt(x * x + y * y);
    }

    /**
     * Calculate the mid point of the first two fingers
     */
    private void midPoint(PointF point, MotionEvent event) {
        float x = event.getX(0) + event.getX(1);
        float y = event.getY(0) + event.getY(1);
        point.set(x / 2, y / 2);
    }

    private float[] scaledPointsToScreenPoints(float[] a) {
        matrix.mapPoints(a);
        return a;
    }

    private float[] screenPointsToScaledPoints(float[] a) {
        matrixInverse.mapPoints(a);
        return a;
    }


    @Override
    public void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                child.layout(left, top, left + child.getMeasuredWidth(), top + child.getMeasuredHeight());
            }
        }
    }

    @Override
    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
       super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (child.getVisibility() != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    @Override
    public void dispatchDraw(Canvas canvas) {
        float[] values = new float[9];
        matrix.getValues(values);
        canvas.save();
        canvas.translate(values[Matrix.MTRANS_X], values[Matrix.MTRANS_Y]);
        canvas.scale(values[Matrix.MSCALE_X], values[Matrix.MSCALE_Y]);
        topLeftCorner[0] = values[Matrix.MTRANS_X];
        topLeftCorner[1] = values[Matrix.MTRANS_Y];
        scaleFactor = values[Matrix.MSCALE_X];
        super.dispatchDraw(canvas);
        canvas.restore();
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // handle touch events here
        mOnTouchEventWorkingArray[0] = event.getX();
        mOnTouchEventWorkingArray[1] = event.getY();

        mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

        event.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);

        switch (event.getAction() & MotionEvent.ACTION_MASK) {
            case MotionEvent.ACTION_DOWN:
                savedMatrix.set(matrix);
                mode = DRAG;
                lastEvent = null;
                downTime = SystemClock.elapsedRealtime();
                if (downTime - lastDownTime < 250l) {
                    doubleTap = true;
                    float density = getResources().getDisplayMetrics().density;
                    if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) < 40.f * density) {
                        savedMatrix.set(matrix);                                                         //repetition of savedMatrix.setmatrix
                        mid.set(event.getX(), event.getY());
                        mode = ZOOM;
                        lastEvent = new float[4];
                        lastEvent[0] = lastEvent[1] = event.getX();
                        lastEvent[2] = lastEvent[3] = event.getY();
                    }    
                    lastDownTime = 0l;
                } else {
                    doubleTap = false;
                    lastDownTime = downTime;
                }
                start.set(event.getX(), event.getY());

                break;
            case MotionEvent.ACTION_POINTER_DOWN:
                oldDist = spacing(event);
                if (oldDist > 10f) {
                    savedMatrix.set(matrix);
                    midPoint(mid, event);
                    mode = ZOOM;
                }
                lastEvent = new float[4];
                lastEvent[0] = event.getX(0);
                lastEvent[1] = event.getX(1);
                lastEvent[2] = event.getY(0);
                lastEvent[3] = event.getY(1);
                break;
            case MotionEvent.ACTION_UP:


                if (doubleTap && scaleFactor < 1.8f){
                    matrix.postScale(2.5f/scaleFactor, 2.5f/scaleFactor, mid.x, mid.y);
                } else if(doubleTap && scaleFactor >= 1.8f){
                    matrix.postScale(1.0f/scaleFactor, 1.0f/scaleFactor, mid.x, mid.y);
                }

                Handler handler = new Handler();
                handler.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        if(topLeftCorner[0] >= 0){
                            matrix.postTranslate(-topLeftCorner[0],0);
                        } else if (topLeftCorner[0] < -getWidth()*(scaleFactor-1)){
                            matrix.postTranslate((-topLeftCorner[0]) - getWidth()*(scaleFactor-1) ,0);
                        }
                        if(topLeftCorner[1] >= 0){
                            matrix.postTranslate(0,-topLeftCorner[1]);
                        } else if (topLeftCorner[1] < -getHeight()*(scaleFactor-1)){
                            matrix.postTranslate(0,(-topLeftCorner[1]) - getHeight()*(scaleFactor-1));
                        }
                        matrix.invert(matrixInverse);
                        invalidate();
                    }
                }, 1);

                break;

            case MotionEvent.ACTION_POINTER_UP:
                mode = NONE;
                lastEvent = null;
                break;
            case MotionEvent.ACTION_MOVE:

                final float density = getResources().getDisplayMetrics().density;
                if (mode == DRAG) {
                    matrix.set(savedMatrix);
                    float dx = event.getX() - start.x;
                    float dy = event.getY() - start.y;
                    matrix.postTranslate(dx, dy);
                    matrix.invert(matrixInverse);
                    if (Math.max(Math.abs(start.x - event.getX()), Math.abs(start.y - event.getY())) > 20.f * density) {
                        lastDownTime = 0l;
                    }
                } else if (mode == ZOOM) {
                    if (event.getPointerCount() > 1) {
                        float newDist = spacing(event);
                        if (newDist > 10f * density) {
                            matrix.set(savedMatrix);
                            float scale = (newDist / oldDist);
                            float[] values = new float[9];
                            matrix.getValues(values);
                            if (scale * values[Matrix.MSCALE_X] >= MAX_ZOOM) {
                                scale = MAX_ZOOM / values[Matrix.MSCALE_X];
                            }
                            if (scale * values[Matrix.MSCALE_X] <= MIN_ZOOM) {
                                scale = MIN_ZOOM / values[Matrix.MSCALE_X];
                            }
                            matrix.postScale(scale, scale, mid.x, mid.y);
                            matrix.invert(matrixInverse);
                        }
                    } else {
                        if ( SystemClock.elapsedRealtime() - downTime > 250l) {
                            doubleTap = false;
                        }
                        matrix.set(savedMatrix);
                        float scale = event.getY() / start.y;
                        float[] values = new float[9];
                        matrix.getValues(values);
                        if (scale * values[Matrix.MSCALE_X] >= MAX_ZOOM) {
                            scale = MAX_ZOOM / values[Matrix.MSCALE_X];
                        }
                        if (scale * values[Matrix.MSCALE_X] <= MIN_ZOOM) {
                            scale = MIN_ZOOM / values[Matrix.MSCALE_X];
                        }
                        matrix.postScale(scale, scale, mid.x, mid.y);
                        matrix.invert(matrixInverse);
                    }
                }
                break;
        }


        invalidate();
        return true;
    }

}

如果我们将一个片段添加到这个ViewGroup中,它并没有按照预期工作。我有什么遗漏吗? - NKR

0
为了获得更好的性能,可以在Alex的代码中添加以下更改来实现缩放。
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

    @Override
    public boolean onScale(ScaleGestureDetector detector) {
        mScaleFactor *= detector.getScaleFactor();
        if (detector.isInProgress()) {
            mFocusX = detector.getFocusX();
            mFocusY = detector.getFocusY();
        }

        mFocusX = (mFocusX + mLastTouchX)/2;  // get center of touch
        mFocusY = (mFocusY + mLastTouchY)/2;  // get center of touch

        mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 2.0f));
        mScaleMatrix.setScale(mScaleFactor, mScaleFactor,mFocusX, mFocusY);
        mScaleMatrix.invert(mScaleMatrixInverse);
        invalidate();
        requestLayout();

        return true;
    }
}

0

对于那些对缩放/平移LinearLayout感兴趣的人,我修改了Alex发布的版本,使其垂直布局并将平移限制在可见视图范围内。我使用它来处理PDFRenderer中的位图。我已经测试过了,但如果您注意到任何错误,请发帖告诉我,我也想知道!

注意:我选择不实现双击操作,因为QuickScale可以胜任。

public class ZoomableLinearLayout extends ViewGroup {

   private static final int INVALID_POINTER_ID = 1;
   private int mActivePointerId = INVALID_POINTER_ID;

   private float mScaleFactor = 1;
   private ScaleGestureDetector mScaleDetector;
   private Matrix mScaleMatrix = new Matrix();
   private Matrix mScaleMatrixInverse = new Matrix();

   private float mPosX;
   private float mPosY;
   private Matrix mTranslateMatrix = new Matrix();
   private Matrix mTranslateMatrixInverse = new Matrix();

   private float mLastTouchX;
   private float mLastTouchY;

   private float mFocusY;
   private float mFocusX;

   private int mCanvasWidth;
   private int mCanvasHeight;

   private float[] mInvalidateWorkingArray = new float[6];
   private float[] mDispatchTouchEventWorkingArray = new float[2];
   private float[] mOnTouchEventWorkingArray = new float[2];

   private boolean mIsScaling;

   public ZoomableLinearLayout(Context context) {
      super(context);
      mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
      mTranslateMatrix.setTranslate(0, 0);
      mScaleMatrix.setScale(1, 1);
   }

   public ZoomableLinearLayout(Context context, AttributeSet attributeSet) {
      super(context, attributeSet);
      mScaleDetector = new ScaleGestureDetector(context, new ScaleListener());
      mTranslateMatrix.setTranslate(0, 0);
      mScaleMatrix.setScale(1, 1);
   }

   @Override
   protected void onLayout(boolean changed, int l, int t, int r, int b) {
      int childCount = getChildCount();
      for (int i = 0; i < childCount; i++) {
         View child = getChildAt(i);
         if (child.getVisibility() != GONE) {
            child.layout(l, t, l+child.getMeasuredWidth(), t += child.getMeasuredHeight());
         }
      }
   }

   @Override
   protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);

      int height = 0;
      int width = 0;
      int childCount = getChildCount();
      for (int i = 0; i < childCount; i++) {
         View child = getChildAt(i);
         if (child.getVisibility() != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
            height += child.getMeasuredHeight();
            width = Math.max(width, child.getMeasuredWidth());
         }
      }
      mCanvasWidth = width;
      mCanvasHeight = height;
   }

   @Override
   protected void dispatchDraw(Canvas canvas) {
      canvas.save();
      canvas.translate(mPosX, mPosY);
      canvas.scale(mScaleFactor, mScaleFactor, mFocusX, mFocusY);
      super.dispatchDraw(canvas);
      canvas.restore();
   }

   @Override
   public boolean dispatchTouchEvent(MotionEvent ev) {
      mDispatchTouchEventWorkingArray[0] = ev.getX();
      mDispatchTouchEventWorkingArray[1] = ev.getY();
      mDispatchTouchEventWorkingArray = screenPointsToScaledPoints(mDispatchTouchEventWorkingArray);
      ev.setLocation(mDispatchTouchEventWorkingArray[0],
            mDispatchTouchEventWorkingArray[1]);
      return super.dispatchTouchEvent(ev);
   }

   /**
    * Although the docs say that you shouldn't override this, I decided to do
    * so because it offers me an easy way to change the invalidated area to my
    * likening.
    */
   @Override
   public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

      mInvalidateWorkingArray[0] = dirty.left;
      mInvalidateWorkingArray[1] = dirty.top;
      mInvalidateWorkingArray[2] = dirty.right;
      mInvalidateWorkingArray[3] = dirty.bottom;

      mInvalidateWorkingArray = scaledPointsToScreenPoints(mInvalidateWorkingArray);
      dirty.set(Math.round(mInvalidateWorkingArray[0]), Math.round(mInvalidateWorkingArray[1]),
            Math.round(mInvalidateWorkingArray[2]), Math.round(mInvalidateWorkingArray[3]));

      location[0] *= mScaleFactor;
      location[1] *= mScaleFactor;
      return super.invalidateChildInParent(location, dirty);
   }

   private float[] scaledPointsToScreenPoints(float[] a) {
      mScaleMatrix.mapPoints(a);
      mTranslateMatrix.mapPoints(a);
      return a;
   }

   private float[] screenPointsToScaledPoints(float[] a){
      mTranslateMatrixInverse.mapPoints(a);
      mScaleMatrixInverse.mapPoints(a);
      return a;
   }

   @Override
   public boolean onTouchEvent(MotionEvent ev) {
      mOnTouchEventWorkingArray[0] = ev.getX();
      mOnTouchEventWorkingArray[1] = ev.getY();

      mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);

      ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
      mScaleDetector.onTouchEvent(ev);

      final int action = ev.getAction();
      switch (action & MotionEvent.ACTION_MASK) {
         case MotionEvent.ACTION_DOWN: {
            final float x = ev.getX();
            final float y = ev.getY();

            mLastTouchX = x;
            mLastTouchY = y;

            // Save the ID of this pointer
            mActivePointerId = ev.getPointerId(0);
            break;
         }

         case MotionEvent.ACTION_MOVE: {
            // Find the index of the active pointer and fetch its position
            final int pointerIndex = ev.findPointerIndex(mActivePointerId);
            final float x = ev.getX(pointerIndex);
            final float y = ev.getY(pointerIndex);

            if (mIsScaling && ev.getPointerCount() == 1) {
               // Don't move during a QuickScale.
               mLastTouchX = x;
               mLastTouchY = y;

               break;
            }

            float dx = x - mLastTouchX;
            float dy = y - mLastTouchY;

            float[] topLeft = {0f, 0f};
            float[] bottomRight = {getWidth(), getHeight()};
            /*
             * Corners of the view in screen coordinates, so dx/dy should not be allowed to
             * push these beyond the canvas bounds.
             */
            float[] scaledTopLeft = screenPointsToScaledPoints(topLeft);
            float[] scaledBottomRight = screenPointsToScaledPoints(bottomRight);

            dx = Math.min(Math.max(dx, scaledBottomRight[0] - mCanvasWidth), scaledTopLeft[0]);
            dy = Math.min(Math.max(dy, scaledBottomRight[1] - mCanvasHeight), scaledTopLeft[1]);

            mPosX += dx;
            mPosY += dy;

            mTranslateMatrix.preTranslate(dx, dy);
            mTranslateMatrix.invert(mTranslateMatrixInverse);

            mLastTouchX = x;
            mLastTouchY = y;

            invalidate();
            break;
         }

         case MotionEvent.ACTION_UP: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
         }

         case MotionEvent.ACTION_CANCEL: {
            mActivePointerId = INVALID_POINTER_ID;
            break;
         }

         case MotionEvent.ACTION_POINTER_UP: {
            // Extract the index of the pointer that left the touch sensor
            final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
            final int pointerId = ev.getPointerId(pointerIndex);
            if (pointerId == mActivePointerId) {
               // This was our active pointer going up. Choose a new
               // active pointer and adjust accordingly.
               final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
               mLastTouchX = ev.getX(newPointerIndex);
               mLastTouchY = ev.getY(newPointerIndex);
               mActivePointerId = ev.getPointerId(newPointerIndex);
            }
            break;
         }
      }
      return true;
   }

   private float getMaxScale() {
      return 2f;
   }

   private float getMinScale() {
      return 1f;
   }

   private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
      @Override
      public boolean onScaleBegin(ScaleGestureDetector detector) {
         mIsScaling = true;

         mFocusX = detector.getFocusX();
         mFocusY = detector.getFocusY();

         float[] foci = {mFocusX, mFocusY};
         float[] scaledFoci = screenPointsToScaledPoints(foci);

         mFocusX = scaledFoci[0];
         mFocusY = scaledFoci[1];

         return true;
      }

      @Override
      public void onScaleEnd(ScaleGestureDetector detector) {
         mIsScaling = false;
      }

      @Override
      public boolean onScale(ScaleGestureDetector detector) {
         mScaleFactor *= detector.getScaleFactor();
         mScaleFactor = Math.max(getMinScale(), Math.min(mScaleFactor, getMaxScale()));
         mScaleMatrix.setScale(mScaleFactor, mScaleFactor, mFocusX, mFocusY);
         mScaleMatrix.invert(mScaleMatrixInverse);
         invalidate();

         return true;
      }
   }

}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接