Accès rapide :
Création et configuration d'un nouveau projet
Implémentation de votre modèle de données
Choix des couleurs
Choix des images de l'application
Création d'un nouveau widget d'affichage de la partie en cours
Ajout du widget d'affichage de la partie en cours dans une activité
Ajout des gestionnaires d'événements pour les premiers mouvements
Un exemple d'application de Solitaire finalisé
Ce tuto va vous montrer comment démarrer la programmation d'un jeu de Solitaire (jeu de cartes) pour Android en utilisant Android Studio. Dans cette première version, nous ne gérerons que :
L'implémentation de votre modèle de données (le stockage en mémoire de l'état de la partie en cours).
La génération aléatoire d'une nouvelle distribution des cartes pour débuter une partie.
L'affichage des cartes pour la partie en cours.
La prise en charge de quelques premiers mouvements de cartes.
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 Solitaire » et choisissez une activité de type « Empty Activity ». Le fait de conserver une API Android minimal en version 15, permet de garantir que l'application marchera sur un maximum de périphériques.
Une fois le projet créé, je vous propose de modifier le fichier de manifeste 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.mysolitaire"> <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> |
Premier point important : même si nous n'allons pas gérer cette fonctionnalité dans ce tutoriel, il est fort probable que vous cherchiez à enregistrer
l'état de la partie en cours afin de pouvoir la reprendre plus tard. Il est donc nécessaire que les classes de votre modèle de données implémentent
l'interface java.io.Serializable
.
La première classe que je vous propose permet de représenter le concept de cartes. Il existe quatre couleurs de cartes : carreau, coeur, pique et trèfle.
Cette classe utilisera le type énuméré CardType
pour définir les couleurs de cartes. Durant la partie, la carte pourra aussi être retournée ou
non : un booléen devrait suffire à gérer cet aspect. Et, bien entendu, une carte aura aussi une valeur. Voici le code de cette classe.
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 |
package fr.koor.mysolitaire; import android.graphics.Color; import java.io.Serializable; class Card implements Serializable { private CardType type; private int value; private boolean returned; public Card( CardType type, int value ) { this( type, value, false ); } public Card( CardType type, int value, boolean returned ) { this.setType( type ); this.setValue( value ); this.setReturned( returned ); } public CardType getType() { return type; } public void setType(CardType type) { this.type = type; } public int getColor() { switch( this.type ) { case COEUR: case CARREAU: return Color.RED; default: return Color.BLACK; } } public int getValue() { return value; } public void setValue(int value) { if ( value < 1 || value > 13 ) throw new IllegalArgumentException( "Value not supported" ); this.value = value; } public String getName() { switch( this.value ) { case 1: return "A"; case 2: return "2"; case 3: return "3"; case 4: return "4"; case 5: return "5"; case 6: return "6"; case 7: return "7"; case 8: return "8"; case 9: return "9"; case 10: return "10"; case 11: return "J"; case 12: return "Q"; case 13: return "K"; default: throw new RuntimeException( "Bad card value" ); } } public boolean isReturned() { return returned; } public void setReturned(boolean returned) { this.returned = returned; } @Override public String toString() { return type.toString() + " " + value; } public static enum CardType { COEUR, CARREAU, PIQUE, TREFLE; } } |
Ensuite, il faut définir la structure du plateau de jeu. J'ai choisi de nommer chaque pile de carte avec les terminologies suivantes afin de mieux comprendre chacune des sections de code proposées par la suite.
Une stack : correspond à l'une des quatre piles de carte à constituer pour gagner la partie. Elles sont situées en haut et à gauche de l'écran. Initialement, les quatre stacks sont vides, comme le montre la capture d'écran ci-dessus.
La pioche : elle est constituée de deux piles de cartes. La pile de carte retournée (à gauche) et la pile de cartes non retournées (à droite). Le fait de cliquer la pile non retournée de la pioche, retourne une carte qui est alors placée sur la pile de cartes retournées de la pioche.
Un deck : une des sept piles de cartes initiales. Au démarrage les cartes d'un deck sont retournées, excepté celle au sommet de la pile.
Pour définir les stacks et les decks, je vous propose de définir deux nouveaux types dérivés de la classe java.util.Stack<T>
.
Ces deux types pourront être définis sous forme de classes internes statiques dans la classe de représentation de la partie en cours.
Les deux piles pour la pioche seront des instances de type java.util.Vector<T>
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
package fr.koor.mysolitaire; import java.io.Serializable; import java.util.Vector; public class Game implements Serializable { public static class Stack extends java.util.Stack<Card> {} public static class Deck extends java.util.Stack<Card> {} public static final int STACK_COUNT = 4; public static final int DECK_COUNT = 7; public Stack [] stacks = new Stack[STACK_COUNT]; public Deck [] decks = new Deck[DECK_COUNT]; public Vector<Card> pioche = new Vector<>(); public Vector<Card> returnedPioche = new Vector(); } |
Game
.
Du coup ces attributs sont qualifiés de public
.
L'étape suivante serait de proposer un constructeur afin d'initialiser l'état de départ des sept decks et de la pioche. Une répartition aléatoire des cartes sur chaque pile de cartes semble une bonne approche. Voici le code de ce constructeur.
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 |
public class Game implements Serializable { // ... Début de la classe ... public Game() { // Step 1 - Toutes les cartes sont instanciées for( int i=1; i<=13; i++ ) { pioche.add( new Card( Card.CardType.CARREAU, i ) ); pioche.add( new Card( Card.CardType.COEUR, i ) ); pioche.add( new Card( Card.CardType.PIQUE, i ) ); pioche.add( new Card( Card.CardType.TREFLE, i ) ); } // Step 2 - On mélange les cartes for( int round=0; round<200; round++ ) { int position = (int) ( Math.random() * pioche.size() ); Card removedCard = pioche.elementAt( position ); pioche.removeElementAt( position ); pioche.add( removedCard ); } // Step 3 - On crée les sept decks avec des cartes tirées aléatoirement dans la pioche for( int deckIndex=0; deckIndex<DECK_COUNT; deckIndex++ ) { decks[deckIndex] = new Deck(); for( int cardIndex = 0; cardIndex < deckIndex+1; cardIndex++ ) { int position = (int) ( Math.random() * pioche.size() ); Card removedCard = pioche.elementAt( position ); pioche.removeElementAt( position ); decks[deckIndex].push( removedCard ); if ( cardIndex == deckIndex ) removedCard.setReturned( true ); } } // Step 4 - On initialise les quatre stacks. for( int stackIndex=0; stackIndex<STACK_COUNT; stackIndex++ ) { stacks[stackIndex] = new Stack(); } } } |
Nous allons maintenant compléter cette classe avec quelques méthodes permettant de savoir ou on en est de la partie en cours et si certains déplacements sont possibles ou non. La première méthode permet de savoir si une carte peut-être déplacée sur l'une des quatre stacks du jeu. En voici son code.
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 |
public class Game implements Serializable { // ... Début de la classe ... /** * Vérifie la carte peut être déposé sur l'une des quatre stacks. * @param card La carte à déposer. * @return L'indice de la stack sur laquelle la carte peut être déposée, * -1 si ce n'est pas possible. */ public int canMoveCardToStack( Card card ) { // Si une stack est vide et que la carte est un as if ( card.getValue() == 1 ) { int stackIndex = 0; while( ! this.stacks[stackIndex].isEmpty() ) { stackIndex++; } return stackIndex; } // Si ce n'est pas un as, peut-on empiler la carte sur une carte de // valeur inférieure dans l'une des piles. for( int stackIndex=0; stackIndex<STACK_COUNT; stackIndex++ ) { Stack stack = this.stacks[stackIndex]; if ( ! stack.isEmpty() ) { if ( stack.lastElement().getType() != card.getType() ) continue; if ( stack.lastElement().getValue() == card.getValue()-1 ) return stackIndex; } } return -1; } } |
La méthode canMoveCardToDeck
permet de savoir si une carte peut être placée sur l'un des decks du jeu.
En voici son code.
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 |
public class Game implements Serializable { // ... Début de la classe ... /** * Vérifie si une carte peut être déposée sur un des sept decks. * @param card La carte à déposer. * @return L'indice du deck sur lequel la carte peut être déposée, * -1 si ce n'est pas possible. */ public int canMoveCardToDeck( Card card ) { // Si la carte est un roi et qu'un deck est vide, alors OK if ( card.getValue() == 13 ) { for( int deckIndex=0; deckIndex<DECK_COUNT; deckIndex++ ) { if ( this.decks[deckIndex].isEmpty() ) return deckIndex; } } // Est-ce que la carte peut être placée sur un deck ? for( int deckIndex=0; deckIndex<DECK_COUNT; deckIndex++ ) { Deck deck = this.decks[deckIndex]; if ( deck.size() > 0 ) { if ( deck.lastElement().getColor() == card.getColor() ) continue; if ( deck.lastElement().getValue() == card.getValue()+1 ) return deckIndex; } } return -1; } } |
La méthode isFinish
permet de savoir si toutes les cartes ont été placées sur les quatre stacks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Game implements Serializable { // ... Début de la classe ... /** * Détermine si le jeu est terminé, c'est à dire que chaque stack possède bien 13 cartes. * @return true si le jeu est terminé, false dans le cas contraire. */ public boolean isFinish() { return stacks[0].isEmpty() == false && stacks[0].lastElement().getValue() == 13 && stacks[1].isEmpty() == false && stacks[1].lastElement().getValue() == 13 && stacks[2].isEmpty() == false && stacks[2].lastElement().getValue() == 13 && stacks[3].isEmpty() == false && stacks[3].lastElement().getValue() == 13; } } |
Enfin, la méthode allIsReturned
permet de savoir si toutes les cartes des sept decks ont bien été retournées.
Si tel est le cas, on pourrait alors proposer une terminaison automatique du jeu (non implémentée dans ce tutoriel).
En voici son code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Game implements Serializable { // ... Début de la classe ... /** * Pour savoir si toutes les cartes du jeu sont retournées et * qu'on peut lancer une terminaison automatique du jeu. * @return true si toutes les cartes sont retournées, false dans le cas contraire. */ public boolean allIsReturned() { for( int i=0; i<DECK_COUNT; i++ ) { Deck deck = decks[i]; if ( deck.size() > 0 && deck.firstElement().isReturned() == false ) return false; } return true; } } |
Je vous propose de définir les couleurs utilisées par l'application dans les ressources : cela vous permettra de facilement les changer si elles ne
vous plaisent pas. Ouvrez le dossier relatif aux ressources de l'application et modifier le fichier res/values/colors.xml
ainsi.
1 2 3 4 5 6 7 8 9 10 11 12 |
<?xml version="1.0" encoding="utf-8"?> <resources> <!-- Pour les couleurs utilisées par la barre de notification --> <color name="colorPrimary">#000000</color> <color name="colorPrimaryDark">#000000</color> <color name="colorAccent">#e61408</color> <!-- Les couleurs utilisées par l'application --> <color name="headerForegroundColor">#e0e0e0</color> <color name="backgroundColor">#026a15</color> <color name="redColor">#e61408</color> </resources> |
Il nous faut aussi choisir quelques images pour dessiner les cartes. Placez vos images dans le dossier de ressources res/drawable
.
Pour ma part, j'ai choisi de nommer les images ainsi.
Localisation et nom du fichier | L'image |
---|---|
/res/drawable/back.png | |
/res/drawable/carreau.png | |
/res/drawable/pique.png | |
/res/drawable/coeur.png | |
/res/drawable/treffle.png |
R
).
Ce sont des images en haute résolution est j'ai opté pour un retaillage aux dimensions adaptées par code, plutôt que de mettre les images dans les différentes résolutions habituelles.
Nous allons dériver un nouveau widget à partir de la classe View
: ce widget permettre d'afficher la partie en cours.
Dans un premier temps, nous allons chercher à charger les couleurs à partir du fichier de ressources de couleurs.
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 |
public class GameView extends View { private int headerBackgroundColor; private int headerForegroundColor; private int backgroundColor; private int redColor; public GameView(Context context) { super(context); postConstruct(); } public GameView(Context context, AttributeSet attrs) { super(context, attrs); postConstruct(); } private void postConstruct() { Resources res = getResources(); headerBackgroundColor = res.getColor( R.color.colorPrimaryDark ); headerForegroundColor = res.getColor( R.color.headerForegroundColor ); backgroundColor = res.getColor( R.color.backgroundColor ); redColor = res.getColor( R.color.redColor ); } } |
Maintenant que nous avons les couleurs, il nous faut aussi charger les images et les redimensionner à une taille adaptée de la fenêtre du périphérique
utilisé. Le mieux, c'est de réaliser le redimensionnement des images dans la méthode onSizeChanged
: elle déclenche à chaque changement
de taille de la fenêtre (une première fois à l'ouverture, puis lors des permutations du format d'écran (portrait/paysage)).
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 |
public class GameView extends View { private int headerBackgroundColor; private int headerForegroundColor; private int backgroundColor; private int redColor; public Game game = new Game(); private Bitmap imgPique; private Bitmap imgPiqueLittle; private Bitmap imgTreffle; private Bitmap imgTreffleLittle; private Bitmap imgCarreau; private Bitmap imgCarreauLittle; private Bitmap imgCoeur; private Bitmap imgCoeurLittle; private Bitmap imgBack; private float deckWidth; private float deckHeight; private float deckMargin; /** * Class constructor */ public GameView(Context context) { super(context); postConstruct(); } /** * Class constructor */ public GameView(Context context, AttributeSet attrs) { super(context, attrs); postConstruct(); } /** * Chargement des codes couleurs. */ private void postConstruct() { Resources res = getResources(); headerBackgroundColor = res.getColor( R.color.colorPrimaryDark ); headerForegroundColor = res.getColor( R.color.headerForegroundColor ); backgroundColor = res.getColor( R.color.backgroundColor ); redColor = res.getColor( R.color.redColor ); } /** * Retaillage des bitmaps en fonction de la taille de la fenêtre. */ @Override protected void onSizeChanged( int width, int height, int oldw, int oldh ) { super.onSizeChanged( width, height, oldw, oldh ); deckMargin = width * 0.025f; deckWidth = (width - (Game.DECK_COUNT + 1) * deckMargin) / Game.DECK_COUNT; deckHeight = deckWidth * 1.4f; try { int imageSize = (int) (deckWidth * 0.66); int imageLittleSize = (int) (deckWidth / 3); imgPique = BitmapFactory.decodeResource(getResources(), R.drawable.pique); imgPiqueLittle = Bitmap.createScaledBitmap(imgPique, imageLittleSize, imageLittleSize, true); imgPique = Bitmap.createScaledBitmap(imgPique, imageSize, imageSize, true); imgTreffle = BitmapFactory.decodeResource(getResources(), R.drawable.treffle); imgTreffleLittle = Bitmap.createScaledBitmap(imgTreffle, imageLittleSize, imageLittleSize, true); imgTreffle = Bitmap.createScaledBitmap(imgTreffle, imageSize, imageSize, true); imgCoeur = BitmapFactory.decodeResource(getResources(), R.drawable.coeur); imgCoeurLittle = Bitmap.createScaledBitmap(imgCoeur, imageLittleSize, imageLittleSize, true); imgCoeur = Bitmap.createScaledBitmap(imgCoeur, imageSize, imageSize, true); imgCarreau = BitmapFactory.decodeResource(getResources(), R.drawable.carreau); imgCarreauLittle = Bitmap.createScaledBitmap(imgCarreau, imageLittleSize, imageLittleSize, true); imgCarreau = Bitmap.createScaledBitmap(imgCarreau, imageSize, imageSize, true); imgBack = BitmapFactory.decodeResource(getResources(), R.drawable.back); imgBack = Bitmap.createScaledBitmap(imgBack, (int)deckWidth, (int)deckHeight, true); } catch (Exception exception) { Log.e("ERROR", "Cannot load card images"); } } } |
Ensuite, on définit quelques méthodes afin de calculer les coordonnées graphiques des cartes sur les différentes piles de l'aire de jeu.
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 |
public class GameView extends View { // ... Début de la classe ... /** * Calcul de la "bounding box" de la stack spécifiée en paramètre. */ private RectF computeStackRect( int index ) { float x = deckMargin + (deckWidth + deckMargin) * index; float y = getHeight() * 0.17f; return new RectF( x, y, x+deckWidth, y+deckHeight ); } /** * Calcul de la "bounding box" de la pile retournée associée à la pioche. */ private RectF computeReturnedPiocheRect() { float x = deckMargin + (deckWidth + deckMargin) * 5; float y = getHeight() * 0.17f; return new RectF( x, y, x+deckWidth, y+deckHeight ); } /** * Calcul de la "bounding box" de la pile découverte associée à la pioche. */ private RectF computePiocheRect() { float x = deckMargin + (deckWidth + deckMargin) * 6; float y = getHeight() * 0.17f; return new RectF( x, y, x+deckWidth, y+deckHeight ); } /** * Calcul de la "bounding box" du deck spécifié en paramètre. */ private RectF computeDeckRect( int index, int cardIndex ) { float x = deckMargin + (deckWidth + deckMargin) * index; float y = getHeight() * 0.30f + cardIndex * computeStepY(); return new RectF( x, y, x+deckWidth, y+deckHeight ); } /** * Calcul du décalage en y pour toutes les cartes d'un deck. */ public float computeStepY() { return ( getHeight()*0.9f - getHeight()*0.3f ) / 17f; } } |
Maintenant, nous allons coder la partie « affichage » de la partie en cours. On y utilise une grande partie des éléments définis précédemment.
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 |
public class GameView extends View { // ... Début de la classe ... private Paint paint = new Paint( Paint.ANTI_ALIAS_FLAG ); /** * Cette méthode permet de tracer une carte à la position spécifiée en paramètre. * @param canvas Le canvas à utiliser. * @param card La carte à dessiner. Si vous passez un pointeur nul, * juste le contour de la carte sera tracé (état initial des stacks par exemple). * @param x La position en x de tracé. * @param y La position en y de tracé. */ public void drawCard(Canvas canvas, Card card, float x, float y ) { float cornerWidth = deckWidth / 10f; RectF rectF = new RectF( x, y, x + deckWidth, y + deckHeight ); // Si card == null alors on ne trace que le contour de la carte if ( card == null ) { paint.setStyle(Paint.Style.STROKE); paint.setColor(0xff_40_40_40); canvas.drawRoundRect(rectF, cornerWidth, cornerWidth, paint); paint.setStyle(Paint.Style.FILL); return; } paint.setStyle(Paint.Style.FILL); paint.setColor( card.isReturned() ? 0xff_ff_ff_ff : 0xff_a0_c0_a0 ); canvas.drawRoundRect(rectF, cornerWidth, cornerWidth, paint); paint.setStyle(Paint.Style.STROKE); paint.setColor( 0xff_00_00_00 ); canvas.drawRoundRect(rectF, cornerWidth, cornerWidth, paint); if ( card.isReturned() ) { Bitmap image; Bitmap imageLittle; int color; switch (card.getType()) { case CARREAU: image = imgCarreau; imageLittle = imgCarreauLittle; color = 0xff_e6_14_08; break; case COEUR: image = imgCoeur; imageLittle = imgCoeurLittle; color = 0xff_e6_14_08; break; case PIQUE: image = imgPique; imageLittle = imgPiqueLittle; color = 0xff_00_00_00; break; default: image = imgTreffle; imageLittle = imgTreffleLittle; color = 0xff_00_00_00; } paint.setStyle(Paint.Style.FILL); paint.setTextSize( deckWidth / 2.4f ); paint.setFakeBoldText( true ); paint.setTextAlign( Paint.Align.LEFT ); paint.setColor( color ); if ( card.getValue() != 10 ) { canvas.drawText(card.getName(), x + deckWidth * 0.1f, y + deckHeight * 0.32f, paint); } else { canvas.drawText( "1", x + deckWidth * 0.1f, y + deckHeight * 0.32f, paint); canvas.drawText( "0", x + deckWidth * 0.3f, y + deckHeight * 0.32f, paint); } canvas.drawBitmap( imageLittle, x + deckWidth*0.9f - imageLittle.getWidth(), y + deckHeight * 0.1f, paint ); canvas.drawBitmap( image, x + (deckWidth - image.getWidth())/ 2f, y + (deckHeight*0.9f - image.getHeight()) , paint ); paint.setFakeBoldText( false ); } else { canvas.drawBitmap(imgBack, x, y, paint); } } /** * On trace l'aire de jeu * @param canvas Le canvas à utiliser. */ @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // --- Background --- paint.setColor(backgroundColor); paint.setStyle( Paint.Style.FILL ); canvas.drawRect(0, 0, getWidth(), getHeight(), paint); // --- Draw the Header --- float widthDiv10 = getWidth() / 10f; float heightDiv10 = getHeight() / 10f; paint.setColor( headerBackgroundColor ); RectF rectF = new RectF(0, 0, getWidth(), getHeight() * 0.15f); canvas.drawRect(rectF, paint); paint.setColor(redColor); paint.setTextAlign( Paint.Align.CENTER ); paint.setTextSize( (int) (getWidth() / 8.5) ); canvas.drawText( getResources().getString(R.string.app_name), widthDiv10 * 5, (int) (heightDiv10 * 0.8), paint ); paint.setColor( headerForegroundColor ); paint.setTextAlign( Paint.Align.LEFT ); paint.setTextSize( getWidth() / 20f ); paint.setStrokeWidth(1); canvas.drawText( "V 1.0", (int) (widthDiv10 * 0.5), (int) (heightDiv10 * 1.3), paint ); paint.setTextAlign( Paint.Align.RIGHT ); canvas.drawText( "By KooR.fr", (int) (widthDiv10 * 9.5), (int) (heightDiv10 * 1.3), paint ); // --- Draw the fourth stacks --- paint.setStrokeWidth( getWidth() / 200f ); for (int i = 0; i < Game.STACK_COUNT; i++) { Game.Stack stack = game.stacks[i]; rectF = computeStackRect( i ); drawCard( canvas, stack.isEmpty() ? null : stack.lastElement(), rectF.left, rectF.top ); } // --- Draw the pioche --- rectF = computeReturnedPiocheRect(); drawCard( canvas, game.returnedPioche.isEmpty() ? null : game.returnedPioche.lastElement(), rectF.left, rectF.top ); rectF = computePiocheRect(); drawCard(canvas, game.pioche.isEmpty() ? null : game.pioche.lastElement(), rectF.left, rectF.top); // --- Draw the seven decks --- for ( int i = 0; i < Game.DECK_COUNT; i++ ) { Game.Deck deck = game.decks[i]; if ( deck.isEmpty() ) { rectF = computeDeckRect(i, 0); drawCard( canvas, null, rectF.left, rectF.top ); } else { for ( int cardIndex = 0; cardIndex < deck.size(); cardIndex++ ) { Card card = deck.get(cardIndex); rectF = computeDeckRect(i, cardIndex); drawCard(canvas, card, rectF.left, rectF.top); } } } } } |
Nous allons maintenant chercher à injecter le widget dans l'activité principale de l'application pour voir si le tracé se passe correctement. Premièrement, éditer la ressource XML de layout ainsi afin d'y injecter notre widget de tracé de la zone de jeu.
1 2 3 4 5 6 7 8 9 10 |
<?xml version="1.0" encoding="utf-8"?> <fr.koor.mysolitaire.GameView 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:id="@+id/gameView" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> </fr.koor.mysolitaire.GameView> |
Modifiez maintenant le code de la classe d'activité afin de récupérer un pointeur vers le widget. Pour l'heure, ce pointeur nous sera inutile. Mais si par la suite vous cherchez à enregistrer les données de la partie en cours, à la fermeture de l'activité, pour permettre une reprise ultérieure, ce pointeur vous sera bien utile. Effectivement, c'est le widget qui connaît l'instance de jeu en cours et c'est l'activité qui reçoit les événements d'arrêt et de reprise de l'application : il faudra donc pouvoir lier tout ça.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package fr.koor.mysolitaire; import android.app.Activity; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends Activity { private GameView gameView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); gameView = findViewById( R.id.gameView ); } } |
Vous pouvez maintenant tenter un premier démarrage de votre application. Et vous devriez obtenir le résultat suivant.
Dernière étape de ce tuto, nous allons commencer à coder des gestionnaires d'événements liés à la dalle tactile afin de permettre nos premiers mouvements.
Tout d'abord, il faut modifier la déclaration de notre classe de widget (la classe GameView
) afin d'y adjoindre le support des événements
tactiles. Il faut implémenter l'interface GestureDetector.OnGestureListener
.
1 2 3 4 5 6 7 8 9 10 |
package fr.koor.mysolitaire; import android.view.GestureDetector; // --- autres imports --- public class GameView extends View implements GestureDetector.OnGestureListener { // --- Corps de la classe --- } |
Il faut ensuite modifier notre méthode postConstruct
afin de démarrer le support du GestureDetector
(ligne 11).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public class GameView extends View implements GestureDetector.OnGestureListener { private GestureDetector gestureDetector; // --- Début de la classe --- /** * Chargement des codes couleurs et initialisation du GestureDectector. */ private void postConstruct() { gestureDetector = new GestureDetector(getContext(), this); Resources res = getResources(); headerBackgroundColor = res.getColor( R.color.colorPrimaryDark ); headerForegroundColor = res.getColor( R.color.headerForegroundColor ); backgroundColor = res.getColor( R.color.backgroundColor ); redColor = res.getColor( R.color.redColor ); } // --- Suite de la classe --- } |
Pour finir, nous allons ajouter les méthodes liées à la gestion des événements tactiles et à l'interface GestureDetector.OnGestureListener
.
Notez que les déplacements produits par les codes proposés seront instantanés et sans animations. Si vous souhaitez avoir des déplacements avec des
animations un peu plus sympathiques, il vous faudra certainement propuire un peu plus de code.
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 |
public class GameView extends View implements GestureDetector.OnGestureListener { // --- Début de la classe --- // --- OnGestureDetector interface ---- @Override public boolean onTouchEvent(MotionEvent event) { return gestureDetector.onTouchEvent(event); // Le widget repasse la main au GestureDetector. } // On réagit à un appui simple sur le widget. @Override public boolean onSingleTapUp(MotionEvent e) { RectF rect; // --- Un tap sur les cartes non retournées de la pioche --- rect = computePiocheRect(); if ( rect.contains( e.getX(), e.getY() ) ) { if ( ! game.pioche.isEmpty() ) { Card card = game.pioche.remove(0); card.setReturned( true ); game.returnedPioche.add( card ); } else { game.pioche.addAll( game.returnedPioche ); game.returnedPioche.clear(); for( Card card : game.pioche ) card.setReturned( false ); } postInvalidate(); return true; } // --- Un tap sur les cartes retournées de la pioche --- rect = computeReturnedPiocheRect(); if ( rect.contains( e.getX(), e.getY() ) && ! game.returnedPioche.isEmpty() ) { final int stackIndex = game.canMoveCardToStack( game.returnedPioche.lastElement() ); if ( stackIndex > -1 ) { Card selectedCard = game.returnedPioche.remove(game.returnedPioche.size() - 1); game.stacks[stackIndex].add( selectedCard ); postInvalidate(); return true; } final int deckIndex = game.canMoveCardToDeck( game.returnedPioche.lastElement() ); if ( deckIndex > -1 ) { Card selectedCard = game.returnedPioche.remove( game.returnedPioche.size() - 1 ); game.decks[deckIndex].add( selectedCard ); postInvalidate(); return true; } } // --- Un tap sur une carte d'une deck --- for( int deckIndex=0; deckIndex<Game.DECK_COUNT; deckIndex++ ) { final Game.Deck deck = game.decks[deckIndex]; if ( ! deck.isEmpty() ) { for( int i=deck.size()-1; i>=0; i-- ) { rect = computeDeckRect(deckIndex, i); if ( rect.contains(e.getX(), e.getY()) ) { // Click sur carte non retournée de la deck => on sort Card currentCard = deck.get(i); if ( ! currentCard.isReturned() ) return true; // Peut-on déplacer la carte du sommet de la deck vers une stack ? if ( i == deck.size() - 1 ) { // On vérifie de bien être sur le sommet int stackIndex = game.canMoveCardToStack(currentCard); if (stackIndex > -1) { Card selectedCard = deck.remove(deck.size() - 1); if ( ! deck.isEmpty() ) deck.lastElement().setReturned(true); game.stacks[stackIndex].add( selectedCard ); postInvalidate(); return true; } } // Peut-on déplacer la carte de la deck vers une autre deck ? final int deckIndex2 = game.canMoveCardToDeck( currentCard ); if (deckIndex2 > -1) { if ( i == deck.size() - 1 ) { // On déplace qu'un carte Card selectedCard = deck.remove(deck.size() - 1); if ( ! deck.isEmpty() ) { deck.lastElement().setReturned(true); } game.decks[deckIndex2].add( selectedCard ); } else { // On déplace plusieurs cartes final ArrayList<Card> selectedCards = new ArrayList<>(); for( int ci=deck.size()-1; ci>=i; ci-- ) { selectedCards.add( 0, deck.remove( ci ) ); } if ( ! deck.isEmpty() ) { deck.lastElement().setReturned(true); } game.decks[deckIndex2].addAll( selectedCards ); } postInvalidate(); return true; } return true; } } } } return true; } @Override public boolean onDown(MotionEvent e) { return true; } @Override public void onShowPress(MotionEvent e) { } @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; } } |
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-Solitaire et vous pouvez la télécharger sur le Play Store Android. Vous pouvez la tester pour voir à quoi pourrait ressembler votre application 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 :