image/svg+xml $ $ ing$ ing$ ces$ ces$ Res Res ea ea Res->ea ou ou Res->ou r r ea->r ch ch ea->ch r->ces$ r->ch ch->$ ch->ing$ T T T->ea ou->r

Les menus et items de menu

Introduction

Exemple de ressource XML de menu

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:id="@+id/menu_search"
		 android:icon="@drawable/menu_search"
		 android:title="@string/menu_search"
		 android:showAsAction="ifRoom|collapseActionView"
		 android:actionViewClass="android.widget.SearchView" />
    <!-- We define a menu group -->
    <group android:id="@+id/group_temporal">
        <item android:id="@+id/menu_start"
              android:title="@string/menu_start" />
        <item android:id="@+id/menu_stop"
              android:title="@string/menu_stop" />
    </group>
</menu>

Les menus de la barre d'action

ActionBar

Éléments de navigation

Items d'action

Exemple : un diaporama d'images

Réalisons une activité affichant différentes images extraites du répertoire assets de l'application.

Tout d'abord nous réalisons un menu statique XML permettant d'avancer, reculer ou sélectionner une image aléatoire à afficher :

<?xml version="1.0" encoding="utf-8"?>
<!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License -->

<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=".menu.DiaporamaActivity">

    <TextView
        android:id="@+id/textView4"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:text="Use the menu to go to a new random picture or for direct access to a picture"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ImageView
        android:id="@+id/pictureView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginLeft="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginRight="8dp"
        android:layout_marginBottom="8dp"
        android:scaleType="fitCenter"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView4"
        tools:src="@tools:sample/avatars" />
</androidx.constraintlayout.widget.ConstraintLayout>

Remarquons l'existence d'un groupe vide dans le menu : nous allons le peupler à l'exécution en fonction des images listées dans le répertoire assets.

Écrivons ensuite l'activité qui récupère les images, peuple le menu avec les items dans onCreateOptionsMenu(...) et prépare le menu en cochant le bon item (correspondant à l'image actuellement affichée) dans onPrepareOptionsMenu(...).

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.menu;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

import fr.upem.coursand.R;

/**
 * An activity displaying pictures from the assets directory.
 */
public class DiaporamaActivity extends Activity {

    /** Path of the pictures into the assets directory */
    public static final String PICTURES_PATH = "desserts";

    public static List<String> getPicturePaths(Context context) {
        List<String> result = new ArrayList<>();
        try {
            for (String filename : context.getResources().getAssets().list(PICTURES_PATH))
                result.add(PICTURES_PATH + "/" + filename);
        } catch (IOException e) {
            Log.e(DiaporamaActivity.class.getName(), "IOException while fetching picture paths", e);
        }
        return result;
    }

    /** Random number generator */
    private static final Random rng = new Random();

    private List<String> picturesPaths;

    /** The index of the currently displayed picture */
    private int currentPictureIndex = -1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_diaporama);
        picturesPaths = getPicturePaths(this);
        displayPicture(0);
    }

    private final int firstMenuItemID = Menu.FIRST;

    private Bitmap loadAssetBitmap(String path) {
        try {
            return BitmapFactory.decodeStream(getAssets().open(path));
        } catch (IOException e) {
            Log.e(this.getClass().getName(), "Cannot load bitmap for path " + path);
            return null;
        }
    }

    /** Display a picture on the ImageView */
    protected void displayPicture(int index) {
        if (index != currentPictureIndex) {
            ImageView iv = findViewById(R.id.pictureView);
            iv.setImageBitmap(loadAssetBitmap(picturesPaths.get(index)));
            currentPictureIndex = index;
        }
    }

    /** Called once in the life of the activity when the menu is created */
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // load the menu from the XML file
        getMenuInflater().inflate(R.menu.diaporama_menu, menu);
        int i = 0;
        for (String path: picturesPaths) {
            int start = path.indexOf("/") + 1;
            int stop = path.lastIndexOf(".");
            String name = path.substring(start, stop);
            MenuItem mi = menu.add(R.id.directPictureAccess /* group id */, firstMenuItemID + i  /* item id */, 0 /* the order */, name /* title */);
            mi.setCheckable(true);
            mi.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); // never put the individual picture items in the action bar
            i++;
        }
        return true; // do not forget to return true to display the menu
    }

    /** Called before each time the menu is displayed */
    @Override
    public boolean onPrepareOptionsMenu(Menu menu) {
        // Check the correct item
        for (int i = firstMenuItemID; i < firstMenuItemID + picturesPaths.size(); i++)
            menu.findItem(i).setChecked(currentPictureIndex == i - firstMenuItemID);
        return true; // in order to display the menu
    }

    /** Called when an element of the menu is clicked */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        if (item.getItemId() == R.id.randomPictureAccess)
            displayPicture(rng.nextInt(picturesPaths.size()));
        else if (item.getItemId() == R.id.previousPictureAccess)
            displayPicture((currentPictureIndex - 1) % picturesPaths.size());
        else if (item.getItemId() == R.id.nextPictureAccess)
            displayPicture((currentPictureIndex + 1) % picturesPaths.size());
        else {
            // direct access to a picture
            int index = item.getItemId() - firstMenuItemID;
            if (index >= 0 && index < picturesPaths.size())
                displayPicture(index);
            else
                return false; // the menu item is not managed by our method (if we have forgotten a case)
        }
        return true;
    }
}

Tiroir de navigation (navigation drawer)

Principe d'implantation

Exemple : informations sur des villes

Réalisons une activité pour afficher diverses informations sur les plus grandes villes du monde.

Nous fournissons un fichier texte au format TSV (colonnes séparées par des tabulations) placé dans le répertoire assets avec les informations pour chaque ville sur une ligne. Nous écrivons une classe en Kotlin chargée de lire ce fichier :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.topcities

import android.content.Context
import java.io.Serializable
import java.lang.Math.*
import java.util.*

class City(val name: String, val latitude: Float, val longitude: Float, val population: Int, val elevation: Float, val timeZone: TimeZone): Serializable {

    companion object {
        /** Load a city from a CSV text line */
        fun loadFromLine(line: String): City {
            val c = line.split("\t")
            return City("${c[0]} ${c[3]}", c[1].toFloat(), c[2].toFloat(), c[4].toInt(), c[5].toFloat(), TimeZone.getTimeZone(c[6]))
        }

        /** Load all the cities from a CSV text file */
        fun loadFromAsset(context: Context, path: String): List<City> {
            return context.assets.open(path).reader().readLines().map { loadFromLine(it) }
        }

        const val R = 6372.8 // in kilometers

        /** Compute the distance between two geographical points */
        fun haversine(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
            val λ1 = toRadians(lat1)
            val λ2 = toRadians(lat2)
            val Δλ = toRadians(lat2 - lat1)
            val Δφ = toRadians(lon2 - lon1)
            return 2 * R * asin(sqrt(pow(sin(Δλ / 2), 2.0) + pow(sin(Δφ / 2), 2.0) * cos(λ1) * cos(λ2)))
        }

        fun findNearest(cities: List<City>, latitude: Float, longitude: Float): City? {
            return cities.minByOrNull { haversine(latitude.toDouble(), longitude.toDouble(), it.latitude.toDouble(), it.longitude.toDouble()) }
        }
    }
}

Écrivons maintenant le layout pour notre activité avec inclusion du tiroir (qui repose sur l'inclusion d'un layout pour le contenu affiché pour une ville) :

<?xml version="1.0" encoding="utf-8"?>
<!-- Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License -->

<!-- Use DrawerLayout as root container for activity -->
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/drawer_layout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true">

    <!-- Layout to contain contents of main body of screen (drawer will slide over this) -->
    <LinearLayout
        android:id="@+id/content_frame"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
       android:orientation="vertical">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="?attr/colorPrimary"
            android:theme="@style/ThemeOverlay.AppCompat.ActionBar" />

        <include layout="@layout/content_top_cities" />
    </LinearLayout>

    <!-- Container for contents of drawer - use NavigationView to make configuration easier -->
    <com.google.android.material.navigation.NavigationView
        android:id="@+id/nav_view"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_gravity="start"
        android:fitsSystemWindows="true" />

</androidx.drawerlayout.widget.DrawerLayout>

La classe Java de l'activité réalise le travail de configuration nécessaire afin d'installer la barre d'actions, y rajouter un bouton déclenchant l'apparition du tiroir de navigation avec une méthode permettant de réaliser une action (afficher les informations) lors du clic sur une des villes du menu :

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.topcities;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ImageView;
import android.widget.TextView;

import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;

import com.google.android.material.navigation.NavigationView;

import java.util.Calendar;
import java.util.List;
import java.util.Random;

import fr.upem.coursand.R;
import fr.upem.coursand.configchange.ConfigChangeSupportActivity;

/** An activity displaying essential facts about top cities using a navigation drawer */
public class TopCitiesActivity extends ConfigChangeSupportActivity {

    /** Where the cities data are in the assets directory */
    public static final String CITIES_FILEPATH = "cities/topcities.csv";

    static class State {
        List<City> cities;
        int selectedCity = -1;
    }

    private State state = new State();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_top_cities);
        if (state.cities == null) loadCities();
        initToolbar();
        initDrawer();
        // display a random city
        displayCity(new Random().nextInt(state.cities.size()));
    }

    private void loadCities() {
        state.cities = City.Companion.loadFromAsset(this, CITIES_FILEPATH);
    }

    private void initToolbar() {
        // install the toolbar
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        ActionBar actionbar = getSupportActionBar();
        actionbar.setDisplayHomeAsUpEnabled(true);
        actionbar.setHomeAsUpIndicator(R.drawable.ic_menu);
    }

    private void initDrawer() {
        // configure the drawer with a listener for an item selection
       DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);

       NavigationView navigationView = findViewById(R.id.nav_view);

       // Populate the menu with the cities
       Menu menu = navigationView.getMenu();
       int i = 0;
       for (City city: state.cities)
           menu.add(Menu.NONE, Menu.FIRST + (i++), Menu.NONE, city.getName());

       // add a listener for the city selection
       navigationView.setNavigationItemSelectedListener(
                menuItem -> {
                    menuItem.setChecked(true);
                    displayCity(menuItem.getItemId() - Menu.FIRST);
                    drawerLayout.closeDrawers();
                    return true;
                });
    }

    /** To open the drawer when the menu icon is clicked */
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
        if (item.getItemId() == android.R.id.home) {
            drawerLayout.openDrawer(GravityCompat.START);
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    protected void setText(int id, Object value) {
        ((TextView)findViewById(id)).setText(value.toString());
    }

    protected void displayCity(int i) {
        City city = state.cities.get(i);

        // update selection about the menu
        NavigationView navigationView = findViewById(R.id.nav_view);
        Menu menu = navigationView.getMenu();
        if (state.selectedCity >= 0)
            menu.findItem(state.selectedCity + Menu.FIRST).setChecked(false);
        menu.findItem(i + Menu.FIRST).setChecked(true);

        state.selectedCity = i;

        // display information about the city
        getSupportActionBar().setTitle(city.getName());
        setText(R.id.latitudeTextView, city.getLatitude());
        setText(R.id.longitudeTextView, city.getLongitude());
        setText(R.id.populationTextView, city.getPopulation());
        setText(R.id.meanAltitudeTextView, city.getElevation());

        // display the local hour of the city
        Calendar calendar = Calendar.getInstance(city.getTimeZone());
        setText(R.id.localHourTextView, String.format("%02d:%02d", calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE)));

        // display the planisphere
        ImageView planisphere = findViewById(R.id.planisphereView);
        Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.equirectangular_world_map);
        bm = bm.copy(Bitmap.Config.ARGB_8888, true); // to allow modification of the bitmap
        Canvas canvas = new Canvas(bm);
        Paint paint = new Paint();
        paint.setColor(Color.RED);
        canvas.drawCircle(bm.getWidth() / 360.0f * (city.getLongitude() + 180.0f), bm.getHeight() - bm.getHeight()/180.0f * (city.getLatitude() + 90.0f), 50.0f, paint);
        planisphere.setImageBitmap(bm);
    }
}

Menu contextuel

Enregistrement d'un listener sur un clic long :

someView.setOnLongClickListener(new View.OnLongClickListener() {
    public boolean onLongClick(View view) {
        if (mActionMode != null) return false ;
        mActionMode = getActivity().startActionMode(callback);
        return true;
    }
});

Exemple : activité avec un bouton

// Code sample from Coursand [http://igm.univ-mlv.fr/~chilowi/], under the Apache 2.0 License

package fr.upem.coursand.toaster;

import android.os.Build;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Button;
import android.widget.Toast;

import java.util.Date;

import fr.upem.coursand.R;

public class ToasterActivity extends AppCompatActivity {

    private int lastSelectedItemId = -1;
    private String message = "no message";

    protected void updateMessage(int itemId) {
        String message = null;
        switch (itemId) {
            case R.id.showHelloWorldItem: message = "Hello World!"; break;
            case R.id.showDateItem: message = new Date().toString(); break;
            case R.id.showVersionItem: message = Build.VERSION.CODENAME; break;
            default: message = "42"; break;
        }
        lastSelectedItemId = itemId;
        this.message = message;
    }

    private final ActionMode.Callback actionModeCallback = new ActionMode.Callback() {

        @Override
        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
            getMenuInflater().inflate(R.menu.toaster_menu, menu);
            return true; // something is done
        }

        @Override
        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
            // put a check mark for the last selected item
            for (int i = 0; i < menu.size(); i++)
                menu.getItem(i).setChecked(menu.getItem(i).getItemId() == lastSelectedItemId);
            return true; // return false since nothing is done
        }

        @Override
        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
           updateMessage(item.getItemId());
           mode.finish();
           return false;
        }

        @Override
        public void onDestroyActionMode(ActionMode mode) {
            // executed when we exit from the action mode
            currentActionMode = null;
        }
    };

    private ActionMode currentActionMode = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_toaster);
        Button toastButton = findViewById(R.id.toastButton);

        // display the toast on click
        toastButton.setOnClickListener( v-> Toast.makeText(this, message, Toast.LENGTH_SHORT).show());

        // display the context menu allowing to select the message to display
        // when we press a long click on the button
        toastButton.setOnLongClickListener( v -> {
            if (currentActionMode == null) {
                currentActionMode = startActionMode(actionModeCallback);
                return true;
            } else
                return false;
        });
    }
}