Il y a 6 ans -

Temps de lecture 18 minutes

Développer pour tablette (pour le mag Programmez)

Les tablettes sont définitivement entrées dans notre vie quotidienne ! On le voit au succès de l’iPad et des diverses tablettes sous Android, comme la Nexus 7 (certains voient déjà leurs ventes dépasser celles des PC de bureau standards). Dans l’entreprise aussi, l’adoption des tablettes devient une réalité.

Mais alors, quelles sont les choses à savoir lorsque l’on souhaite développer une application sur tablette ou porter son application mobile existante sur tablette ? 

La tablette n’est ni un smartphone plus large, ni un PC portable aux dimensions réduites. Elle est les deux à la fois : elle a su se faire une place à part entière dans le monde des périphériques connectés que nous côtoyons aujourd’hui. Ainsi, même si mobiles et tablettes partagent le même système d’exploitation (principalement iOS et Android), proposer exactement la même application pour les deux formats est très rarement adapté..

En premier lieu, les résolutions et les tailles d’écrans ne sont pas les mêmes : il est possible d’afficher beaucoup plus d’éléments et de fonctionnalités sur une tablette. Pour un même cas d’utilisation, il sera donc possible, sur tablette par exemple, de diminuer le nombre d’écrans par rapport à une application mobile. Les usages mêmes sont très différents : le mobile s’utilise abondamment dans un cadre itinérant (dans les transports par exemple) avec une connexion type 3G (il faut alors prendre en compte le chargement des pages ou des données et la perte de connexion potentielle). À l’opposé, la tablette est utilisée dans un cadre domestique ou professionnel, souvent à proximité d’un réseau WIFI. L’usage du mobile est principalement personnel, tandis que la tablette a une utilisation plus partagée : l’écran peut être vu par plusieurs personnes et permet plus facilement de partager la donnée visualisée. La tablette invite à l’interaction sociale. Enfin, les applications mobiles préfèrent rester consistantes dans leur IHM vu le peu de place disponible, alors que dans le cadre des tablettes, la capacité à innover et inventer de nouvelles interfaces – plus sexy, plus naturelles – permet de surprendre l’utilisateur.

La tablette est rapidement devenu un format à prendre en compte lors de la réalisation d’une application mobile. Cet article va vous éclairer sur les aspects ergonomiques à prendre en compte et les développements spécifiques que vous devrez mettre en oeuvre aussi bien sur iOS que sur Android.

Adapter l’expérience utilisateur à la tablette

L’écran d’une tablette est plus grand que celui d’un téléphone, il est donc possible de placer plus d’éléments. Par exemple, l’application GMail pour tablette affiche en permanence la liste des labels dans une colonne de gauche contrairement à l’application mobile où il faut aller les sélectionner par un menu en bas de l’application.

image2013-2-21-12241.pngimage2013-2-21-12255.png

Les fonctionnalités dossiers, liste des mails et le corps du message sur mobile pour l’application GMail.

image2013-2-21-12421.png

image2013-2-21-1224.png

Les mêmes fonctionnalités de l’application vues sur une tablette.

Il faut tout de même faire attention à ne pas surcharger l’IHM d’une tablette bien que celle-ci ait un plus grand écran.
D’autre part, il est possible de changer la façon d’interagir avec une tablette. Sur mobile, la façon dont l’utilisateur le tient classiquement fait que le placement d’une barre d’action en bas de l’écran est plus ergonomique car elle se retrouve plus proche du pouce. Au contraire sur une tablette, on peut sans problème mettre la barre d’action en haut de l’écran car on dispose plus souvent d’une main de libre qui pourra facilement aller cliquer sur ces éléments. Un autre exemple est le clavier virtuel SwiftKey qui propose une disposition différente sur tablette en mode paysage car les mains peuvent être alors disposées de chaque coté de la tablette pour accélérer la saisie. Si vous voulez comprendre n’hésitez pas à prendre un mobile ou une tablette et essayer !

Enfin, si on peut choisir de se limiter au mode portrait pour les applications mobiles, il faut que les applications tablettes supportent les deux types d’orientation en sachant que l’orientation paysage est la plus utilisée.

Le clavier en orientation portrait sur mobile
image2013-2-15-11334.png

Le même clavier en orientation paysage sur tablette
image2013-2-21-12110.png

Prendre en compte techniquement les tablettes et l’orientation sur Android

Avec la version Honeycomb d’Android est apparu un nouveau type de composant permettant de faciliter le développement sur tablette : le Fragment. Pour faire simple, il s’agit d’ une portion réutilisable de l’interface utilisateur d’une Activity. Une activité peut contenir plusieurs fragments qu’il est possible d’ajouter ou retirer même si elle est en cours d’exécution. Cette modularité permet de construire une interface qui s’adaptera à l’espace disponible sur l’écran.

Le Fragment gère ses propres évènements et a un cycle de vie dédié. Cependant ce dernier est directement lié au cycle de vie de l’activité qui le contient. D’ailleurs, les callback méthodes d’un Fragment sont similaires à celles d’une Activity, on retrouve des méthodes comme onCreate(), onPause(), etc.

Maintenant nous allons voir au travers d’un exemple comment mettre en place des fragments au sein d’une application qui aura un workflow comme suit :

Tout d’abord créons le fragment qui affichera la liste d’éléments que l’utilisateur pourra sélectionner pour en afficher le détail. Pour cela nous allons utiliser un dérivé de la classe Fragment : ListFragment.

public class ItemsListFragment extends ListFragment {
    
    private static final String[] DATA = {"Item 1", "Item 2", "Item 3"};
    
    private ArrayAdapter<String> adapter;
    private Callback activityCallback;


    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Création de l'adapter avec les éléments à afficher
        adapter = new ArrayAdapter<String>(getActivity(), R.layout.simple_list_item_1, DATA);
        setListAdapter(adapter);
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        // On récupère une référence vers l'activité hôte via l'interface Callback créée plus bas
        activityCallback = (Callback) activity;
    }

    @Override
    public void onListItemClick(ListView l, View v, int position, long id) {
        super.onListItemClick(l, v, position, id);
        Bundle selectedItem = new Bundle(1);
        selectedItem.putString("name", DATA[position]);
        // On avertit l'activité hôte qu'un item a été sélectionné
        activityCallback.onItemSelected(selectedItem);
    }
    
    /**
     * Interface que devra implémenter l'activité hôte pour pouvoir être notifié
     * lorsqu'un item est sélectionné
     */
    public interface Callback {
        void onItemSelected(Bundle selectedItem);
    }
}

Quand au fragment affichant le détail d’un item :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:gravity="center">
    <TextView android:id="@+id/itemName"
              android:layout_width="wrap_content"
              android:layout_height="wrap_content" />
</RelativeLayout>

public class ItemDetailsFragment extends Fragment {
    
    private TextView itemNameTxt;
    
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.item_details_layout, container, false);
        itemNameTxt = (TextView) view.findViewById(R.id.itemName);
        if(getArguments() != null){
            // On a reçu les données à afficher via les arguments du fragment
            updateItemDetails(getArguments());
        }
        return view;
    }
    
    public void updateItemDetails(Bundle selectedItem){
        // On récupère le nom de l'item et on l'affiche
        itemNameTxt.setText(selectedItem.getString("name"));
    }
}

Maintenant que nous avons implémenté nos deux fragments, nous devons créer un layout adapté suivant le device. A partir de la version 3.2 d’Android il est possible de spécifier la largeur minimum que doit faire un écran pour afficher un layout donné. Nous mettons celui de notre tablette dans le dossier res/layout-sw600dp (sw pour smallest width), ce qui signifie que seul un appareil ayant une capacité d’au moins 600dp en largeur (qu’importe l’orientation) comme le nexus 7 pourra l’afficher. Pour les tablettes pre 3.2 le dossier res/layout-x-large devra être privilégié car les ressources du dossier layout-sw600dp seront tout simplement ignorées. En ce qui concerne le layout des téléphones, il sera composé d’un simple FrameLayout qui jouera le rôle de conteneur de fragments et sera placé dans le répertoire res/layout.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="horizontal"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent" >
    <fragment
        class="fr.programmez.tablette.ItemsListFragment"
        android:id="@+id/items_list_frag"
        android:layout_height="match_parent"
        android:layout_width="0dp"
        android:layout_weight="1" />
    <fragment
            class="fr.programmez.tablette.ItemDetailsFragment"
            android:id="@+id/item_details_frag"
            android:layout_height="match_parent"
            android:layout_width="0dp"
            android:layout_weight="3" />
</LinearLayout>

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
             android:id="@+id/fragment_container"
             android:orientation="vertical"
             android:layout_width="fill_parent"
             android:layout_height="fill_parent"/>

Il est temps de faire entrer en jeu notre activité qui a pour rôle de gérer nos deux fragments. Cette dernière a besoin de savoir sur quel type de device elle est lancée afin de déterminer quel mode d’affichage sera utilisé. Pour y parvenir nous créons une ressource de type boolean multi_pane qui vaut false par défaut sauf dans les dossiers res/values-sw600dp et res/values-x-large.

Dans le cas d’une application s’exécutant sur un téléphone, nous faisons appel au FragmentManager qui va nous permettre de remplacer le contenu du FrameLayout par le fragment souhaité. Cela se passe sous forme d’une transaction au cours de laquelle nous précisons quel fragment nous souhaitons voir affiché. Il est possible d’ historiser ces transactions (méthode addToBackStack) afin qu’un appui sur la touche back ait pour conséquence un retour à l’état précédent. Il est important de préciser que ce FragmentManager ne peut agir que sur des fragments ajoutés au runtime. Pour finir dans le callback de sélection d’un item, nous remarquons que pour transmettre l’item sélectionné nous utilisons les arguments du fragment. Il s’agit d’un bundle que l’on peut passer à un fragment lors de sa création et ainsi le récupérer au moment où la vue sera créée.

Lors d’une exécution sur tablette, pas besoin de transaction nous récupérons directement une référence vers le fragment déclaré dans le fichier xml grâce à la méthode findFragmentById du FragmentManager. De même le traitement est simplifié dans le callback de sélection d’un item car nous sommes sûrs que le fragment est créé et attaché à l’activité en cours. Il est alors possible d’appeler directement la méthode updateItemDetails() exposée par notre fragment.

public class SampleAvtivity extends Activity implements ItemsListFragment.Callback {
   
   /**
     * Référence vers le fragment affichant le détail (uniquement utilisé pour les tablettes)
     */
    private ItemDetailsFragment itemDetailsFrag;
    
   /**
     * Sommes-nous sur une tablette ?
     */
    private boolean multiPane;
  
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        multiPane = getResources().getBoolean(R.bool.multi_pane);
        if (multiPane) {
            // Le fragment est déclaré dans le xml, on récupère sa référence
            itemDetailsFrag = (ItemDetailsFragment) getFragmentManager().findFragmentById(R.id.item_details_frag);
        } else {
            // On remplace le contenu du FrameLayout par le fragment affichant la liste des items
            getFragmentManager().beginTransaction() //
                    .replace(R.id.fragment_container, new ItemsListFragment()) //
                    .commit();
        }
    }
  
    @Override
    public void onItemSelected(Bundle selectedItem) {
        if (multiPane) {
            itemDetailsFrag.updateItemDetails(selectedItem);
        } else {
            ItemDetailsFragment fragment = new ItemDetailsFragment();
            // On met l'item sélectionné dans les arguments du Fragment pour pouvoir le récupérer lors de sa création
            fragment.setArguments(selectedItem);
            // On remplace le fragment affichant la liste par celui du détail de l'item
            getFragmentManager().beginTransaction() //
                    .replace(R.id.fragment_container, fragment) //
                    .addToBackStack(null) //
                    .commit();
        }
    }
}

Nous obtenons le résultat suivant sur tablette (un fond blanc a été utilisé afin de distinguer les deux fragments) :

Alors que sur téléphone, l’accès à l’ensemble des écrans se fait en 2 temps : 

Prendre en compte techniquement les tablettes et l’orientation sur iOS

Depuis la sortie de l’iPad, et avec iOS 3.2, Apple a introduit le UISplitViewController, un composant natif permettant de gérer automatiquement l’affichage des vues sur l’écran d’une tablette.

Le UISplitViewController consiste donc en un conteneur hébergeant les composants visuels de deux ViewControllers. La vue du UIViewController de gauche, plus petit, a une taille de 320 points et est toujours affiché en mode paysage mais caché automatiquement en mode portrait. Dans ce dernier cas, il peut être affiché en utilisant un bouton placé sur la barre de navigation ou (à partir d’iOS 5.1) en glissant le doigt de gauche à droite avec un geste de swipe. La vue de droite du UIViewController est par conséquent affichée en plein écran quand l’iPad se trouve en mode portrait.

Le UISplitViewController est très souvent utilisé pour mettre en oeuvre une interface de type master-detail qui consiste à afficher, dans la vue de gauche (le master), une liste d’éléments et, dans la vue de droite (le détail), les détails de l’élément sélectionné dans le master. La taille du master n’est par ailleurs pas une coïncidence : il s’agit en fait de la largeur en points de l’écran de l’iPhone. Ce choix permet aux développeurs de réutiliser complètement l’interface utilisateur conçue pour afficher une liste sur le téléphone. 

Nous allons expliquer, avec un exemple simple, comment mettre en oeuvre notre logique master-detail sur iPhone et iPad.

La méthode la plus simple pour utiliser le composant UISplitViewController est de créer un nouveau projet avec XCode et d’utiliser le gabarit "Master-Detail Application". Avec cette opération on obtiendra deux Controllers (un MasterViewController et un DetailViewController) aussi bien que les Storyboards pour iPhone et iPad. 

Comment faire dans le cas où nous avons déjà une application iPhone avec une UITableView et que nous voulons la rendre compatible avec iPad ? 

Nous allons supposer, pour notre exemple, que nous avons déjà codé un UITableViewController, nommé "ItemTableViewController", contenant une liste d’éléments, et un UIViewController nommé "ItemDetailViewController" contenant le détail d’un élément. 

#import <UIKit/UIKit.h>

@interface ItemTableViewController : UITableViewController

@end
#import "ItemTableViewController.h"
#import "ItemDetailViewController.h"
 

@interface ItemTableViewController () {
    NSArray *_items;
}
@end


 
@implementation ItemTableViewController


- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    _items = @[@"Item 1", @"Item 2", @"Item 3", @"Item 4"];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return _items.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
    cell.textLabel.text = _items[indexPath.row];
    
    return cell;
}

#pragma mark - Table view delegate

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([[segue identifier] isEqualToString:@"showItemDetail"]) {
        NSIndexPath *indexPath = [self.tableView indexPathForSelectedRow];
        NSString *item = _items[indexPath.row];
        [[segue destinationViewController] setItem:item];
    }
}
@end
#import <UIKit/UIKit.h>

@interface ItemDetailViewController : UIViewController

@property (weak, nonatomic) IBOutlet UILabel *itemInfo;

@property (strong, nonatomic) NSString *item;

@end
#import "ItemDetailViewController.h"

@interface ItemDetailViewController ()
@end
 

@implementation ItemDetailViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    [self showItemDetail];
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

- (void)showItemDetail {
    if (self.item) {
        self.itemInfo.text = self.item;
    }
}

@end

Dans le cadre de l’interface master-detail, ItemTableViewController sera notre master et ItemDetailViewController notre détail.

Nous commençons par changer le type de dispositif ciblé par notre application : sélectionnons le fichier de projet, puis notre target, et dans le menu déroulant "Devices" activons "Universal". Plus bas, dans la section "iPad Deployment Info" changeons le nom du "Main Storyboard" et saisissons, par exemple, "MainStoryboard_iPad". Ce storyboard contiendra l’interface utilisateur de notre application universelle et, si nous ne l’avons pas encore générée, il ne nous reste qu’à en créer une nouvelle à partir du menu File -> New -> User Interface. N’oublions pas de sélectionner, ensuite, la "Device Family" qui, pour notre application, sera "iPad".

À l’intérieur de notre nouveau Storyboard nous allons ajouter à partir de l’Object Library un UISplitViewController. Plusieurs scènes seront créées automatiquement, avec leurs segue. Nous allons ensuite sélectionner le Table View Controller, dans l’Identity Inspector à droite de l’écran, on changera la Class, en saisissant "ItemTableViewController". La même chose doit être faite pour le Detail View Controller et sa classe devra être "ItemDetailViewController". Nous pouvons ensuite personnaliser les cellules du tableau sans oublier de définir le même "Identifier" utilisé sur iPhone.

Il ne reste plus qu’à adapter notre ItemTableViewController pour le rendre compatible avec iPad. On a tout d’abord besoin de référencer notre ItemDetailViewController dans le ItemTableViewController.

Dans l’extension du ItemTableViewController on ajoute donc la ligne suivante, permettant de créer une property qui référencera le détail :

@property (weak, nonatomic) ItemDetailViewController *detailViewController;

À l’intérieur de la méthode viewDidLoad on va ensuite ajouter la ligne suivante :

self.detailViewController = (ItemDetailViewController *)[self.splitViewController.viewControllers lastObject];

qui enregistre dans la propriété que nous avons créée l’instance courante du ItemDetailViewController.

Dans le block "implementation", on ajoute ensuite la méthode suivante :

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
        NSString *item = _items[indexPath.row];
        self.detailViewController.item = item;
    }
}

Cette méthode, issue de UITableViewDelegate, communique au delegate de la UITableView (qui n’est rien d’autre que notre instance du ItemDetailViewController) qu’une ligne du tableau a été sélectionnée. Avec le bloc (if [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) nous vérifions si le dispositif que l’utilisateur est en train d’utiliser est bien un iPad et, dans ce cas, on définit la propriété item du detailViewController.

À l’intérieur du ItemDetailViewController on ajoute la méthode suivante, permettant de forcer l’affichage des informations à chaque fois qu’on changera la propriété item.

- (void)setItem:(NSString *)item {
    _item = item;
    [self showItemDetail];
}

On peut d’ores et déjà essayer l’application sur iPad : la liste des items s’affiche seulement en paysage et la sélection d’un élément change le contenu de la vue de détail.

Mais comment faire pour pouvoir accéder aux éléments de notre tableau même si on est en mode portrait ?

À partir de iOS 5.1, le UISplitViewController permet aux utilisateurs d’afficher la vue de gauche (notre master) avec un geste de swipe de gauche à droite sur l’écran. Afin d’activer cette fonctionnalité, on devra definir le delegate du UISplitViewController sur notre ItemDetailViewController.

Dans ItemDetailViewController.h il faudra modifier la ligne 

@interface ItemDetailViewController : UIViewController
@interface ItemDetailViewController : UIViewController<UISplitViewControllerDelegate>

Et, finalement, dans la méthode viewDidLoad du ItemDetailViewController.m nous allons ajouter la ligne suivante :

self.splitViewController.delegate = self;

Si on voulait accéder aux éléments de notre tableau avec un bouton, nous pourrions utiliser les fonctionnalités du UISplitViewControllerDelegate qui permettent au splitViewController de notifier son delegate à chaque fois qu’on change le mode de présentation en mettant en oeuvre, dans ItemDetailViewController.m des méthodes du protocol UISplitViewControllerDelegate comme les suivantes :

- (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)popoverController

- (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem

Conclusion

Dans cet article, nous avons fait une introduction au développement sur tablette. Que l’on parte de zéro ou que l’on souhaite adapter son application mobile existante, il est important de comprendre les paradigmes de navigation employés sur ce nouveau périphérique et également les usages attendus. 

Lorsque vous vous lancez dans le développement d’une application mobile, nous vous conseillons de réfléchir au plus tôt à la déclinaison que votre application va prendre sur tablette : cela vous permettra de mutualiser une grande partie du code développé. En effet, même si l’IHM proposée est souvent différente, ou encore la navigation, la façon de récupérer les données ou encore de les stocker varie peu. Pensé dès le début de votre projet, le développement d’une application tablette vous prendra généralement moins de 50% de l’effort consacré au développement de votre application mobile. 

Sur iOS comme sur Android, vous disposerez des mêmes "astuces" pour mettre en oeuvre une ergonomie adaptée sur tablette, ou encore, faire l’adaptation de votre application mobile.

Publié par Simone Civetta

Simone est responsable technique des équipes mobilités chez Xebia. Il est également formateur au sein de Xebia Training .

Publié par Thomas Guerin

Consultant Java

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Xebian, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.