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

He visto a algunas personas preguntar cómo ampliar un ViewGroup entero (como un RelativeLayout) de una sola 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 sola variable en alguna parte; Y en cada uno de los métodos 'Views' onDraw () de Child Views, ese factor de escala se aplicaría al lienzo antes de dibujar los gráficos.

Sin embargo, antes de hacer eso, he intentado ser inteligente (ha – generalmente una mala idea) y extender RelativeLayout, en una clase llamada ZoomableRelativeLayout. Mi idea es que cualquier transformación de escala podría aplicarse una sola vez al Canvas en la función dispatchDraw (), por lo que no sería absolutamente necesario aplicar el zoom de forma independiente en ninguna de las vistas secundarias.

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

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

El mScaleFactor es manipulado por un ScaleListener en la misma clase.

Realmente funciona. Puedo pellizcar para hacer zoom en el ZoomableRelativeLayout, y todas las vistas mantenidas dentro de correctamente rescale juntos.

Excepto que hay un problema. Algunas de esas vistas de los niños están animadas, y por lo tanto periódicamente llamo invalidate () a ellos. Cuando la escala es 1, las vistas de los niños se ven a redibujar periódicamente perfectamente bien. Cuando la escala es distinta de 1, las vistas animadas de niño sólo se ven actualizar en una parte de su área de visualización – o no en absoluto – en función de la escala de zoom.

Mi pensamiento inicial fue que cuando se invoca invalidate () de una vista individual de un hijo, es posible que sea rediseñado individualmente por el sistema, en lugar de pasar un Canvas del disppartDraw () de RelativeLayout, lo que significa que la vista secundaria termina actualizándose Sin la escala de zoom aplicada. Pero curiosamente, los elementos de las vistas de niño que se vuelven a dibujar en la pantalla son a la escala de zoom correcta. Es casi como si el área que el sistema decide actualizar realmente en el mapa de bits de respaldo permanece sin escala – si eso tiene sentido. Para decirlo de otra manera, si tengo un único niño animado, veo y gradualmente me acerco más y más desde una escala inicial de 1, y si colocamos un cuadro imaginario en el área donde está esa vista secundaria cuando la escala de zoom es 1 , Entonces las llamadas a invalidate () sólo causan una actualización de los gráficos en ese cuadro imaginario. Pero los gráficos que se ven a la actualización se están haciendo a la escala correcta. Si se amplía hasta el punto en que la vista secundaria se ha movido completamente lejos de donde estaba con una escala de 1, entonces ninguna parte de ella en absoluto se verá actualizar. Voy a dar otro ejemplo: imagine mi niño vista es una pelota que se anima cambiando entre amarillo y rojo. Si hago zoom en un poco tal que la pelota se mueve a la derecha y hacia abajo, en un cierto punto que sólo verá el cuarto superior izquierdo de la pelota animar los colores.

Si continúo acercando y alejando, veo que las vistas secundarias se animan correctamente y completamente. Esto se debe a que todo el ViewGroup se está redibujando.

Espero que esto tenga sentido; He tratado de explicar lo mejor que he podido. ¿Estoy en un poco de perdedor con mi estrategia de ViewGroup zoomable? ¿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: envío de eventos táctiles, invalidaciones, etc.

Así que además de dispatchDraw (), tendrá que anular y adaptar adecuadamente el comportamiento de al menos estos otros métodos. Para hacerse cargo de invalidates, necesitará anular este método para ajustar las coordenadas de niño apropiadamente:

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 tendrá que anular esta opción para ajustar adecuadamente las coordenadas táctiles 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 implementar todo esto dentro de una subclase ViewGroup simple que tenga una sola vista secundaria que administre. Esto eliminará cualquier complejidad de comportamiento que RelativeLayout esté introduciendo en su propio ViewGroup, simplificando lo que necesita para tratar y depurar en su propio código. Ponga el RelativeLayout como un hijo de su ViewGroup de zoom especial.

Por último, una mejora de su código – en dispatchDraw () desea guardar el estado de lienzo después de aplicar el factor de escala. Esto garantiza que el niño no puede modificar la transformación que ha establecido.

La excelente respuesta de hackbod me ha recordado que necesito publicar la solución a la que acabo de llegar. Tenga en cuenta que esta solución, que funcionó para mí para la aplicación que estaba haciendo en ese momento, podría mejorarse con las sugerencias de hackbod. En particular yo no necesitaba manejar eventos de toque, y hasta leer el post de hackbod no se me ocurrió que si lo hiciera entonces tendría que escalarlos también.

Para recapitular, para mi aplicación, lo que necesitaba lograr era tener un diagrama grande (específicamente, la disposición del piso de un edificio) con otros pequeños símbolos "marcadores" superpuestos sobre él. El diagrama 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 por la que desea crear todos los gráficos de esta manera, en lugar de utilizar sólo recursos de mapa de bits, es porque los gráficos se convierten en tiempo de ejecución utilizando mi conversor de imágenes SVG.

El requisito era que el diagrama y los símbolos de marcador asociados fueran todos hijos de un grupo de vistas, y todos podían ser ampliados con pinzamiento.

Mucho del código parece desordenado (fue un trabajo urgente para una demostración), así que en lugar de copiarlo todo, en vez de eso trataré de explicar cómo lo hice con los bits de código pertinentes citados.

En primer lugar, tengo un ZoomableRelativeLayout.

 public class ZoomableRelativeLayout extends RelativeLayout { ... 

Esta clase incluye clases de escucha que amplían ScaleGestureDetector y SimpleGestureListener para que el diseño pueda ser panorámico y ampliado. De particular interés aquí es el receptor de gestos de escala, que establece una variable de factor de escala y luego llama 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; } } 

La siguiente cosa que tenía que hacer en mi ZoomableRelativeLayout era anular onLayout (). Para hacer esto me pareció útil para mirar a los intentos de otras personas en un diseño zoomable, y también me pareció muy útil para mirar el código fuente original de Android para RelativeLayout. Mi método anulado copia gran parte 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<count;i++){ View child = getChildAt(i); if(child.getVisibility()!=GONE){ RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams(); child.layout( (int)(params.leftMargin * mScaleFactor), (int)(params.topMargin * mScaleFactor), (int)((params.leftMargin + child.getMeasuredWidth()) * mScaleFactor), (int)((params.topMargin + child.getMeasuredHeight()) * mScaleFactor) ); } } } 

Lo importante aquí es que al llamar 'layout ()' a todos los niños, estoy aplicando el factor de escala a los parámetros de diseño, así como para esos niños. Este es un paso hacia la solución del problema de recorte, y también importantemente establece correctamente la posición x, y de los niños en relación entre sí para diferentes factores de escala.

Otra cosa clave es que ya no estoy tratando de escalar el lienzo en dispatchDraw (). En cambio, cada niño View escala su Canvas después de obtener el factor de escala del ZoomableRelativeLayout padre a través de un método getter.

A continuación, voy a pasar a lo que tenía que hacer en el niño Vistas de mi ZoomableRelativeLayout. Sólo 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 SVG no es relevante 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.restore(); } 

Una vez más – como una renuncia, hay probablemente algunas verrugas en lo que he fijado allí y todavía estoy cuidadosamente para ir a través de sugerencias del hackbod y para incorporarlas. Tengo la intención de volver y editar esto más. Mientras tanto, espero que pueda comenzar a proporcionar punteros útiles a otros sobre cómo implementar un ViewGroup zoomable.

FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.