Tingimento de item de menu na barra de ferramentas AppCompat

93

Quando eu uso drawables da AppCompatbiblioteca para meus Toolbaritens de menu, o tingimento funciona conforme o esperado. Como isso:

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

Mas se eu usar meus próprios drawables ou mesmo copiar os drawables da AppCompatbiblioteca para meu próprio projeto, ele não tingirá de forma alguma.

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

Existe alguma magia especial em AppCompat Toolbarque apenas tingem os drawables dessa biblioteca? Alguma maneira de fazer isso funcionar com meus próprios drawables?

Executando isso no dispositivo API de nível 19 com compileSdkVersion = 21e targetSdkVersion = 21, e também usando tudo, desdeAppCompat

abc_ic_clear_mtrl_alpha_copyé uma cópia exata do abc_ic_clear_mtrl_alphapng deAppCompat

Editar:

O tingimento é baseado no valor que eu defini para o android:textColorPrimarymeu tema.

Por exemplo <item name="android:textColorPrimary">#00FF00</item>, me daria uma cor de tonalidade verde.

Capturas de tela

Tingimento funcionando como esperado com drawable do AppCompat Tingimento funcionando como esperado com drawable do AppCompat

O tingimento não funciona com drawable copiado do AppCompat O tingimento não funciona com drawable copiado do AppCompat

greve
fonte
Ambos os estilos têm o mesmo pai? E se você estender o estilo superior com o seu próprio?
G_V
Não há diferença nos estilos. A única diferença é o drawable, que são ambos arquivos .png
greve
O drawable parece uma cópia exata do drawable AppCombat original no código?
G_V
São arquivos png, que copiei. Eles são exatamente os mesmos.
greve
Então, onde exatamente o seu código difere do original se tiver o mesmo estilo e a mesma imagem?
G_V

Respostas:

31

Porque se você der uma olhada no código-fonte do TintManager no AppCompat, você verá:

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

O que significa que eles têm resourceIds específicos na lista de permissões para serem coloridos.

Mas acho que você sempre pode ver como eles estão tingindo essas imagens e fazer o mesmo. É tão fácil quanto definir o ColorFilter em um drawable.

EvilDuck
fonte
Ugh, era disso que eu tinha medo. Não encontrei o código-fonte do AppCompat no SDK, por isso não encontrei essa parte sozinho. Acho que terei que navegar em googlesource.com então. Obrigado!
greve
8
Eu sei que é uma questão tangencial, mas por que existe uma lista branca? Se ele pode tingir com esses ícones, por que não podemos tingir nossos próprios ícones? Além disso, qual é o ponto em tornar quase tudo compatível com versões anteriores (com AppCompat) quando você deixa de fora uma das coisas mais importantes: ter ícones de barra de ação (com cores personalizadas).
Zsolt Safrany,
1
Há um problema para isso no rastreador de problemas do Google que foi marcado como corrigido, mas não funciona para mim, mas você pode rastreá-lo aqui: issuetracker.google.com/issues/37127128
niknetniko
Eles afirmam que isso é consertado, mas não é. Puxa, eu detesto o mecanismo de tema Android, AppCompat e todas as porcarias associadas a ele. Ele só funciona para aplicativos de amostra do “navegador de repositório Github”.
Martin Marconcini
97

Após a nova Biblioteca de suporte v22.1, você pode usar algo semelhante a este:

  @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
fonte
1
Eu diria que, neste caso, o antigo setColorFilter()é preferível.
natario
@mvai, por que setColorFilter () é mais preferível?
wilddev,
4
@wilddev brevity. Por que incomodar a classe DrawableCompat de suporte quando você pode ir menu.findItem (). GetIcon (). SetColorFilter ()? Um forro e limpo.
natario
4
O argumento de uma linha é irrelevante quando você abstrai toda a lógica em seu próprio método TintingUtils.tintMenuIcon (...) ou como você quiser chamá-lo. Se você precisar alterar ou ajustar a lógica no futuro, faça isso em um só lugar, não em todo o aplicativo.
Dan Dar3
1
Isso é incrível!
islã Shariful
82

Definir a ColorFilter(matiz) em a MenuItemé simples. Aqui está um exemplo:

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

O código acima é muito útil se você deseja oferecer suporte a temas diferentes e não deseja ter cópias extras apenas para a cor ou transparência.

Clique aqui para uma classe auxiliar para definir umColorFilterem todos os drawables em um menu, incluindo o ícone de estouro.

Em onCreateOptionsMenu(Menu menu)apenas chamada MenuColorizer.colorMenu(this, menu, color);depois de inflar seu menu e pronto; seus ícones são coloridos.

Jared Rummler
fonte
Obrigado, com certeza vou experimentar!
greve de
3
Tenho batido minha cabeça contra minha mesa tentando descobrir por que todos os meus ícones estão sendo coloridos, obrigado por alertar sobre drawable.mutate ()!
Scott Cooper
48

app:iconTintatributo é implementado na SupportMenuInflaterbiblioteca de suporte (pelo menos em 28.0.0).

Testado com sucesso com API 15 e superior.

Arquivo de recurso de 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>

(Neste caso, ?attr/appIconColorEnabledera um atributo de cor personalizada nos temas do aplicativo, e os recursos de ícone eram drawables vetoriais.)

Afilu
fonte
5
Esta deve ser a nova resposta aceita! Além disso, observe android:iconTinte android:iconTintModenão funcione, mas prefixar com em app:vez de android:funciona como um encanto (em meus próprios drawables vetoriais, API> = 21)
Sebastiaan Alvarez Rodriguez
Se chamar programaticamente: nota que SupportMenuInflaternão vai aplicar qualquer lógica personalizada se o menu não é um SupportMenucomo MenuBuilder, ele só cai de volta ao normal MenuInflater.
geekley
Nesse caso, o uso AppCompatActivity.startSupportActionMode(callback)e as implementações de suporte apropriadas de androidx.appcompatserão passados ​​para o retorno de chamada.
geekley
30

Eu pessoalmente preferi essa abordagem a partir deste link

Crie um layout XML com o seguinte:

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

e referencie este drawable em seu menu:

<item
    android:id="@+id/option_menu_item_something"
    android:icon="@drawable/ic_action_something_tined"
N Jay
fonte
2
Embora este link possa responder à pergunta, é melhor incluir as partes essenciais da resposta aqui e fornecer o link para referência. As respostas somente com link podem se tornar inválidas se a página vinculada mudar.
tomloprod
Obrigado pelo seu comentário, editei a pergunta. @tomloprod
N Jay
4
Esta é minha solução preferida. No entanto, é importante observar que, por enquanto, essa solução não parece funcionar quando você está usando os novos tipos de drawable vetorial como fonte.
Michael De Soto
1
@haagmm esta solução precisa de API> = 21. Também funciona para vetores.
Neurotransmissor
1
E não deve funcionar com vetores, a tag raiz é bitmap. Existem outras maneiras de colorir vetores. Talvez alguém possa adicionar coloração vetorial aqui também ...
milosmns
11

A maioria das soluções neste segmento usa uma API mais recente, ou usa reflexão, ou usa pesquisa de visualização intensiva para chegar ao MenuItem .

No entanto, há uma abordagem mais elegante para fazer isso. Você precisa de uma barra de ferramentas personalizada, já que seu caso de uso de "aplicar tonalidade personalizada" não funciona bem com a API de estilo / tema público.

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

}

Apenas certifique-se de chamar seu código de atividade / fragmento:

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

Sem reflexo, sem visualização de visualização e sem tanto código, hein?

E agora você pode ignorar o ridículo onCreateOptionsMenu/onOptionsItemSelected.

Desenhou
fonte
Tecnicamente, você está fazendo uma pesquisa de exibição. Você está iterando as visualizações e garantindo que elas não sejam nulas. ;)
Martin Marconcini
Você está definitivamente certo de certa forma :-) No entanto, a Menu#getItem()complexidade é O (1) na barra de ferramentas, porque os itens são armazenados em ArrayList. O que é diferente de View#findViewByIdtravessia (a que me referi como pesquisa de visualização na minha resposta), cuja complexidade está longe de ser constante :-)
Drew
Concordo, na verdade, fiz uma coisa muito semelhante. Ainda estou chocado que o Android não tenha simplificado tudo isso depois de tantos anos ...
Martin Marconcini
Como posso alterar as cores do ícone de estouro e do ícone de hambúrguer com essa abordagem?
Sandra
8

Aqui está a solução que utilizo; você pode chamá-lo após onPrepareOptionsMenu () ou em um local equivalente. O motivo para mutate () é se você usar os ícones em mais de um local; sem a mutação, todos eles assumirão a mesma tonalidade.

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

Isso não vai cuidar do estouro, mas para isso, você pode fazer o seguinte:

Layout:

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

Estilos:

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

Isso funciona a partir de appcompat v23.1.0.

Aprenda OpenGL ES
fonte