Cyril Mottier

“It’s the little details that are vital. Little things make big things happen.” – John Wooden

Masking Effect Sur Android

Un tel titre d'article peut paraitre un peu bizarre lorsqu'on ne sait pas ce que représente le masking effect. Cet outil est souvent utilisé par les designers et consiste à n'afficher qu'une certaine partie d'une image. La zone affichée de l'image est fonction du canal alpha d'une seconde image (le masque). C'est une technique facilement utilisable sous les éditeurs graphiques avancés comme Photoshop ou The Gimp et dont je pourrais difficilement me passer lorsque je désigne une application ou un site internet.

Bien qu'Android fonctionne généralement sur des terminaux contraints - et donc peu adaptés à de lourds calculs sur des images - il permet de créer et reproduire bon nombre d'effets graphiques basiques - n'allez tout de même pas chercher à faire du content-aware fill ou du patch match comme proposé dans la future et très attendue version CS5 de Photoshop. Il est donc possible de dessiner des ovales ou des rectangles à bords arrondis, de créer des dégradés, des pointillés, etc. Le masquage d'images est lui aussi disponible sous Android et permet d'obtenir des effets assez sympas et permettant à votre UI de se distinguer.

Pour bien présenter le problème, j'ai souhaité reproduire l'effet glossy des icônes présentes sur iPhone OS. Pour ceux qui ne le savent pas, les icônes des applications iPhone sont toutes consistantes car le système les modifie avant de les afficher sur le Springboard (l'équivalent du Launcher sous iPhone OS). Cette modification consiste à arrondir les angles de l'icône et à y ajouter un effet glossy (cet effet n'est pas obligatoire et peut être supprimé par le développeur).

Comme à mon habitude, le code est décrit ci-dessous mais est accessible directement dans ce zip. Je vous conseille vivement de réaliser vous même un projet de test pour mieux comprendre/assimiler toutes ces lignes. Nous avons donc 3 images :

  • Une icône brute de l'application (icon_metromap_fake.png). Comme vous le remarquerez, j'ai souhaité parodier l'icône de MetroMap !

  • Une image permettant de simuler un effet glossy (icon_glossy.png). Ici représentée sur fond vert pour mieux comprendre à quoi elle ressemble, cette image est en fait translucide et dispose d'un dégradé oval de blanc (alpha ≈ 60 %) vers un autre blanc encore plus transparent (alpha ≈ 20 %) :

  • Il ne nous reste plus qu'une image permettant d'arrondir les coins de notre icône. C'est la fameuse notion de masque qui rentre en jeu. Le masque peut être vu comme un pochoir qui donne la forme de l'image finale. Ce dernier est généralement représenté sous la forme d'une image rouge (icon_mask.png) car cela permet de mieux distinguer les masques des “véritables” images :

Il ne nous reste maintenant plus qu'à dessiner notre icône. L'astuce réside dans l'utilisation d'un Paint ayant un Xfermode à new PorterDuffXfermode(PorterDuff.Mode.DST_IN) :

MainActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.cyrilmottier.android.masking;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.PorterDuffXfermode;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends Activity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new IconView(this));
    }

    private class IconView extends View {

        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        private Bitmap mIcon;
        private Bitmap mIconGlossy;
        private Bitmap mIconMask;

        public IconView(Context context) {
            super(context);

            // Prepares the paint that will be used to draw our icon mask. Using
            // PorterDuff.Mode.DST_IN means the image that will be drawn will
            // mask the already drawn image.
            mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));

            // Let's retrieve all icon pieces as Bitmaps.
            final Resources res = context.getResources();
            mIcon = BitmapFactory.decodeResource(res, R.drawable.icon_metromap_fake);
            mIconGlossy = BitmapFactory.decodeResource(res, R.drawable.icon_glossy);
            mIconMask = BitmapFactory.decodeResource(res, R.drawable.icon_mask);
        }

        @Override
        protected void onDraw(Canvas canvas) {
            super.onDraw(canvas);
            canvas.save();

            // Translate the canvas in order to draw the icon in the center of
            // the view
            canvas.translate((getWidth() - mIcon.getWidth()) >> 1, (getHeight() - mIcon.getHeight()) >> 1);

            // We're now ready to drawn our iPhone-like icon :)
            canvas.drawBitmap(mIcon, 0, 0, null);
            canvas.drawBitmap(mIconGlossy, 0, 0, null);
            canvas.drawBitmap(mIconMask, 0, 0, mPaint);

            canvas.restore();
        }

    }
}

Le résultat obtenu est parfaitement en accord avec le style iPhone :

Cette fonctionnalité autorise de nombreuses facéties mais il ne faut pas perdre de vue qu'effectuer le maximum de travail graphique off-device est souvent gage de performances. Effectuer ces manipulations on-device implique d'effectuer le dessin au niveau Bitmap et non au niveau Drawable, classe permettant une abstraction plus intéressante à l'affichage. Il est pourtant parfois impossible d'effectuer le travail de masquage off-device. Prenons par exemple le cas d'images rapatriées d'un web service comme Facebook ou Twitter. Arrondir les angles des images ne peut s'effectuer que sur le terminal. Il vous faut alors mettre en place un système de cache conservant les images préparées pour éviter d'effectuer le masquage de façon répétitive.