Ce guide détaille la création d'un menu radial interactfi, similaire au système "PATH", en utilisant le développement Android natif. L'objectif est de proposer une expérience utilisateur dynamique avec des animations fluides pour l'apparition et la disparition des options du menu.
Structure de la mise en page (XML)
La disposition principale organise les éléments interactifs du menu. Un conteneur principal englobe les boutons d'action du menu et un bouton central qui contrôle l'affichage et la dissimulation de ces actions.
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/default_homebg">
<!-- Conteneur pour les boutons d'action du menu -->
<RelativeLayout
android:id="@+id/action_buttons_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:clipChildren="false"
android:clipToPadding="false" >
<!-- Boutons d'action individuels (ex: Photo, Personnes, Lieu) -->
<ImageButton
android:id="@+id/action_button_photo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="142dp"
android:layout_marginEnd="10.667dp"
android:background="@drawable/composer_camera"
android:visibility="gone" />
<ImageButton
android:id="@+id/action_button_people"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginBottom="135.333dp"
android:layout_marginEnd="52dp"
android:background="@drawable/composer_with"
android:visibility="gone" />
<!-- ... autres boutons d'action ... -->
</RelativeLayout>
<!-- Bouton principal pour afficher/masquer les actions -->
<RelativeLayout
android:id="@+id/toggle_action_button"
android:layout_width="60dp"
android:layout_height="57.333dp"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:background="@drawable/composer_button" >
<ImageView
android:id="@+id/toggle_action_button_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:src="@drawable/composer_icn_plus" />
</RelativeLayout>
</RelativeLayout>
Animations personnalisées
Deux animations XML sont définies pour gérer la transition des boutons : une pour l'apparition (in_animation.xml) et une pour la disparition (out_animation.xml). Ces animations utilisent principalement la rotation et des interpolateurs pour un effet dynamique.
<!-- in_animation.xml -->
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator"
android:duration="150"
android:fromDegrees="0.0"
android:toDegrees="-225.0"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:fillAfter="true"
android:fillEnabled="true" />
<!-- out_animation.xml -->
<rotate
xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/linear_interpolator"
android:duration="150"
android:fromDegrees="-225.0"
android:toDegrees="0.0"
android:pivotX="50.0%"
android:pivotY="50.0%"
android:fillAfter="true"
android:fillEnabled="true" />
Classse d'animations utilitaires
La classe AnimationHelper centralise la logique d'animation. Elle gère la création des animations de rotation et de translation, ainsi que leur application aux boutons. Des interpolateurs comme OvershootInterpolator et AnticipateInterpolator sont utilisés pour un effet plus naturel.
import android.content.Context;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnticipateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.RotateAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.ImageButton;
public class AnimationHelper {
private static int horizontalOffset;
private static int verticalOffset;
/**
* Initialise les décalages basés sur la densité de l'écran.
* @param context Le contexte de l'application.
*/
public static void initializeOffsets(Context context) {
float density = context.getResources().getDisplayMetrics().density;
horizontalOffset = (int) (10.667 * density);
verticalOffset = -(int) (8.667 * density);
}
/**
* Crée une animation de rotation.
* @param fromDegrees Angle de départ.
* @param toDegrees Angle d'arrivée.
* @param durationMillis Durée de l'animation en millisecondes.
* @return L'objet RotateAnimation configuré.
*/
public static Animation createRotationAnimation(float fromDegrees, float toDegrees, int durationMillis) {
RotateAnimation rotation = new RotateAnimation(fromDegrees, toDegrees,
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
rotation.setDuration(durationMillis);
rotation.setFillAfter(true);
return rotation;
}
/**
* Lance les animations d'apparition pour les boutons d'action.
* @param parentContainer Le conteneur des boutons.
* @param durationMillis Durée de l'animation.
*/
public static void animateIn(ViewGroup parentContainer, int durationMillis) {
int childCount = parentContainer.getChildCount();
for (int i = 0; i < childCount; i++) {
ImageButton actionButton = (ImageButton) parentContainer.getChildAt(i);
actionButton.setVisibility(View.VISIBLE);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) actionButton.getLayoutParams();
// Calcul du décalage relatif à la position finale
float startX = params.rightMargin - horizontalOffset;
float startY = verticalOffset + params.bottomMargin;
Animation animation = new TranslateAnimation(startX, 0f, startY, 0f);
animation.setFillAfter(true);
animation.setDuration(durationMillis);
// Création d'un décalage temporel pour un effet d'arc
animation.setStartOffset((i * 100) / (-1 + childCount));
animation.setInterpolator(new OvershootInterpolator(2F));
actionButton.startAnimation(animation);
}
}
/**
* Lance les animations de disparition pour les boutons d'action.
* @param parentContainer Le conteneur des boutons.
* @param durationMillis Durée de l'animation.
*/
public static void animateOut(ViewGroup parentContainer, int durationMillis) {
int childCount = parentContainer.getChildCount();
for (int i = 0; i < childCount; i++) {
final ImageButton actionButton = (ImageButton) parentContainer.getChildAt(i);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) actionButton.getLayoutParams();
// Calcul du décalage pour le retour à la position initiale
float endX = params.rightMargin - horizontalOffset;
float endY = verticalOffset + params.bottomMargin;
Animation animation = new TranslateAnimation(0f, endX, 0f, endY);
animation.setFillAfter(true);
animation.setDuration(durationMillis);
// Inversion du décalage temporel pour un effet de fermeture ordonné
animation.setStartOffset(((childCount - i) * 100) / (-1 + childCount));
animation.setInterpolator(new AnticipateInterpolator(2F));
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation anim) {}
@Override
public void onAnimationRepeat(Animation anim) {}
@Override
public void onAnimationEnd(Animation anim) {
// Cacher le bouton une fois l'animation terminée
actionButton.setVisibility(View.GONE);
}
});
actionButton.startAnimation(animation);
}
}
}
Activité principale
L'activité PathMenuActivity gère l'interaction utilisateru. Elle initialise les animations, lie les événements aux clics sur les boutons et bascule l'état d'affichage du menu radial.
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.RelativeLayout;
public class PathMenuActivity extends Activity {
private boolean isMenuVisible = false;
private RelativeLayout actionButtonsContainer;
private ImageView menuToggleButtonIcon;
private RelativeLayout menuToggleButton;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main_layout); // Assurez-vous que le nom du layout correspond
// Initialisation des décalages pour les animations
AnimationHelper.initializeOffsets(this);
// Récupération des vues
actionButtonsContainer = findViewById(R.id.action_buttons_container);
menuToggleButton = findViewById(R.id.toggle_action_button);
menuToggleButtonIcon = findViewById(R.id.toggle_action_button_icon);
// Configuration du listener pour le bouton de bascule
menuToggleButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (!isMenuVisible) {
// Afficher le menu
AnimationHelper.animateIn(actionButtonsContainer, 300);
menuToggleButtonIcon.startAnimation(AnimationHelper.createRotationAnimation(0, -270, 300));
} else {
// Masquer le menu
AnimationHelper.animateOut(actionButtonsContainer, 300);
menuToggleButtonIcon.startAnimation(AnimationHelper.createRotationAnimation(-270, 0, 300));
}
isMenuVisible = !isMenuVisible; // Inverse l'état du menu
}
});
// Ajout de listeners aux boutons d'action pour future implémentation
for (int i = 0; i < actionButtonsContainer.getChildCount(); i++) {
actionButtonsContainer.getChildAt(i).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Logique à exécuter lors du clic sur un bouton d'action
// Exemple : Toast.makeText(PathMenuActivity.this, "Action cliquée !", Toast.LENGTH_SHORT).show();
}
});
}
// Animation initiale du bouton de bascule
menuToggleButton.startAnimation(AnimationHelper.createRotationAnimation(0, 360, 200));
}
}