1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.appcompat.app;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertNull;
21import static org.junit.Assert.assertSame;
22
23import android.content.res.ColorStateList;
24import android.content.res.Resources;
25import android.graphics.Color;
26import android.graphics.PorterDuff;
27import android.graphics.drawable.Drawable;
28import android.support.test.annotation.UiThreadTest;
29import android.support.test.filters.SmallTest;
30import android.support.test.rule.ActivityTestRule;
31import android.support.test.runner.AndroidJUnit4;
32import android.view.Menu;
33import android.view.MenuItem;
34
35import androidx.annotation.ColorInt;
36import androidx.annotation.NonNull;
37import androidx.appcompat.test.R;
38import androidx.appcompat.testutils.TestUtils;
39import androidx.core.content.res.ResourcesCompat;
40import androidx.core.graphics.ColorUtils;
41import androidx.core.view.MenuItemCompat;
42
43import org.junit.Before;
44import org.junit.Rule;
45import org.junit.Test;
46import org.junit.runner.RunWith;
47
48/**
49 * Test icon tinting in {@link MenuItem}s
50 */
51@SmallTest
52@RunWith(AndroidJUnit4.class)
53public class AppCompatMenuItemIconTintingTest {
54    private AppCompatMenuItemIconTintingTestActivity mActivity;
55    private Resources mResources;
56    private Menu mMenu;
57
58    @Rule
59    public ActivityTestRule<AppCompatMenuItemIconTintingTestActivity> mActivityTestRule =
60            new ActivityTestRule<>(AppCompatMenuItemIconTintingTestActivity.class);
61
62    @Before
63    public void setup() {
64        mActivity = mActivityTestRule.getActivity();
65        mResources = mActivity.getResources();
66        mMenu = mActivity.getToolbarMenu();
67    }
68
69    @UiThreadTest
70    @Test
71    public void testIconTinting() throws Throwable {
72        final MenuItem firstItem = mMenu.getItem(0);
73        final MenuItem secondItem = mMenu.getItem(1);
74        final MenuItem thirdItem = mMenu.getItem(2);
75
76        // These are the default set in layout XML
77        assertNull(MenuItemCompat.getIconTintMode(firstItem));
78        assertEquals(Color.WHITE, MenuItemCompat.getIconTintList(firstItem).getDefaultColor());
79
80        assertEquals(PorterDuff.Mode.SCREEN, MenuItemCompat.getIconTintMode(secondItem));
81        assertNull(MenuItemCompat.getIconTintList(secondItem));
82
83        assertNull(MenuItemCompat.getIconTintMode(thirdItem));
84        assertNull(MenuItemCompat.getIconTintList(thirdItem));
85
86        // Change tint color list and mode and verify that they are returned by the getters
87        final ColorStateList colors = ColorStateList.valueOf(Color.RED);
88
89        MenuItemCompat.setIconTintList(firstItem, colors);
90        MenuItemCompat.setIconTintMode(firstItem, PorterDuff.Mode.XOR);
91        assertSame(colors, MenuItemCompat.getIconTintList(firstItem));
92        assertEquals(PorterDuff.Mode.XOR, MenuItemCompat.getIconTintMode(firstItem));
93
94        // Ensure the tint is preserved across drawable changes.
95        firstItem.setIcon(R.drawable.icon_yellow);
96        assertSame(colors, MenuItemCompat.getIconTintList(firstItem));
97        assertEquals(PorterDuff.Mode.XOR, MenuItemCompat.getIconTintMode(firstItem));
98
99        // Change tint color list and mode again and verify that they are returned by the getters
100        final ColorStateList colorsNew = ColorStateList.valueOf(Color.MAGENTA);
101        MenuItemCompat.setIconTintList(firstItem, colorsNew);
102        MenuItemCompat.setIconTintMode(firstItem, PorterDuff.Mode.SRC_IN);
103        assertSame(colorsNew, MenuItemCompat.getIconTintList(firstItem));
104        assertEquals(PorterDuff.Mode.SRC_IN, MenuItemCompat.getIconTintMode(firstItem));
105    }
106
107    private void verifyIconIsColoredAs(String description, @NonNull Drawable icon,
108            @ColorInt int color, int allowedComponentVariance) {
109        TestUtils.assertAllPixelsOfColor(description,
110                icon, icon.getIntrinsicWidth(), icon.getIntrinsicHeight(), true,
111                color, allowedComponentVariance, false);
112    }
113
114
115    /**
116     * This method tests that icon tinting is not applied when the
117     * menu item has no icon.
118     */
119    @UiThreadTest
120    @Test
121    public void testIconTintingWithNoIcon() {
122        final MenuItem sixthItem = mMenu.getItem(5);
123
124        // Note that all the asserts in this test check that the menu item icon
125        // is null. This is because the matching entry in the XML doesn't define any
126        // icon, and there is nothing to tint.
127        assertNull("No icon after XML loading", sixthItem.getIcon());
128
129        // Load a new color state list, set it on the menu item icon and check that the icon
130        // is still null.
131        final ColorStateList sandColor = ResourcesCompat.getColorStateList(
132                mResources, R.color.color_state_list_sand, null);
133        MenuItemCompat.setIconTintList(sixthItem, sandColor);
134        assertNull("No icon after setting icon tint list", sixthItem.getIcon());
135
136        // Set tint mode on the menu item icon and check that the icon is still null.
137        MenuItemCompat.setIconTintMode(sixthItem, PorterDuff.Mode.MULTIPLY);
138        assertNull("No icon after setting icon tint mode", sixthItem.getIcon());
139    }
140
141    /**
142     * This method tests that icon tinting is applied across a number of
143     * <code>ColorStateList</code>s set as icon tint lists on the same menu item.
144     */
145    @UiThreadTest
146    @Test
147    public void testIconTintingAcrossTintListChange() {
148        final MenuItem firstItem = mMenu.getItem(0);
149
150        final @ColorInt int sandDefault = ResourcesCompat.getColor(
151                mResources, R.color.sand_default, null);
152        final @ColorInt int oceanDefault = ResourcesCompat.getColor(
153                mResources, R.color.ocean_default, null);
154
155        // Test the default state for tinting set up in the menu XML file.
156        verifyIconIsColoredAs("Default white tinting", firstItem.getIcon(), Color.WHITE, 0);
157
158        // Load a new color state list, set it on the menu item and check that the icon has
159        // switched to the matching entry in newly set color state list.
160        final ColorStateList sandColor = ResourcesCompat.getColorStateList(
161                mResources, R.color.color_state_list_sand, null);
162        MenuItemCompat.setIconTintList(firstItem, sandColor);
163        verifyIconIsColoredAs("Default white tinting", firstItem.getIcon(), sandDefault, 0);
164
165        // Load another color state list, set it on the menu item and check that the icon has
166        // switched to the matching entry in newly set color state list.
167        final ColorStateList oceanColor = ResourcesCompat.getColorStateList(
168                mResources, R.color.color_state_list_ocean, null);
169        MenuItemCompat.setIconTintList(firstItem, oceanColor);
170        verifyIconIsColoredAs("Default white tinting", firstItem.getIcon(), oceanDefault, 0);
171    }
172
173    /**
174     * This method tests that opaque icon tinting is applied correctly after changing the icon
175     * itself of the menu item.
176     */
177    @UiThreadTest
178    @Test
179    public void testIconOpaqueTintingAcrossIconChange() {
180        final MenuItem secondItem = mMenu.getItem(1);
181
182        // This is the fill color of R.drawable.icon_black set on our menu icon
183        // that we'll be testing in this method
184        final @ColorInt int iconColorBlack = 0xFF000000;
185
186        // At this point we shouldn't have any tinting since it's not defined in the menu XML
187        verifyIconIsColoredAs("Black icon before any tinting", secondItem.getIcon(),
188                iconColorBlack, 0);
189
190        // Now set up the tinting
191        final ColorStateList lilacColor = ResourcesCompat.getColorStateList(
192                mResources, R.color.color_state_list_lilac, null);
193        final @ColorInt int lilacDefault = ResourcesCompat.getColor(
194                mResources, R.color.lilac_default, null);
195        MenuItemCompat.setIconTintList(secondItem, lilacColor);
196        MenuItemCompat.setIconTintMode(secondItem, PorterDuff.Mode.SRC_OVER);
197
198        // Check that the icon is now tinted
199        verifyIconIsColoredAs("Lilac icon after tinting the black icon",
200                secondItem.getIcon(), lilacDefault, 0);
201
202        // Set a different icon on our menu item
203        secondItem.setIcon(R.drawable.test_drawable_red);
204
205        // Check that the icon is still tinted with the same color as before
206        verifyIconIsColoredAs("Lilac icon after changing icon to red",
207                secondItem.getIcon(), lilacDefault, 0);
208    }
209
210    /**
211     * This method tests that translucent icon tinting is applied correctly after changing the icon
212     * itself of the menu item.
213     */
214    @UiThreadTest
215    @Test
216    public void testIconTranslucentTintingAcrossIconChange() {
217        final MenuItem secondItem = mMenu.getItem(1);
218
219        // This is the fill color of R.drawable.icon_black set on our menu icon
220        // that we'll be testing in this method
221        final @ColorInt int iconColorBlack = 0xFF000000;
222
223        // At this point we shouldn't have any tinting since it's not defined in the menu XML
224        verifyIconIsColoredAs("Black icon before any tinting", secondItem.getIcon(),
225                iconColorBlack, 0);
226
227        final ColorStateList emeraldColor = ResourcesCompat.getColorStateList(
228                mResources, R.color.color_state_list_emerald_translucent, null);
229        final @ColorInt int emeraldDefault = ResourcesCompat.getColor(
230                mResources, R.color.emerald_translucent_default, null);
231        // This is the fill color of R.drawable.test_background_red that will be set on our
232        // menu icon that we'll be testing in this method
233        final @ColorInt int iconColorRed = ResourcesCompat.getColor(
234                mResources, R.color.test_red, null);
235
236        // Set up the tinting of our menu item. The tint list is using translucent color, and the
237        // tint mode is going to be src_over, which will create a "mix" of the original icon with
238        // the translucent tint color.
239        MenuItemCompat.setIconTintList(secondItem, emeraldColor);
240        MenuItemCompat.setIconTintMode(secondItem, PorterDuff.Mode.SRC_OVER);
241
242        // From this point on in this method we're allowing a margin of error in checking the
243        // color of the menu icon. This is due to both translucent colors being used
244        // in the color state list and off-by-one discrepancies of SRC_OVER when it's compositing
245        // translucent color on top of solid fill color. This is where the allowed variance
246        // value of 2 comes from - one for compositing and one for color translucency.
247        final int allowedComponentVariance = 2;
248
249        // Test the tinting set up with the just loaded tint list.
250        verifyIconIsColoredAs("Emerald tinting on green icon",
251                secondItem.getIcon(), ColorUtils.compositeColors(emeraldDefault, iconColorBlack),
252                allowedComponentVariance);
253
254        // Set a different icon on our menu item
255        secondItem.setIcon(R.drawable.test_drawable_red);
256
257        // Test the tinting of the new menu icon with the same color state list
258        verifyIconIsColoredAs("Emerald tinting on red icon",
259                secondItem.getIcon(), ColorUtils.compositeColors(emeraldDefault, iconColorRed),
260                allowedComponentVariance);
261    }
262}
263