Una de las herramientas más útiles dentro del arsenal del framework de Android son las imagenes nine-patch. Estas permiten a una imagen adaptarse a su contenedor para una misma densidad. El ejemplo por excelencia son los botones. Si se utiliza una imagen para el boton, por lo general es una imagen con un tamaño predeterminado. Esta imagen tal vez se vea muy bien cuando el dispositivo se encuentra en posicion vertical, pero cuando se gira el dispositivo, el boton mantiene su tamaño y no se “estira”.

Button Landscape

Boton en orientacion horizontal

Button Portrait

Boton en orientacion vertical

 

Cuando se sabe cual imagen se va a utilizar, esta imagen es alojada junto con el resto de la aplicaicon en el APK. Se puede utilizar una herramienta del mismo SDK para crear las marcar necesarias para indicarle al sistema operativo que partes de la imagen deben estirarse e inclusive cual va a ser la zona disponible para albergar contenido.

Boton con nine patch

Todo muy bien hasta aquí. Pero ¿Qué pasaría si se ocupa obtener un boton de manera remota? Es decir, al momento de hacer la aplicación, aún no es posible saber como va a ser el botón, por lo que no sería posible usar la herramienta anterior.

“Compile” vs “Source”

Cuando se crea un nine-patch con la herramienta, esto genera un archivo png, con la terminación 9.png. Este archivo es similar al original, pero tiene una transparencia de 1px con ciertas marcas. Cuando se compila el proyecto, esto genera por debajo el “chunk” metadata necesario para poder manipular la imagen en el UI.

La clase Bitmap tiene un metodo llamado getNinePatchChunk que permite obtener ese metadata. La clase NinePatch puede verificar si el metadata es válido y de ser así, se puede hacer un NinePatchDrawable.

 
InputStream stream = .. //whatever Bitmap bitmap = BitmapFactory.decodeStream(stream);
byte[] chunk = bitmap.getNinePatchChunk();
boolean result = NinePatch.isNinePatchChunk(chunk);
NinePatchDrawable patchy = new NinePatchDrawable(bitmap, chunk, new Rect(), null); 

Para poder hacer esto funcional, en un escenario con la imagen remota, se ocuparía que el servidor pueda enviar una versión compilada de la imagen. Esto presenta el problema de que no existe una herramienta oficial para “compilar” un nine-patch. Si bien existen herramientas de terceros que lo permiten, pueden haber situaciones donde esto no sea posible.

Este fue el caso que se presentó recientemente, donde se solicitó soportar nine-patch para unos botones, cuyas imagenes venian de manera remota. Para ello se creo un drawable nuevo que pudiera emular el nine patch. Eso si, debe recibir los componentes por separado. Por ahora no es posible recibir una imagen con las marcas de la herramienta del nine-patch y solo soporta 3 parches, aunque en un futuro se planea agregar soporte para los 9 en total.

La parte mas importante de este componente es el método draw. Que simplemente pinta los parches de los bordes (izquierda y derecha) y replica el parche del centro tantas veces como sea necesario para llenar el espacio.

	@Override
	public void draw(Canvas canvas) {
		int left = 0;
		int mTopRightWidth = 0;

		if (mTopLeftBG != null) {
			Rect dst = new Rect(0, 0,
					(int) (mTopLeftBG.getWidth() * mDensityScale),
					(int) (mTopLeftBG.getHeight() * mDensityScale));

			canvas.drawBitmap(mTopLeftBG, null, dst, mPaint);
			left = (int) (mTopLeftBG.getWidth() * mDensityScale);
		}

		if (mTopRightBG != null) {
			Rect dst = new Rect(getBounds().right
					- ((int) (mTopRightBG.getWidth() * mDensityScale)), 0,
					getBounds().right,
					((int) (mTopRightBG.getHeight() * mDensityScale)));

			canvas.drawBitmap(mTopRightBG, null, dst, mPaint);
			mTopRightWidth = (int) (mTopRightBG.getWidth() * mDensityScale);
		}

		if (mTopCenterBG != null) {
			int bitmapScaledWidth = (int) (mTopCenterBG.getWidth() * mDensityScale);
			for (int i = left; i < getBounds().right - mTopRightWidth; i += bitmapScaledWidth) {
				Rect dst = new Rect(i, 0, i + bitmapScaledWidth,
						(int) (mTopCenterBG.getHeight() * mDensityScale));
				canvas.drawBitmap(mTopCenterBG, null, dst, mPaint);
			}
		}
	}

Para poder soportar diferentes densidades, se requiere que la imagen sea para la densidad máxima (en este momento xxhdpi), por ejemplo una imagen que tenga 144px de alto. A partir de ahi a la hora de recibir la imagen se escala hacia abajo deacuerdo a la densidad del dispositivo.

Próximamente voy a publicar un proyectos en github de ejmplo y el código fuente del PatchDrawable.