lundi 10 mai 2010

Créer des Popup Android personnalisées

L'API Android fournit avec la classe AlertDialog une facilité pour construire diverses fenêtres de Dialog en s'appuyant sur un Builder. Il n'est cependant anisi pas possible de personnaliser l'apparence de la fenêtre produite. Nous allons voir dans ce tutorial comment créer nous même notre propre générateur de fenêtres personnalisées. Vous trouverez à la fin de ce billet des exemples d'applications de l'Android Market ayant utilisé ce tutoriel pour réaliser leurs propres fenêtres personnalisées.

Définition de l'apparence

La première étape consiste à définir l'apparence que nous souhaitons donner aux fenêtres de dialogue personnalisées de notre application. Nous allons voir comment transformer l'apparence par défaut fournie par Android en l'apparence présentée ci dessous :



Notre fenêtre va fournir les fonctionnalités suivantes :
  1. Spécification du titre par ressource ou chaine de caractères
  2. Spécification du contenu par ressource, chaine de caractères ou layout personnalisé
  3. Utilisation de zéro, un ou deux boutons

Implémentation du thème et du layout

Notre Dialog personnalisé s'appuie sur un layout qui lui est propre et qui permet de positionner les différents éléments qui le composent. Des id sont déclarés afin de permettre la récupération programmatique des titre, boutons ainsi que du corp de la fenêtre :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:minWidth="280dip"
    android:layout_height="wrap_content">
 
    <LinearLayout 
        android:orientation="vertical"
        android:background="@drawable/header"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
 
        <TextView
            style="@style/DialogText.Title"
            android:id="@+id/title"
            android:paddingRight="8dip"
            android:paddingLeft="8dip"
            android:background="@drawable/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
 
    </LinearLayout>
 
    <LinearLayout 
        android:id="@+id/content"
        android:orientation="vertical"
        android:background="@drawable/center"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
 
        <TextView
            style="@style/DialogText"
            android:id="@+id/message"
            android:padding="5dip"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"/>
 
    </LinearLayout>
 
    <LinearLayout 
        android:orientation="horizontal"
        android:background="@drawable/footer"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content">
 
        <Button 
            android:id="@+id/positiveButton"
            android:layout_marginTop="3dip"
            android:layout_width="0dip"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:singleLine="true"/>
 
        <Button 
            android:id="@+id/negativeButton"
            android:layout_marginTop="3dip"
            android:layout_width="0dip"
            android:layout_weight="1"
            android:layout_height="wrap_content"
            android:singleLine="true"/>
 
    </LinearLayout>
 
</LinearLayout>
Le Layout de base utilisé est un LinearLayout fill_parent avec un minimum de 280dip pour que notre fenêtre personnalisée occupe toujours au moins 87,5% de la largeur de l'écran.

Un thème est également créé pour notre Dialog :
<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <style name="Dialog" parent="android:style/Theme.Dialog">
        <item name="android:windowBackground">@null</item>
        <item name="android:windowNoTitle">true</item>
        <item name="android:windowIsFloating">true</item>
    </style>
 
</resources>
Ainsi que pour l'apparence que nous souhaitons donner aux textes contenus dans notre fenêtre :
<?xml version="1.0" encoding="utf-8"?>
<resources>
 
    <style name="DialogText">
        <item name="android:textColor">#FF000000</item>
        <item name="android:textSize">12sp</item>
    </style>
 
    <style name="DialogText.Title">
        <item name="android:textSize">16sp</item>
        <item name="android:textStyle">bold</item>
    </style>
 
</resources>

Classe de génération de la fenêtre

Notre Builder personnalisé va reprendre les mêmes méthodes que celles définies par la classe AletDialog.Builder de l'API Android.
package net.androgames.blog.sample.customdialog.dialog;
 
import net.androgames.blog.sample.customdialog.R;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.LayoutParams;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
 
/**
 * 
 * Create custom Dialog windows for your application
 * Custom dialogs rely on custom layouts wich allow you to 
 * create and use your own look & feel.
 * 
 * Under GPL v3 : http://www.gnu.org/licenses/gpl-3.0.html
 * 
 * @author antoine vianey
 *
 */
public class CustomDialog extends Dialog {
 
    public CustomDialog(Context context, int theme) {
        super(context, theme);
    }
 
    public CustomDialog(Context context) {
        super(context);
    }
 
    /**
     * Helper class for creating a custom dialog
     */
    public static class Builder {
 
        private Context context;
        private String title;
        private String message;
        private String positiveButtonText;
        private String negativeButtonText;
        private View contentView;
 
        private DialogInterface.OnClickListener 
                        positiveButtonClickListener,
                        negativeButtonClickListener;
 
        public Builder(Context context) {
            this.context = context;
        }
 
        /**
         * Set the Dialog message from String
         * @param title
         * @return
         */
        public Builder setMessage(String message) {
            this.message = message;
            return this;
        }
 
        /**
         * Set the Dialog message from resource
         * @param title
         * @return
         */
        public Builder setMessage(int message) {
            this.message = (String) context.getText(message);
            return this;
        }
 
        /**
         * Set the Dialog title from resource
         * @param title
         * @return
         */
        public Builder setTitle(int title) {
            this.title = (String) context.getText(title);
            return this;
        }
 
        /**
         * Set the Dialog title from String
         * @param title
         * @return
         */
        public Builder setTitle(String title) {
            this.title = title;
            return this;
        }
 
        /**
         * Set a custom content view for the Dialog.
         * If a message is set, the contentView is not
         * added to the Dialog...
         * @param v
         * @return
         */
        public Builder setContentView(View v) {
            this.contentView = v;
            return this;
        }
 
        /**
         * Set the positive button resource and it's listener
         * @param positiveButtonText
         * @param listener
         * @return
         */
        public Builder setPositiveButton(int positiveButtonText,
                DialogInterface.OnClickListener listener) {
            this.positiveButtonText = (String) context
                    .getText(positiveButtonText);
            this.positiveButtonClickListener = listener;
            return this;
        }
 
        /**
         * Set the positive button text and it's listener
         * @param positiveButtonText
         * @param listener
         * @return
         */
        public Builder setPositiveButton(String positiveButtonText,
                DialogInterface.OnClickListener listener) {
            this.positiveButtonText = positiveButtonText;
            this.positiveButtonClickListener = listener;
            return this;
        }
 
        /**
         * Set the negative button resource and it's listener
         * @param negativeButtonText
         * @param listener
         * @return
         */
        public Builder setNegativeButton(int negativeButtonText,
                DialogInterface.OnClickListener listener) {
            this.negativeButtonText = (String) context
                    .getText(negativeButtonText);
            this.negativeButtonClickListener = listener;
            return this;
        }
 
        /**
         * Set the negative button text and it's listener
         * @param negativeButtonText
         * @param listener
         * @return
         */
        public Builder setNegativeButton(String negativeButtonText,
                DialogInterface.OnClickListener listener) {
            this.negativeButtonText = negativeButtonText;
            this.negativeButtonClickListener = listener;
            return this;
        }
 
        /**
         * Create the custom dialog
         */
        public CustomDialog create() {
            LayoutInflater inflater = (LayoutInflater) context
                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            // instantiate the dialog with the custom Theme
            final CustomDialog dialog = new CustomDialog(context, 
                    R.style.Dialog);
            View layout = inflater.inflate(R.layout.dialog, null);
            dialog.addContentView(layout, new LayoutParams(
                    LayoutParams.FILL_PARENT, LayoutParams.WRAP_CONTENT));
            // set the dialog title
            ((TextView) layout.findViewById(R.id.title)).setText(title);
            // set the confirm button
            if (positiveButtonText != null) {
                ((Button) layout.findViewById(R.id.positiveButton))
                        .setText(positiveButtonText);
                if (positiveButtonClickListener != null) {
                    ((Button) layout.findViewById(R.id.positiveButton))
                            .setOnClickListener(new View.OnClickListener() {
                                public void onClick(View v) {
                                    positiveButtonClickListener.onClick(
                                            dialog, 
                                            DialogInterface.BUTTON_POSITIVE);
                                }
                            });
                }
            } else {
                // if no confirm button just set the visibility to GONE
                layout.findViewById(R.id.positiveButton).setVisibility(
                        View.GONE);
            }
            // set the cancel button
            if (negativeButtonText != null) {
                ((Button) layout.findViewById(R.id.negativeButton))
                        .setText(negativeButtonText);
                if (negativeButtonClickListener != null) {
                    ((Button) layout.findViewById(R.id.negativeButton))
                            .setOnClickListener(new View.OnClickListener() {
                                public void onClick(View v) {
                                    positiveButtonClickListener.onClick(
                                            dialog, 
                                            DialogInterface.BUTTON_NEGATIVE);
                                }
                            });
                }
            } else {
                // if no confirm button just set the visibility to GONE
                layout.findViewById(R.id.negativeButton).setVisibility(
                        View.GONE);
            }
            // set the content message
            if (message != null) {
                ((TextView) layout.findViewById(
                        R.id.message)).setText(message);
            } else if (contentView != null) {
                // if no message set
                // add the contentView to the dialog body
                ((LinearLayout) layout.findViewById(R.id.content))
                        .removeAllViews();
                ((LinearLayout) layout.findViewById(R.id.content))
                        .addView(contentView, 
                                new LayoutParams(
                                        LayoutParams.WRAP_CONTENT, 
                                        LayoutParams.WRAP_CONTENT));
            }
            dialog.setContentView(layout);
            return dialog;
        }
 
    }
 
}

Création d'une fenêtre personnalisée

La création d'une fenêtre personnalisé s'effectue de la même manière qu'avec l'utilisation de la classe AletDialog.Builder :
/**
 * Build the desired Dialog
 * CUSTOM or DEFAULT
 */
@Override
public Dialog onCreateDialog(int dialogId) {
    Dialog dialog = null;
    switch (dialogId) {
        case CUSTOM_DIALOG :
            CustomDialog.Builder customBuilder = new
                CustomDialog.Builder(CustomDialogActivity.this);
            customBuilder.setTitle("Custom title")
                .setMessage("Custom body")
                .setNegativeButton("Cancel", 
                        new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        CustomDialogActivity.this
                        .dismissDialog(CUSTOM_DIALOG);
                    }
                })
                .setPositiveButton("Confirm", 
                        new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                });
            dialog = customBuilder.create();
            break;
        case DEFAULT_DIALOG :
            AlertDialog.Builder alertBuilder = new
                AlertDialog.Builder(CustomDialogActivity.this);
            alertBuilder.setTitle("Default title")
                .setMessage("Default body")
                .setNegativeButton("Cancel", 
                        new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }
                })
                .setPositiveButton("Confirm", 
                        new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int which) {
                        CustomDialogActivity.this
                        .dismissDialog(DEFAULT_DIALOG);
                    }
                });
            dialog = alertBuilder.create();
            break;
    }
    return dialog;
}
Vous pouvez récupérer le code source de ce tutorial à cette adresse SampleCustomDialog.

Comme toujours, n'hésitez à faire un retour si vous avez des améliorations à apporter ou si vous souhaitez partager vos créations.

Exemples...

Popup personnalisée pour le jeu Yams disponible sur le market
(réalisée à partir de ce tutoriel)
Fork me on GitHub