web-dev-qa-db-fra.com

Modification de l’icône de menu Dépassement Android par programmation

Je cherchais une méthode pour modifier par programme la couleur de l'icône de menu de débordement dans Android.

La seule option que j'ai trouvée est de changer l'icône de façon permanente en ajoutant un style personnalisé . Le problème est que, dans un proche avenir, nous devrons le changer lors de l'utilisation de notre application.

Notre application est une extension d'une série de plates-formes en ligne. Par conséquent, un utilisateur peut entrer l'URL Web de sa plate-forme. Ceux-ci ont leurs propres styles et seront récupérés par un appel API vers l'application.

Ceux-ci pourraient m'adresser pour changer la couleur de l'icône ...

Actuellement, je change d'autres icônes dans la barre d'actions comme ceci:

if (ib != null){
            Drawable resIcon = getResources().getDrawable(R.drawable.navigation_refresh);
            resIcon.mutate().setColorFilter(StyleClass.getColor("color_navigation_icon_overlay"), PorterDuff.Mode.SRC_ATOP);
            ib.setIcon(resIcon);
}

Pour l'instant je vais devoir utiliser les styles.

12
Mathijs Segers

Vous pouvez réellement modifier l’icône de débordement par programmation à l’aide d’une petite astuce. Voici un exemple:

Créez un style pour le menu débordement et transmettez une description du contenu

<style name="Widget.ActionButton.Overflow" parent="@Android:style/Widget.Holo.ActionButton.Overflow">
    <item name="Android:contentDescription">@string/accessibility_overflow</item>
</style>

<style name="Your.Theme" parent="@Android:style/Theme.Holo.Light.DarkActionBar">
    <item name="Android:actionOverflowButtonStyle">@style/Widget.ActionButton.Overflow</item>
</style>

Maintenant, appelez ViewGroup.findViewsWithText et transmettez votre description du contenu. Donc, quelque chose comme:

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // The content description used to locate the overflow button
    final String overflowDesc = getString(R.string.accessibility_overflow);
    // The top-level window
    final ViewGroup decor = (ViewGroup) getWindow().getDecorView();
    // Wait a moment to ensure the overflow button can be located
    decor.postDelayed(new Runnable() {

        @Override
        public void run() {
            // The List that contains the matching views
            final ArrayList<View> outViews = new ArrayList<>();
            // Traverse the view-hierarchy and locate the overflow button
            decor.findViewsWithText(outViews, overflowDesc,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            // Guard against any errors
            if (outViews.isEmpty()) {
                return;
            }
            // Do something with the view
            final ImageButton overflow = (ImageButton) outViews.get(0);
            overflow.setImageResource(R.drawable.ic_action_overflow_round_red);

        }

    }, 1000);
}

@Override
public boolean onCreateOptionsMenu(Menu menu) {
    // Add a dummy item to the overflow menu
    menu.add("Overflow");
    return super.onCreateOptionsMenu(menu);
}

View.findViewsWithText a été ajouté à l'API niveau 14, vous devrez donc utiliser votre propre méthode de compatibilité:

static void findViewsWithText(List<View> outViews, ViewGroup parent, String targetDescription) {
    if (parent == null || TextUtils.isEmpty(targetDescription)) {
        return;
    }
    final int count = parent.getChildCount();
    for (int i = 0; i < count; i++) {
        final View child = parent.getChildAt(i);
        final CharSequence desc = child.getContentDescription();
        if (!TextUtils.isEmpty(desc) && targetDescription.equals(desc.toString())) {
            outViews.add(child);
        } else if (child instanceof ViewGroup && child.getVisibility() == View.VISIBLE) {
            findViewsWithText(outViews, (ViewGroup) child, targetDescription);
        }
    }
}

Résultats

Example

32
adneal

La réponse d'Adneal est excellente et je l'utilisais jusqu'à récemment. Mais ensuite, je voulais que mon application utilise la conception des matériaux et donc le style Theme.AppCompat.* et Android.support.v7.widget.Toolbar

Oui, cela a cessé de fonctionner et j’essayais de résoudre le problème en définissant le parent de Your.Theme sur @style/Widget.AppCompat.ActionButton.Overflow. Cela a fonctionné en définissant correctement contentDescription, mais cela a échoué lors de la conversion en ImageButton. Il s'est avéré que dans le dernier (version 23) Android.support.v7class OverflowMenuButton s'étend de AppCompatImageView. Changer de classe de casting était suffisant pour que cela fonctionne avec Toolbar sur Lusipop sous Nexus 5.

Ensuite, je l'ai exécuté sur Galaxy S4 avec KitKat et peu importe ce que j'ai essayé, je ne pouvais pas définir contentDescription de dépassement de capacité à ma valeur personnalisée. Mais dans styles AppCompat j'ai trouvé qu'il a déjà une valeur par défaut:

<item name="Android:contentDescription">@string/abc_action_menu_overflow_description</item>

Alors pourquoi ne pas l'utiliser? Aussi par l'idée de Hannes (dans les commentaires), j'ai implémenté auditeur, pour me débarrasser d'un peu de temps aléatoire pour le retard dans postDelayed. Et comme l’icône de débordement se trouve déjà dans la bibliothèque AppCompat, je l’utiliserais également - j’applique un filtre de couleur, ce qui fait que je n’ai pas besoin de ressources d’icône seules.

Mon code basé sur le travail d'Adneal avec les améliorations d'Android Lollipop:

public static void setOverflowButtonColor(final Activity activity) {
    final String overflowDescription = activity.getString(R.string.abc_action_menu_overflow_description);
    final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
    final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            final ArrayList<View> outViews = new ArrayList<View>();
            decorView.findViewsWithText(outViews, overflowDescription,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            if (outViews.isEmpty()) {
                return;
            }
            AppCompatImageView overflow=(AppCompatImageView) outViews.get(0);
            overflow.setColorFilter(Color.CYAN);
            removeOnGlobalLayoutListener(decorView,this);
        }
    });
}

et selon une autre réponse StackOverflow :

public static void removeOnGlobalLayoutListener(View v, ViewTreeObserver.OnGlobalLayoutListener listener) {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
        v.getViewTreeObserver().removeGlobalOnLayoutListener(listener);
    }
    else {
        v.getViewTreeObserver().removeOnGlobalLayoutListener(listener);
    }
}

bien sûr, au lieu de Color.CYAN, vous pouvez utiliser votre propre couleur - activity.getResources().getColor(R.color.black);

EDIT: Ajout de la prise en charge de la dernière bibliothèque AppCompat (23), qui utilise AppCompatImageView Pour AppCompat 22, vous devez convertir le bouton de débordement dans TintImageView 

20
michalbrz

En tant que support 23.1, la barre d’outils a maintenant les méthodes getOverflowIcon () et setOverflowIcon () , afin que nous puissions le faire beaucoup plus facilement:

public static void setOverflowButtonColor(final Toolbar toolbar, final int color) {
    Drawable drawable = toolbar.getOverflowIcon();
    if(drawable != null) {
        drawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(drawable.mutate(), color);
        toolbar.setOverflowIcon(drawable);
    }
}
17
Lorne Laliberte

Il y a une bien meilleure solution. Vous pouvez le faire par programme dans le runtime

toolbar.overflowIcon?.setColorFilter(colorInt, PorterDuff.Mode.SRC_ATOP)

Alto!

7
Jacek Kwiecień

Il existe une solution moins astucieuse pour changer l’icône de débordement. Il existe un exemple sur la façon de changer la couleur de l'icône de débordement, mais vous pouvez l'adapter pour changer l'image:

 private void setOverflowIconColor(int color) {
        Drawable overflowIcon = toolbar.getOverflowIcon();

        if (overflowIcon != null) {
            Drawable newIcon = overflowIcon.mutate();
            newIcon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
            toolbar.setOverflowIcon(newIcon);
        }
    }
7
arts777

Basé sur la réponse de @michalbrz, j'ai utilisé ce qui suit pour changer l'icône elle-même. :)

public static void setOverflowButtonColor(final Activity activity) {
        final String overflowDescription = activity.getString(R.string.abc_action_menu_overflow_description);
        final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
        final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
        viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
        public void onGlobalLayout() {
            final ArrayList<View> outViews = new ArrayList<View>();
            decorView.findViewsWithText(outViews, overflowDescription,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            if (outViews.isEmpty()) {
                return;
            }
            TintImageView overflow = (TintImageView) outViews.get(0);
            //overflow.setColorFilter(Color.CYAN); //changes color
            overflow.setImageResource(R.drawable.dots);
            removeOnGlobalLayoutListener(decorView, this);
        }
    });
1
Atul O Holic

Utiliser appcompat-v7: 23.0.1 aucun des @adneal ou @ michalbrz n'a fonctionné pour moi. J'ai dû changer 2 lignes de code de la réponse de @ michalbrz pour que cela fonctionne.

J'ajoute une réponse car les deux réponses actuelles peuvent être utiles à quelqu'un, mais si vous utilisez la dernière version de appcompat comme moi, vous devriez utiliser celle-ci basée sur @michalbrz:

private static void setOverflowButtonColor(final AppCompatActivity activity, final PorterDuffColorFilter colorFilter) {
    final String overflowDescription = activity.getString(R.string.abc_action_menu_overflow_description);
    final ViewGroup decorView = (ViewGroup) activity.getWindow().getDecorView();
    final ViewTreeObserver viewTreeObserver = decorView.getViewTreeObserver();
    viewTreeObserver.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
        @Override
        public void onGlobalLayout() {
            final ArrayList<View> outViews = new ArrayList<>();
            decorView.findViewsWithText(outViews, overflowDescription,
                    View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
            if (outViews.isEmpty()) {
                return;
            }
            ActionMenuItemView overflow = (ActionMenuItemView)outViews.get(0);
            overflow.getCompoundDrawables()[0].setColorFilter(colorFilter);
            removeOnGlobalLayoutListener(decorView,this);
        }
    });
}

En utilisant le code michalbrz, je rencontrais cette erreur:

Java.lang.ClassCastException: Android.support.v7.internal.view.menu.ActionMenuItemView cannot be cast to Android.support.v7.internal.widget.TintImageView

Ainsi, après avoir creusé un peu dans le code de ActionMenuItemView, j'ai trouvé comment obtenir l'icône dessinable (en regardant dans setIcon()), puis je viens de changer le casting en ActionMenuItemView, le filtre de couleur appliqué à gauche pouvant être tiré de getCompoundDrawables() et Voila! Ça marche!

1
MiguelHincapieC

Les gars, je l'ai fait d'une manière simple, s'il vous plaît regardez cet extrait comme suit:

@Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_dashboard, menu);
        MenuItem item = menu.findItem(R.id.help);
        Drawable drawable = item.getIcon();
        drawable.setColorFilter(Color.RED, PorterDuff.Mode.SRC_ATOP);
        return super.onCreateOptionsMenu(menu);
    }

S'il vous plaît laissez-moi savoir si d'autres précisions ici.

0
Deva