【问题标题】:View with horizontal and vertical pan/drag and pinch-zoom使用水平和垂直平移/拖动和捏缩放查看
【发布时间】:2012-09-10 21:14:49
【问题描述】:

是否可以有一个支持水平和垂直平移/拖动的视图。最重要的是,我希望能够捏缩放和双击缩放。 这个视图是否存在于 Android 上,或者有人知道有一个项目吗?

为了让它变得更加困难,需要将另一个视图(Button、TextView、VideoView、...)添加到视图中。当第一个/父视图放大或移动时,子视图(按钮)需要随着父视图移动。

我尝试了多种解决方案,但没有一个具有我正在寻找的所有选项。

【问题讨论】:

  • 嗨,你得到这个问题的最终解决方案了吗?可以分享一下吗?
  • 如果您能分享您的解决方案,那就太好了...

标签: android android-layout


【解决方案1】:

我认为有可能实现您想要的,但据我所知,有内置解决方案。从你问题的第二部分,我猜你不想要一个可缩放的View,而是一个ViewGroup,它是所有可以包含其他视图(例如布局)的视图的超类。下面是一些代码,您可以从构建自己的 ViewGroup 开始,其中大部分来自 this 博客文章:

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 中布局子元素有任何疑问,我发现此 video 非常有帮助,或者如果您对单个方法的工作方式有任何疑问,或者有任何其他问题,请随时在 cmets 中提问。

【讨论】:

  • 这里有一些错误,包括您需要扩展 ViewGroup 并且您错过了一些'}'。
  • @Artjom 我想知道,为什么不在视图组上使用 setScaleX/Y 而不必自己转换剪辑、接触点等?
  • View 达到 1:1 比例时是否可以停止缩小?实现起来简单吗?
  • 必须进行一些修改才能使其正常工作,但它确实可以完成工作。最终使用了单个矩阵和基于this 的 onTouchEvent 方法。
  • 翻译时如何限制ZoomableViewGroup?
【解决方案2】:

此自定义视图是 Android 标准 imageView 的子类,并向其添加(多点)触控平移和缩放(以及双击缩放):

https://github.com/sephiroth74/ImageViewZoom

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

它类似于您已经知道的 MikeOrtiz 的 TouchImageView,但增加了一些功能。

您可以在视图“堆栈”(Android FrameLayout 或类似的东西)中使用它以及您需要的其他 textView。 (我的意思是“一堆”视图,比如一堆盘子或一堆卡片。换句话说,一堆视图在 Z 轴上一个接一个。)

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

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

【讨论】:

    【解决方案3】:

    重新发布@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中添加了一个子视图,缩放后,子视图的触摸坐标会出错。我该如何解决?
    • 对于像我这样面临问题的人:将方法的顺序
      private float[] screenPointsToScaledPoints(float[] a)改为:
      private float[] screenPointsToScaledPoints(float[] a) { mScaleMatrixInverse.mapPoints(a); mTranslateMatrixInverse.mapPoints(a); return a; }
    【解决方案4】:

    根据给定的答案,我使用此代码使平移和缩放功能正常工作。一开始的枢轴点有问题。

    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 之间。
    • 您可以定义一个MAX_ZOOM:int MAX_ZOOM = 4;,然后在发布比例之前添加此行(matrix.postScale):float[] values = new float[9]; matrix.getValues(values); if(scale*values[Matrix.MSCALE_X] &gt;= MAX_ZOOM){ scale = MAX_ZOOM/values[Matrix.MSCALE_X]; }
    • 好代码!缩放效果更好(立即开始,在 Alex 的代码中不是这种情况)并且没有枢轴点问题。但是,它不允许通过双击拖动操作进行缩放。
    • 在我的手机(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]);,则效果很好...
    • 默认情况下似乎忽略了边距。为了支持他们,请将onLayout 中的行更改为child.layout(child.getLeft(), child.getTop(), child.getLeft() + child.getMeasuredWidth(), child.getTop() + child.getMeasuredHeight());。此外,将super.onLayout(changed, l, t, r, b) 添加到方法的开头。
    【解决方案5】:

    为了在 Alex 的代码中获得更好的 Zooming 性能,请添加以下更改

    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;
        }
    }
    

    【讨论】:

      【解决方案6】:

      Thomas 答案几乎是最好的(我的手机上有一个位置错误):缩放立即开始(Alex 的代码不是这种情况),并且缩放是在正确的枢轴上进行的-点。

      但是,与Alex 的代码相反,无法使用“双击-拖动”手势进行缩放(不是众所周知的手势,但仅用一根手指进行缩放非常有用,例如谷歌浏览器或谷歌地图应用程序)。所以这里对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 坐标?
      • 我对 Thomas 的回答有疑问。我的视图的命中框低于实际视图的位置(对于 android
      • 这是最好的解决方案。
      • 我对此解决方案有疑问。本来这个方案是完美的,但前提是这个viewgroup里面不包含可点击的view,因为子view的点击接收到onTouchEvent()。你能看看你是否可以添加对内部可点击视图的拖动支持?我认为使用 onInterceptTouchEvent() 是可能的,但我不是管理触摸事件的专家。谢谢
      • 惊人的答案好友! ,没有人提到如何使用它,schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> put_your_views_here 。 com.movablefloatingbutton.ZoomableView>
      【解决方案7】:

      对于那些对缩放/平移 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;
            }
         }
      
      }
      

      【讨论】:

        【解决方案8】:

        我正在使用此处发布的代码的一些修改版本。此 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;
                }
        
            }
        

        【讨论】:

          【解决方案9】:

          我带来了这个解决方案(你的代码和我的一些想法的混合):

          • 双击缩放和取消缩放
          • 双击后缩放和取消缩放(用一根手指)
          • 左右平移正常
          • 捏缩放ok,它会缩放你指向的地方
          • 子视图是可触摸的
          • 带边框的布局! (不能通过取消缩放或平移来脱离布局)

          它没有动画,但可以正常工作。享受

          用法:

          <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;
              }
          
          }
          

          【讨论】:

          • 如果我们将片段添加到此视图组中,它不会按预期工作。我错过了什么吗?
          猜你喜欢
          • 2023-03-27
          • 1970-01-01
          • 2013-12-12
          • 1970-01-01
          • 2019-10-12
          • 1970-01-01
          • 2013-03-11
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多