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.
Il vous appartiendra de poursuivre, ou non, le développement de cette application pour arriver à un logiciel complet et fini.
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> |
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 :
Savoir si une cellule découverte l'était initialement, ou si c'est le joueur qui l'a trouvée. Nous afficherons les cellules initialement découvertes avec une couleur particulière (fond gris).
Savoir si une cellule est déjà découverte ou non.
Se rapeller des éventuelles notes que le joueur a pu associer à une cellule
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 } |
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) } }); } } |
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 :
L'attribut realValue
: il représente la valeur intrinséque (celle qu'il faut trouver) de la cellule.
L'attribut assumedValue
: la valeur que l'utilisateur pense avoir trouvé.
L'attribut isInitial
: indique si la case est dévoilée au début du jeu.
L'attribut marks
: un tableau qui mémorise les éventuelles marques que le joueur peut poser sur une case.
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 :
L'attribut level
: contient le niveau associé à cette grille
L'attribut bigNumber
: un booléen qui permet de savoir si l'on va déposer un chiffre ou une marque sur la cellule.
Vous pourrez choisir la valeur de cet attribut en cliquant sur ce bouton (on dépose un chiffre)
qui pourra aussi passer dans cet état (on dépose une marque).
Enfin, les attributs currentCellX
et currentCellY
: ils représentent les coordonnées de la case actuellement sélectionnée.
Vous devrez cliquer sur une cellule pour la sélectionner.
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.
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 :
faire dériver notre classe de android.view.View
,
rajouter les constructeurs nécessaires afin de garantir que les appels aux constructeurs parents se passeront bien,
rajouter un attribut de type android.graphics.Paint
(cet attribut sera notre « stylo » permettant de dessiner sur le widget),
rajouter une méthode onDraw
pour dessiner notre widget (pour l'heure on se contentera d'une croix rouge).
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 ); } } |
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> |
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.
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 ); } } |
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 }; } |
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; } } |
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.
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); } } } } |
L'image ci-dessous vous montre le résultat de l'assistance visuelle produite en fonction de la cellule en cours de sélection.
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 et vous pouvez la 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.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :