Mettre en place des onglets dans Android

Affichage avec les onglets

Ce site ne sera plus alimenté de contenu après août 2014. Tous les nouveaux articles seront redigés pour www.waitingforcode.com

Les onglets dans les navigateurs web amènent beaucoup de confort aux utilisateurs. Au lieu de sauter d'une fenêtre à l'autre, ils peuvent se déplacer au sein de la même fenêtre sans trop se déconcentrer. Le concepts des onglets a été donc implementé sous Android. Cependant, leur gestion sera certainement l'une des tâches les plus lourdes à comprendre parmi les articles qu'on a traversés jusque là. L'article ci-dessus montrer comment intégrer la navigation par onglets dans notre exemple d'application Android. Au début on verra cependant quelques aspects théoriques qui se retrouveront dans cet article.

Les onglets sous Android

Un écran d'onglets est composé de quelques parties. La première contient les onglets à travers lesquels l'utilisateur peut naviguer. La navigation signifie ici le chargement de nouvelles informations dans la seconde partie, celle du contenu.

Techniquement, l'élément qui englobe les deux parties mentionnées, est TabHost. Son identifiant doit correspondre au @android:id/tabhost. Ensuite, l'élément qui regroupe les onglets (liens qui servent à déterminer le contenu qu'on souhaite voir) s'appelle TabWidget. L'identifiant de cet élément doit être @android:id/tabs. Quand l'utilisateur clique sur un lien d'onglet, ce dernier envoie un message à TabHost. Le message contient l'information qui informe du chargement d'un nouveau contenu.

En ce qui concerne cette fameuse partie du contenu, elle est représentée par FrameLayout qui doit posséder l'identifiant @android:id/tabcontent. Pourquoi ce type de layout ? FrameLayout possède une spécificité important. Il "se réserve" une partie de l'écran pour afficher son contenu. Cependant, il devrait contenir juste une seule vue-enfant car des problèmes de positionnement (des vues qui se marchent sur le pied) peuvent se produire.

Plusieurs types d'onglets existent sous Android :
- onglets scrollables : à cause du nombre important des onglets, ils peuvent devenir "scrollables".
- onglets fixes : tous les onglets sont affichés avec la même largeur. S'il n'y a pas suffisamenet de place sur l'écran, les libellés des onglets deviendront scrollables. C'est pourquoi il est conseillé d'utiliser ce type dans le cas de 3 onglets au maximum.
- onglets empilés : à utiliser quand les onglets constituent l'élément essentiel de navigation. Dans cette situation, les onglets sont placés dans Action Bar.

Dans notre application on présentera pour le moment l'intégration du second type, des onglets fixes.

Implémenter les onglets fixes sous Android

Cependant, on compliquera un peu la tâche car pour la présentation on utilisera l'activité qui charge les onglets dynamiquement. Il s'agit du "mon compte" qui, après avoir saisi les données de connexion correctes, se connecte au web service et récupère les préférences de l'utilisateur. Cette activité place ensuite les données chargées dans un onglet nouvellement créé - préférences. Un autre onglet, à part celui avec les données de connexion, contiendra la liste des livres favoris que l'utilisateur voudra emprunter plus tard. Cette liste s'appelera "liste d'attente" et sera créée sans la nécessité de saisir des identifiants corrects. Commençons l'illustration par présenter le layout utilisé :

afficher le code

Le layout correspond parfaitement à ce qu'on avait annoncé au premier paragraphe. L'élément-conteneur s'appelle TabHost. Il contient TabWidget qui affiche les libellés des onglets et FrameLayout qui présente le contenu. Maintenant passons à la classe SubscriberFragmentActivity qui va s'occuper de générer les onglets depuis le code :

public class SubscriberFragmentActivity extends BaseFragmentActivity {

    private static final String LOG_TAG = "SubscriberFragmentActivity";
    private static final String TAB_ACCOUNT = "Account";  
    private static final String TAB_PREFERENCIES = "Preferencies";
    private static final String TAB_WAITING_LIST = "WaitingList";
    private String titleCredentials, titlePreferencies, titleWaitingList;
    private int choosedFrequency;
    private LinearLayout manualContainer;
    public Map preferencies;
    public SubscriberDataSource subscriberDataSource;
    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        try {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.my_account);
            subscriberDataSource = new SubscriberDataSource(this);
            Resources resources = getResources();
            titleCredentials = resources.getString(R.string.account_credentials_title);
            titlePreferencies = resources.getString(R.string.account_preferencies_title);
            titleWaitingList = resources.getString(R.string.account_waiting_list_title);
            initialiseTabHost(savedInstanceState); 
        }
        catch(Exception e) {
            Log.e(LOG_TAG, "An exception was catched", e);
        }
    }

    private void initialiseTabHost(Bundle savedInstanceState) {
        subscriber = subscriberDataSource.getSubscriber();
        Log.d(LOG_TAG, "Found subscriber on loading" + subscriber);
        tabHost = (TabHost)findViewById(android.R.id.tabhost);
        tabHost.setup();
        addTab(this, tabHost, tabHost.newTabSpec(TAB_ACCOUNT).setIndicator(titleCredentials), new TabInfo(TAB_ACCOUNT, SubscriberAccountFragment.class, savedInstanceState, this, ""));
        addTab(this, tabHost, tabHost.newTabSpec(TAB_WAITING_LIST).setIndicator(titleWaitingList), new TabInfo(TAB_WAITING_LIST, SubscriberWaitingListFragment.class, savedInstanceState, this, ""));
        if(subscriber != null) {
            showCredentialsDialog();
            submitCredentials();
        } else  { 
            onTabChanged(TAB_ACCOUNT);
        }
        // Default to first tab
        tabHost.setOnTabChangedListener(this);
    }
    // ...
}

Les onglets sont initialisés dans la méthode initialiseTabHost(). Elle récupère TabHost et appelle la méthode setup(). Elle doit être obligatoirement invoquée avant l'ajout des onglets. Ensuite on invoque les méthodes addTab(), définies dans la classe BaseFragmentActivity qui est la sous-classe du BaseActivity. On la verra plus loin. A la fin on enregistre le callback qui doit être appelée à chaque fois où l'utilisateur change l'onglet.

Notre classe d'activité ne contenait rien de surprenant. Les choses plus complexes, celles qui concernent notamment la création des onglets, sont définies dans BaseFragmentActivity. Comme on a evoqué, elle hérite de la classe BaseActivity. En plus de cela, elle implémente TabHost.OnTabChangeListener, grâce à quoi on peut la traiter en tant qu'écouter dans la situation où l'utilisateur clique sur un autre libellé des onglets. La BaseFragmentActivity contient la méthode rajoute un onglet, une autre qui réagit suite au changement de l'onglet et une classe privée qui s'occupe de créer le contenu de l'onglet. Regardons cela dans le code :

public class BaseFragmentActivity extends BaseActivity implements TabHost.OnTabChangeListener {

    private final static String LOG_TAG = "BaseFragmentActivity";
    protected TabHost tabHost;
    protected HashMap mapTabInfo = new HashMap();
    protected TabInfo lastTabOpened = null;
    public TabInfo lastTab = null;

    // rajoute un onglet
    protected void addTab(BaseFragmentActivity activity, TabHost tabHost, TabHost.TabSpec tabSpec, TabInfo tabInfo) {
        // Attach a Tab view factory to the spec
        tabSpec.setContent(activity.new TabFactory(activity));
        String tag = tabSpec.getTag();
        // Check to see if we already have a fragment for this tab, probably
        // from a previously saved state.  If so, deactivate it, because our
        // initial state is that a tab isn't shown.
        tabInfo.setFragment(activity.getSupportFragmentManager().findFragmentByTag(tag));
        if(tabInfo.getFragment() != null && !tabInfo.getFragment().isDetached()) {
            FragmentTransaction ft = activity.getSupportFragmentManager().beginTransaction();
            ft.detach(tabInfo.getFragment());
            ft.commit();
            activity.getSupportFragmentManager().executePendingTransactions();
        }
        tabHost.addTab(tabSpec);
        mapTabInfo.put(tabInfo.getTag(), tabInfo);
    }

    class TabFactory implements TabContentFactory {
        private final Context mContext;

        public TabFactory(Context context) {
            mContext = context;
        }
        public View createTabContent(String tag) {
            Log.d(LOG_TAG, "Creating tab content for tag" + tag);
            View view = new View(mContext);
            view.setMinimumWidth(0);
            view.setMinimumHeight(0);
            return view;
        }
    }

    @Override
    public void onTabChanged(String tag) { 
        TabInfo newTab = (TabInfo) this.mapTabInfo.get(tag);
        if(lastTab != newTab) {
            FragmentTransaction ft = this.getSupportFragmentManager().beginTransaction();
            if(lastTab != null) {
                if(lastTab.getFragment() != null) {
                    ft.detach(lastTab.getFragment());
                }
            }
            if(newTab != null) {
                if(newTab.getFragment() == null) {
                    newTab.setFragment(Fragment.instantiate(this,
                    newTab.getClss().getName(), newTab.getArgs()));
                    // add = ajout d'un Fragment dans l'état d'une activité
                    ft.add(R.id.realtabcontent, newTab.getFragment(), newTab.getTag());
                } else {
                    // attach = attachement d'un Fragment dans l'état d'une activité 
                    //          Un Fragment peut être attaché uniquement quand il a été préalablément détaché, ce qui est forcément
                    //          le cas avec ft.detach() de l'onglet récemment ouvert.
                    ft.attach(newTab.getFragment());
                }
            }
            lastTab = newTab;
            ft.commit(); // envoi des opérations sur FragmentTransactions
            // la méthode ci-dessous doit être appélée après ft.commit(). 
            // elle classe la transaction en étant celle qui peut être exécutée d'une manière asynchrone par le thread principal
            this.getSupportFragmentManager().executePendingTransactions();
        }
    }

}

Au début de la méthode addTab() on précise le contenu du TabHost.TabSpec. Ensuite, grâce à l'aide du FragmentManager on vérifie si le Fragment de notre JavaBeans TabInfo existe et s'il n'a pas été détaché de l'interface utilisateur. Un Fragment est un bout de vue ou d'action qui peut être placée dans une activité. Si c'est le cas, on est obligés de le désactiver car potentiellement il s'agit d'un état autre qu'initial (avec les modifications sur l'interface sauvegardés. A la fin on rajoute le contenu généré à TabHost.

Voici le fragment du Logger qui illustre le fonctionnement des onglets sous Android:

afficher le code

En ce qui concerne la classe privée TabFactory, elle implémente l'interface TabHost.TabContentFactory dont le rôle consiste à créer le contenu d'un onglet quand celui-ci est appelé.

La dernière méthode, onTabChanged(), est propre au TabHost.OnTabChangeListener. Elle définit la fonction qui doit être appelée quand l'utilisateur clique sur un autre onglet. On récupère d'abord le dernier contenu de l'onglet. S'il est différent que celui que l'utilisateur affiche, on le détache d'abord. Grâce à cela il est possible d'attacher un nouveau contenu plus loin (ft.attach()). A la fin on valide la transaction en invoquant commit().

Quel est le contenu attaché au FragmentLayout ? Pour le constater, il faut regarder la méthode addTab() appelée dans l'activité SubscriberFragmentActivity qui prend en paramètre TabInfo avec un Fragment correspondant. Ce Fragment est ensuite attaché au contenu de la vue. Voici une des 3 classes qui sont utilisées dans ce bout de code, SubscriberAccountFragment, qui contient le formulaire de connexion :

public class SubscriberAccountFragment extends Fragment {

    private static final String LOG_TAG = "SubscriberAccountFragment"; 
    private EditText login, password;

    @Override
    // Crée la vue associée au fragment
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        if (container == null) {
            return null;
        }
        Log.d(LOG_TAG, "Getting layout for SubscriberAccountFragment");
        LinearLayout layout = (LinearLayout)inflater.inflate(R.layout.my_account_form, container, false);

        login =  (EditText) layout.findViewById(R.id.myAccountLogin);
        login.setOnTouchListener(new EditTextTouchListener());
        password = (EditText) layout.findViewById(R.id.myAccountPassword);
        password.setOnTouchListener(new EditTextTouchListener());

        Subscriber subscriber = ((SubscriberFragmentActivity) getActivity()).subscriberDataSource.getSubscriber();
        if(subscriber != null) {
            login.setText(subscriber.getLogin());
            password.setText(subscriber.getPassword());
        }

        return layout;
    }

}

On ne remarque rien d'extraordinaire. Il suffit d'une création simple de layout dans le code.

Bartosz KONIECZNY Onglets

Une question ? Une remarque ?

*

*

Moi

Développeur d'applications Internet et journaliste passionné par l'adjectif français. Un aigle polonais orienté vers la progression, volant très haut et écoutant du zouk après les matches du foot français.

Vous appréciez mon travail ?

Pour contribuer au développement de ce site, ou pour remercier pour des articles rédigés, vous pouvez faire un don.

Un conseil Symfony2

Comment valider les checkboxes avec Symfony2 ?

La validation des checkboxes sous Symfony2 se déroule avec une contrainte appelée ChoiceConstraint.

Voici l'exemple de l'utilisation:

$metadata->addPropertyConstraint('orderPreferedGift', new Choice(array('choices' => Gifts::getGiftTypes(true), 'multiple' => true, 'min' => 1,  'multipleMessage' => "Veuillez choisir au moins un type de cadeau", 'groups' => array('validationGroup'))));


Cette contrainte est très puissante. On peut déterminer par exemple la quantité des champs minimale ou maximale à cocher par utilisateur. Il est également possible de vérifier le types des valeurs.