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 designe 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) :

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.

Les technologies mobiles reposent sur un ensemble de contraintes dont une des principales est probablement le manque d’espace d’affichage. Personnellement, je dois avouer que je prend cette contrainte plutôt comme un avantage car cela me rappelle énormément mes débuts en programmation. A l’époque, je codais en Casio-Basic sur Casio avant d’acquérir une Texas Instrument Voyage 200 et de découvrir le Ti-Basic et surtout le C (aie des pointeurs ^^). L’environnement général me plaisait énormément car je trouvais l’ensemble très puissant (13Mhz) et restreint (pas de threads, pas de multi-processus, etc.). L’écran de cette calculatrice était de taille réduite et monochrome. Remplir cet “amat de pixel” était donc une tâche largement plus simple pour le non-designer que je suis que de réaliser une application PC ou un site web. L’écran n’était malheureusement pas tactile. C’est uniquement lorsque j’ai commencé à coder sur iPhone et Android que j’ai découvert ces possibilités …

Les interfaces mobiles se basent donc sur des concepts ergonomiques et tactiles qui sont absents des plateformes classiques : les gestes (ou gestures). Pour surmonter ces problèmes de tailles de terminal réduit, de nombreux mouvements sont apparus. Les principaux sont donnés dans la liste ci-dessous :

  • Simple click : Consiste à appuyer sur l’écran et à relacher sans avoir bouger son doigt.
  • Double click : Obtenu en effectuant deux clicks au même endroit à la suite. Le temps entre les deux clicks doit également être assez bref.
  • Scroll : Action d’appuyer à l’écran et de déplacer son doigt sans relâcher la pression.
  • Fling : Obtenu lorsqu’on appuie à l’écran, qu’on effectue un mouvement brusque et qu’on relâche rapidement l’écran
  • Long click : Généré en pressant l’écran de façon prolongée et sans bouger

Créer une interface graphique sur Android revient souvent à utiliser l’ensemble de ces gestes. MetroMap utilise par exemple plusieurs d’entre-eux (le simple click, le scroll, et le fling). Lors de la conception de cette application, j’ai décidé d’effectuer manuellement la détection de ces mouvements en utilisant les valeur “standards” d’Android données dans ViewConfiguration. Il existe pourtant une classe qui facilite grandement la reconnaissance de ces gestures : la classe GestureDetector. Son utilisation est on-ne-peut-plus-simple :

package com.cyrilmottier.android.gesturedetector;
 
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.GestureDetector.OnGestureListener;
 
public class MyView extends View implements OnGestureListener {
 
    private GestureDetector mGestureDetector;
 
    public MyView(Context context) {
        super(context);
        mGestureDetector = new GestureDetector(this);
    }
 
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
 
    public boolean onDown(MotionEvent arg0) {
    	// Don't forget to return true here to get the following touch events
        return true;
    }
 
    public boolean onFling(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
        return false;
    }
 
    public void onLongPress(MotionEvent arg0) {
    }
 
    public boolean onScroll(MotionEvent arg0, MotionEvent arg1, float arg2, float arg3) {
        // You can do here whatever you want to handle scrolling
        return true;
    }
 
    public void onShowPress(MotionEvent arg0) {
    }
 
    public boolean onSingleTapUp(MotionEvent arg0) {
        return false;
    }
 
}

Implémenter l’interface OnGestureListener oblige le développeur à définir l’intégralité des méthodes (notion de classe abstraite pure). C’est un “problème” inhérent au langage qui n’autorise tout simplement pas les méthode optionnelles d’interface (contrairement à l’Objective-C par exemple) et qui peut être contourné en utilisant une classe qui pré-implémente l’intégralité des méthodes.

Note : Ceux qui s’intéresse à la raison de l’abscence des méthode d’interface optionnelle comprendront que la notion d’interface Java est tout simplement vu comme un contrat qui DOIT obligatoirement être respecté. L’Objective-C quant à lui autorise les méthodes d’interface (ou plus précisément de protocole) optionnelles car c’est un langage qui effectue les vérifications au runtime et non à la compilation comme le fait Java.

Android fournit une classe permettant d’effectuer cette manipulation. Si notre code consiste à seulement gérer le scroll, on aura seulement :

package com.cyrilmottier.android.gesturedetector;
 
import android.content.Context;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
 
public class MyView extends View {
 
    private GestureDetector mGestureDetector;
 
    private GestureDetector.SimpleOnGestureListener mScrollHandler = new GestureDetector.SimpleOnGestureListener() {
 
    	@Override
    	public boolean onDown(MotionEvent arg0) {
            // Don't forget to return true here to get the following touch events
            return true;
        }
 
        @Override
        public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
            // You can do here whatever you want to handle scrolling
            return true;
        }
    }
 
    public MyView(Context context) {
        super(context);
        mGestureDetector = new GestureDetector(mScrollHandler);
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return mGestureDetector.onTouchEvent(event);
    }
 
}

Malgré l’impressionante facilité, la classe GestureDetector ne permet malheureusement pas de détecter l’intégralité des mouvements. On regrette, par exemple, la présence de callbacks sur des mouvements “multi-touch” : le pinch, le rotate, etc. Je suis certain qu’il y a de bonne raison pour cette absence et je ne pourrais donc que motiver les plus courageux à coder leur propre MultitouchGestureDetector. N’hésitez surtout pas à m’informer dans un commentaire ou par courriel si vous développez votre propre librairie. Happy coding!