Participer au site avec un Tip
Rechercher
 

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 :

Commencer à coder un Solitaire avec Android Studio

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 :

Si vous ne maîtrisez pas bien le principe du jeu du Solitaire, je vous propose de lire le document suivant : https://fr.wikipedia.org/wiki/Solitaire_(patience).

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 maîtrisez 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.

Apprendre à coder un jeu de Solitaire 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 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>
Le fichier de manifeste de votre application Android

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

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;
    }
}
Fichier Card.java : permet de représenter les cartes utilisées dans le jeu

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.

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();
            
}
On débute le codage de notre classe Game qui représente la partie en cours.
je sais, c'est pas bien, mais j'ai choisi de ne pas fournir les getters et les setters pour l'accès aux attributs de la classe 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();
        }

    }

}
Un constructeur pour initialiser l'état d'une nouvelle partie.

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 Game.canMoveCardToStack( Card )

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 Game.canMoveCardToDeck( Card )

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;
    }

}
La méthode Game.isFinish()

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;
    }

}
La méthode Game.allIsReturned( Card )

Choix des couleurs

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>
Fichier de définition des couleurs de l'application

Choix des images de l'application

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 arrière face d'une carte
/res/drawable/carreau.png Couleur carreau
/res/drawable/pique.png Couleur pique
/res/drawable/coeur.png Couleur coeur
/res/drawable/treffle.png Couleur treffle
n'oubliez pas que le nom d'un fichier de ressource (et donc aussi pour les images) doit être obligatoirement en minuscules et définit le nom de la constante de ressource associée (portée par la classe 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.

Création d'un nouveau widget d'affichage de la partie en cours

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 );
    }
    
            
}
Changement des codes couleurs

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");
        }

    }

            
}
Changement des bitmaps

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;
    }
    
}
Méthodes de calculs de positions des cartes

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);
                }
            }

        }
    }
    
}
Méthodes de calculs des positions des cartes

Ajout du widget d'affichage de la partie en cours dans une activité

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>
Fichier res/layout/activity_main.xml : la ressource de layout

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 );

    }
}
Fichier MainActivity.java : le code de l'activité.

Vous pouvez maintenant tenter un premier démarrage de votre application. Et vous devriez obtenir le résultat suivant.

Résultat visuel du premier lancement

Ajout des gestionnaires d'événements pour les premiers mouvements

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 ---

}
Implémentation de l'interface GestureDetector.OnGestureListener

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 ---

}
Initialisation du GestureDetector.

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;
    }

}
Ajout des méthodes de gestion d'événements.

Un exemple d'application de Solitaire 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-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.