Implementando RoundedImageView para Android

Para uno de los proyectos de Possible Worldwide, era necesario mostrar imágenes que se bajaban de un servidor, con las esquinas redondeadas. La primera implementación constaba de bajar la imagen, y “recortar los bordes” antes de guardar la imagen en el almacenamiento del teléfono.

El método simplemente recortaba la imagen generando un nuevo Bitmap:

    public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, final float roundPx) {
        if (bitmap == null) {
            throw new IllegalArgumentException("Bitmap to round corners can not be null");
        }
        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Config.ARGB_8888);
        Canvas canvas = new Canvas(output);
        final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
        final RectF rectF = new RectF(rect);
        final int color = 0xff424242;
        final Paint paint = new Paint();
        paint.setAntiAlias(true);
        canvas.drawARGB(0, 0, 0, 0);
        paint.setColor(color);
        canvas.drawRoundRect(rectF, roundPx, roundPx, paint);
        paint.setXfermode(new PorterDuffXfermode(Mode.SRC_IN));
        canvas.drawBitmap(bitmap, rect, rect, paint);
        return output;
    }

Pero ciertas imágenes tenia ciertos problemas causando recortes execivos, además de estar alojando dos veces la misma imagen en el heap. Aun cuando se puede mandar a “reciclar” el bitmap viejo, es un gasto innecesario de memoria.

Luego, investigando un poco más, me encontre esta solución en StackOverflow. Básicamente es crear una clase que herede de ImageView y sobreescribir el método onDraw, y utilizando un clipPath, “ocultar” las esquinas con un rectangulo redondeado.

public class RoundedImageView extends ImageView {
    public RoundedImageView(Context context) {
        super(context);
    }
    public RoundedImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public RoundedImageView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }
    @Override
    protected void onDraw(Canvas canvas) {
        Path clipPath = new Path();
        int w = this.getWidth();
        int h = this.getHeight();
        clipPath.addRoundRect(new RectF(0, 0, w, h), 10.0f, 10.0f, Path.Direction.CW);
        canvas.clipPath(clipPath);
        super.onDraw(canvas);
    }
}

Ésta implementación es muy sencilla, pero se podría modificar para que acepte un parámetro desde el Layout (XML) para definir la cantidad de redondeo

De esta manera, no es necesario procesar la imagen, reduciendo el uso de heap nativo y mostrando la imagen más rapidamente.

Deshabilitar escalado al fondo de un layout

Me tope por ahi un problema con los fodos para los layouts. Estos son escalados y al parecer no hay forma de decirle al layout de que modo queremos que escale (o mas bien que no lo haga) el fondo.

Por ejemplo, supongamos que queremos usar un “tablero de ajedrez” de fondo para el siguiente layout

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content" 
android:orientation="vertical"
android:layout_width="fill_parent" 
android:background="@drawable/
<TextView android:id="@+id/TextView01" 
android:layout_width="wrap_content"
		android:layout_height="wrap_content" 
		android:text="Name:" />
	<EditText android:id="@+id/EditText01" 
		android:layout_height="wrap_content"
		android:layout_width="fill_parent" 
		android:hint="Type your name here" />
	<TextView android:id="@+id/TextView02" 
		android:layout_width="wrap_content"
		android:layout_height="wrap_content" 
		android:text="Email:" />
	<EditText android:id="@+id/EditText02" 
		android:layout_height="wrap_content"
		android:inputType="textEmailAddress" 
		android:layout_width="fill_parent"
		android:hint="Type your email address" />

	<Button android:id="@+id/Button01" 
		android:layout_width="wrap_content" 
		android:layout_height="wrap_content" 
		android:text="Save" 
		android:layout_gravity="center_horizontal" 
		android:paddingLeft="40dip" 
		android:paddingRight="40dip" />
</LinearLayout>

nos da como resultado, en portrait:

Portrait Layout with background

Portrait Layout with background

y en landscape

Landscape Layout with Background

Landscape Layout with Background

Como ven, el fondo es escalado sin posibilidad de poder definirle algún modo de escalado. Leyendo en foros me encontre esta solución, bastante ingeniosa, ojo, yo no soy el autor, pero si me parece importante rescatar la información y comentarla

Básicamente es no usar el backgraound del layout, sino más bien utilizar un FrameLayout y un ImageView.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" 
	android:layout_width="fill_parent"
	android:layout_height="fill_parent">

	<ImageView 
		android:layout_width="fill_parent"
		android:layout_height="fill_parent" 
		android:src="@drawable/back"
		android:scaleType="center" />

	<LinearLayout>
		<!-- your views -->
	</LinearLayout>
</FrameLayout>

Para el ejemplo anterior, el layout quedaria como:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
	android:layout_width="fill_parent"
	android:layout_height="fill_parent"
>
	<ImageView 
		android:src="@drawable/hdpi_bg"
		android:layout_width="fill_parent"
		android:layout_height="fill_parent"
		android:scaleType="center"
	/>
	<LinearLayout 
		android:layout_height="wrap_content" 
		android:orientation="vertical"
		android:layout_width="fill_parent" 
	>
		
		<TextView android:id="@+id/TextView01" 
			android:layout_width="wrap_content"
			android:layout_height="wrap_content" 
			android:text="Name:" />
		<EditText android:id="@+id/EditText01" 
			android:layout_height="wrap_content"
			android:layout_width="fill_parent" 
			android:hint="Type your name here" />
		<TextView android:id="@+id/TextView02" 
			android:layout_width="wrap_content"
			android:layout_height="wrap_content" 
			android:text="Email:" />
		<EditText android:id="@+id/EditText02" 
			android:layout_height="wrap_content"
			android:inputType="textEmailAddress" 
			android:layout_width="fill_parent"
			android:hint="Type your email address" />
	
		<Button android:id="@+id/Button01" 
			android:layout_width="wrap_content" 
			android:layout_height="wrap_content" 
			android:text="Save" 
			android:layout_gravity="center_horizontal" 
			android:paddingLeft="40dip" 
			android:paddingRight="40dip" />
	</LinearLayout>
</FrameLayout>

Y se veria así

Layout "framed" in FrameLayout

Layout "framed" in FrameLayout

Y en landscape

LinearLayout "framed" in FrameLayout (landscape)

LinearLayout "framed" in FrameLayout (landscape)

Me parece una buena forma de aprovechar la propiedad de modo de escalado del ImageView y la verdad no agrega mayor complicación envolver nuestro layout original con el FrameLayout. Si alguien tiene otra solución me encantaría conocerla.