Vista con desplazamiento vertical / horizontal y vertical y zoom de pellizco

¿Es posible tener una vista que admita pan / drag horizontal y vertical? Además de eso, quiero poder pellizcar para acercar y hacer doble toque para acercar. ¿Esta vista existe en Android o alguien sabe un proyecto que lo hace?

Para hacerlo aún más difícil, se necesita agregar a la vista otra vista (Button, TextView, VideoView, …). Cuando la vista principal / primaria se acerca o mueve, la subvista (Botón) necesita moverse con el padre.

Intenté varias soluciones, pero ninguna de ellas tiene todas las opciones que estoy buscando.

  • https://github.com/MikeOrtiz/TouchImageView/tree/master/src/com/example/touch
  • http://vivin.net/2011/12/04/implementing-pinch-zoom-and-pandrag-in-an-android-view-on-the- canvas /

Creo que es posible lograr lo que quieres, pero hay, hasta donde sé construir en solución para ello. A partir de la segunda parte de su pregunta, supongo que no desea una View zoom, sino un ViewGroup que es la ViewGroup de todas las vistas que pueden contener otra vista (por ejemplo, diseños). Aquí hay un código que puedes comenzar construyendo tu propio ViewGroup. La mayoría proviene de esta publicación de blog:

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

Lo que esta clase debería ser capaz de hacer, es arrastrar el contenido y permitir pellizcar para acercar, no es posible hacer doble clic para acercar, pero debería ser fácil de implementar en el método onTouchEvent .

Si tiene alguna pregunta sobre cómo diseñar los elementos secundarios en su ViewGroup, encontré que este video es muy útil o si tiene más preguntas sobre cómo funcionan los métodos individuales o cualquier otra cosa que no dude en preguntar en los comentarios.

Reenviar la respuesta de @Artjom con errores menores corregidos, es decir llaves, importaciones y ampliación de 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; } } } 

En función de las respuestas dadas, utilicé este código para que funcionen las funciones de panorámica y zoom. Tuve problemas con los puntos de pivote al principio.

 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", "mx: "+pan_x+" my: "+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 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; } } 

Los créditos para la función onTouch van a: http://judepereira.com/blog/multi-touch-in-android-translate-scale-and-rotate/ Gracias a Artjom por su enfoque para dipatch los eventos táctiles.

Agregué un método zoomToFit que se comenta en este punto porque la mayoría de la gente no lo necesitará. Encaja a los niños al tamaño del contenedor y toma al primer hijo como referencia para el factor de escala.