Ressources ‐ Android - vbridonneau/CoursSysteme GitHub Wiki

Introduction à la programmation Android

Brève présentation de l'interface

Le Manifest

C'est l'endroit où est stocké le fichier AndroidManifest.xml. Ce fichier contient plusieurs informations clés d'une application comme les permissions, les icônes et les différentes activités contenues dans l'application. Ce fichier est au format XML. Dans le cas d'une activité vide, il contient le code suivant :

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Appel"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

L'activité principale est donnée dans la balise <activity> qui contient le nom de l'activité : MainActivity. On peut également trouver d'autres informations comme l'icône représentant l'application sur le téléphone, donnée par android:icon, ou encore le nom de l'application donné par android:label.

Les Ressources

Les ressources dans Android sont stockées dans le dossier res et contiennent plusieurs éléments importants pour l'application, notamment :

  • Drawable (res/drawable/) : Contient les images et formes graphiques utilisées dans l'application.
  • Layout (res/layout/) : Contient les fichiers XML qui définissent l'apparence des interfaces utilisateur.
  • Values (res/values/) : Contient des fichiers XML pour stocker les chaînes de caractères (strings.xml), les couleurs (colors.xml), les dimensions (dimens.xml) et les styles (styles.xml).
  • Mipmap (res/mipmap/) : Contient les icônes de l'application pour différentes résolutions d'écran.

Exemple d'un fichier res/values/strings.xml :

<resources>
    <string name="app_name">MonApplication</string>
    <string name="hello_world">Bonjour le monde !</string>
</resources>

Le Code

Le code de l'application Android est principalement écrit en Java ou Kotlin. La classe principale de l'application est généralement une activité qui hérite de AppCompatActivity et nommée MainActivity.

Exemple d'une activité de base en java :

package com.example.appel;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.os.Bundle;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    }
}

Dans cet exemple :

  • La classe MainActivity hérite de AppCompatActivity.
  • onCreate est une méthode appelée au lancement de l'application. Elle est essentielle au cycle de vie de l'activité.
  • setContentView(R.layout.activity_main) définit l'interface utilisateur en utilisant le fichier XML activity_main.xml situé dans res/layout/.

Exemple de Layout avec ConstraintLayout

Le fichier res/layout/activity_main.xml définit l'interface utilisateur de l'application. Un des gestionnaires de mise en page les plus couramment utilisés est ConstraintLayout, qui permet d'organiser les éléments avec des contraintes flexibles.

Exemple de activity_main.xml utilisant ConstraintLayout :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    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=".MainActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Bonjour le monde !"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

Dans cet exemple :

  • Le ConstraintLayout est utilisé comme conteneur principal.
  • Un TextView est ajouté au centre de l'écran grâce aux contraintes. Il affiche le texte "Bonjour le monde !". Ce qui permet de centrer le texte dans l'écran est donné par les attributs app:layout_constraintBottom_toBottomOf="parent", app:layout_constraintLeft_toLeftOf="parent", app:layout_constraintRight_toRightOf="parent" et app:layout_constraintTop_toTopOf="parent".

Les Activités

Cycle de vie d'une activité

Le cycle de vie d'une activité est important pour comprendre comment les activités interagissent entre elles et comment elles réagissent aux événements. Une activité passe par plusieurs états tout au long de son cycle de vie. Les états principaux sont création, démarrage, reprise, pause, arrêt et destruction. Chaque état est associé à une méthode spécifique qui est appelée à ce moment-là. Ces méthodes sont onCreate, onStart, onResume, onPause, onStop et onDestroy respectivement. La figure suivante illustre le cycle de vie d'une activité :

Cycle de vie d'une activité, Source : https://developer.android.com/guide/components/activities/activity-lifecycle

Premières applications Android

Dans les sections qui suivent, nous allons explorer quelques concepts de base pour créer nos premières applications Android. Parmis eux, nous allons voir comment créer une nouvelle activité, comment passer des données entre activités, comment gérer les événements de clic sur un bouton, comment obtenir des données de l'utilisation des capteurs, comment gérer les permissions, etc.

Création d'une activité simple

Dans cette première application, nous allons créer une activité simple qui affiche une date à l'écran. L'activité contiendra un CalendarView (widget Android permettant de gérer des dates) et un TextView qui affichera la date choisie par l'utilisateur. Notre première application consiste en la création d'une seule activité : MainActivity. Cette activité est associée à un fichier XML qui définit l'interface utilisateur. Nous allons étudier les deux fichiers ainsi que leurs connexions. Le code de l'activité principal est le suivant :

package com.example.calendrieractivite;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    CalendarView cv;
    TextView     tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        cv = findViewById(R.id.calendarView);
        tv = findViewById(R.id.tv);

        /* Gestion du calendrier */
        cv.setOnDateChangeListener((v, a, m, j) -> {
            tv.setText(a + "/" + (m + 1) + "/" + j);
        });
    }
}

Le fichier XML associé à cette activité est le suivant :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <CalendarView
        android:id="@+id/calendarView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Date"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Explication du code

Dans l'activité MainActivity, nous avons plusieurs éléments importants. Dans le code Java, nous avons un moyen de récupérer les éléments de l'interface utilisateur définis dans le fichier XML. Cela se fait en utilisant la méthode findViewById qui prend en paramètre l'identifiant de l'élément à récupérer. Par exemple, pour récupérer le CalendarView et le TextView, nous utilisons les lignes suivantes :

cv = findViewById(R.id.calendarView);
tv = findViewById(R.id.tv);

La gestion de l'événement de changement de date est faite avec la méthode setOnDateChangeListener qui prend un écouteur en paramètre. Cet écouteur réagit lorsque l'utilisateur sélectionne une date dans le CalendarView. Dans notre cas, nous mettons à jour le TextView avec la date sélectionnée.

cv.setOnDateChangeListener((v, a, m, j) -> {
    tv.setText(a + "/" + (m + 1) + "/" + j);
});

Cette ligne de code met à jour le TextView avec la date sélectionnée au format aaaa/mm/jj. Elle utilise une lambda expression pour définir le comportement à adopter lorsqu'une date est sélectionnée. On peut s'en passer en utilisant une classe anonyme qui implémente l'interface CalendarView.OnDateChangeListener.:

cv.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
    @Override
    public void onSelectedDayChange(@NonNull CalendarView view, int year, int month, int dayOfMonth) {
        tv.setText(year + "/" + (month + 1) + "/" + dayOfMonth);
    }
});

Dans le fichier XML, nous utilisons un ConstraintLayout pour organiser les éléments de l'interface utilisateur. Pour centrer le CalendarView dans l'écran, nous utilisons les contraintes suivantes :

<CalendarView
    android:id="@+id/calendarView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

Si nous voulons placer le CalendarView sous le TextView, on peut changer la contrainte app:layout_constraintTop_toTopOf="parent" en app:layout_constraintTop_toBottomOf="@id/tv" :

<CalendarView
    ...
    app:layout_constraintTop_toBottomOf="@id/tv" />  <!-- Placer en dessous du TextView identifié par @id/tv -->

Création d'une activité et retour d'un résultat

Dans cette première application, nous allons créer une activité qui permet d'obtenir un résultat sous forme de texte, en l'occurrence une date. Notre première application consiste en la création de deux activités : MainActivity et DateActivity. L'activité MainActivity déclenche l'affichage de l'activité DateActivity lorsqu'un bouton est cliqué. Une fois que l'activité DateActivity est affichée, elle affiche un calendrier et retourne la date sélectionnée à l'activité MainActivity.

Nous avons donc deux activités :

  1. MainActivity : L'activité principale qui déclenche l'affichage de la seconde activité.
  2. DateActivity : L'activité qui retourne une date sous forme de texte.

L'activité MainActivity

L'activité principale MainActivity contient un bouton qui, lorsqu'il est cliqué, déclenche l'affichage de l'activité DateActivity. Une fois que l'activité DateActivity retourne une date, elle est affichée dans un TextView de l'activité MainActivity.

package com.example.calendrieractivite;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    Button   b;
    TextView tv;

    ActivityResultLauncher<Intent> launcher = registerForActivityResult(
        new ActivityResultContracts.StartActivityForResult(),
        o -> {
            if (o.getResultCode() == RESULT_OK) {
                Intent data = o.getData();
                if (data != null) {
                    tv.setText(data.getStringExtra("donnee"));
                }
            }
        });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        b  = findViewById(R.id.button);
        tv = findViewById(R.id.tv);

        /* Gestion d'un bouton */
        b.setOnClickListener(v -> {
            Intent intent = new Intent(
                    v.getContext(),
                    Calendrier.class
            );
            launcher.launch(intent);
        });
    }
}

Plusieurs éléments sont à noter dans cette activité. Premièrement l'attribut laucher permet de gérer le résultat retourné par une activité. Il permet de définir le comportement à adopter lorsqu'une activité retourne un résultat. Pour définir ce comportement, on utilise une expression lambda qui vérifie si le résultat retourné est correct. Si c'est le cas, on récupère la date retournée par l'activité DateActivity et on l'affiche dans un TextView.

ActivityResultLauncher<Intent> launcher = registerForActivityResult(
    new ActivityResultContracts.StartActivityForResult(),
    o -> { /* Lambda expression */
        if (o.getResultCode() == RESULT_OK) {
            Intent data = o.getData(); /* Récupérer les données retournées */
            if (data != null) {
                tv.setText(data.getStringExtra("donnee"));
            }
        }
    });

Dans ce code, l'objet Intent est utilisé pour définir l'activité à lancer. C'est le type de données passé en entré à l'activité DateActivity. La méthode registerForActivityResult permet de définir le comportement à adopter lorsqu'une activité retourne un résultat. Ce comportement est implémenté sous forme d'une expression lambda qui vérifie si le résultat retourné est correct. Cette lambda est donnée par o -> { ... }. Le type de o est ActivityResult, qui contient le résultat retourné par l'activité. On vérifie si le résultat est correct en utilisant o.getResultCode() == RESULT_OK. Si le résultat est correct, on récupère la date retournée par l'activité DateActivity en utilisant data.getStringExtra("donnee"). D'une manière générale, le résultat retourné par une activité est stocké dans un objet Intent qui peut contenir plusieurs types de données. Les données résultats peuvent être récupérées à l'aide de méthodes comme getStringExtra, getIntExtra, etc. faisant pensées à un tableau associatif. La date est ensuite affichée dans un TextView en utilisant tv.setText(data.getStringExtra("donnee")).

La seconde chose à noter est la gestion du clic sur un bouton. Dans notre cas, nous utilisons un Button qui, lorsqu'il est cliqué, déclenche l'affichage de l'activité DateActivity. Pour cela, nous utilisons une expression lambda qui crée un objet Intent pour lancer l'activité DateActivity et utilise le launcher pour lancer l'activité.

b.setOnClickListener(v -> { /* v de type View */
    Intent intent = new Intent(
            v.getContext(),
            Calendrier.class
    );
    launcher.launch(intent);
});

De la même manière, on peut utiliser une classe anonyme qui implémente l'interface View.OnClickListener :

b.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(
                v.getContext(),
                Calendrier.class
        );
        launcher.launch(intent);
    }
});

Dans ce code, on crée un objet Intent pour lancer l'activité DateActivity et on utilise le launcher pour lancer l'activité. Les paramètres de l'objet Intent sont le contexte de la vue v et la classe de l'activité DateActivity. Le contexte est nécessaire pour lancer une activité et est obtenu à partir de la vue v en utilisant v.getContext(). L'activité DateActivity est lancée en utilisant launcher.launch(intent).

L'activité DateActivity

Pour cette activité, il n'est nécessaire que de créer un calendrier et de retourner la date sélectionnée à l'activité MainActivity. Le code de l'activité DateActivity est le suivant :

package com.example.calendrieractivite;

import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.CalendarView;
import android.widget.ImageButton;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class Calendrier extends AppCompatActivity {

    CalendarView cv;
    ImageButton  b;
    String       date;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_calendrier);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        cv = findViewById(R.id.calendarView);
        b  = findViewById(R.id.retour);

        cv.setOnDateChangeListener((v, a, m, j) -> {
            date = a + "/" + (m + 1) + "/" + j;
        });

        b.setOnClickListener(v -> {
            Intent intent = getIntent();
            intent.putExtra("donnee", date);
            setResult(RESULT_OK, intent);
            finish();
        });
    }
}

Dans cette activité, nous avons la gestion du bouton qui soit important. Lorsque le bouton est cliqué, la date sélectionnée est retournée à l'activité MainActivity. Pour cela, nous utilisons un objet Intent pour stocker la date sélectionnée et la retourner à l'activité MainActivity.

b.setOnClickListener(v -> {
    Intent intent = getIntent();
    intent.putExtra("donnee", date);
    setResult(RESULT_OK, intent);
    finish();
});

Dans ce code, on récupère l'objet Intent associé à l'activité DateActivity en utilisant getIntent(). On stocke la date sélectionnée dans l'objet Intent en utilisant intent.putExtra("donnee", date). La date est stockée avec une clé "donnee" pour pouvoir la récupérer dans l'activité MainActivity. On définit le résultat à retourner à l'activité MainActivity en utilisant setResult(RESULT_OK, intent). Enfin, on termine l'activité DateActivity et on retourne le résultat à l'activité MainActivity en utilisant finish(). Le résultat retourné est stocké dans l'objet Intent et peut être récupéré dans l'activité MainActivity en utilisant data.getStringExtra("donnee").

Les permissions

Dans cette application nous allons voir comment demander des permissions à l'utilisateur pour accéder à des fonctionnalités du téléphone. Pour cela, nous allons créer une application qui demande la permission d'effectuer un appel téléphonique. L'application contient deux boutons : un pour demander la permission et un pour effectuer un appel téléphonique. Avant de passer au code de l'activité il est important de déclarer la permission dans le fichier AndroidManifest.xml. Cela se fait en rajoutant les lignes suivantes dans le fichier :

<uses-feature android:name="android.hardware.telephony" android:required="yes" />
<uses-permission android:name="android.permission.CALL_PHONE" />

La première ligne déclare que l'application nécessite la fonctionnalité de téléphonie. Cela permet à Google Play de filtrer les appareils qui ne disposent pas de cette fonctionnalité et de ne pas proposer l'application à ces appareils. La seconde ligne déclare que l'application nécessite la permission d'effectuer un appel téléphonique.

Le code de l'activité MainActivity est le suivant :

package com.example.appelcorrection;

import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    Button demandePermission;
    Button appel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_appel);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        demandePermission = findViewById(R.id.demandePermission);
        appel             = findViewById(R.id.appel);

        demandePermission.setOnClickListener(v -> {
            if(ContextCompat.checkSelfPermission(this,android.Manifest.permission.CALL_PHONE) !=
                    PackageManager.PERMISSION_GRANTED) {
                ActivityCompat.requestPermissions(
                    (Activity) this,
                    new String[]{android.Manifest.permission.CALL_PHONE},
                    0);
            }
        });

        appel.setOnClickListener(v -> {
            if(ContextCompat.checkSelfPermission(this,android.Manifest.permission.CALL_PHONE) ==
                    PackageManager.PERMISSION_GRANTED) {
                startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + "0612345678")));
            }
        });
    }
}

Dans ce code ce qui est important est la gestion des permissions. Pour demander une permission, on utilise la méthode ActivityCompat.requestPermissions qui prend en paramètre l'activité, un tableau de permissions et un code de requête. Vu que la méthode requestPermissions prend en paramètre un tableau de permissions, on peut demander plusieurs permissions en même temps. Dans notre cas, on demande la permission d'effectuer un appel téléphonique en utilisant android.Manifest.permission.CALL_PHONE.

if(ContextCompat.checkSelfPermission(this,android.Manifest.permission.CALL_PHONE) !=
        PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(
        (Activity) this,
        new String[]{android.Manifest.permission.CALL_PHONE},
        0);
}

Si la permission est accordée, on peut effectuer un appel téléphonique en utilisant startActivity(). Dans cet exemple, on utilise l'intent Intent.ACTION_CALL pour effectuer un appel téléphonique. L'URI Uri.parse("tel:" + "0612345678") permet de spécifier le numéro de téléphone à appeler.

startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + "0612345678")));

Le toucher

Dans cette application, nous allons voir comment gérer les événements de toucher sur un écran tactile. Pour cela, nous allons créer une application qui affiche les coordonnées du point touché sur l'écran. L'application contient un TextView qui affiche les coordonnées du point touché. Le code de l'activité MainActivity est le suivant :

package com.example.motionandtoat;

import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_DOWN: // L'utilisateur a appuyé sur l'écran
                break;
            case MotionEvent.ACTION_MOVE: // L'utilisateur a déplacé son doigt sur l'écran
                int x = (int) event.getX();
                int y = (int) event.getY();
                /* ... */
                break;
            case MotionEvent.ACTION_UP: // L'utilisateur a relâché l'écran
                break;
        }

        return true;
    }
}

Dans ce code, on utilise la méthode onTouchEvent pour gérer les événements de toucher sur l'écran tactile. Cette méthode est appelée chaque fois qu'un événement de toucher est détecté. Elle prend en paramètre un objet MotionEvent qui contient des informations sur l'événement de toucher. Dans notre cas, on récupère l'action de l'événement en utilisant event.getAction(). L'action de l'événement peut être ACTION_DOWN si l'utilisateur appuie sur l'écran ou ACTION_UP si l'utilisateur relâche l'écran. Si l'utilisateur déplace son doigt sur l'écran, l'action de l'événement est ACTION_MOVE. On peut récupérer les coordonnées du point touché en utilisant event.getX() et event.getY(). On peut afficher les coordonnées du point touché en utilisant un Toast par exemple (voir plus bas pour plus de détails).

case MotionEvent.ACTION_MOVE: // L'utilisateur a déplacé son doigt sur l'écran
    int x = (int) event.getX();
    int y = (int) event.getY();
    Toast.makeText(this, "Coordonnées : " + x + ", " + y, Toast.LENGTH_SHORT).show();
    break;

Les capteurs

Dans cette application, nous allons voir comment obtenir des données de l'utilisation des capteurs d'un téléphone. Pour cela, nous allons créer une application qui affiche les valeurs des capteurs de l'accéléromètre. L'application contient un TextView qui affiche les valeurs des capteurs de l'accéléromètre. Le code de l'activité MainActivity est le suivant :

package com.example.capteurs;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Bundle;
import android.widget.TextView;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity implements SensorEventListener {
    TextView tv;

    private SensorManager sensorManager;
    private Sensor        accelerometre;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        accelerometre = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

        tv = findViewById(R.id.tv);
    }

    @Override
    protected void onResume() {
        /* On implemente cette méthode pour */
        super.onResume();
        sensorManager.registerListener(this, accelerometre, SensorManager.SENSOR_DELAY_GAME);
    }

    @Override
    protected void onPause() {
        super.onPause();
        sensorManager.unregisterListener(this);
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            // Récupération des valeurs de l'accéléromètre
            float x = event.values[0];
            float y = event.values[1];

            tv.setText("X : " + x + "Y : " + y);
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int i) {
        // Doit être implémenté
    }
}

La gestion des capteurs est faite en utilisant les méthodes en utilisant les classes SensorManager et Sensor. La méthode getSystemService permet de récupérer le service des capteurs en utilisant Context.SENSOR_SERVICE. La méthode getDefaultSensor permet de récupérer le capteur de l'accéléromètre en utilisant Sensor.TYPE_ACCELEROMETER. La méthode registerListener permet d'enregistrer un écouteur pour les valeurs des capteurs. Les fait d'enregistrer et de désenregistrer un écouteur pour les valeurs des capteurs sont faits dans les méthodes onResume et onPause. Cela permet de ne pas consommer de ressources inutilement lorsque l'activité n'est pas visible.

Pour pouvoir récupérer les valeurs des capteurs, il faut implémenter les méthodes onSensorChanged et onAccuracyChanged de l'inteface SensorEventListener. La méthode onSensorChanged est appelée chaque fois que les valeurs des capteurs changent. Dans notre cas, on récupère les valeurs de l'accéléromètre en utilisant event.values[0] et event.values[1].

Les sons

Dans cette application, nous allons voir comment jouer un son dans une application Android. Pour cela, nous allons créer une application qui joue un son dès que l'utilisateur appuie sur un bouton.

Le code de l'activité MainActivity est le suivant :

package com.example.myapplication;

import android.media.AudioAttributes;
import android.media.AudioManager;
import android.media.SoundPool;
import android.os.Bundle;
import android.widget.Button;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

import java.util.HashMap;

public class MainActivity extends AppCompatActivity {
    private SoundPool pool;
    private AudioManager manager;
    private final int MAX_STREAMS = 2;
    private HashMap<String, Integer> map;

    private Button b1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        manager = (AudioManager) getSystemService(AUDIO_SERVICE);

        /* Initialisation */
        map = new HashMap<>();

        /* Attrributs audios */
        AudioAttributes attributes = new AudioAttributes.Builder().
                setUsage(AudioAttributes.USAGE_GAME).build();

        /* Pool de sons */
        pool = new SoundPool.Builder()
                .setMaxStreams(MAX_STREAMS)
                .setAudioAttributes(attributes).build();
        chargerSon();

        b1 = findViewById(R.id.button);

        b1.setOnClickListener(v -> {
            int id = map.get("son1");
            pool.play(id, 1, 1, 0, 0, 1);
        });
    }

    private void chargerSon() {
        int id = pool.load(getApplicationContext(), R.raw.son1, 1);
        map.put("son1", id);
    }
}

Plusieurs choses sont à noter dans ce code. Premièrement, l'usage des services audio. Dans notre cas, on utilise le service audio pour jouer un son. Pour cela, on utilise la méthode getSystemService pour récupérer le service audio en utilisant AUDIO_SERVICE.

manager = (AudioManager) getSystemService(AUDIO_SERVICE);

Le service audio permet de gérer les flux audio de l'application. On peut l'utiliser pour gérer le volume, les attributs audio, etc. Les attributs audios sont gérés par la classe AudioAttributes qui permet de définir les attributs d'un flux audio. Dans notre cas, on définit les attributs d'un flux audio en utilisant la classe AudioAttributes.Builder. Cette classe permet de construire en plusieurs étapes les attributs d'un flux audio. Quand on définit les attributs d'un flux audio, on se pause trois questions : pourquoi, quoi et comment:

  • Pourquoi : On définit l'usage du flux audio, c'est-à-dire pourquoi on utilise ce flux audio. Est-ce pour un jeu vidéo, pour de la musique, pour une notification, etc. Pour chaque usage, une constante est définie dans la classe AudioAttributes et qui permet de définir l'usage du flux audio. Dans notre cas, on utilise USAGE_GAME pour définir l'usage du flux audio.
  • Quoi : On définit le type de contenue du flux, quand celui-ci est connue et spécifique. Par exemple, si on joue de la musique, on peut définir le type de contenu du flux audio en utilisant CONTENT_TYPE_MUSIC.
  • Comment : On définit la façon dont le flux audio est utilisé. Par exemple, si on utilise un flux audio pour une notification, on peut définir la façon dont le flux audio est utilisé en utilisant USAGE_NOTIFICATION.

Deuxièmement, l'usage de la classe SoundPool qui permet de jouer des sons dans une application Android. La classe SoundPool permet de charger des sons et de les jouer. Elle est utilisée pour jouer des sons courts et répétitifs, comme des effets sonores dans un jeu vidéo. La classe SoundPool est initialisée en utilisant la méthode new SoundPool.Builder(). On peut définir le nombre maximum de flux audio en utilisant la méthode setMaxStreams. On peut définir les attributs audio en utilisant la méthode setAudioAttributes. Dans notre cas, on définit le nombre maximum de flux audio à 2 et on définit les attributs audio en utilisant attributes.

pool = new SoundPool.Builder()
        .setMaxStreams(MAX_STREAMS)
        .setAudioAttributes(attributes).build();

On peut charger un son en utilisant la méthode load de la classe SoundPool. Cette méthode prend en paramètre le contexte de l'application, le son à charger et la priorité du son. Le son est chargé et son identifiant est retourné puis stocké dans une HashMap.

int id = pool.load(getApplicationContext(), R.raw.son1, 1);
map.put("son1", id);

Le son est stocké dans le dossier res/raw de l'application. Enfin, on peut jouer un son en utilisant la méthode play de la classe SoundPool. Cette méthode prend en paramètre l'identifiant du son à jouer, le volume gauche et droit, la priorité du son, le nombre de répétitions et la vitesse de lecture. Dans notre cas, on joue le son en utilisant pool.play(id, 1, 1, 0, 0, 1). Le premier paramètre est l'identifiant du son à jouer, le second et le troisième paramètres sont le volume gauche et droit, le quatrième paramètre est la priorité du son, le cinquième paramètre est le nombre de répétitions et le sixième paramètre est la vitesse de lecture.

Les toasts

Dans cette application, nous allons voir comment afficher des messages temporaires à l'utilisateur en utilisant des toasts. Un toast est un message qui apparaît à l'écran pendant un court instant et disparaît automatiquement. Pour cela, nous allons créer une application qui affiche un toast dès que l'application est lancée.

Le code de l'activité MainActivity est le suivant :

package com.example.motionandtoat;

import android.os.Bundle;
import android.view.MotionEvent;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });
        Toast.makeText(this, "Notification toast", Toast.LENGTH_SHORT).show(); // Afficher un toast
    }
}

Dans ce code, on utilise la méthode Toast.makeText pour créer un toast. Cette méthode prend en paramètre le contexte de l'application, le message à afficher et la durée d'affichage du toast. Le contexte de l'application est obtenu en utilisant this. Le message à afficher est donné par "Notification toast". La durée d'affichage du toast est donnée par Toast.LENGTH_SHORT. Elle correspond à une durée de 2 secondes. Elle peut être remplacée par Toast.LENGTH_LONG pour une durée de 3,5 secondes. Enfin, on affiche le toast en utilisant show().

Afficher une image

Dans cette application, nous allons voir comment afficher une image dans une application Android pour quelle suive le mouvement de l'utilisateur. Pour cela, nous allons créer une application qui affiche une image à l'écran. L'image est stockée dans le dossier res/drawable de l'application. Pour ce faire, nous allons utiliser une image de croix disponible dans le dossier ressource du dépôt git de ce cours. L'image doit être stockée dans le dossier AndroidStudioProjects/[Nom de votre projet]/app/src/main/res/drawable/ de l'application. Le code de l'activité MainActivity est le suivant :

package com.example.imagedeplacement;

import android.os.Bundle;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.widget.ImageView;
import android.widget.Toast;

import androidx.activity.EdgeToEdge;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;

public class MainActivity extends AppCompatActivity {

    ImageView iw;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        iw = findViewById(R.id.imageView);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);

        int action = event.getAction();

        switch (action) {
            case MotionEvent.ACTION_MOVE: // L'utilisateur a déplacé son doigt sur l'écran
                int x = (int) event.getX();
                int y = (int) event.getY();
                iw.setX(x); // Déplacer l'image horizontalement
                iw.setY(y); // Déplacer l'image verticalement
                break;
            default:
                break;
        }

        return true;
    }
}

La gestion de la position de l'image est faite dans la méthode onTouchEvent. Pour récupérer les coordonnées de, l'image, on utilise iw.getX() et iw.getY(). Pour déplacer l'image, on utilise iw.setX(x) et iw.setY(y). Ce qui importe le plus pour cette application est la gestion de l'image dans le fichier XML. Le code XML du fichier activity_main.xml est le suivant :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="50dp"
        android:layout_height="50dp"
        app:srcCompat="@drawable/croix"
        android:scaleType="fitXY"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Dans ce code, on utilise un ImageView pour afficher l'image. L'image est donnée par l'attribut app:srcCompat="@drawable/croix". L'image croix est stockée dans le dossier res/drawable de l'application. On peut remarquer que le nom de l'image est donné sans l'extension .png et qu'elle est identifié avec le prefixe @drawable/ indiquant qu'elle est stockée dans le dossier res/drawable. L'attribut android:scaleType="fitXY" permet de redimensionner l'image pour qu'elle remplisse l'espace disponible. La taille de l'image est donnée par les attributs android:layout_width="50dp" et android:layout_height="50dp". Ces attributs définissent la largeur et la hauteur de l'image en pixels.

Jouer à un jeu

On se propose de créer un jeu simple qui consiste ressemble au jeu du Simon. Le but du jeu est de reproduire une séquence de couleurs qui s'affiche à l'écran. Le jeu est composé de quatre couleurs : rouge, vert, bleu et jaune. Le joueur doit reproduire la séquence de couleurs en appuyant sur les boutons de couleur. On pourra utiliser la classe suivante qui étend SurfaceView pour créer le jeu.

package com.example.simon;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.media.SoundPool;
import android.os.Handler;
import android.util.Log;
import android.view.SurfaceView;

public class GameView extends SurfaceView {
    private Paint paint;
    protected int selectedColor;
    protected int colors[] = {Color.RED, Color.GREEN, Color.BLUE, Color.YELLOW};
    private Handler handler;
    protected SoundPool sp;


    public GameView(Context context) {
        super(context);
        this.selectedColor= Color.BLACK;
        paint = new Paint();
        paint.setColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL);
        setWillNotDraw(false);
        handler = new Handler();

        // TODO : Initialiser le SoundPool ici si nécessaire

        invalidate();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        drawSimonButtons(canvas);
    }

    private void drawSimonButtons(Canvas canvas) {
        int width = getWidth();
        int height = getHeight();

        for (int i = 0; i < 4; i++) {
            paint.setColor(colors[i]);
            if (this.selectedColor == colors[i])
                paint.setColor(assombrirCouleur(colors[i]));
            else
                paint.setColor(colors[i]);

            canvas.drawRect(i % 2 * width / 2, i / 2 * height / 2, (i % 2 + 1) * width / 2, (i / 2 + 1) * height / 2, paint);
        }

    }

    //get selectedColor
    public int getSelectedColor() {
        return selectedColor;
    }
    //set selectedColor
    public void setSelectedColor(int selectedColor) {
        this.selectedColor = selectedColor;
        // invalidate(); // Redessiner la vue après avoir changé la couleur
    }

    private int assombrirCouleur(int color) {
        float ratio = 1.0f - 0.5f; // Ratio pour assombrir la couleur
        int a = (color >> 24) & 0xFF; // Extraire le canal alpha
        int r = (int) (((color >> 16) & 0xFF) * ratio); // Modifier le canal rouge
        int g = (int) (((color >> 8) & 0xFF) * ratio); // Modifier le canal vert
        int b = (int) ((color & 0xFF) * ratio); // Modifier le canal bleu
        return (a << 24) | (r << 16) | (g << 8) | b; // Combiner les canaux pour obtenir la couleur assombrie
    }

    public void changeColorTemporarily(int color) {
        setSelectedColor(color);
        // Jouer le son correspondant
        int indexSon=1;
        if (color == Color.RED)
            indexSon = 1;
        else if (color == Color.GREEN)
            indexSon = 2;
        else if (color == Color.BLUE)
            indexSon = 3;
        else if (color == Color.YELLOW)
            indexSon = 4;

        // TODO: Jouer le son correspondant
        invalidate();

        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                setSelectedColor(0);
                invalidate();
            }
        }, 300); // 1 seconde
    }
}

Dans cette classe, on utilise la méthode onDraw pour dessiner les boutons de couleur. Cette méthode est appelée chaque fois que la vue doit être redessinée. On utilise la méthode drawSimonButtons pour dessiner les boutons de couleur. Cette méthode utilise la classe Paint pour dessiner les boutons de couleur. On utilise la méthode drawRect pour dessiner un rectangle de couleur. On utilise la méthode setColor pour définir la couleur du bouton.

Pour simuler le jeu, on utilise la méthode changeColorTemporarily pour changer temporairement la couleur du bouton. Cette méthode utilise la méthode setSelectedColor pour changer la couleur du bouton. On utilise la méthode postDelayed pour changer la couleur du bouton après un certain temps. On utilise la méthode invalidate pour redessiner la vue après avoir changé la couleur du bouton. On utilise la méthode assombrirCouleur pour assombrir la couleur du bouton.

⚠️ **GitHub.com Fallback** ⚠️