Tinte de MenuItem en la barra de herramientas de AppCompat

93

Cuando utilizo elementos de diseño de la AppCompatbiblioteca para los Toolbarelementos de mi menú, el teñido funciona como se esperaba. Me gusta esto:

<item
    android:id="@+id/action_clear"
    android:icon="@drawable/abc_ic_clear_mtrl_alpha"  <-- from AppCompat
    android:title="@string/clear" />

Pero si utilizo mis propios dibujables o incluso copio los dibujables de la AppCompatbiblioteca a mi propio proyecto, no se teñirá en absoluto.

<item
    android:id="@+id/action_clear"
    android:icon="@drawable/abc_ic_clear_mtrl_alpha_copy"  <-- copy from AppCompat
    android:title="@string/clear" />

¿Hay alguna magia especial en el AppCompat Toolbarque solo tiñe los dibujables de esa biblioteca? ¿Alguna forma de hacer que esto funcione con mis propios diseños?

Ejecutando esto en un dispositivo API de nivel 19 con compileSdkVersion = 21y targetSdkVersion = 21, y también usando todo, desdeAppCompat

abc_ic_clear_mtrl_alpha_copyes una copia exacta del abc_ic_clear_mtrl_alphapng deAppCompat

Editar:

El tinte se basa en el valor que he establecido android:textColorPrimaryen mi tema.

Por ejemplo <item name="android:textColorPrimary">#00FF00</item>, me daría un color de tinte verde.

Capturas de pantalla

El teñido funciona como se esperaba con dibujable de AppCompat El teñido funciona como se esperaba con dibujable de AppCompat

El tinte no funciona con dibujable copiado de AppCompat El tinte no funciona con dibujable copiado de AppCompat

greve
fuente
¿Ambos estilos tienen el mismo padre? ¿Qué pasa si extiendes el estilo superior con el tuyo?
G_V
No hay diferencia en los estilos. La única diferencia es el dibujable, que son ambos archivos .png
greve
¿El dibujable parece una copia exacta del dibujable de AppCombat original en el código?
G_V
Son archivos png, que copié. Son exactamente lo mismo.
Greve
Entonces, ¿en qué se diferencia exactamente su código del original si tiene el mismo estilo y la misma imagen?
G_V

Respuestas:

31

Porque si echas un vistazo al código fuente de TintManager en AppCompat, verás:

/**
 * Drawables which should be tinted with the value of {@code R.attr.colorControlNormal},
 * using the default mode.
 */
private static final int[] TINT_COLOR_CONTROL_NORMAL = {
        R.drawable.abc_ic_ab_back_mtrl_am_alpha,
        R.drawable.abc_ic_go_search_api_mtrl_alpha,
        R.drawable.abc_ic_search_api_mtrl_alpha,
        R.drawable.abc_ic_commit_search_api_mtrl_alpha,
        R.drawable.abc_ic_clear_mtrl_alpha,
        R.drawable.abc_ic_menu_share_mtrl_alpha,
        R.drawable.abc_ic_menu_copy_mtrl_am_alpha,
        R.drawable.abc_ic_menu_cut_mtrl_alpha,
        R.drawable.abc_ic_menu_selectall_mtrl_alpha,
        R.drawable.abc_ic_menu_paste_mtrl_am_alpha,
        R.drawable.abc_ic_menu_moreoverflow_mtrl_alpha,
        R.drawable.abc_ic_voice_search_api_mtrl_alpha,
        R.drawable.abc_textfield_search_default_mtrl_alpha,
        R.drawable.abc_textfield_default_mtrl_alpha
};

/**
 * Drawables which should be tinted with the value of {@code R.attr.colorControlActivated},
 * using the default mode.
 */
private static final int[] TINT_COLOR_CONTROL_ACTIVATED = {
        R.drawable.abc_textfield_activated_mtrl_alpha,
        R.drawable.abc_textfield_search_activated_mtrl_alpha,
        R.drawable.abc_cab_background_top_mtrl_alpha
};

/**
 * Drawables which should be tinted with the value of {@code android.R.attr.colorBackground},
 * using the {@link android.graphics.PorterDuff.Mode#MULTIPLY} mode.
 */
private static final int[] TINT_COLOR_BACKGROUND_MULTIPLY = {
        R.drawable.abc_popup_background_mtrl_mult,
        R.drawable.abc_cab_background_internal_bg,
        R.drawable.abc_menu_hardkey_panel_mtrl_mult
};

/**
 * Drawables which should be tinted using a state list containing values of
 * {@code R.attr.colorControlNormal} and {@code R.attr.colorControlActivated}
 */
private static final int[] TINT_COLOR_CONTROL_STATE_LIST = {
        R.drawable.abc_edit_text_material,
        R.drawable.abc_tab_indicator_material,
        R.drawable.abc_textfield_search_material,
        R.drawable.abc_spinner_mtrl_am_alpha,
        R.drawable.abc_btn_check_material,
        R.drawable.abc_btn_radio_material
};

/**
 * Drawables which contain other drawables which should be tinted. The child drawable IDs
 * should be defined in one of the arrays above.
 */
private static final int[] CONTAINERS_WITH_TINT_CHILDREN = {
        R.drawable.abc_cab_background_top_material
};

Lo que básicamente significa que tienen ResourceIds particulares en la lista blanca para ser teñidos.

Pero supongo que siempre puedes ver cómo tiñen esas imágenes y hacen lo mismo. Es tan fácil como configurar ColorFilter en un dibujable.

EvilDuck
fuente
Uf, eso es lo que temía. No encontré el código fuente de AppCompat en el SDK, por eso no encontré esta parte yo mismo. Supongo que tendré que navegar en googlesource.com entonces. ¡Gracias!
greve
8
Sé que es una pregunta tangencial, pero ¿por qué hay una lista blanca? Si puede teñir con estos iconos, ¿por qué no podemos teñir nuestros propios iconos? Además, ¿cuál es el punto de hacer casi todo compatible con versiones anteriores (con AppCompat) cuando omite una de las cosas más importantes: tener íconos de barra de acción (con color personalizado)?
Zsolt Safrany
1
Hay un problema para esto en el rastreador de problemas de Google que se ha marcado como solucionado, pero no funciona para mí, pero puede rastrearlo aquí: issuetracker.google.com/issues/37127128
niknetniko
Afirman que esto está arreglado, pero no lo es. Dios, detesto el motor de temas de Android, AppCompat y toda la basura asociada a él. Solo funciona para aplicaciones de muestra del "navegador de repositorios Github".
Martin Marconcini
97

Después de la nueva biblioteca de soporte v22.1, puede usar algo similar a esto:

  @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_home, menu);
        Drawable drawable = menu.findItem(R.id.action_clear).getIcon();

        drawable = DrawableCompat.wrap(drawable);
        DrawableCompat.setTint(drawable, ContextCompat.getColor(this,R.color.textColorPrimary));
        menu.findItem(R.id.action_clear).setIcon(drawable);
        return true;
    }
Mahdi Hijazi
fuente
1
Yo diría que en este caso lo antiguo setColorFilter()es preferible.
natario
@mvai, ¿por qué setColorFilter () es más preferible?
wilddev
4
@wilddev brevedad. ¿Por qué molestarse en la clase de soporte DrawableCompat cuando puede ir a menu.findItem (). GetIcon (). SetColorFilter ()? Un trazador de líneas y claro.
natario
4
El argumento de una sola línea es irrelevante cuando abstrae toda la lógica en su propio método TintingUtils.tintMenuIcon (...) o como quiera llamarlo. Si necesita cambiar o ajustar la lógica en el futuro, hágalo en un solo lugar, no en toda la aplicación.
Dan Dar3
1
¡Esto es asombroso!
shariful islam
82

Establecer un ColorFilter(tinte) en a MenuItemes simple. Aquí hay un ejemplo:

Drawable drawable = menuItem.getIcon();
if (drawable != null) {
    // If we don't mutate the drawable, then all drawable's with this id will have a color
    // filter applied to it.
    drawable.mutate();
    drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
    drawable.setAlpha(alpha);
}

El código anterior es muy útil si desea admitir diferentes temas y no desea tener copias adicionales solo por el color o la transparencia.

Haga clic aquí para que una clase de ayuda establezcaColorFiltertodos los elementos de diseño de un menú, incluido el icono de desbordamiento.

En onCreateOptionsMenu(Menu menu)solo llamar MenuColorizer.colorMenu(this, menu, color);después de inflar su menú y listo; sus iconos están teñidos.

Jared Rummler
fuente
¡Gracias, definitivamente lo intentaré!
Greve
3
He estado golpeando mi cabeza contra mi escritorio tratando de averiguar por qué todos mis íconos están siendo teñidos, ¡gracias por los avisos sobre drawable.mutate ()!
Scott Cooper
50

app:iconTintEl atributo se implementa SupportMenuInflaterdesde la biblioteca de soporte (al menos en 28.0.0).

Probado con éxito con API 15 y superior.

Archivo de recursos del menú:

<menu
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <item
        android:id="@+id/menu_settings"
        android:icon="@drawable/ic_settings_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"        <!-- using app name space instead of android -->
        android:menuCategory="system"
        android:orderInCategory="1"
        android:title="@string/menu_settings"
        app:showAsAction="never"
        />

    <item
        android:id="@+id/menu_themes"
        android:icon="@drawable/ic_palette_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"
        android:menuCategory="system"
        android:orderInCategory="2"
        android:title="@string/menu_themes"
        app:showAsAction="never"
        />

    <item
        android:id="@+id/action_help"
        android:icon="@drawable/ic_help_white_24dp"
        app:iconTint="?attr/appIconColorEnabled"
        android:menuCategory="system"
        android:orderInCategory="3"
        android:title="@string/menu_help"
        app:showAsAction="never"
        />

</menu>

(En este caso, ?attr/appIconColorEnabledera un atributo de color personalizado en los temas de la aplicación y los recursos del icono eran elementos de diseño vectoriales).

Afilu
fuente
5
¡Esta debería ser la nueva respuesta aceptada! Además, tenga en cuenta android:iconTinty android:iconTintModeno funciona, pero prefijar con en app:lugar de android:funciona como un encanto (en mis propios elementos de diseño vectoriales, API> = 21)
Sebastiaan Alvarez Rodriguez
Si llama mediante programación: tenga en cuenta que SupportMenuInflaterno se aplicará ninguna lógica personalizada si el menú no es un me SupportMenugusta MenuBuilder, simplemente vuelve a la normalidad MenuInflater.
geekley
En este caso, el uso AppCompatActivity.startSupportActionMode(callback)y las implementaciones de soporte apropiadas de androidx.appcompatse pasarán a la devolución de llamada.
geekley
30

Personalmente preferí este enfoque desde este enlace.

Cree un diseño XML con lo siguiente:

<?xml version="1.0" encoding="utf-8"?>
<bitmap
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:src="@drawable/ic_action_something"
    android:tint="@color/color_action_icons_tint"/>

y haga referencia a este elemento de dibujo desde su menú:

<item
    android:id="@+id/option_menu_item_something"
    android:icon="@drawable/ic_action_something_tined"
N Jay
fuente
2
Si bien este enlace puede responder la pregunta, es mejor incluir aquí las partes esenciales de la respuesta y proporcionar el enlace como referencia. Las respuestas de solo enlace pueden dejar de ser válidas si cambia la página enlazada.
tomloprod
Gracias por tu comentario, he editado la pregunta. @tomloprod
N Jay
4
Esta es mi solución preferida. Sin embargo, es importante tener en cuenta que, por ahora, esta solución no parece funcionar cuando está utilizando los nuevos tipos de diseño vectorial como fuente.
Michael De Soto
1
@haagmm esta solución necesita API> = 21. También funciona para vectores.
Neurotransmisor
1
Y no debería funcionar con vectores, la etiqueta raíz es bitmap. Hay otras formas de colorear vectores. Quizás alguien podría añadir vector de coloración aquí también ...
milosmns
11

La mayoría de las soluciones en este hilo usan una API más nueva, o usan la reflexión, o usan la búsqueda de vista intensiva para llegar al inflado MenuItem.

Sin embargo, hay un enfoque más elegante para hacer eso. Necesita una barra de herramientas personalizada, ya que su caso de uso de "aplicar tinte personalizado" no funciona bien con la API de estilo / tematización pública.

public class MyToolbar extends Toolbar {
    ... some constructors, extracting mAccentColor from AttrSet, etc

    @Override
    public void inflateMenu(@MenuRes int resId) {
        super.inflateMenu(resId);
        Menu menu = getMenu();
        for (int i = 0; i < menu.size(); i++) {
            MenuItem item = menu.getItem(i);
            Drawable icon = item.getIcon();
            if (icon != null) {
                item.setIcon(applyTint(icon));
            }
        }
    }
    void applyTint(Drawable icon){
        icon.setColorFilter(
           new PorterDuffColorFilter(mAccentColor, PorterDuff.Mode.SRC_IN)
        );
    }

}

Solo asegúrese de llamar a su código de Actividad / Fragmento:

toolbar.inflateMenu(R.menu.some_menu);
toolbar.setOnMenuItemClickListener(someListener);

Sin reflejo, sin búsqueda de vistas y sin tanto código, ¿eh?

Y ahora puedes ignorar lo ridículo onCreateOptionsMenu/onOptionsItemSelected.

Dibujó
fuente
Técnicamente, está haciendo una búsqueda de vista. Está iterando las vistas y asegurándose de que no sean nulas. ;)
Martin Marconcini
Definitivamente tienes razón en cierto modo :-) No obstante, la Menu#getItem()complejidad es O (1) en la barra de herramientas, porque los elementos se almacenan en ArrayList. Que es diferente del View#findViewByIdrecorrido (al que me referí como búsqueda de vista en mi respuesta), cuya complejidad está lejos de ser constante :-)
Drew
De acuerdo, de hecho, hice algo muy similar. Todavía me sorprende que Android no haya simplificado todo esto después de tantos años…
Martin Marconcini
¿Cómo puedo cambiar los colores del icono de desbordamiento y de la hamburguesa con este enfoque?
Sandra
8

Aquí está la solución que utilizo; puede llamarlo después de onPrepareOptionsMenu () o el lugar equivalente. El motivo de mutate () es si utiliza los iconos en más de una ubicación; sin el mutado, todos adquirirán el mismo tono.

public class MenuTintUtils {
    public static void tintAllIcons(Menu menu, final int color) {
        for (int i = 0; i < menu.size(); ++i) {
            final MenuItem item = menu.getItem(i);
            tintMenuItemIcon(color, item);
            tintShareIconIfPresent(color, item);
        }
    }

    private static void tintMenuItemIcon(int color, MenuItem item) {
        final Drawable drawable = item.getIcon();
        if (drawable != null) {
            final Drawable wrapped = DrawableCompat.wrap(drawable);
            drawable.mutate();
            DrawableCompat.setTint(wrapped, color);
            item.setIcon(drawable);
        }
    }

    private static void tintShareIconIfPresent(int color, MenuItem item) {
        if (item.getActionView() != null) {
            final View actionView = item.getActionView();
            final View expandActivitiesButton = actionView.findViewById(R.id.expand_activities_button);
            if (expandActivitiesButton != null) {
                final ImageView image = (ImageView) expandActivitiesButton.findViewById(R.id.image);
                if (image != null) {
                    final Drawable drawable = image.getDrawable();
                    final Drawable wrapped = DrawableCompat.wrap(drawable);
                    drawable.mutate();
                    DrawableCompat.setTint(wrapped, color);
                    image.setImageDrawable(drawable);
                }
            }
        }
    }
}

Esto no se encargará del desbordamiento, pero para eso, puede hacer esto:

Diseño:

<android.support.v7.widget.Toolbar
    ...
    android:theme="@style/myToolbarTheme" />

Estilos:

<style name="myToolbarTheme">
        <item name="colorControlNormal">#FF0000</item>
</style>

Esto funciona a partir de appcompat v23.1.0.

Aprenda OpenGL ES
fuente