Rechercher
 

Commencer à coder un Sudoku avec Android Studio

Accès rapide :
Création et configuration d'un nouveau projet
Implémentation de votre modèle de données
Création d'un nouveau widget d'affichage de la grille
Tracé de la grille et des boutons de contrôle
Gestion des événements
Ajout de l'aide visuelle
Un exemple d'application de Sudoku finalisé

Ce tuto va vous montrer comment démarrer la programmation d'un jeu de Sudoku pour Android en utilisant Android Studio. Dans cette version, nous ne gérerons qu'une unique grille et nous ne proposerons qu'une seule activité permettant de remplir la grille de Sudoku.

Si vous ne maitrisez pas bien le principe du jeu de Sudoku, je vous propose de lire le document suivant : https://fr.wikipedia.org/wiki/Sudoku.

Il vous appartiendra de poursuivre, ou non, le développement de cette application pour arriver à un logiciel complet et fini.

ce tuto s'adresse à des programmeurs Android expérimentés. Si vous ne maitrisez pas bien Java, je vous conseille de commencer par approfondir vos connaissances sur Java : le tutoriel Java. De même, si vous n'avez pas encore produit d'application Android, je vous recommande vivement de débuter par les bases en suivant, au minimum, ce tutoriel.

Commencer à coder un Sudoku avec Android Studio

Création et configuration d'un nouveau projet

Donc la première étape consiste à créer un nouveau projet Android. Appelez-le « My Sudoku » et choisissez une activité de type « Empty Activity ». Une fois le projet créé, je vous propose de modifier le fichier de manifest de votre application afin de figer l'orientation de votre activité au mode « Portrait » : ce sera plus simple dans un premier temps. L'exemple de code ci-dessous vous montre la modification à effectuer (ligne 12, attribut XML android:screenOrientation="portrait").

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="fr.koor.mysudoku">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity" android:screenOrientation="portrait">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
Le fichier de manifest de votre application Android

Implémentation de votre modèle de données

La seconde partie consiste à définir les classes qui nous permettront de maintenir en mémoire l'état courant de la partie. Il nous faudra notamment :

A noter aussi qu'une grille pourra être associée à un niveau de difficulté, bien que dans ce tuto nous en gérerons qu'un seul (niveau intermédaire). Il vous appartiendra d'ajouter des grilles pour les autres niveaux, ou d'implémenter un algorithme de génération de grilles en fonction d'un niveau de difficulté.

Ce premier extrait de code défini un type énuméré représentant les différents niveaux proposés par le jeu.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
package fr.koor.mysudoku;

/**
 * This enum type specify all difficulty levels proposed by the game.  
 */
public enum GameLevel {
    VERY_EASY, EASY, MEDIUM, HARD, EVIL
}
Fichier GameLevel.java : un type énuméré pour représenter les différents niveaux de jeux supportés par l'application

Ce deuxième extrait de code permet de maintenir les données associées à une partie. Notez qu'une méthode de fabrique (factory method) permet la construction d'une instance de grille, à partir du niveau spécifié. Vous pourrez par la suite étoffer cette méthode pour gérer d'autres grilles.

 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 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
 100 
 101 
 102 
 103 
 104 
 105 
 106 
 107 
 108 
 109 
 110 
 111 
 112 
 113 
 114 
 115 
 116 
 117 
 118 
 119 
 120 
 121 
 122 
 123 
 124 
 125 
 126 
 127 
 128 
 129 
 130 
 131 
 132 
 133 
 134 
 135 
 136 
 137 
 138 
 139 
 140 
 141 
 142 
 143 
 144 
 145 
 146 
 147 
 148 
 149 
package fr.koor.mysudoku;


import java.io.Serializable;

/**
 * This class represent the grid. The grid contains 81 cells (of type GameCell).
 */
public class GameBoard {

    /**
     * This class represent one cell and it's informations.
     */
    public static class GameCell {
        public int realValue;
        public int assumedValue;
        public boolean isInitial = false;
        public boolean [] marks = { false, false, false, false, false, false, false, false, false };

        public GameCell( int realValue ) {
            this.realValue = realValue;
        }

        public GameCell( int realValue, int isInitial ) {
            this.realValue = realValue;
            this.isInitial = isInitial == 1;
            if ( this.isInitial ) this.assumedValue = realValue;
        }
    }


    public GameLevel level;
    public boolean bigNumber = true;

    public int currentCellX = -1;
    public int currentCellY = -1;


    public GameCell [][] cells;

    /**
     * The class constructor
     * @param level     The associated level.
     * @param cells     The states for each cells of the grid.
     */
    private GameBoard( GameLevel level, GameCell [][] cells ) {
        this.level = level;
        this.cells = cells;
    }
    
    /**
     * Return the currently selected value. A cell must be selected, otherwise 0 is returned.
     */
    public int getSelectedValue() {
        // We need to know the current cell
        if ( this.currentCellX == -1 ) return 0;
        if ( this.currentCellY == -1 ) return 0;

        GameCell currentCell = this.cells[ this.currentCellY ][ this.currentCellX ];
        return currentCell.assumedValue;
    }

    /**
     * This method change the state of the selected cell for this grid.
     * If no cell is selected, the method do nothing.
     * We cannot change the state af an initial state. 
     * @param value     The value to insert in the selected cell
     */
    public void pushValue( int value ) {
        // We need to know the current cell
        if ( this.currentCellX == -1 ) return;
        if ( this.currentCellY == -1 ) return;

        GameCell currentCell = this.cells[ this.currentCellY ][ this.currentCellX ];
        // We cannot update an initial cell
        if ( currentCell.isInitial ) return;

        if ( this.bigNumber ) {
            // Change the assumed value
            currentCell.assumedValue = value;
        } else {
            // Change the mark states
            currentCell.marks[value-1] = ! currentCell.marks[value-1];
        }
    }
    
    /**
     * Clear all informations (value and marks) for the selected cell.
     * We cannot change the state af an initial state. 
     */
    public void clearCell() {
        // We need to know the current cell
        if ( this.currentCellX == -1 ) return;
        if ( this.currentCellY == -1 ) return;

        GameCell currentCell = this.cells[ this.currentCellY ][ this.currentCellX ];

        // We cannot update an initial cell
        if ( currentCell.isInitial ) return;

        currentCell.assumedValue = 0;
        currentCell.marks = new boolean[] { false, false, false, false, false, false, false, false, false };
    }
    
    /**
     * A factory method that produce an initial grid to solve.
     * @param level     Just the medium level is actually supported
     * @return          A new grid to solve.
     */
    public static GameBoard getGameBoard( GameLevel level ) {

        if ( level != GameLevel.MEDIUM ) throw new RuntimeException( "Not actually implemented" );

        // TODO add code for generate differents Grid for each level

        return new GameBoard( level, new GameCell[][] {
            { new GameCell(9,1), new GameCell(2,0), new GameCell(8,0), 
                new GameCell(7,1), new GameCell(5,0), new GameCell(4,0), 
                    new GameCell(1,1), new GameCell(3,0), new GameCell(6,0) },
            { new GameCell(6,0), new GameCell(7,0), new GameCell(1,0), 
                new GameCell(8,1), new GameCell(2,0), new GameCell(3,1), 
                    new GameCell(5,0), new GameCell(4,0), new GameCell(9,0) },
            { new GameCell(3,0), new GameCell(5,1), new GameCell(4,1), 
                new GameCell(9,0), new GameCell(1,1), new GameCell(6,0), 
                    new GameCell(2,0), new GameCell(7,1), new GameCell(8,0) },

            { new GameCell(4,1), new GameCell(9,1), new GameCell(6,0), 
                new GameCell(2,0), new GameCell(3,0), new GameCell(7,0), 
                    new GameCell(8,1), new GameCell(5,1), new GameCell(1,0) },
            { new GameCell(8,0), new GameCell(1,1), new GameCell(5,0), 
                new GameCell(4,1), new GameCell(6,0), new GameCell(9,1), 
                    new GameCell(7,0), new GameCell(2,1), new GameCell(3,0) },
            { new GameCell(7,0), new GameCell(3,1), new GameCell(2,1), 
                new GameCell(5,0), new GameCell(8,0), new GameCell(1,0), 
                    new GameCell(9,0), new GameCell(6,1), new GameCell(4,1) },

            { new GameCell(5,0), new GameCell(4,1), new GameCell(3,0), 
                new GameCell(1,0), new GameCell(9,1), new GameCell(2,0), 
                    new GameCell(6,1), new GameCell(8,1), new GameCell(7,0) },
            { new GameCell(2,0), new GameCell(6,0), new GameCell(9,0), 
                new GameCell(3,1), new GameCell(7,0), new GameCell(8,1), 
                    new GameCell(4,0), new GameCell(1,0), new GameCell(5,0) },
            { new GameCell(1,0), new GameCell(8,0), new GameCell(7,1), 
                new GameCell(6,0), new GameCell(4,0), new GameCell(5,1), 
                    new GameCell(3,0), new GameCell(9,0), new GameCell(2,1) }
        });
    }

}
Fichier GameBoard.java : permet de représenter l'état de la grille en cours de résolution.

Quelques explications complémentaires sont peut être nécessaires. La classe GameCell permet de représenter l'état d'une cellule. C'est une classe statique portée par la class GameBoard. On y retrouve notamment :

La classe GameBoard permet de représenter une grille de Sodoku, cette grille étant constituée des plusieurs cellules (plus précisément 9x9 cellules soit 81 au total). Cette classe défini les attributs suivants :

J'ai préféré, en mon âme et conscience laisser les attributs de ces deux classes publiques. Cela m'évite de générer les getters/setters en sachant que je n'ai pas de contraintes particulières à garantir. Si vous préférez rajouter ces getters/setters, n'hésitez surtout pas et dans ce cas privatisez les attributs associés.

La méthode getSelectedValue renvoi la valeur supposée de la cellule en cours de sélection. Si aucune cellule n'est sélectionnée, la valeur 0 sera renvoyée.

La méthode pushValue permet de changer l'état de la grille de Sudoku en modifiant la cellule en cours de sélection. Si aucune cellule n'est actuellement sélectionnée, la méthode sortira sans avoir effectué de modification. De plus, il n'est pas possible de modifier une cellule initialement dévoilée. La modification engagée sera dépendante de l'état de l'attribut bigNumber : si l'attribut vaut true, cette valeur deviendra la nouvelle valeur supposée de la cellule, dans l'autre cas, c'est la connaissance des marques qui sera mis à jour.

La méthode clearCell permet de réinitialiser l'état d'une cellule. Toutes les marques seront aussi perdues. De plus, il n'est pas possible de modifier une cellule initialement dévoilée.

Enfin, la méthode statique getGameBoard renvoie une grille codée en dur dans le code.

Création d'un nouveau widget d'affichage de la grille

Ajouter une nouvelle classe dans votre projet : je vous propose de la nommer GameView. Cette classe va devenir un widget : pour ce faire, il est nécessaire de :

Voici un exemple de code pour cette classe de widget.

 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 
package fr.koor.mysudoku;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

/**
 * This class is a new android widget. You can put it an Android Layout.
 */
public class GameView extends View {

    /**
     * Your pencil for draw all elements
     */
    private Paint paint = new Paint( Paint.ANTI_ALIAS_FLAG );

    /**
     * My first class constructor.
     */
    public GameView( Context context ) {
        super( context );
    }

    /**
     * My second class constructor. 
     * If you put this widget in a layout, using the Android Studio layout Wizard,
     * you must provide this constructor. 
     */
    public GameView( Context context, @Nullable AttributeSet attrs ) {
        super( context, attrs );
    }


    /**
     * In this method you must draw each graphical element on the canvas, with your paint.
     * As example, we draw a red cross on the canvas.
     */
    @Override
    protected void onDraw( Canvas canvas ) {

        paint.setColor( Color.RED );
        canvas.drawLine( 0, 0, getWidth(), getHeight(), paint );
        canvas.drawLine( 0, getHeight(), getWidth(), 0, paint );

    }
    
}
Démarrage de notre classe de Widget
il est important maintenant de réaliser un « build » de votre projet. Effectivement, si vous souhaitez avoir un résultat visuel dans l'éditeur de layout d'Android Studio, cette étape est obligatoire car Android Studio va instancier votre composant graphique puis exécuter la méthode onDraw pour obtenir un visuel.

Maintenant, veuillez ajouter ce widget dans votre layout d'activité. Vous pouvez soit éditer directement le code XML, soit utiliser l'éditeur graphique d'Android Studio : à votre convenance. Voici comment doit être votre fichier XML après la modification.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    tools:context="fr.koor.mysudoku.MainActivity">


    <view
        class="fr.koor.mysudoku.GameView"
        id="@+id/view"
        layout_alignParentEnd="true"
        layout_alignParentRight="true"
        layout_alignParentTop="true"
        layout_marginEnd="136dp"
        layout_marginRight="136dp"
        layout_marginTop="165dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        
</RelativeLayout>
Injection de notre widget dans le layout de notre activité

Voici maintenant le résultat observable dans Android Studio : n'oubliez pas de lancer un « build » avant !!!

Il est maintenant temps de commencer à réellement dessiner notre grille de Sudoku.

Tracé de la grille et des boutons de contrôle

Pour les boutons, j'ai fait un choix particulier : j'aurais pu utiliser des boutons Android classiques que j'aurais alors du placer dans le layout de l'activité. Dans ce cas, j'aurais pu rencontrer quelques difficultés pour faire en sorte que chaque bouton occupe au mieux l'espace disponible. A la place, j'ai décidé que mes boutons seraient dessinés, dans le onDraw de la classe GameView.

La capture d'écran ci-dessous vous montre le visuel que nous souhaitons obtenir. Notez que deux images sont utilisées : soit vous vous débrouillez pour faire les votres, soit vous pouvez télécharger ce zip (il contient les deux images proposées). Je vous conseille vivement d'utiliser des images transparentes au format PNG. Cela vous permettra de facilement changer la couleur d'arrière plan si celle-ci ne vous plait plus.

Vous trouverez ci-dessous le code permettant de produire ce visuel. Pour toute explications complémentaires, je vous renvoie vers la vidéo associée à ce tuto (voir tout en haut de ce document).

 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 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
 100 
 101 
 102 
 103 
 104 
 105 
 106 
 107 
 108 
 109 
 110 
 111 
 112 
 113 
 114 
 115 
 116 
 117 
 118 
 119 
 120 
 121 
 122 
 123 
 124 
 125 
 126 
 127 
 128 
 129 
 130 
 131 
 132 
 133 
 134 
 135 
 136 
 137 
 138 
 139 
 140 
 141 
 142 
 143 
 144 
 145 
 146 
 147 
 148 
 149 
 150 
 151 
 152 
 153 
 154 
 155 
 156 
 157 
 158 
 159 
 160 
 161 
 162 
 163 
 164 
 165 
 166 
 167 
 168 
 169 
 170 
 171 
 172 
 173 
 174 
 175 
 176 
 177 
 178 
 179 
 180 
 181 
 182 
 183 
 184 
 185 
 186 
 187 
 188 
 189 
 190 
 191 
 192 
 193 
 194 
 195 
 196 
 197 
 198 
 199 
 200 
 201 
 202 
 203 
 204 
 205 
 206 
 207 
 208 
 209 
 210 
 211 
 212 
 213 
 214 
 215 
 216 
 217 
 218 
 219 
 220 
 221 
 222 
 223 
 224 
 225 
 226 
 227 
 228 
 229 
 230 
 231 
 232 
 233 
 234 
 235 
 236 
 237 
 238 
 239 
 240 
 241 
package fr.koor.mysudoku;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;


public class GameView extends View {

    private Paint paint = new Paint( Paint.ANTI_ALIAS_FLAG );
    private GameBoard gameBoard = GameBoard.getGameBoard( GameLevel.MEDIUM );

    private float gridWidth;
    private float gridSeparatorSize;
    private float cellWidth;
    private float buttonWidth;
    private float buttonRadius;
    private float buttonMargin;

    private Bitmap eraserBitmap;
    private Bitmap pencilBitmap;
    private Bitmap littlePencilBitmap;


    public GameView(Context context) {
        super(context);
        this.init();
    }

    public GameView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }

    private void init() {

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);

        // We compute some sizes
        gridSeparatorSize = (w / 9f) / 20f;

        gridWidth = w;                                  // Size of the grid (it's a square)
        cellWidth = gridWidth / 9f;                     // Size of a cell (it's a square too)
        buttonWidth = w / 7f;                           // Size of a button
        buttonRadius = buttonWidth / 10f;               // Size of the rounded corner for a button
        buttonMargin = (w - 6*buttonWidth) / 7f;        // Margin between two buttons


        // We resize for this screen the two images
        eraserBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.eraser);
        eraserBitmap = Bitmap.createScaledBitmap(eraserBitmap, 
                (int) (buttonWidth*0.8f), (int) (buttonWidth*0.8f), false);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.pencil);
        pencilBitmap = Bitmap.createScaledBitmap(bitmap, 
                (int) (buttonWidth*0.8f), (int) (buttonWidth*0.8), false);
        littlePencilBitmap = Bitmap.createScaledBitmap(bitmap, 
                (int) (buttonWidth/3), (int) (buttonWidth/3), false);

    }

    @Override
    protected void onDraw(Canvas canvas) {
        
        // --- Draw cells ---

        paint.setTextAlign( Paint.Align.CENTER );

        for( int y=0; y<9; y++ ) {
            for( int x=0; x<9; x++ ) {
                int backgroundColor = Color.WHITE;

                // Check if cell is initially proposed: in this case, the background is grey
                if ( gameBoard.cells[y][x].isInitial ) {
                    backgroundColor = 0xFFF0F0F0;
                }

                // Draw the background for the current cell
                paint.setColor( backgroundColor );
                canvas.drawRect(x * cellWidth,
                                y * cellWidth ,
                                (x+1) * cellWidth,
                                (y+1) * cellWidth,
                                 paint);

                if (gameBoard.cells[y][x].assumedValue != 0) {

                    // Draw the assumed value for the cell.
                    paint.setColor(0xFF000000);
                    paint.setTextSize( cellWidth*0.7f );
                    canvas.drawText("" + gameBoard.cells[y][x].assumedValue,
                            x * cellWidth + cellWidth / 2,
                            y * cellWidth + cellWidth * 0.75f, paint);

                } else {

                    // Draw each mark if exists
                    paint.setTextSize( cellWidth*0.33f );
                    paint.setColor( 0xFFA0A0A0 );
                    if ( gameBoard.cells[y][x].marks[0] ) {
                        canvas.drawText("1",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[1] ) {
                        canvas.drawText("2",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[2] ) {
                        canvas.drawText("3",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[3] ) {
                        canvas.drawText("4",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[4] ) {
                        canvas.drawText("5",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[5] ) {
                        canvas.drawText("6",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[6] ) {
                        canvas.drawText("7",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[7] ) {
                        canvas.drawText("8",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[8] ) {
                        canvas.drawText("9",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                }
            }
        }

        // --- Draw the grid lines ---
        paint.setColor( Color.GRAY );
        paint.setStrokeWidth( gridSeparatorSize/2 );
        for( int i=0; i<=9; i++ ) {
            canvas.drawLine( i*cellWidth, 0, i*cellWidth, cellWidth*9, paint );
            canvas.drawLine( 0,i*cellWidth, cellWidth*9, i*cellWidth, paint );
        }
        paint.setColor( Color.BLACK );
        paint.setStrokeWidth( gridSeparatorSize );
        for( int i=0; i<=3; i++ ) {
            canvas.drawLine( i*(cellWidth*3), 0, i*(cellWidth*3), cellWidth*9, paint );
            canvas.drawLine( 0,i*(cellWidth*3), cellWidth*9, i*(cellWidth*3), paint );
        }


        // --- Draw border for the current selected cell ---
        if ( gameBoard.currentCellX != -1 && gameBoard.currentCellY != -1 ) {
            paint.setColor( 0xFF_30_3F_9F );
            paint.setStrokeWidth( gridSeparatorSize * 1.5f );
            paint.setStyle( Paint.Style.STROKE );
            canvas.drawRect( gameBoard.currentCellX * cellWidth,
                             gameBoard.currentCellY * cellWidth,
                             (gameBoard.currentCellX+1) * cellWidth,
                             (gameBoard.currentCellY+1) * cellWidth,
                             paint);
            paint.setStyle( Paint.Style.FILL_AND_STROKE );
            paint.setStrokeWidth( 1 );
        }

        // --- Buttons bar ---

        float buttonsTop = 9*cellWidth + gridSeparatorSize/2;

        paint.setColor(0xFFC7DAF8);
        canvas.drawRect(0, buttonsTop, gridWidth, getHeight(), paint);

        float buttonLeft = buttonMargin;
        float buttonTop = buttonsTop + buttonMargin;

        paint.setTextAlign(Paint.Align.CENTER);
        paint.setTextSize(buttonWidth * 0.7f);

        for (int i = 1; i <= 9; i++) {
            paint.setColor( 0xFFFFFFFF );
            // Attention aux new !!! Mais ici, on n'est pas trop gourmand
            // Il existe une autre version de drawRoundRect, mais elle necessite
            // que vous modifiez la version minimale supportee pour Android :-(
            RectF rectF = new RectF(buttonLeft, buttonTop, 
                                    buttonLeft + buttonWidth, buttonTop + buttonWidth);
            canvas.drawRoundRect(rectF, buttonRadius, buttonRadius, paint);

            paint.setColor( 0xFF000000 );
            canvas.drawText("" + i, rectF.centerX(), rectF.top + rectF.height() * 0.75f, paint);

            if (i != 6) {
                buttonLeft += buttonWidth + buttonMargin;
            } else {
                buttonLeft = buttonMargin;
                buttonTop += buttonWidth + buttonMargin;
            }
        }

        int imageWidth = (int) (buttonWidth * 0.8f);
        int imageMargin = (int) (buttonWidth * 0.1f);

        // --- eraser ---
        paint.setColor(0xFFFFFFFF);
        RectF rectF = new RectF( buttonLeft, buttonTop, 
                                 buttonLeft + buttonWidth, buttonTop + buttonWidth );
        canvas.drawRoundRect( rectF, buttonRadius, buttonRadius, paint );
        canvas.drawBitmap( eraserBitmap, 
                           buttonLeft + imageMargin, buttonTop + imageMargin, paint );
        buttonLeft += buttonWidth + buttonMargin;

        // --- pencil ---
        paint.setColor(0xFFFFFFFF);
        rectF = new RectF( buttonLeft, buttonTop, buttonLeft + buttonWidth, buttonTop + buttonWidth );
        canvas.drawRoundRect( rectF, buttonRadius, buttonRadius, paint );
        Bitmap bitmap = gameBoard.bigNumber ? pencilBitmap : littlePencilBitmap;
        canvas.drawBitmap( bitmap, buttonLeft + imageMargin, buttonTop + imageMargin, paint );

    }
}
Tracé de la grille et des boutons de contrôle

Pour facilement tester l'affichage des marques, vous pouvez temporairement modifier le code de la classe GameBoard. De même, vous pouvez facilement simuler une sélection de cellule. Voici un petit exemple (pensez à revenir à l'état initial après vérification).

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
    public int currentCellX = 5;    // HERE
    public int currentCellY = 5;    // HERE
    public int currentSelectedValue = -1;


    public GameCell [][] cells;

    private GameBoard( GameLevel level, GameCell [][] cells ) {
        this.level = level;
        this.cells = cells;

        // HERE
        this.cells[0][1].marks = new boolean[] { true, true, true, true, true, true, true, true, true };
    }
modification de la classe GameBoard pour vérifier vos affichages

Gestion des événements

Maintenant on ajoute les gestionnaires d'événements pour que nous puissions commencer à jouer. Pour ce faire, je vais utiliser un GestureDetector. Vous trouverez ci-dessous le code à ajouter à votre classe GameView. Pour toute explications complémentaires, je vous renvoie vers la vidéo associée à ce tuto (voir tout en haut de ce document).

 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 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
 100 
 101 
 102 
 103 
 104 
 105 
 106 
 107 
 108 
 109 
 110 
 111 
 112 
 113 
 114 
 115 
 116 
 117 
 118 
 119 
 120 
 121 
 122 
 123 
 124 
 125 
 126 
 127 
 128 
 129 
 130 
 131 
 132 
 133 
package fr.koor.mysudoku;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;


public class GameView extends View implements GestureDetector.OnGestureListener {

    private GestureDetector gestureDetector;
    // ... other attributes ...


    public GameView(Context context) {
        super(context);
        this.init();
    }

    public GameView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        this.init();
    }

    private void init() {
        // Activate the gesture detector
        gestureDetector = new GestureDetector( getContext(),  this );
    }

    // ... onSizeChanged, onDraw, ...
    
    // --- Events handlers ---

    // Override from View
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return gestureDetector.onTouchEvent(event);
    }

    // Override from OnGestureDectector
    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public void onShowPress(MotionEvent e) {

    }

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        RectF rectF;

        // --- Check grid cell click ---
        if ( e.getY() < gridWidth ) {
            int cellX = (int)( e.getX() / cellWidth );
            int cellY = (int)( e.getY() / cellWidth );

            gameBoard.currentCellX = cellX;
            gameBoard.currentCellY = cellY;
            postInvalidate();
            return true;
        }

        float buttonLeft = buttonMargin;
        float buttonTop = 9 * cellWidth + gridSeparatorSize / 2;

        if ( gameBoard.currentCellX != -1 && gameBoard.currentCellY != -1 ) {

            // --- Check number buttons ---
            for (int i = 1; i <= 9; i++) {
                rectF = new RectF(buttonLeft, buttonTop, buttonLeft + buttonWidth, buttonTop + buttonWidth);
                if (rectF.contains(e.getX(), e.getY())) {
                    gameBoard.pushValue(i);
                    postInvalidate();
                    return true;
                }

                if (i != 6) {
                    buttonLeft += buttonWidth + buttonMargin;
                } else {
                    buttonLeft = buttonMargin;
                    buttonTop += buttonWidth + buttonMargin;
                }
            }

            // --- eraser button ---
            rectF = new RectF(buttonLeft, buttonTop, buttonLeft + buttonWidth, buttonTop + buttonWidth);
            if (rectF.contains(e.getX(), e.getY())) {
                gameBoard.clearCell();
                this.invalidate();
                return true;
            }
            buttonLeft += buttonWidth + buttonMargin;
        }

        // --- pencil button ---
        rectF = new RectF( buttonLeft, buttonTop, buttonLeft+buttonWidth, buttonTop+buttonWidth );
        if ( rectF.contains( e.getX(), e.getY() ) ) {
            gameBoard.bigNumber = ! gameBoard.bigNumber;
            this.invalidate();
            return true;
        }

        return true;
    }

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {

    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

}
Ajouts des gestionnaires d'événements
les appels à la méthode postInvalidate() permettent de forcer une réactualisation de la vue suite aux changements d'états de notre modèle de données. Si vous enlevez ces appels, vous ne constateriez pas les changements suite à vos appuis sur les différents boutons.

Ajout de l'aide visuelle

Il ne reste plus qu'à ajouter un peu d'assistance visuelle pour montrer toutes les autres occurences de la valeur présente dans la cellule en cours de sélection. Le code à modifier dans la méthode onDraw vous est proposé ci-dessous. Encore une fois, pour toute explications complémentaires, je vous renvoie vers la vidéo associée à ce tuto (voir tout en haut de ce document).

 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 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
 100 
 101 
 102 
 103 
 104 
 105 
 106 
 107 
 108 
 109 
 110 
 111 
 112 
 113 
 114 
 115 
 116 
 117 
 118 
 119 
 120 
 121 
 122 
 123 
 124 
 125 
 126 
 127 
 128 
 129 
 130 
 131 
 132 
 133 
 134 
 135 
 136 
 137 
 138 
 139 
 140 
 141 
 142 
 143 
 144 
 145 
 146 
 147 
 148 
 149 
 150 
 151 
 152 
 153 
    @Override
    protected void onDraw(Canvas canvas) {
        paint.setColor( Color.RED );

        // --- Draw cells ---

        paint.setTextAlign( Paint.Align.CENTER );
        paint.setTextSize( cellWidth*0.7f );


        for( int y=0; y<9; y++ ) {
            for( int x=0; x<9; x++ ) {
                int backgroundColor = Color.WHITE;

                // Highlight the current row, current column and the current block
                // A value can be appeared only one time into all highlighted cells.
                if ( gameBoard.currentCellX != -1 && gameBoard.currentCellY != -1 ) {
                    if ( (x / 3 == gameBoard.currentCellX / 3 && y / 3 == gameBoard.currentCellY / 3) ||
                         (x == gameBoard.currentCellX && y != gameBoard.currentCellY) ||
                         (x != gameBoard.currentCellX && y == gameBoard.currentCellY)  ) {
                        backgroundColor = 0xFF_FF_F0_F0;
                    }
                }

                // Check if cell is initially proposed: in this case, the background is grey
                if ( gameBoard.cells[y][x].isInitial ) {
                    if ( backgroundColor == 0xFF_FF_F0_F0 ) {
                        backgroundColor = 0xFF_F4_F0_F0;
                    } else {
                        backgroundColor = 0xFF_F0_F0_F0;
                    }
                }

                // Change the color for the currently selected value
                if ( gameBoard.getSelectedValue() > 0 &&
                     gameBoard.cells[y][x].assumedValue == gameBoard.getSelectedValue() ) {
                    backgroundColor = 0xFF_C7_DA_F8;
                }

                // Display errors (conflicts) in red color: an error appear if a value is present
                // at least two times in the same line, column or block.
                if ( gameBoard.cells[y][x].assumedValue > 0 ) {
                    for( int tx=0; tx<9; tx++ ) {
                        if ( tx != x &&
                             gameBoard.cells[y][tx].assumedValue == gameBoard.cells[y][x].assumedValue ) {
                            backgroundColor = 0xFF_FF_00_00;
                            break;
                        }
                    }
                    if ( backgroundColor != 0xFF_FF_00_00 ) {
                        for (int ty = 0; ty < 9; ty++) {
                            if ( ty != y &&
                                 gameBoard.cells[ty][x].assumedValue == gameBoard.cells[y][x].assumedValue ) {
                                backgroundColor = 0xFF_FF_00_00;
                                break;
                            }
                        }
                    }
                    if ( backgroundColor != 0xFF_FF_00_00 ) {
                        int bx = x / 3;
                        int by = y / 3;
                        for (int dy = 0; dy < 3; dy++) {
                            for (int dx = 0; dx < 3; dx++) {
                                int tx = bx * 3 + dx;
                                int ty = by * 3 + dy;
                                if ( tx != x && ty != y &&
                                     gameBoard.cells[ty][tx].assumedValue == gameBoard.cells[y][x].assumedValue ) {
                                    backgroundColor = 0xFF_FF_00_00;
                                    break;
                                }
                            }
                        }
                    }
                }

                // Draw the background for the current cell
                paint.setColor( backgroundColor );
                canvas.drawRect(x * cellWidth,
                                y * cellWidth ,
                                (x+1) * cellWidth,
                                (y+1) * cellWidth,
                                paint);

                if (gameBoard.cells[y][x].assumedValue != 0) {

                    // Draw the assumed value for the cell.
                    paint.setColor(0xFF000000);
                    paint.setTextSize( cellWidth*0.7f );
                    canvas.drawText("" + gameBoard.cells[y][x].assumedValue,
                            x * cellWidth + cellWidth / 2,
                            y * cellWidth + cellWidth * 0.75f, paint);

                } else {

                    // Draw each mark if exists
                    paint.setTextSize( cellWidth*0.33f );
                    if ( gameBoard.cells[y][x].marks[0] ) {
                        paint.setColor(gameBoard.getSelectedValue()==1 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("1",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[1] ) {
                        paint.setColor(gameBoard.getSelectedValue()==2 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("2",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[2] ) {
                        paint.setColor(gameBoard.getSelectedValue()==3 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("3",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.3f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[3] ) {
                        paint.setColor(gameBoard.getSelectedValue()==4 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("4",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[4] ) {
                        paint.setColor(gameBoard.getSelectedValue()==5 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("5",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[5] ) {
                        paint.setColor(gameBoard.getSelectedValue()==6 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("6",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.6f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[6] ) {
                        paint.setColor(gameBoard.getSelectedValue()==7 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("7",
                                x * cellWidth + cellWidth * 0.2f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[7] ) {
                        paint.setColor(gameBoard.getSelectedValue()==8 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("8",
                                x * cellWidth + cellWidth * 0.5f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                    if ( gameBoard.cells[y][x].marks[8] ) {
                        paint.setColor(gameBoard.getSelectedValue()==9 ? 0xFF4084EF : 0xFFA0A0A0);
                        canvas.drawText("9",
                                x * cellWidth + cellWidth * 0.8f,
                                y * cellWidth + cellWidth * 0.9f, paint);
                    }
                }
            }
        }
Ajout des aides visuelles dans la méthode onDraw

L'image ci-dessous vous montre le résultat de l'assistance visuelle produite en fonction de la cellule en cours de sélection.

Un exemple d'application de Sudoku finalisé

En fait, les codes proposés sont extraits d'une application que j'ai développée et finalisée : cette application se nomme E-Sudoku que vous pouvez télécharger sur le Play Store Android. Vous pouvez la tester pour voir à quoi pourrait ressembler votre version une fois finalisée. Voici une capture d'écran de cette application.