Barwienie elementów MenuItem na pasku narzędzi AppCompat

95

Kiedy używam rysunków z AppCompatbiblioteki dla moich Toolbarpozycji menu, zabarwienie działa zgodnie z oczekiwaniami. Lubię to:

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

Ale jeśli użyję własnych rysunków lub nawet skopiuję rysunki z AppCompatbiblioteki do własnego projektu, to w ogóle nie zabarwi się .

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

Czy jest jakaś szczególna magia w AppCompat Toolbartym jedynym odcieniu rysunków z tej biblioteki? Czy jest jakiś sposób, aby to działało z moimi własnymi rysunkami?

Uruchomienie tego na urządzeniu z interfejsem API Level 19 z compileSdkVersion = 21i targetSdkVersion = 21, a także przy użyciu wszystkiego zAppCompat

abc_ic_clear_mtrl_alpha_copyjest dokładną kopią pliku abc_ic_clear_mtrl_alphapng zAppCompat

Edytować:

Zabarwienie jest oparte na wartości, którą ustawiłem android:textColorPrimaryw moim motywie.

Np. <item name="android:textColorPrimary">#00FF00</item>Dałoby mi zielony odcień.

Zrzuty ekranu

Barwienie działa zgodnie z oczekiwaniami dzięki możliwości rysowania z AppCompat Barwienie działa zgodnie z oczekiwaniami dzięki możliwości rysowania z AppCompat

Barwienie nie działa z rysunkiem skopiowanym z AppCompat Barwienie nie działa z rysunkiem skopiowanym z AppCompat

greve
źródło
Oba style mają tego samego rodzica? Co jeśli przedłużysz topowy styl swoim własnym?
G_V
Nie ma różnicy w stylach. Jedyną różnicą jest możliwość rysowania, czyli oba pliki .png
greve
To, co można narysować, wygląda jak dokładna kopia oryginalnego AppCombat do rysowania w kodzie?
G_V
Są to pliki png, które skopiowałem. Są dokładnie takie same.
greve
Więc gdzie dokładnie twój kod różni się od oryginału, jeśli ma ten sam styl i ten sam obraz?
G_V

Odpowiedzi:

31

Ponieważ jeśli spojrzysz na kod źródłowy TintManagera w AppCompat, zobaczysz:

/**
 * 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
};

Co właściwie oznacza, że ​​mają określone identyfikatory zasobów na białej liście do przyciemnienia.

Ale myślę, że zawsze możesz zobaczyć, jak zabarwiają te obrazy i zrobić to samo. To tak proste, jak ustawienie ColorFilter na rysowalnym.

EvilDuck
źródło
Ugh, tego się bałem. Nie znalazłem kodu źródłowego AppCompat w SDK, dlatego sam nie znalazłem tej części. Chyba będę musiał wtedy przeglądać na googlesource.com. Dzięki!
greve
8
Wiem, że to styczne pytanie, ale dlaczego istnieje biała lista? Jeśli potrafi zabarwić te ikony, dlaczego nie możemy zabarwić naszych własnych ikon? Ponadto, jaki jest sens w uczynieniu prawie wszystkiego wstecznie kompatybilnym (z AppCompat), jeśli pominiesz jedną z najważniejszych rzeczy: posiadanie ikon paska akcji (z niestandardowym kolorem).
Zsolt Safrany
1
Wystąpił problem z tym w narzędziu Google do śledzenia problemów, który został oznaczony jako naprawiony, ale nie działa dla mnie, ale możesz go śledzić tutaj: issuetracker.google.com/issues/37127128
niknetniko
Twierdzą, że to naprawiono, ale tak nie jest. O rany, nienawidzę silnika motywów Androida, AppCompat i wszystkich bzdur z tym związanych. Działa tylko z przykładowymi aplikacjami „Przeglądarka repozytoriów Github”.
Martin Marconcini
98

Po nowej bibliotece pomocy technicznej w wersji 22.1 możesz użyć czegoś podobnego do tego:

  @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
źródło
1
Powiedziałbym, że w tym przypadku stare setColorFilter()jest o wiele lepsze.
natario
@mvai, dlaczego metoda setColorFilter () jest korzystniejsza?
wilddev
4
@wilddev zwięzłość. Po co zawracać sobie głowę obsługą klasy DrawableCompat, skoro można przejść do menu.findItem (). GetIcon (). SetColorFilter ()? Jedna wkładka i przezroczysta.
natario
5
Argument jednowierszowy jest nieistotny, gdy wyodrębniasz całą logikę do własnej metody TintingUtils.tintMenuIcon (...) lub jakkolwiek chcesz to nazwać. Jeśli w przyszłości zajdzie potrzeba zmiany lub dostosowania logiki, robimy to w jednym miejscu, a nie w całej aplikacji.
Dan Dar3
Nie ma potrzeby barwienia różnych logik i nie ma potrzeby zmiany w przyszłości.
Jemshit Iskenderov
84

Ustawienie ColorFilter(odcień) na a MenuItemjest proste. Oto przykład:

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);
}

Powyższy kod jest bardzo pomocny, jeśli chcesz obsługiwać różne motywy i nie chcesz mieć dodatkowych kopii tylko dla koloru lub przezroczystości.

Kliknij tutaj, aby klasa pomocnicza ustawiała aColorFilterna wszystkich elementach do rysowania w menu, w tym na ikonie przepełnienia.

W onCreateOptionsMenu(Menu menu)wystarczy zadzwonić MenuColorizer.colorMenu(this, menu, color);po pompowania swoje menu i voila; Twoje ikony są zabarwione.

Jared Rummler
źródło
4
Waliłem głową w biurko, próbując dowiedzieć się, dlaczego wszystkie moje ikony są przyciemniane, dzięki za ostrzeżenia o drawable.mutate ()!
Scott Cooper
51

app:iconTintatrybut jest zaimplementowany SupportMenuInflaterz biblioteki obsługi (przynajmniej w 28.0.0).

Przetestowane pomyślnie z API 15 i nowszymi.

Plik zasobów menu:

<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>

(W tym przypadku ?attr/appIconColorEnabledbył to niestandardowy atrybut koloru w motywach aplikacji, a zasoby ikon były rysunkami wektorowymi).

Afilu
źródło
5
To powinna być nowa zaakceptowana odpowiedź! Ponadto, proszę zauważyć android:iconTinti android:iconTintModenie działa, ale prefiksowanie app:zamiast android:działa jak urok (na moich własnych rysunkach wektorowych, API> = 21)
Sebastiaan Alvarez Rodriguez
Jeśli wywołujesz programowo: pamiętaj, że SupportMenuInflaternie zastosuje żadnej niestandardowej logiki, jeśli menu nie jest SupportMenupodobne MenuBuilder, po prostu wróci do zwykłego MenuInflater.
geekley
W takim przypadku do wywołania zwrotnego zostaną przekazane użycie AppCompatActivity.startSupportActionMode(callback)i odpowiednie implementacje wsparcia androidx.appcompat.
geekley
30

Osobiście wolałem to podejście z tego linku

Utwórz układ XML z następującymi elementami:

<?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"/>

i odwołaj się do tego, co można wyciągnąć z menu:

<item
    android:id="@+id/option_menu_item_something"
    android:icon="@drawable/ic_action_something_tined"
N Jay
źródło
2
Chociaż ten link może odpowiedzieć na pytanie, lepiej jest zawrzeć tutaj zasadnicze części odpowiedzi i podać link do odniesienia. Odpowiedzi zawierające tylko łącze mogą stać się nieprawidłowe, jeśli połączona strona ulegnie zmianie.
tomloprod
Dziękuję za komentarz. Zredagowałem pytanie. @tomloprod
N Jay
4
To jest moje preferowane rozwiązanie. Należy jednak pamiętać, że na razie to rozwiązanie nie działa, gdy jako źródła używasz nowych typów rysowalnych wektorów.
Michael De Soto
1
@haagmm to rozwiązanie wymaga API> = 21. Działa również dla wektorów.
Neurotransmiter
1
I nie powinien działać z wektorami, znacznik główny to bitmap. Istnieją inne sposoby kolorowania wektorów. Może ktoś mógłby tu też dodać kolorowanie wektorowe ...
milosmns
11

Większość rozwiązań w tym wątku używa nowszego interfejsu API, używa odbicia lub intensywnego wyszukiwania widoku, aby uzyskać zawyżone MenuItem .

Istnieje jednak bardziej eleganckie podejście do tego. Potrzebujesz niestandardowego paska narzędzi Toolbar, ponieważ Twój przypadek użycia „Zastosuj niestandardowy odcień” nie współgra dobrze z publicznym interfejsem API stylizacji / motywów.

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)
        );
    }

}

Po prostu upewnij się, że wywołujesz kod aktywności / fragmentu:

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

Bez odbicia, bez podglądu i nie tyle kodu, co?

A teraz możesz zignorować śmieszne onCreateOptionsMenu/onOptionsItemSelected.

Rysował
źródło
Technicznie rzecz biorąc, robisz wyszukiwanie widoku. Iterujesz widoki i upewniasz się, że nie są zerowe. ;)
Martin Marconcini
W pewnym sensie masz rację :-) Niemniej Menu#getItem()złożoność paska narzędzi wynosi O (1), ponieważ elementy są przechowywane w ArrayList. Co różni się od View#findViewByIdtraversal (które w mojej odpowiedzi określiłem jako przeglądanie widoku ), którego złożoność jest daleka od stałej :-)
Drew
Zgoda, w rzeczywistości zrobiłem bardzo podobną rzecz. Nadal jestem zszokowany, że Android nie usprawnił tego wszystkiego po tylu latach…
Martin Marconcini
Jak mogę zmienić kolor ikony przepełnienia i ikony hamburgera, stosując to podejście?
Sandra
8

Oto rozwiązanie, którego używam; możesz go wywołać po onPrepareOptionsMenu () lub równoważnym miejscu. Powodem mutate () jest używanie ikon w więcej niż jednym miejscu; bez mutatu wszystkie nabiorą tego samego odcienia.

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);
                }
            }
        }
    }
}

To nie zajmie się przepełnieniem, ale możesz to zrobić:

Układ:

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

Style:

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

Działa to od wersji 23.1.0 appcompat.

Naucz się OpenGL ES
źródło
2

To zadziałało dla mnie:

override fun onCreateOptionsMenu(menu: Menu?): Boolean {

        val inflater = menuInflater
        inflater.inflate(R.menu.player_menu, menu)

        //tinting menu item:
        val typedArray = theme.obtainStyledAttributes(IntArray(1) { android.R.attr.textColorSecondary })
        val textColor = typedArray.getColor(0, 0)
        typedArray.recycle()

        val item = menu?.findItem(R.id.action_chapters)
        val icon = item?.icon

        icon?.setColorFilter(textColor, PorterDuff.Mode.SRC_IN);
        item?.icon = icon
        return true
    }

Lub możesz użyć tinta w formacie XML do rysowania:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:tint="?android:textColorSecondary"
    android:viewportWidth="384"
    android:viewportHeight="384">
    <path
        android:fillColor="#FF000000"

        android:pathData="M0,277.333h384v42.667h-384z" />
    <path
        android:fillColor="#FF000000"
        android:pathData="M0,170.667h384v42.667h-384z" />
    <path
        android:fillColor="#FF000000"
        android:pathData="M0,64h384v42.667h-384z" />
</vector>
Selena Smiertin
źródło