Migrer son ActionBar vers la ToolBar (Troisieme Partie) : Mise en place d'une ActionView

b2ap3_thumbnail_Android2ee-logo-BIG_blue-1024x375.png

Et bonjour tout le monde,

Alors pour continuer à visiter l'ActionBar... euh non, la ToolBar, je vous propose d'apprendre à afficher une vue dans votre ToolBar. L'exemple que j'utilise souvent pour ça est de mettre une zone de recherche, même si ce n'est pas très pertinent car la SearchWidget est un peu là pour ça.

Mais, ce n'est pas grave, je le fais quand même pour vous expliquer le comportement.

Tout d'abord, à quoi cela ressemble-t-il ?

Lorsque j'appuie sur l'icone (le MenuItem) "<" dans la ToolBar, celui-ci se déplie et affiche une vue avec une fléche <-, une EditText et une ImageButton qui montrant une loupe. Lorsque j'appuie sur le bouton loupe, cela replie la vue et récupère la valeur contenue dans l'EditText. Dans mon exemple, je l'affiche dans un Toast.  

La flèche "<-" est nativement ajoutée par le système, elle ne dépend pas de vous.

Dans les copies d'écrans ci-dessous, je vous montre le résultat sur un émulateur avec GingerBread. 

ActionView 0        ActionView 1
ActionView 2        ActionView 3

Ce principe est le principe de base de l'ActionView: Il remplace un icone de la ToolBar par un layout.

Mise en place

Le fichier de Menu

Commençons par examiner le fichier de menu (car c'est lui qui porte l'information principale):

res\menu\menu_action_view.xml

<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:actionbarcompat_mse="http://schemas.android.com/apk/res-auto" >
<item
android:id="@+id/menu_item_actionview"
actionbarcompat_mse:actionLayout="@layout/actionview_view"
actionbarcompat_mse:showAsAction="always|collapseActionView"
android:icon="@drawable/ic_action_provider_extends"
android:title="ActionView"/>
<item
android:id="@+id/action_one"
actionbarcompat_mse:showAsAction="always"
android:icon="@drawable/ic_action_one"
android:orderInCategory="0"
android:title="One"/>
</menu>

Comme d'habitude, je définis mon propre namespace xml pour que le système ne zappe pas purement et simplement les balises de la support library et je l'utilise pour ajouter deux propriétés à mon MenuItem menu_item_actionview qui sont:

  • actionLayout qui me permet de définir quel est le layout qui sera affiché à la place de l'item lorsque celui-ci sera déplié;
  • showAsAction qui me permet de définir le comportement de mon MenuItem, à savoir, toujours visible et au démarrage affiche l'item et non pas le layout.

Les fichiers de layout 

Le layout qui remplacera le MenuItem est un layout rien de plus normal. Il faut quand même faire attention à un truc important, ce n'est pas lui qui définit la taille de la ToolBar. C'est à dire que s'il doit s'afficher avec 128dp de haut, il vous faudra changer la hauteur de la ToolBar vous-même lors de l'inflation du menu (quand non affichera la layout) dans le code Java (n'oubliez pas de rétablir cette taille lors du collapse du layout).

res\layout\actionview_view.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:focusable="true">
<EditText
android:id="@+id/edtActionView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ems="12"
android:hint="I am an EditText but I could be whatever"
android:textSize="12sp" />

<ImageButton
android:id="@+id/btnActionView"
android:layout_width="32dip"
android:layout_height="32dip"
android:layout_gravity="center"
android:adjustViewBounds="true"
android:background="@drawable/ic_action_search"
android:scaleType="fitCenter" />
</LinearLayout>

Le fichier de layout de l'activité est toujours le même, en particulier, il n'oublie pas de déclarer la ToolBar :

res\layout\activity_action_view.xml

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.android2ee.formation.lollipop.toolbar.sample.ActionViewActivity">

<include layout="@layout/my_toolbar"/>

<TextView android:text="@string/hello_world"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />

</RelativeLayout>

res\layout\my_toolbar.xml

<android.support.v7.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/toolbar"
android:minHeight="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.v7.widget.Toolbar>

Le code Java

Le code Java de l'activité est assez simple. Il y a trois étapes aux quelles il faut faire attention :

  • instancier la ToolBar dans le onCreate
  • récupérer les pointeurs vers les composants graphiques qui seront affichés dans la ToolBar quand l'ActionView s'affiche (pour les mettre à jour, rajouter des listeners...)
  • écouter dans le onOptionItemSelected, le menuItem de votre ActionView et le laisser être traité par le système.

 Ainsi, comme d'habitude, dans la méthode onCreate, on instancie la ToolBar:

protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_action_view);
//find the toolbar
toolbar = (Toolbar) findViewById(R.id.toolbar);
postICS =getResources().getBoolean(R.bool.postICS);
postLollipop =getResources().getBoolean(R.bool.postLollipop);
if(postLollipop){
toolbar.setElevation(15);
}
//define the toolbar as the ActionBar
setSupportActionBar(toolbar);
actionBar=getSupportActionBar();
}

 Ensuite dans la onCreateOptionsMenu, on instancie le menu et on met en place l'actionView, en particulier on récupère les pointeurs vers les composants graphiques qui nous intéresse, on leur rajoute des listeners au besoin :

public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_action_view, menu);
//Find your menuItem that handles your actionView
menuItemActionView = menu.findItem(R.id.menu_item_actionview);
//Find the root layout encapsulated by the MenuItem
lilActionView = (LinearLayout) MenuItemCompat.getActionView(menuItemActionView);
//then find your graphical elements
edtActionView = (EditText) lilActionView.findViewById(R.id.edtActionView);
btnActionView = (ImageButton) lilActionView.findViewById(R.id.btnActionView);
btnActionView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
actionOfTheActionView();
}
});

Où la méthode actionOfTheActionView est assez triviale, elle affiche un Toast et ferme l'ActionView:

/**
* Handling Action from the btnActionView contained by the ActionView
*/
private void actionOfTheActionView() {
Log.e("ActionViewActivity", "ActionView edt " + edtActionView.getText().toString());
Toast.makeText(this, "ActionView edt = " + edtActionView.getText().toString(), Toast.LENGTH_SHORT).show();
MenuItemCompat.collapseActionView(menuItemActionView);
 //what ever is the version, hide the keyboard:
 InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
  imm.hideSoftInputFromWindow(edtActionView.getWindowToken(), 0);
}

Et enfin, dans la méthode onOptionItemSelected, on ne gère pas le MenuItem qui porte l'ActionView (mais on gère le up, ce qui n'a rien à voir avec notre exemple):

public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// This ID represents the Home or Up button. In the case of this
// activity, the Up button is shown. Use NavUtils to allow users
// to navigate up one level in the application structure. For
// more details, see the Navigation pattern on Android Design:
//
// http://developer.android.com/design/patterns/navigation.html#up-vs-back
//
NavUtils.navigateUpFromSameTask(this);
return true;
case R.id.menu_item_actionview:
// Because the system expands the action view when the user selects the action, you do
// not need to respond to the item in the onOptionsItemSelected() callback. The system
// still calls onOptionsItemSelected(), but if you return true (indicating you've
// handled the event instead), then the action view will not expand.
Log.e("ActionViewActivity", "menu_item_actionview get");
return false;
}
return super.onOptionsItemSelected(item);
}

Et voilà, c'est tout, en fait, c'était pas compliqué.

 Ah oui, j'oubliais, dans les tutos, les gars qui oublient de déclarer leur attributs de classes, c'est super chi**t, tu sais jamais ce qu'il a déclaré (je vous mets aussi les imports qui utilise la support-library, pas les autres):

import android.support.v4.app.NavUtils;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.app.ActionBar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
public class ActionViewActivity extends AppCompatActivity {
/**
* The action Bar
*/
private ActionBar actionBar;
/**
* The ToolBar
*/
private Toolbar toolbar;
/**
* The menuItem that handles the ActionView
*/
MenuItem menuItemActionView;
/**
* The root Layout of the View of the ActionView
*/
LinearLayout lilActionView;
/**
* The EditText to listen for the user input
*/
EditText edtActionView;
/**
* The searchButton
*/
ImageButton btnActionView;
/***
* Boolean to know which version is running
*/
private boolean postICS,postLollipop;

 Ajouter des animations

 J'avais envie d'ajouter quelques animations quand l'ActionView s'affiche pour les version post HoneyComb. J'aurais pu le faire pour les versions préHoneyComb, mais quand je l'ai fait, j'ai trouvé ça trop laid. Parfois, il faut savoir s'abstenir.

Donc, pour ça, la première chose à faire est de savoir sur quelle version on se trouve. Et comme j'aime pas le BuildConfig et que je préferre les booléans, je me fais mon petit fichier de versions.

values\versions.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="postICS">false</bool>
<bool name="postLollipop">false</bool>
</resources>

values-v14\versions.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="postICS">true</bool>
<bool name="postLollipop">false</bool>
</resources>

 values-v21\versions.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
<bool name="postICS">true</bool>
<bool name="postLollipop">true</bool>
</resources>

 Et donc dans mon code Java, je fais :

postICS =getResources().getBoolean(R.bool.postICS);
postLollipop =getResources().getBoolean(R.bool.postLollipop);

Et voilà, j'ai mes beaux booleans. Je n'ai pas inventé cette technique, j'ai regardé la conférence suivante: https://www.youtube.com/watch?v=amZM8oZBgfk (GoogleIo 2012, MultiVersionning par Bruno Oliveira, Adam Powell). [Alors surtout dans cette conférence ils vous disent d'utiliser le parrallel activity pattern, NE LE FAITES PAS, avec les fragments, utilisez toujours la support library si vous codez pour Gingerbread. Pas de duplication de code, surtout pas de duplication de code.]

Et maintenant, à moi les animations. Pour ce faire,je souhaite les lancer : 

  • Quand l'ActionView s'affiche
  • Quand j'appuie sur l'ImageButton search (la loupe)

Ainsi, je rajoute un listener pour écouter l'ouverture de l'ActionView au moment de la création du menu:

@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_action_view, menu);
//Find your menuItem that handles your actionView
menuItemActionView = menu.findItem(R.id.menu_item_actionview);
//Find the root layout encapsulated by the MenuItem
lilActionView = (LinearLayout) MenuItemCompat.getActionView(menuItemActionView);
//then find your graphical elements
edtActionView = (EditText) lilActionView.findViewById(R.id.edtActionView);
btnActionView = (ImageButton) lilActionView.findViewById(R.id.btnActionView);
btnActionView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
actionOfTheActionView();
}
});
// When using the support library, the setOnActionExpandListener() method is
// static and accepts the MenuItem object as an argument
MenuItemCompat.setOnActionExpandListener(menuItemActionView, new MenuItemCompat.OnActionExpandListener() {
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
// Do something when collapsed, it's too late for animation
Log.e("ActionViewActivity", "menu_item_actionview collapsing");
return true; // Return true to collapse action view
}
@SuppressLint("NewApi")
@Override
public boolean onMenuItemActionExpand(MenuItem item) {
//the first time, elements are not expanded, It's too soon to have their size
if(postICS) {
btnActionView.animate().rotationBy(360f).alpha(1f).setDuration(300);
edtActionView.animate().rotationBy(360f).alpha(1f).y(0).setDuration(300);
}
// Do something when expanded
return true; // Return true to expand action view
}
});
return super.onCreateOptionsMenu(menu);
}

Et quand l'ActionView est affichée, j'anime mon ImageButton de loupe et mon EditText en les faisant tourner et bouger.

L'autre lancement s'effectue sur le clic de l'ImageButton:

/**
* Handling Action from the btnActionView contained by the ActionView
*/@SuppressLint("NewApi")
private void actionOfTheActionView() {
Log.e("ActionViewActivity", "ActionView edt " + edtActionView.getText().toString());
Toast.makeText(this, "ActionView edt = " + edtActionView.getText().toString(), Toast.LENGTH_SHORT).show();
collapsing=true;
if(postICS) {
//this is called when the user press the search icon, so the
edtActionView.animate().alpha(0).rotationBy(-360f).setDuration(300);
btnActionView.animate().alpha(0).rotationBy(-360f).setDuration(300).setListener(new Animator.AnimatorListener() {
//the listener is set for all the animations (further animations)
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
if (collapsing) {
MenuItemCompat.collapseActionView(menuItemActionView);
collapsing = false;
}
}
@Override
public void onAnimationCancel(Animator animation) {
if (collapsing) {
MenuItemCompat.collapseActionView(menuItemActionView);
collapsing = false;
}
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
}else{
MenuItemCompat.collapseActionView(menuItemActionView);
collapsing = false;
}
//what ever is the version, hide the keyboard:
InputMethodManager imm = (InputMethodManager)getSystemService(
Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(edtActionView.getWindowToken(), 0);
}

 Et là, je fais pareil, j'anime mon ImageButton et mon EditText. Mais surtout, ce qui est important ici, c'est que j'écoute l'animation pour quand elle ese termine, mettre à jour mon IHM, à savoir fermer l'ActionView.

Ce qui complexifie un peu quand même ma méthode, mais est nettement plus agréable pour mon utilisateur.

 Conclusion

Je finis ces blogs, concernant la ToolBar, comme toujours, en remerciant Chris Banes (@chrisbanes) car  c'est à lui que l'on doit cette simplification et surtout cette libération de l'ActionBar,

so Thanks a billion Mr Banes.

Le tutorial ?

Bien sûr, il est fourni, il est sur GitHub, ici : https://github.com/MathiasSeguy-Android2EE/ToolBar4Github (et si vous souhaitez lui ajouter des étoiles à ce projet, je suis à fond, cela me fera plaisir, je l'avoue).

Et n'hésitez pas à l'utiliser, ce billet couvre 20% du tutorial que je vous donne. D'autres viendront pour vous expliquer comment modifier la ToolBar, mettre l'ActionView, le SearchWidget, tout ça tout ça et bien plus encore (la TabNavigation avec la DesignLibrary par exemple:).

Les prochaines formations Android d’Android2EE 
Paris:
Et du 29 Juin au 3 Juillet 2015 à Paris, Formation complète.

Et pour le reste, je reviens en septembre, sur Paris, Toulouse, Lyon 

En septembre, je vais aussi ouvrir de nouvelles formations. En particulier la formation "Ultimate Android" avec Architecture, Librairie, Lollipop et M preview" sur 5 jours... ahaha, j'ai trop hâte :)

A bientôt.

Mathias Séguy
Cette adresse e-mail est protégée contre les robots spammeurs. Vous devez activer le JavaScript pour la visualiser.

Fondateur Android2EE
Formation – Expertise – Consulting Android.
Ebooks pour apprendre la programmation sous Android.
AndroidEvolution

Suivez moi sur Twitter
Rejoignez mon réseau LinkedIn 

Related Posts