Подтвердить что ты не робот

Просмотр с горизонтальным и вертикальным панорамированием/перетаскиванием и усилением

Возможно ли иметь представление, которое поддерживает горизонтальное и вертикальное панорамирование/перетаскивание. Кроме того, я хочу, чтобы иметь возможность щепотку, чтобы увеличить и дважды коснуться для увеличения. Существует ли это мнение на Android или кто-то знает проект, который делает?

Чтобы сделать это еще сложнее, к представлению нужно добавить другое представление (Button, TextView, VideoView,...). Когда первое/родительское представление увеличено или перемещено, подвью (Button) необходимо перемещать с родительским.

Я пробовал несколько решений, но ни один из них не имеет всех параметров, которые я ищу.

4b9b3361

Ответ 1

Я думаю, что можно добиться того, чего вы хотите, но есть, насколько я знаю, в нем решение. Из второй части вашего вопроса я предполагаю, что вам не нужен масштабируемый View, но ViewGroup, который является суперклассом всех представлений, которые могут содержать другое представление (например, макеты). Вот какой код вы можете начать с создания своей собственной ViewGroup, большинство из которых происходит от этого сообщения в блоге:

public class ZoomableViewGroup {

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.

Если у вас есть вопросы о том, как развернуть дочерние элементы в вашей группе просмотра, я нашел этот видео очень полезным, или если у вас есть дополнительные вопросы о том, как работают одни методы или что-то еще, бесплатно спросить в комментариях.

Ответ 2

Отказ от ответа @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;
        }
    }
}

Ответ 3

Основываясь на данных ответах, я использовал этот код, чтобы заставить функции панорамирования и масштабирования работать. Сначала были проблемы с опорными точками.

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, который комментируется в этот момент, потому что большинству людей это не понадобится. Он подходит детям к размеру контейнера и берет первого ребенка в качестве ссылки для scalefactor.

Ответ 4

Томас ответ почти самый лучший (у меня одна ошибка на моем телефоне): зум начинается сразу (это не так с Alex), а масштаб сделан в правой точке поворота.

Однако, в отличие от Alex, невозможно увеличить масштаб с помощью жестов "двойного касания" (не очень известный жест, но очень полезный один для масштабирования одним пальцем, например, в приложениях Google Chrome или Google Maps). Итак, вот модификация Томаса, чтобы сделать это возможным (и исправление ошибки позиции subview):

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

}

Ответ 5

Этот пользовательский вид является подклассом стандартного образа изображения Android и добавляет к нему (мульти) касание панорамирования и масштабирования (а также масштабирование с двойным нажатием):

https://github.com/sephiroth74/ImageViewZoom

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

Он похож на MikeOrtiz TouchImageView, который вы уже знаете, но добавляет еще несколько функций.

Вы можете использовать его в виде "стека" (Android FrameLayout или что-то в этом роде) вместе с другими текстовыми элементами, которые вам нужны. (Я имею в виду "кучу" взглядов, например, кучу блюд или стопку карт. Другими словами, куча представлений сложена одна над другой по оси Z.)

Перемещение всех ваших взглядов требует, чтобы вы взяли на себя управление механизмом Android Gestures (multitouch) и записали требуемый код. Нет готового к использованию решения для вашего (довольно сложного) требования. Взгляните на эту статью:

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

Ответ 6

Чтобы улучшить настройку Масштабирования в коде 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;
    }
}

Ответ 7

Для тех, кто заинтересован в масштабировании/панорамировании LinearLayout, я изменил версию, опубликованную Алексом, чтобы выложить вещи вертикально и закрыл панорамирование видимыми видами. Я использую это для растровых изображений из 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;
        }

    }