Extendiendo RelativeLayout, y reemplazando dispatchDraw () para crear un ViewGroup con zoom

He visto a algunas personas preguntar cómo hacer zoom sobre un ViewGroup completo (como RelativeLayout) de una vez. Por el momento, esto es algo que necesito lograr. El enfoque más obvio, para mí, sería mantener el factor de escala de zoom como una variable única en alguna parte; y en cada uno de los métodos de vista infantil onDraw (), ese factor de escala se aplicaría al canvas antes de dibujar los gráficos.

Sin embargo, antes de hacer eso, he tratado de ser inteligente (ha, por lo general, una mala idea) y extender RelativeLayout, en una clase llamada ZoomableRelativeLayout. Mi idea es que cualquier transformación de escala se podría aplicar una sola vez al canvas en la función overdretded dispatchDraw (), de modo que no habría absolutamente ninguna necesidad de aplicar el zoom por separado en ninguna de las vistas secundarias.

Esto es lo que hice en mi ZoomableRelativeLayout. Es solo una simple extensión de RelativeLayout, con dispatchDraw () anulado:

protected void dispatchDraw(Canvas canvas){ canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.scale(mScaleFactor, mScaleFactor); super.dispatchDraw(canvas); canvas.restre(); } 

MScaleFactor es manipulado por un ScaleListener en la misma clase.

Realmente funciona Puedo pellizcar para ampliar ZoomableRelativeLayout y todas las vistas mantenidas dentro de la escala apropiada.

Excepto que hay un problema. Algunas de esas vistas secundarias están animadas y, por lo tanto, periódicamente invoco invalidate () sobre ellas. Cuando la escala es 1, se ve que las vistas de los niños se vuelven a dibujar periódicamente perfectamente bien. Cuando la escala es distinta de 1, se ve que esas vistas animadas de los niños se actualizan en una parte de su área de visualización, o no se actualizan, según la escala del zoom.

Mi pensamiento inicial fue que cuando se llama a una vista infantil individual invalidada (), el sistema posiblemente la vuelva a dibujar individualmente, en lugar de pasar un canvas del dispatchDraw () de RelativeLayout padre, lo que significa que la vista secundaria termina refrescándose sin la escala de zoom aplicada. Pero, curiosamente, los elementos de las vistas secundarias que se vuelven a dibujar en la pantalla corresponden a la escala de zoom correcta. Es casi como si el área que el sistema decide actualizar en realidad en el bitmap de respaldo permanece sin escala, si tiene sentido. Para decirlo de otra manera, si tengo una sola vista animada para niños y gradualmente me acerco más y más desde una escala inicial de 1, y si coloco un cuadro imaginario en el área donde está la vista secundaria cuando la escala de zoom es 1 , las llamadas a invalidar () solo provocan una actualización de los gráficos en esa caja imaginaria. Pero los gráficos que se ven para actualizar se están haciendo a la escala correcta. Si amplía el zoom hasta el punto en que la vista secundaria ahora se ha alejado completamente de donde estaba con una escala de 1, entonces no se ve ninguna parte de ella para actualizarse. Daré otro ejemplo: imagine que mi vista de niño es una pelota que se anima cambiando de amarillo a rojo. Si hago un acercamiento un poco para que la bola se mueva hacia la derecha y hacia abajo, en cierto punto solo verás el cuarto superior animado de los colores animados de la bola.

Si me acerco y alejé continuamente, veo las vistas secundarias animadas de manera correcta y completa. Esto se debe a que se está redibujando todo el ViewGroup.

Espero que esto tenga sentido; Intenté explicar lo mejor que puedo. ¿Estoy un poco perdedor con mi estrategia de ViewGroup con zoom? ¿Hay otra manera?

Gracias,

Trev

Si está aplicando un factor de escala al dibujo de sus hijos, también debe aplicar el factor de escala apropiado a todas las otras interacciones con ellos: despachar eventos táctiles, invalidar, etc.

Por lo tanto, además de dispatchDraw (), deberá anular y ajustar el comportamiento de al menos estos otros métodos. Para solucionar invalidaciones, deberá anular este método para ajustar las coordenadas del niño adecuadamente:

http://developer.android.com/reference/android/view/ViewGroup.html#invalidateChildInParent (int [], android.graphics.Rect)

Si desea que el usuario pueda interactuar con las vistas secundarias, también deberá sobrescribir esto para ajustar las coordenadas táctiles de manera apropiada antes de que se envíen a los niños:

http://developer.android.com/reference/android/view/ViewGroup.html#dispatchTouchEvent(android.view.MotionEvent )

También recomiendo encarecidamente que implemente todo esto dentro de una subclase simple de ViewGroup que tenga una única vista secundaria que administre. Esto eliminará cualquier complejidad de comportamiento que RelativeLayout esté presentando en su propio ViewGroup, lo que simplificará lo que necesita tratar y depurar en su propio código. Ponga RelativeLayout como un elemento secundario de su grupo de visualización de zoom especial.

Finalmente, una mejora de su código: en dispatchDraw () desea guardar el estado del canvas después de aplicar el factor de escala. Esto asegura que el niño no puede modificar la transformación que ha establecido.

La excelente respuesta de hackbod me ha recordado que tengo que publicar la solución a la que finalmente llegué. Tenga en cuenta que esta solución, que funcionó para mí para la aplicación que estaba haciendo en ese momento, podría mejorarse aún más con las sugerencias de hackbod. En particular, no necesitaba manejar eventos táctiles, y hasta leer la publicación de hackbod no se me ocurrió que, si lo hacía, también tendría que escalarlos.

Para recapitular, para mi aplicación, lo que necesitaba lograr era tener un diagtwig grande (específicamente, el diseño del piso de un edificio) con otros pequeños símbolos de “marcador” superpuestos sobre él. El diagtwig de fondo y los símbolos de primer plano se dibujan utilizando gráficos vectoriales (es decir, los objetos Path () y Paint () aplicados a Canvas en el método onDraw ()). La razón para querer crear todos los gráficos de esta manera, en lugar de solo usar recursos de mapas de bits, es porque los gráficos se convierten en tiempo de ejecución utilizando mi convertidor de imágenes SVG.

El requisito era que el diagtwig y los símbolos de marcador asociados serían todos hijos de un Grupo de Vista, y que todos podrían ser pellizcados juntos.

Gran parte del código parece desordenado (fue un trabajo urgente para una demostración), así que en lugar de simplemente copiarlo todo, trataré de explicar cómo lo hice con los fragmentos de código relevantes citados.

Primero que nada, tengo un ZoomableRelativeLayout.

 public class ZoomableRelativeLayout extends RelativeLayout { ... 

Esta clase incluye clases de escucha que extienden ScaleGestureDetector y SimpleGestureListener para que el diseño se pueda panoramizar y ampliar. De particular interés aquí es el oyente de gesto de escala, que establece una variable de factor de escala y luego llama a invalidate () y requestLayout (). No estoy estrictamente seguro en este momento si invalidate () es necesario, pero de todos modos, aquí está:

 private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector){ mScaleFactor *= detector.getScaleFactor(); // Apply limits to the zoom scale factor: mScaleFactor = Math.max(0.6f, Math.min(mScaleFactor, 1.5f); invalidate(); requestLayout(); return true; } } 

Lo siguiente que tuve que hacer en mi ZoomableRelativeLayout fue anular onLayout (). Para hacer esto, me pareció útil mirar los bashs de otras personas en un diseño con zoom, y también me pareció muy útil mirar el código fuente original de Android para RelativeLayout. Mi método reemplazado copia mucho de lo que está en OnLayout de RelativeLayout () pero con algunas modificaciones.

 @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int count = getChildCount(); for(int i=0;i 

Lo que es significativo aquí es que cuando llamo 'layout ()' a todos los elementos secundarios, también estoy aplicando el factor de escala a los parámetros de diseño para esos niños. Este es un paso hacia la solución del problema de recorte, y también establece de manera importante que la posición x, y de los niños es relativa entre sí para diferentes factores de escala.

Otra cosa clave es que ya no estoy intentando escalar el canvas en dispatchDraw (). En su lugar, cada vista secundaria escala su canvas después de obtener el factor de escala del archivo ZoomableRelativeLayout principal a través de un método getter.

A continuación, pasaré a lo que tenía que hacer dentro de las vistas secundarias de mi ZoomableRelativeLayout. Solo hay un tipo de vista que contengo como niños en mi ZoomableRelativeLayout; es una Vista para dibujar gráficos SVG que yo llamo SVGView. Por supuesto, las cosas de SVG no son relevantes aquí. Aquí está su método onMeasure ():

 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); float parentScale = ((FloorPlanLayout)getParent()).getScaleFactor(); int chosenWidth, chosenHeight; if( parentScale > 1.0f){ chosenWidth = (int) ( parentScale * (float)svgImage.getDocumentWidth() ); chosenHeight = (int) ( parentScale * (float)svgImage.getDocumentHeight() ); } else{ chosenWidth = (int) ( (float)svgImage.getDocumentWidth() ); chosenHeight = (int) ( (float)svgImage.getDocumentHeight() ); } setMeasuredDimension(chosenWidth, chosenHeight); } 

Y el onDraw ():

 @Override protected void onDraw(Canvas canvas){ canvas.save(Canvas.MATRIX_SAVE_FLAG); canvas.scale(((FloorPlanLayout)getParent()).getScaleFactor(), ((FloorPlanLayout)getParent()).getScaleFactor()); if( null==bm || bm.isRecycled() ){ bm = Bitmap.createBitmap( getMeasuredWidth(), getMeasuredHeight(), Bitmap.Config.ARGB_8888); ... Canvas draw operations go here ... } Paint drawPaint = new Paint(); drawPaint.setAntiAlias(true); drawPaint.setFilterBitmap(true); // Check again that bm isn't null, because sometimes we seem to get // android.graphics.Canvas.throwIfRecycled exception sometimes even though bitmap should // have been drawn above. I'm guessing at the moment that this *might* happen when zooming into // the house layout quite far and the system decides not to draw anything to the bitmap because // a particular child View is out of viewing / clipping bounds - not sure. if( bm != null){ canvas.drawBitmap(bm, 0f, 0f, drawPaint ); } canvas.restre(); } 

Nuevamente, como un descargo de responsabilidad, es probable que haya algunas verrugas en lo que he publicado allí y todavía tengo que seguir cuidadosamente las sugerencias de hackbod e incorporarlas. Tengo la intención de volver y editar esto más. Mientras tanto, espero que pueda comenzar a proporcionar consejos útiles a otros sobre cómo implementar un grupo de vistas con zoom.