Animation 2D
Pour traiter cette partie, il faut avoir des notions sur les threads et les activités
Traitements lourds
Recalculer les positions de vos objets animés, les collisions dans le cas d'un jeu entre les sprites, afficher le score... voilà beaucoup de choses que doit faire un programme lors d'une animation 2D.
L'animation est une phase qui peut devenir consommateur de temps, ce qui peut poser quelques problèmes dans l'UI thread.
Et même si vous arrivez à faire un programme suffisement rapide, l'animation devra être temporisée plus ou moins longtemps (suivant la puissance de l'appareil) afin d'avoir une bonne fluidité. Une solution toute bête serait de mettre en sommeil le traitement pendant un certain laps de temps via la méthode Thread.sleep(), ok mais...
Actuellement, vous n'avons utilisé qu'un thread: l'UI thread. Or l'UI thread, comme son nom l'indique (UI = User Interface), a directement à faire avec l'utilisateur. Il doit donc répondre à celui-ci le plus rapidement possible, faute de quoi, l'utilisateur pourrait penser que son appareil ou pire, votre programme ne marche plus (voire l'affichage provenant du système indiquant un ANR) !
Vous pourriez donc vous dire: "Ok, libérons ce thread de tout ce travail annexe. Et pour cela, nous passerons par un autre thread, indépendant".
Et effectivement, cela marcherait. Hormis des problèmes de gestions de l'écran qui doivent passer impérativement par l'UI thread, traiter le cas du retournement possible de l'écran de l'appareil par l'utilisateur qui provoquera la relance de l'activité, tout cela marchera.
Voici d'ailleurs un exemple fontionnel, il s'agit d'un programme qui va simplement faire tomber de la neige.
Dans la classe FloconsActivity:
- affichage d'une ProgressDialogue qui indiquera simplement ce à quoi sert le programme et ceci pendant un certain laps de temps. Ceci sans bloquer pour autant l'UI Thread de l'activité (aucun ANR ne sera affiché).
- affichage des flocons de neige qui tombent. Là encore, il faudra un laps de temps pour faire tomber la neige, et toujours sans passer l'UI thread en ANR.
La seconde classe gère simplement l'instance d'un flocon de neige. Donc code à part, et il ne sera pas nécessaire d'expliquer quoique ce soit, car hors sujet...
Le principal défaut, c'est que l'UI thread doit gérer les flocons. Donc créer les nouvelles instances de flocons, les dessiner, ... Dans cet exemple, le traitement reste assez léger, et cela marche, mais si le traitement est plus lourd ...
Mais évidement, puisque je parle de ce problème, c'est que d'autres personnes se ont rencontrées le problème ! Et donc, il existe une solution...
Elle reprend le même principe que cité ci-dessus. Soit l'utilisation d'un nouveau thread, déchargeant ainsi l'UI thread. A la différence prêt qu'il va aussi pouvoir gérer l'animation, soit le dessin et les pauses entre les différents affichages !
SurfaceView
La surfaceView est un objet qui permet de visualiser le contenu d'une zone mémoire pouvant contenir des pixels, elle sera utilisée pour générer le rendu qui sera affiché à l'écran. Le "screen compositor" (donc ce qui pemet de générer le rendu à l'écran au final) ira lire tous les pixels présents dans la surface et les enverra ensuite au GPU. Elle pourra avoir ses traitements accélérés d'un point de vue matériel et enfin, surfaceView pourra tourner dans un thread indépendant du UI thread !
[...A suivre...]
Il sera possible de forcer l'affichage d'une SurfaceView en avant plan:
surface.setZOrderOnTop(true);
SurfaceHolder
Les bases
Pour pouvoir utiliser/accéder à une surface, vous allez devoir passer par une instance de SurfaceHolder (que je traduirais par gestionnaire de surfaces).
SurfaceHolder holder;
holder = getHolder();
Par exemple, pour accéder au gestionnaire de surface d'une SurfaceView:
SurfaceView surface = (SurfaceView)findViewById(R.id.surfaceView); // On recherche
l'instance du widget surface
SurfaceHolder holder = surface.getHolder(); // On récupère le
gestionnaire de cette surface
A partir de là, vous pourrez vérifier la validité de votre surface et la locker (verrouiller) pour que le code soit le seul à l'utiliser et ainsi pouvoir dessiner sans risque de conflit. Enfin la dévérouiller ce qui permettra au passage de tout envoyer au GPU.
if (holder.getSurface().isValid())
{
Canvas canvas = holder.lockCanvas();
... Code pour dessiner via le canevas que vous récupérez suite au lock ...
holder.unlockCanvasAndPost(canvas); // Qui provoquera l'affichage
}
Via le lock, vous récupérez un canevas, vous pouvez effectuer toutes les opérations graphiques que vous avez pu voir jusqu'à présent.
Il ne reste plus qu'à gérer tout cela dans un thread différent du UI thread et à gérer le fait que votre activité peut être relancée (par exemple lorsque l'appareil est tourné).
Avancés
Il est possible d'ajouter des méthodes callback sur une SurfaceHolder. Ce qui permettra d'effectuer des opérations lors de sa création, lors d'un changement, ou lors de sa destruction.
La procédure
détectant un changement sera au moins lancée une fois, suite à
la création de la SurfaceHolder.
Pour implémenter les procédures callback, il suffit de faire: implements SurfaceHolder.Callback
Par exemple
public class MainActivity extends Activity implements SurfaceHolder.Callback {
}
Les méthodes suivantes seront à ajouter en @Override
- public void surfaceCreated(SurfaceHolder holder)
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
- public void surfaceDestroyed(SurfaceHolder holder)
Voir un exemple de code avec la gestion de la caméra.
Utilisation d'un thread indépendant
Enfin, presque indépendant, car sa vie sera liée à celle du UI thread et à l'activité !
Votre thread devra pouvoir travailler sur une surface. Par conséquent, la classe que vous allez construire devra être une sous classe de SurfaceView.
Pour pouvoir utiliser votre classe dans un thread indépendant, vous devrez passer par une implémentation de Runnable (Votre classe ne peut pas devenir une sous classe de Thread, car déjà sous classe de SurfaceView, hors l'héritage multiple n'existe pas en JAVA).
Vous définirez au final votre classe comme ceci:
class ThreadRenderView extends SurfaceView implements Runnable {
[...]
}
Comme vous implémentez Runnable, vous devrez définir la méthode public void run (). C'est dans cette méthode que vous verrouillerez votre surface afin d'y effectuer votre animation:
Il sera éventuellement
possible d'indiquer dans la méthode run() de Runnable
un niveau de priorité Android (mais correspondant aux priorités
sous Unix):
Android.os.Process.setThreadPriority(Android.os.Process.THREAD_PRIORITY_BACKGROUND);
Cela aura pour effet de donner un niveau faible de priorité.
Des explications sont nécessaires:
execute est un booléen qui indique que le thread peut boucler pour effectuer l'animation. En effet, il ne faut pas oublier que le UI Thread peut mourir. Avant qu'il ne meurt, il serait bien d'arrêter proprement votre thread.
Comment meurt l'UI Thread ? Et bien lorsque votre activité passe en PAUSE. (retournement écran, arrêt du programme). C'est donc à ce moment là que vous pourrez intervenir pour arrêter votre thread. Nous y reviendrons.
Ensuite, c'est du déjà vu: Vous verrouillez votre surface, dessinez dessus, et vous la déverrouillez. Enfin, une pause de 100 ms dans l'exemple avant d'attaquer le dessin de l'animation suivante.
Vous retrouverez du code dans le cas où votre thread en sommeil serait réveillé par une interruption. Si le thread est interrompu dans son sommeil, il se marque comme étant interrompu et positionne execute à false et sort du traitement. Votre thread va ainsi mourir.
Que reste-t-il à faire ? Et bien définir un constructeur de votre classe qui fera les choses suivantes:
public ThreadRenderView(Context context) {
super(context);
holder = getHolder();
// TODO Auto-generated constructor stub}
Et bien oui, il ne faut pas oublier le gestionnaire de surface que vous ne connaissez pas encore, alors voilà, les présentations sont maintenant faites.
Reste la façon de lancer votre thread et de l'arrêter:
Et bien vous allez utiliser le même principe que celui de l'activité soit, la création d'un méthode onResume et onPause
public void onPause()
{
// Arrêt du thread temporisation animation
execute = false;
renderThread.interrupt();
}
public void onResume()
{
// Lancement du Thread permettant de temporiser l'animation
execute = true;
renderThread = new Thread(this);
renderThread.start();
}
Ces méthodes étant lançées elles mêmes par les méthodes equivalentes de l'activité.
@Override
public void onResume()
{
super.onResume();
renderView.onResume();
}
@Override
public void onPause()
{
renderView.onPause();
super.onPause();
}
Lors de la création (onCreate), vous instanciez votre nouvelle classe comme ceci:
renderView = new ThreadRenderView(this);
setContentView(renderView);
Il serait intéressant
d'ajouter les procédures callback d'une SurfaceHolder: Entre autre public
void surfaceDestroyed(SurfaceHolder holder) qui devrait informer le thread que
la surface n'est plus disponible.
Vous allez me dire
que le booléen "execute" n'était pas forcement nécessaire.
C'est vrai, mais il y a beaucoup de chance qu'il vous soit utile pour d'autres
choses dans votre application.
Exemple
D'où le nouveau code pour gérer les flocons de neige:
Et voilà votre UI Thread est maintenant déchargé de cette tâche !