Dessin et Graphique sous Android

Pré-requis

Pour développer des applications Android, il faut au moins disposer du Android SDK et du kit de développement Java. Pour faciliter le développement, il est conseillé d’untiliser un EDI/IDE (Integrated Development Environment) comme Android Studio.

Android Studio est un environnement de développement pour développer des applications Android :

  1. Installer Android Studio

Android Studio permet principalement d’éditer les fichiers Java et les fichiers de configuration d’une application Android.

Il propose entre autres des outils pour gérer le développement d’applications multilingues et permet de visualiser la mise en page des écrans sur des écrans de résolutions variées simultanément.

Quelques raccourcis intéressants pour le développeur :

  • Ctrl + Espace : accès à la documentation de l’API
  • Ctrl + G : aller à une ligne
  • Ctrl + Alt + Maj + N : recherche par symbole
  • F4 : aller à la définition
  1. Créer votre première application

Créer le squelette de l’application

Démarrer Android Studio :

$ studio.sh

Choisir ‘Empty Activity’ :

Cliquer sur ‘Run app’ (pour tester).

Pour la cible, vous avez le choix entre le smartphone (ou la tablette) connecté ou un émulateur (cliquer sur ‘Create New Emulateur’ pour en créer un) :

Par défaut, le dossier de l’application est situé dans $HOME/AndroidStudioProjects.

L’API Android Graphics

Le framework android.graphics divise le dessin en deux concepts :

  • Que dessiner ? géré par Canvas (qui permet de définir les formes à dessiner),
  • Comment dessiner ? géré par Paint (qui permet de personnaliser les formes dessinées).

Par exemple, Canvas fournit une méthode pour dessiner une ligne, tandis que Paint fournit des méthodes pour définir la couleur de cette ligne. Canvas a une méthode pour dessiner un rectangle, tandis que Paint définit s’il faut remplir ce rectangle avec une couleur ou le laisser vide.

La classe Canvas, qui joue un rôle central dans les graphiques 2D, contient les appels de dessin (“draw”). Pour dessiner quelque chose, on aura besoin de 4 composants de base :

  • un Bitmap pour contenir les pixels,
  • un Canvas pour les appels draw (écrire dans le bitmap),
  • une primitive de dessin (drawColor(), drawArc(), drawRect(), drawCircle() et drawText(), …), et
  • un Paint pour décrire les couleurs et les styles pour le dessin.

Voir aussi : Rect, Path, … Color qui représente un code couleur comme un int. La classe Color définit un certain nombre de méthodes pour créer et convertir des couleurs.

Pour dessiner à l’écran, on pourra créer une vue personnalisée en héritant de la classe View ou l’une de ses sous-classes puis on redéfinira sa méthode onDraw(). Cette méthode reçoit en paramètre un objet Canvas dans lequel on dessinera. Il suffira d’utiliser un objet Paint pour personnaliser son dessin.

Lien : Vue personnalisée

Dessiner dans une vue (view)

On définit les layouts :

  • activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.example.tv.myapplicationdessin.MainActivity">

    <include layout="@layout/content_main"/>

</android.support.design.widget.CoordinatorLayout>
  • content_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:orientation="vertical">
        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="15dp"
            android:gravity="center"
            android:text="Dessin 2D sous Android (tv)"
            android:textSize="24sp"/>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_below="@+id/top"
        android:gravity="center"
        android:orientation="horizontal">
    </LinearLayout>
</RelativeLayout>

On crée une activité principale dans laquelle on intègre une vue personnalisée associée au layout précédent :

package com.example.tv.myapplicationdessin;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.LinearLayout;

public class MainActivity extends AppCompatActivity
{
    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        MyView myViewCompteur = new MyView(this);
        LinearLayout myLayout1 = (LinearLayout)findViewById(R.id.myView);
        myLayout1.addView(myViewCompteur);
        setContentView(myViewCompteur);
    }
}

On définit la vue personnalisée et on y dessine quelques formes classiques (lignes, cercles, textes, …) :

package com.example.tv.myapplicationdessin;

import android.view.View;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;

public class MyView extends View
{
    Paint paint = new Paint(Paint.FAKE_BOLD_TEXT_FLAG);
    Path starPath;
    Path curvePath;
    Paint textPaint = new Paint(Paint.LINEAR_TEXT_FLAG);

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

        // create star path
        starPath = createStarPath(300,  500);
        curvePath = createCurvePath();
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        // draw basic shapes
        canvas.drawLine(5, 5, 200, 5, paint);
        canvas.drawLine(5, 15, 200, 15, paint);
        canvas.drawLine(5, 25, 200, 25, paint);

        paint.setColor(Color.YELLOW);
        canvas.drawCircle(50, 70, 35, paint);

        paint.setColor(Color.GREEN);
        canvas.drawRect(new Rect(100, 60, 150, 80), paint);

        paint.setColor(Color.DKGRAY);
        canvas.drawOval(new RectF(160, 60, 250, 80), paint);

        // draw text
        textPaint.setColor(Color.MAGENTA);
        textPaint.setTextSize(40);
        canvas.drawText("Hello World!", 20, 190, textPaint);
        //  transparency
        textPaint.setColor(0xFF465574);
        textPaint.setTextSize(60);
        canvas.drawText("Android Rocks", 20, 340, textPaint);

        // opaque circle
        canvas.drawCircle(80, 300, 20, paint);

        // semi-transparent circle
        paint.setAlpha(110);
        canvas.drawCircle(160, 300, 39, paint);
        paint.setColor(Color.YELLOW);
        paint.setAlpha(140);
        canvas.drawCircle(240, 330, 30, paint);
        paint.setColor(Color.MAGENTA);
        paint.setAlpha(30);
        canvas.drawCircle(288, 350, 30, paint);
        paint.setColor(Color.CYAN);
        paint.setAlpha(100);
        canvas.drawCircle(380, 330, 50, paint);

        // draw text on path
        textPaint.setColor(Color.rgb(155, 20, 10));
        canvas.drawTextOnPath("BTS SN LASALLE 84", curvePath, 10, 10, textPaint);

        // create a star-shaped clip
        canvas.drawPath(starPath, textPaint);
        textPaint.setColor(Color.CYAN);
        canvas.clipPath(starPath);
        //textPaint.setColor(Color.parseColor("yellow"));
        //canvas.drawText("Android", 350, 550, textPaint);
        textPaint.setColor(Color.parseColor("#abde97"));
        canvas.drawText("Android", 400, 600, textPaint);
        canvas.drawText("Android Rocks", 300, 650, textPaint);
        canvas.drawText("Android Rocks", 320, 700, textPaint);
        canvas.drawText("Android Rocks", 360, 750, textPaint);
        canvas.drawText("Android Rocks", 320, 800, textPaint);

        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        Rect r = canvas.getClipBounds();
        canvas.drawRect(r, paint);
    }

    private Path createStarPath(int x,  int y)
    {
        Path path = new Path();
        path.moveTo(0 + x, 150 + y);
        path.lineTo(120 + x, 140 + y);
        path.lineTo(150 + x, 0 + y);
        path.lineTo(180 + x, 140 + y);
        path.lineTo(300 + x, 150 + y);
        path.lineTo(200 + x, 190 + y);
        path.lineTo(250 + x, 300 + y);
        path.lineTo(150 + x, 220 + y);
        path.lineTo(50 + x, 300 + y);
        path.lineTo(100 + x, 190 + y);
        path.lineTo(0 + x, 150 + y);
        return  path;
    }

    private Path createCurvePath()  
    {
        Path path = new Path();
        path.addArc(new RectF(400, 40, 780, 300), -210, 230);
        return  path;
    }

}

On obtient :

Dessiner des widgets techniques

Dans cet exemple, on va créer un objet Drawable qui aura la capacité de se dessiner dans un Canvas. Ceci va permettre de séparer la vue (Drawable) de l’objet à dessiner (View). On va réaliser le dessin d’une boussole et d’un compteur de vitesse.

On va tout d’abord modifier les layouts pour y intégrer notamment une SeekBar pour tester nos deux nouveaux widgets :

  • activity_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context="com.example.tv.myapplicationdessin.MainActivity">

    <include layout="@layout/content_main"/>

</android.support.design.widget.CoordinatorLayout>
  • content_main.xml :
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:orientation="vertical">
        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="15dp"
            android:gravity="center"
            android:text="Dessin 2D sous Android (tv)"
            android:textSize="24sp"/>
        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="360"/>
    </LinearLayout>
    <LinearLayout
        android:id="@+id/myView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentBottom="true"
        android:layout_below="@+id/top"
        android:gravity="center"
        android:orientation="horizontal">
    </LinearLayout>
</RelativeLayout>

On modifie ensuite l’activité principale pour y intégrer nos deux widgets :

package com.example.tv.myapplicationdessin;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.SeekBar;

public class MainActivity extends AppCompatActivity
{
    SeekBar seekBar;
    CompteurVitesse compteurVitesse;
    Boussole boussole;
    MyView myViewCompteur;
    MyView myViewBoussole;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        seekBar = (SeekBar)findViewById(R.id.seekBar);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                if(progress <= 270)
                {
                    compteurVitesse.setVitesse((float) progress);
                    myViewCompteur.invalidate();
                }
                boussole.setDirection((float)progress);
                myViewBoussole.invalidate();
                System.out.println("<Dessin> progress : " + progress);
            }
        });

        boussole = new Boussole(0);
        compteurVitesse = new CompteurVitesse(0);
        //compteurVitesse.setTaille(600);

        LinearLayout myLayout1 = (LinearLayout)findViewById(R.id.myView);

        myViewBoussole = new MyView(this, boussole);
        myLayout1.addView(myViewBoussole);

        myViewCompteur = new MyView(this, compteurVitesse);
        myLayout1.addView(myViewCompteur);
    }
}

La vue personnalisée a changé, elle reçoit maintenant en paramètre l’objet Drawable à dessiner dans le Canvas :

package com.example.tv.myapplicationdessin;

import android.view.View;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;

public class MyView extends View
{
    Paint paint = new Paint(Paint.FAKE_BOLD_TEXT_FLAG);
    Path starPath;
    Path curvePath;
    Paint textPaint = new Paint(Paint.LINEAR_TEXT_FLAG);
    private final Drawable drawable;

    public MyView(Context context, Drawable draw)
    {
        super(context);
        setMinimumWidth(600);
        setMinimumHeight(600);
        drawable = draw;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
    {
        setMeasuredDimension(getSuggestedMinimumWidth(), getSuggestedMinimumHeight());
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        canvas.save();
        drawable.draw(canvas);
        canvas.restore();

        // Un cadre
        /*Paint paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.STROKE);
        Rect r = canvas.getClipBounds();
        canvas.drawRect(r, paint);*/
    }
}

Le widget Boussole :

package com.example.tv.myapplicationdessin;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;

public class Boussole extends Drawable
{
    private ColorFilter filter;
    private int opacity;
    private float direction;
    private int cx;
    private int cy;
    private int lenght;

    public Boussole(float degres)
    {
        direction = degres;
        cx = 300;
        cy = 300;
        lenght = 550;
        opacity = 100;
    }

    public void setDirection(float degres)
    {
        direction = degres;
    }

    @Override
    public void draw(Canvas canvas)
    {
        Paint paint = new Paint();

        paint.setColorFilter(filter);
        paint.setAlpha(opacity);
        paint.setTextSize(30);

        canvas.save();

        Path sudPath = creerSudPath();
        Path nordPath = creerNordPath();

        float fontHeight = paint.getFontMetrics().ascent  +  paint.getFontMetrics().descent;

        // Centrer le compas au milieu de l'écran
        canvas.translate(cx, cy);

        // Effectuer une rotation de canvas, de sorte que la flèche indique le nord
        // quand on la dessine verticalement
        canvas.rotate(direction);

        // Définir le cadran
        paint.setColor(0xFFEEEEEE);
        canvas.drawCircle(0, 0, (lenght+20) / 2, paint);

        // Dessiner le cercle de la boussole
        paint.setColor(Color.GRAY);
        canvas.drawCircle(0, 0, lenght / 2, paint);

        // Dessiner la graduation du compas
        paint.setColor(Color.WHITE);
        float hText = - lenght/2 - fontHeight+3;

        // Tous les 15° faire une graduation
        int step = 15;
        for (int degree = 0; degree < 360; degree = degree + step)
        {
            // Si ce n'est pas un point cardinal, dessiner une graduation
            if ((degree % 90) != 0)
            {
                canvas.drawText("|", 0, hText, paint);
            }
            canvas.rotate(-step);
        }

        // Dessiner les points cardinaux
        canvas.drawText("N", 0, hText, paint);
        canvas.rotate(-90);
        canvas.drawText("W", 0, hText, paint);
        canvas.rotate(-90);
        canvas.drawText("S", 0, hText, paint);
        canvas.rotate(-90);
        canvas.drawText("E", 0, hText, paint);
        canvas.rotate(-90);

        // Dessiner les flèches
        paint.setStyle(Paint.Style.FILL);
        paint.setColor(Color.RED);
        canvas.drawPath(nordPath, paint);
        paint.setColor(Color.BLUE);
        canvas.drawPath(sudPath, paint);

        // Restaurer l'état initial du canvas
        canvas.restore();
    }

    private Path creerSudPath()
    {
        Path sudPath = new Path();

        sudPath.moveTo(-10,0);
        sudPath.lineTo(0,lenght/3);
        sudPath.lineTo(10,0);
        sudPath.close();

        return  sudPath;
    }

    private Path creerNordPath()
    {
        Path nordPath = new Path();

        nordPath.moveTo(0, -(lenght/3));
        nordPath.lineTo(-10, 0);
        nordPath.lineTo(10,0);
        nordPath.close();

        return  nordPath;
    }

    @Override
    public int getOpacity()
    {
        return PixelFormat.TRANSLUCENT;
    }

    @Override
    public void setAlpha(int alpha)
    {
    }

    @Override
    public void setColorFilter(ColorFilter cf)
    {
    }
}

public class CompteurVitesse extends Drawable
{
    private ColorFilter filter;
    private int opacity;
    private float vitesse;
    private int taille;
    private int cx;
    private int cy;
    private int longueur;
    private int pas;
    private int graduation;

    ...
}

On obtient :

Lien : Digression graphique sur mathias-seguy.developpez.com

Dessiner des graphiques

Ici, on va utiliser la bibliothèque GraphView pour dessiner des graphiques dans une vue.

Liens :

Il faut tout d’abord ajouter compile 'com.jjoe64:graphview:4.2.1' à votre fichier app/build.gradle :

apply plugin: 'com.android.application'

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "com.example.tv.myapplicationdessin"
        minSdkVersion 16
        targetSdkVersion 25
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.jjoe64:graphview:4.2.1'
    compile 'com.android.support:appcompat-v7:25.3.1'
    compile 'com.android.support:design:25.3.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'
}

Ensuite, il faut ajouter une vue GraphView dans le layout :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                android:layout_width="match_parent"
                android:layout_height="match_parent">
    <LinearLayout
        android:id="@+id/top"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:orientation="vertical">
        <TextView
            android:id="@+id/textView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_margin="15dp"
            android:gravity="center"
            android:text="Graphique sous Android (tv)"
            android:textSize="24sp"/>
        <SeekBar
            android:id="@+id/seekBar"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:max="100"/>
    </LinearLayout>
    <com.jjoe64.graphview.GraphView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentBottom="true"
        android:layout_below="@+id/top"
        android:gravity="center"
        android:id="@+id/graph" />
</RelativeLayout>

Pour terminer, on dessine un graphique à partir de l’activité principale :

package com.example.tv.myapplicationgraph;

import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.SeekBar;

import com.jjoe64.graphview.GraphView;
import com.jjoe64.graphview.LegendRenderer;
import com.jjoe64.graphview.series.BarGraphSeries;
import com.jjoe64.graphview.series.DataPoint;
import com.jjoe64.graphview.series.LineGraphSeries;

public class MainActivity extends AppCompatActivity
{
    SeekBar seekBar;
    GraphView graph;
    private LineGraphSeries<DataPoint> mSeries;
    private double graphLastXValue = -1d;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        seekBar = (SeekBar)findViewById(R.id.seekBar);
        seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {
            }

            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                graphLastXValue += 1d;
                mSeries.appendData(new DataPoint(graphLastXValue, progress), true, 40);
                System.out.println("<Dessin> progress : " + progress);
            }
        });

        graph = (GraphView) findViewById(R.id.graph);
        // Exemple 1
        /*LineGraphSeries<DataPoint> series = new LineGraphSeries<DataPoint>(new DataPoint[] {
                new DataPoint(0, 1),
                new DataPoint(1, 5),
                new DataPoint(2, 3),
                new DataPoint(3, 2),
                new DataPoint(4, 6)
        });
        graph.addSeries(series);*/

        // Exemple 2
        /*BarGraphSeries<DataPoint> series = new BarGraphSeries<>(new DataPoint[] {
                new DataPoint(0, 1),
                new DataPoint(1, 5),
                new DataPoint(2, 3),
                new DataPoint(3, 2),
                new DataPoint(4, 6)
        });
        graph.addSeries(series);*/

        // Exemple 3 : avec la seekbar
        graph.setTitle("Un graphique");
        graph.setTitleTextSize(40);
        graph.setTitleColor(Color.BLUE);

        mSeries = new LineGraphSeries<>();
        graph.addSeries(mSeries);
        graph.getViewport().setYAxisBoundsManual(true);
        graph.getViewport().setXAxisBoundsManual(true);
        graph.getViewport().setMinY(0);
        graph.getViewport().setMaxY(100);
        graph.getViewport().setMinX(0);
        graph.getViewport().setMaxX(40);

        // Légende
        mSeries.setTitle("SeekBar");
        graph.getLegendRenderer().setVisible(true);
        graph.getLegendRenderer().setAlign(LegendRenderer.LegendAlign.TOP);

        // Zooming and scrolling
        //graph.getViewport().setScalable(true); // enables horizontal zooming and scrolling
        //graph.getViewport().setScalableY(true); // enables vertical zooming and scrolling
        //graph.getViewport().setScrollable(true); // enables horizontal scrolling
        //graph.getViewport().setScrollableY(true); // enables vertical scrolling
    }
}

On obtient (l’exemple 3 avec la seekbar) :

Documentation

Retour