MaterialViewPager – Créer facilement une application au look Android Material !

Vous souhaitez embellir votre application en lui donnant un look Material Design ? et bien ce tuto est fait pour vous !

Je vais vous montrer dans ce tutorial comment reproduire l’ergonomie de l’application Google Play Kiosque

kiosque_small

Vous allez voir, ce n’est pas si compliqué que ça n’en a l’air 😉

MaterialViewPager

Notre principal outil pour ce tutorial sera MaterialViewPager, une librairie open-source dont je suis l’auteur, qui est maintenant soutenue par la communauté Github :

https://github.com/florent37/MaterialViewPager

screenshot_2_small

Définir nos vues

1. Importer

Commençons par importer MaterialViewPager dans notre projet

build.gradle

compile 'com.android.support:appcompat-v7:22.0.0'
compile ('com.github.florent37:materialviewpager:1.0.3@aar'){
    transitive = true
}

2. Ajouter

Ajoutons une vue MaterialViewPager à notre activity
layout/activity_main.xml

<com.github.florent37.materialviewpager.MaterialViewPager
    android:id="@+id/materialViewPager"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:viewpager_logo="@layout/header_logo"
    app:viewpager_logoMarginTop="100dp"
    app:viewpager_color="@color/colorPrimary"
    app:viewpager_headerHeight="200dp"
    app:viewpager_headerAlpha="1.0"
    app:viewpager_hideLogoWithFade="false"
    app:viewpager_hideToolbarAndTitle="true"
    app:viewpager_enableToolbarElevation="true"
    app:viewpager_parallaxHeaderFactor="1.5"
    app:viewpager_headerAdditionalHeight="20dp"
    />

Nous voyons que l’objet MaterialViewPager possède une multitude d’attributs, essayons de jouer avec afin de reproduire l’application Kiosque !   Ajoutons un peu de transparence à notre image d’entête

    app:viewpager_headerAlpha="0.6"

Essayons de faire disparaitre la toolbar lors du scroll

    app:viewpager_hideToolbarAndTitle="true"

Utilisons les onglets du newstand (la page courante est affichée au centre)

    app:viewpager_pagerTitleStrip="@layout/material_view_pager_pagertitlestrip_newstand"

Remplaçons le titre qui se range dans la toolbar par un logo personnalisé

    app:viewpager_hideLogoWithFade="true"

Entête

Avec comme vue d’entête un cercle contenant une image : layout/header_logo.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/headerLogo"
    android:layout_width="80dp"
    android:layout_height="80dp"
    android:background="@drawable/circle">
    <ImageView
        android:id="@+id/headerLogoContent"
        android:layout_width="40dp"
        android:layout_height="40dp"
        tools:src="@drawable/tennis"
        android:layout_gravity="center"/>
</FrameLayout>

drawable/circle.xml

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
    <solid android:color="@color/colorPrimary"/>
</shape>

header_logo

Pensez à récupérer les images à l’adresse suivante : Lien vers les images

Style

N’oublions pas d’affecter un thème material à notre projet, en modifiant fichier style.xml values/styles.xml

  • drawerArrowStyle & colorControlNormal : couleur de la flèche de retour et du burger menu
  • colorPrimary / colorPrimaryDark / colorAccent : les 3 couleurs à définir pour avoir un thème matérial
  • windowActionBar=false & windowNoTitle=true : permet d’utiliser la toolbar en tant que vue
<resources xmlns:tools="http://schemas.android.com/tools">

    <style name="AppBaseTheme" parent="@style/Theme.AppCompat.Light">
    </style>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="AppBaseTheme">

        <item name="android:textColorPrimary">@android:color/white</item>
        <item name="drawerArrowStyle">@style/DrawerArrowStyle</item>
        <item name="android:windowTranslucentStatus" tools:targetApi="21">true</item>

        <item name="android:windowContentOverlay">@null</item>
        <item name="windowActionBar">false</item>

        <!-- Toolbar Theme / Apply white arrow -->
        <item name="colorControlNormal">@android:color/white</item>
        <item name="actionBarTheme">@style/AppTheme.ActionBarTheme</item>

        <!-- Material Theme -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/accent_color</item>

        <item name="android:statusBarColor" tools:targetApi="21">@color/statusBarColor</item>
        <item name="android:navigationBarColor" tools:targetApi="21">@color/navigationBarColor</item>
        <item name="android:windowDrawsSystemBarBackgrounds" tools:targetApi="21">true</item>

    </style>

    <style name="AppTheme.ActionBarTheme" parent="@style/ThemeOverlay.AppCompat.ActionBar">
        <!-- White arrow -->
        <item name="colorControlNormal">@android:color/white</item>
    </style>

    <style name="DrawerArrowStyle" parent="Widget.AppCompat.DrawerArrowToggle">
        <item name="spinBars">true</item>
        <item name="color">@color/drawerArrowColor</item>
    </style>

</resources>

Avec comme fichier de couleurs values/colors.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="blue">#303F9F</color>
    <color name="green">#4CAF50</color>
    <color name="purple">#673AB7</color>
    <color name="cyan">#00BCD4</color>
    <color name="orange">#e95609</color>

    <color name="accent_color">@color/blue</color>
    <color name="colorPrimary">@color/blue</color>
    <color name="colorPrimaryDark">@color/blue</color>
    <color name="statusBarColor">@color/blue</color>
    <color name="navigationBarColor">@android:color/black</color>
    <color name="drawerArrowColor">@android:color/white</color>
</resources>

Resumé

    <com.github.florent37.materialviewpager.MaterialViewPager
        android:id="@+id/materialViewPager"
        app:viewpager_color="@color/colorPrimary"
        app:viewpager_enableToolbarElevation="true"
        app:viewpager_headerAlpha="0.6"
        app:viewpager_headerHeight="200dp"
        app:viewpager_hideLogoWithFade="true"
        app:viewpager_hideToolbarAndTitle="true"
        app:viewpager_logo="@layout/header_logo"
        app:viewpager_pagerTitleStrip="@layout/material_view_pager_pagertitlestrip_newstand"
        app:viewpager_logoMarginTop="80dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

Vous devriez avoir comme aperçu l’écran suivantCapture d’écran 2015-05-11 à 16.03.23

RecyclerView & CardView

Chaque page contiendra une RecyclerView, permettant d’afficher une liste de vue, il faut donc les définir dans un layout layout/fragment_recyclerview.xml

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

Et créer un layout CardView, qui sera utilisé comme contenu de la RecyclerView : layout/list_item_card.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <android.support.v7.widget.CardView
        android:id="@+id/card_view"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        app:cardCornerRadius="2dp"
        app:cardElevation="2dp"
        android:layout_marginBottom="@dimen/cardMarginVertical"
        android:layout_marginLeft="@dimen/cardMarginHorizontal"
        android:layout_marginRight="@dimen/cardMarginHorizontal"
        android:layout_marginTop="@dimen/cardMarginVertical"
        app:cardPreventCornerOverlap="false"
        app:contentPadding="0dp"/>
</FrameLayout>

cardview

Pour ce tuto, nous laisserons cette Cards vide afin de nous concentrer uniquement sur l’affichage des pages.

Implémentation

1. Fragment

Nous allons commencer par créer nos pages, dans notre cas ce sera une page contenant une RecyclerView, affichant une liste de CardView RecyclerViewFragment.java

public class RecyclerViewFragment extends Fragment {

    private RecyclerView mRecyclerView;
    private RecyclerView.Adapter mAdapter;

    public static RecyclerViewFragment newInstance() {
        return new RecyclerViewFragment();
    }

    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_recyclerview, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerView);

        //permet un affichage sous forme liste verticale
        RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(getActivity());
        mRecyclerView.setLayoutManager(layoutManager);
        mRecyclerView.setHasFixedSize(true);

        //100 faux contenu
        List<Object> mContentItems = new ArrayList<>();
        for (int i = 0; i < 100; ++i)
            mContentItems.add(new Object());

        //penser à passer notre Adapter (ici : TestRecyclerViewAdapter) à un RecyclerViewMaterialAdapter
        mAdapter = new RecyclerViewMaterialAdapter(new TestRecyclerViewAdapter(mContentItems));
        mRecyclerView.setAdapter(mAdapter);

        //notifier le MaterialViewPager qu'on va utiliser une RecyclerView
        MaterialViewPagerHelper.registerRecyclerView(getActivity(), mRecyclerView, null);
    }
}

Comme pour les ListView, nous avons besoin ici d’un Adapter, plus précisément un RecyclerView.Adapter

Contrairement aux ArrayAdapter, ceux-ci se décomposent en deux méthodes

  • onCreateViewHolder : qui va créer les vues et leur attacher un ViewHolder
  • onBindViewHolder : qui va remplir la cellule via le ViewHolder

Ici j’utiliserai des simples RecyclerView.ViewHolder parce que mes cellules sont vides, ce qui explique aussi le fait que la méthode onBindViewHolder() soit vide.

TestRecyclerViewAdapter.java

public class TestRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    List<Object> contents;

    public TestRecyclerViewAdapter(List<Object> contents) {
        this.contents = contents;
    }

    @Override
    public int getItemCount() {
        return contents.size();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.list_item_card, parent, false);
        return new RecyclerView.ViewHolder(view) {
        };
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    }
}

2. Remplir notre ViewPager

Il nous faut récupérer notre MaterialViewPager, comme toute autre vue avec un findViewById, ainsi que les vues que nous avons définit dans notre headerLogo. Puis Remplir notre ViewPager grâce à un FragmentStatePagerAdapter

public class MainActivity extends ActionBarActivity {

    MaterialViewPager materialViewPager;
    View headerLogo;
    ImageView headerLogoContent;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //4 onglets
        final int tabCount = 4;

        //les vues définies dans @layout/header_logo
        headerLogo = findViewById(R.id.headerLogo);
        headerLogoContent = (ImageView) findViewById(R.id.headerLogoContent);

        //le MaterialViewPager
        this.materialViewPager = (MaterialViewPager) findViewById(R.id.materialViewPager);

        //remplir le ViewPager
        this.materialViewPager.getViewPager().setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()) {

            @Override
            public Fragment getItem(int position) {
                //je créé pour chaque onglet un RecyclerViewFragment
                return RecyclerViewFragment.newInstance();
            }

            @Override
            public int getCount() {
                return tabCount;
            }

            //le titre à afficher pour chaque page
            @Override
            public CharSequence getPageTitle(int position) {
                switch (position) {
                    case 0:
                        return getResources().getString(R.string.divertissement);
                    case 1:
                        return getResources().getString(R.string.sports);
                    case 2:
                        return getResources().getString(R.string.technologie);
                    case 3:
                        return getResources().getString(R.string.international);
                    default:
                        return "Page " + position;
                }
            }
        });

        //permet au viewPager de garder 4 pages en mémoire (à ne pas utiliser sur plus de 4 pages !)
        this.materialViewPager.getViewPager().setOffscreenPageLimit(tabCount);
        //relie les tabs au viewpager
        this.materialViewPager.getPagerTitleStrip().setViewPager(this.materialViewPager.getViewPager());
    }
}

Entête dynamique

A cette étape il nous manque plus que modifier le contenu de l’entête en fonction de la page sélectionnée Pour cela il faut ajouter à notre FragmentStatePagerAdapter la méthode setPrimaryItem(), qui sera appelée à chaque changement de page

int oldItemPosition = -1;

@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);

//seulement si la page est différente
if (oldItemPosition != position) {
    oldItemPosition = position;

    //définir la nouvelle couleur et les nouvelles images
    String imageUrl = null;
    int color = Color.BLACK;
    Drawable newDrawable = null;

    switch (position) {
        case 0:
            imageUrl = "http://www.skyscanner.fr/sites/default/files/image_import/fr/micro.jpg";
            color = getResources().getColor(R.color.purple);
            newDrawable = getResources().getDrawable(R.drawable.ticket);
            break;
        case 1:
            imageUrl = "http://www.larousse.fr/encyclopedie/data/images/1311904-Balle_de_tennis_et_filet.jpg";
            color = getResources().getColor(R.color.orange);
            newDrawable = getResources().getDrawable(R.drawable.tennis);
            break;
        case 2:
            imageUrl = "http://soocurious.com/fr/wp-content/uploads/2014/03/8-facettes-de-notre-cerveau-qui-ont-evolue-avec-la-technologie8.jpg";
            color = getResources().getColor(R.color.cyan);
            newDrawable = getResources().getDrawable(R.drawable.light);
            break;
        case 3:
            imageUrl = "http://graduate.carleton.ca/wp-content/uploads/prog-banner-masters-international-affairs-juris-doctor.jpg";
            color = getResources().getColor(R.color.green);
            newDrawable = getResources().getDrawable(R.drawable.earth);
            break;
    }

    //puis modifier les images/couleurs
    int fadeDuration = 400;
    materialViewPager.setColor(color, fadeDuration);
    materialViewPager.setImageUrl(imageUrl, fadeDuration);
    toggleLogo(newDrawable,color,fadeDuration);
}

N’oubliez pas d’ajouter la permission INTERNET dans votre manifest

<uses-permission android:name="android.permission.INTERNET"/>

Ajouter toggleLogo(), permettant de modifier notre logo contenu dans l’entête, avec une animation d’ouverture/fermeture

private void toggleLogo(final Drawable newLogo, final int newColor, int duration){

    //animation de disparition
    final AnimatorSet animatorSetDisappear = new AnimatorSet();
    animatorSetDisappear.setDuration(duration);
    animatorSetDisappear.playTogether(
            ObjectAnimator.ofFloat(headerLogo, "scaleX", 0),
            ObjectAnimator.ofFloat(headerLogo, "scaleY", 0)
    );

    //animation d'apparition
    final AnimatorSet animatorSetAppear = new AnimatorSet();
    animatorSetAppear.setDuration(duration);
    animatorSetAppear.playTogether(
            ObjectAnimator.ofFloat(headerLogo, "scaleX", 1),
            ObjectAnimator.ofFloat(headerLogo, "scaleY", 1)
    );

    //après la disparition
    animatorSetDisappear.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);

            //modifie la couleur du cercle
            ((GradientDrawable) headerLogo.getBackground()).setColor(newColor);

            //modifie l'image contenue dans le cercle
            headerLogoContent.setImageDrawable(newLogo);

            //démarre l'animation d'apparition
            animatorSetAppear.start();
        }
    });

    //démarre l'animation de disparition
    animatorSetDisappear.start();
}

Et voila ! je vous avais dit que ce serait simple 😀

v2

 

Je vous offre même un petit aperçu vidéo afin de vous rendre compte du résultat :

 

Vous pourrez retrouver les sources de ce projet à l’adresse suivante : Sources

Si vous avez aimé ce tuto, n’hésitez pas à ajouter www.tutos-android-france.com à vos favoris,

et soutenez le projet sur github en ajoutant une étoile 🙂 https://github.com/florent37/MaterialViewPager

8 commentaire sur “MaterialViewPager – Créer facilement une application au look Android Material !

  1. Hello, j’ai une problème avec le setPrimaryItem() voici mon MainActivity ;
    public class MainActivity extends ActionBarActivity {

    MaterialViewPager materialViewPager;
    View headerLogo;
    ImageView headerLogoContent;
    int oldItemPosition = -1;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    //4 onglets
    final int tabCount = 4;

    //les vues définies dans @layout/header_logo
    headerLogo = findViewById(R.id.headerLogo);
    headerLogoContent = (ImageView) findViewById(R.id.headerLogoContent);

    //le MaterialViewPager
    this.materialViewPager = (MaterialViewPager) findViewById(R.id.materialViewPager);

    //remplir le ViewPager
    this.materialViewPager.getViewPager().setAdapter(new FragmentStatePagerAdapter(setPrimaryItem()) {

    @Override
    public Fragment getItem(int position) {
    //je créé pour chaque onglet un RecyclerViewFragment
    return RecyclerViewFragment.newInstance();
    }

    @Override
    public int getCount() {
    return tabCount;
    }

    //le titre à afficher pour chaque page
    @Override
    public CharSequence getPageTitle(int position) {
    switch (position) {
    case 0:
    return getResources().getString(R.string.divertissement);
    case 1:
    return getResources().getString(R.string.sports);
    case 2:
    return getResources().getString(R.string.technologie);
    case 3:
    return getResources().getString(R.string.international);
    default:
    return “Page ” + position;
    }
    }
    });

    //permet au viewPager de garder 4 pages en mémoire (à ne pas utiliser sur plus de 4 pages !)
    this.materialViewPager.getViewPager().setOffscreenPageLimit(tabCount);
    //relie les tabs au viewpager
    this.materialViewPager.getPagerTitleStrip().setViewPager(this.materialViewPager.getViewPager());
    }

    int oldItemPosition = -1;

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
    super.setPrimaryItem(container, position, object); //ici est l’erreur

    //seulement si la page est différente
    if (oldItemPosition != position) {
    oldItemPosition = position;

    //définir la nouvelle couleur et les nouvelles images
    String imageUrl = null;
    int color = Color.BLACK;
    Drawable newDrawable = null;

    switch (position) {
    case 0:
    imageUrl = “http://www.skyscanner.fr/sites/default/files/image_import/fr/micro.jpg”;
    color = getResources().getColor(R.color.purple);
    newDrawable = getResources().getDrawable(R.drawable.ticket);
    break;
    case 1:
    imageUrl = “http://www.larousse.fr/encyclopedie/data/images/1311904-Balle_de_tennis_et_filet.jpg”;
    color = getResources().getColor(R.color.orange);
    newDrawable = getResources().getDrawable(R.drawable.tennis);
    break;
    case 2:
    imageUrl = “http://soocurious.com/fr/wp-content/uploads/2014/03/8-facettes-de-notre-cerveau-qui-ont-evolue-avec-la-technologie8.jpg”;
    color = getResources().getColor(R.color.cyan);
    newDrawable = getResources().getDrawable(R.drawable.light);
    break;
    case 3:
    imageUrl = “http://graduate.carleton.ca/wp-content/uploads/prog-banner-masters-international-affairs-juris-doctor.jpg”;
    color = getResources().getColor(R.color.green);
    newDrawable = getResources().getDrawable(R.drawable.earth);
    break;
    }

    //puis modifier les images/couleurs
    int fadeDuration = 400;
    materialViewPager.setColor(color, fadeDuration);
    materialViewPager.setImageUrl(imageUrl, fadeDuration);
    toggleLogo(newDrawable, color, fadeDuration);
    }

    }

    private void toggleLogo(final Drawable newLogo, final int newColor, int duration) {

    //animation de disparition
    final AnimatorSet animatorSetDisappear = new AnimatorSet();
    animatorSetDisappear.setDuration(duration);
    animatorSetDisappear.playTogether(
    ObjectAnimator.ofFloat(headerLogo, “scaleX”, 0),
    ObjectAnimator.ofFloat(headerLogo, “scaleY”, 0)
    );

    //animation d’apparition
    final AnimatorSet animatorSetAppear = new AnimatorSet();
    animatorSetAppear.setDuration(duration);
    animatorSetAppear.playTogether(
    ObjectAnimator.ofFloat(headerLogo, “scaleX”, 1),
    ObjectAnimator.ofFloat(headerLogo, “scaleY”, 1)
    );

    //après la disparition
    animatorSetDisappear.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationEnd(Animator animation) {
    super.onAnimationEnd(animation);

    //modifie la couleur du cercle
    ((GradientDrawable) headerLogo.getBackground()).setColor(newColor);

    //modifie l’image contenue dans le cercle
    headerLogoContent.setImageDrawable(newLogo);

    //démarre l’animation d’apparition
    animatorSetAppear.start();
    }
    });

    //démarre l’animation de disparition
    animatorSetDisappear.start();
    }
    }

  2. c’est plutot

    this.materialViewPager.getViewPager().setAdapter(new FragmentStatePagerAdapter(getSupportFragmentManager()){
    public void setPrimaryItem() {

    }

  3. Hello, j’ai une problème avec le mRecyclerView.setLayoutManager(new GridLayoutManager(getActivity(),2));

    lorsque je veut des card view non plus en linear mais en gridlayout il me place un cardview dans l’entete du view pager .. une solution ?

    merci d’avance

  4. Salut Florent, merci pour ce tutoriel!
    Mais j’ai un probleme, quand j’essaie d’utiliser cette library et n’importe quel autre library dans mon projet, si je mets
    compile (‘com.github.florent37:materialviewpager:1.0.3@aar’){
    transitive = true
    }
    Quand je mets Run, ca mets : ” Process ‘command ‘C:\Program Files\Java\jdk1.7.0_25\bin\java.exe” finished with non-zero exit value 2 ”
    mais si j’inclue :
    {
    exclude module: ‘support-v4’
    }
    comme ca :
    compile (‘com.github.florent37:materialviewpager:1.0.3@aar’){
    transitive = true
    exclude module: ‘support-v4’
    }
    L’erreur disparait mais aprés, quand je lance, l’application va arreter de fonctionner et signale : android.view.InflateException: Binary XML file line #44: Error inflating class android.support.v7.widget.Toolbar

    J’essaie de resoudre ce probleme depuis 5 jours.
    Merci

Laisser un commentaire

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