Java tutorial
/* * The MIT License (MIT) * * Copyright (c) 2016 Carlos Andres Jimenez <apps@carlosandresjimenez.co> * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package co.carlosjimenez.android.currencyalerts.app; import android.app.Activity; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.os.Build; import android.support.v4.view.MenuItemCompat; import android.support.v7.view.menu.MenuItemImpl; import android.support.v7.widget.ActionMenuView; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.MenuItem.OnActionExpandListener; import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.SearchView; import java.lang.reflect.Method; /** * <p>Apply colors and/or transparency to menu icons in a {@link Menu}.</p> * <p/> * <p>Example usage:</p> * <p/> * <pre class="prettyprint"> * public boolean onCreateOptionsMenu(Menu menu) { * ... * int color = getResources().getColor(R.color.your_awesome_color); * int alpha = 204; // 80% transparency * MenuTint.on(menu).setMenuItemIconColor(color).setMenuItemIconAlpha(alpha).apply(this); * ... * } * </pre> */ public class MenuTint { private static final String LOG_TAG = MenuTint.class.getSimpleName(); private static Method nativeIsActionButton; private final Menu menu; private final Integer originalMenuItemIconColor; private final Integer menuItemIconAlpha; private final Integer subMenuIconColor; private final Integer subMenuIconAlpha; private final Integer overflowDrawableId; private final boolean reApplyOnChange; private final boolean forceIcons; private Integer menuItemIconColor; private ImageView overflowButton; private ViewGroup actionBarView; private MenuTint(Builder builder) { menu = builder.menu; originalMenuItemIconColor = builder.originalMenuItemIconColor; menuItemIconColor = builder.menuItemIconColor; menuItemIconAlpha = builder.menuItemIconAlpha; subMenuIconColor = builder.subMenuIconColor; subMenuIconAlpha = builder.subMenuIconAlpha; overflowDrawableId = builder.overflowDrawableId; reApplyOnChange = builder.reApplyOnChange; forceIcons = builder.forceIcons; } /** * Check if an item is showing (not in the overflow menu). * * @param item the MenuItem. * @return {@code true} if the MenuItem is visible on the ActionBar. */ public static boolean isActionButton(MenuItem item) { if (item instanceof MenuItemImpl) { return ((MenuItemImpl) item).isActionButton(); } if (nativeIsActionButton == null) { try { Class<?> MenuItemImpl = Class.forName("com.android.internal.view.menu.MenuItemImpl"); nativeIsActionButton = MenuItemImpl.getDeclaredMethod("isActionButton"); if (!nativeIsActionButton.isAccessible()) { nativeIsActionButton.setAccessible(true); } } catch (Exception ignored) { } } try { return (boolean) nativeIsActionButton.invoke(item, (Object[]) null); } catch (Exception e) { e.printStackTrace(); } return true; } /** * Check if an item is in the overflow menu. * * @param item the MenuItem * @return {@code true} if the MenuItem is in the overflow menu. * @see #isActionButton(MenuItem) */ public static boolean isInOverflow(MenuItem item) { return !isActionButton(item); } /** * Sets the color filter and/or the alpha transparency on a {@link MenuItem}'s icon. * * @param menuItem The {@link MenuItem} to theme. * @param color The color to set for the color filter or {@code null} for no changes. * @param alpha The alpha value (0...255) to set on the icon or {@code null} for no changes. */ public static void colorMenuItem(MenuItem menuItem, Integer color, Integer alpha) { if (color == null && alpha == null) { return; // nothing to do. } Drawable drawable = menuItem.getIcon(); if (drawable != null) { // If we don't mutate the drawable, then all drawables with this id will have the ColorFilter drawable.mutate(); if (color != null) { drawable.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP)); } if (alpha != null) { drawable.setAlpha(alpha); } } } /** * Set the menu to show MenuItem icons in the overflow window. * * @param menu the menu to force icons to show */ public static void forceMenuIcons(Menu menu) { try { Class<?> MenuBuilder = menu.getClass(); Method setOptionalIconsVisible = MenuBuilder.getDeclaredMethod("setOptionalIconsVisible", boolean.class); if (!setOptionalIconsVisible.isAccessible()) { setOptionalIconsVisible.setAccessible(true); } setOptionalIconsVisible.invoke(menu, true); } catch (Exception ignored) { } } public static Builder on(Menu menu) { return new Builder(menu); } /** * Apply a ColorFilter with the specified color to all icons in the menu. * * @param activity the Activity. * @param menu the menu after items have been added. * @param color the color for the ColorFilter. */ public static void colorIcons(Activity activity, Menu menu, int color) { MenuTint.on(menu).setMenuItemIconColor(color).apply(activity); } /** * @param activity the Activity * @return the OverflowMenuButton or {@code null} if it doesn't exist. */ public static ImageView getOverflowMenuButton(Activity activity) { return findOverflowMenuButton(activity, findActionBar(activity)); } private static ImageView findOverflowMenuButton(Activity activity, ViewGroup viewGroup) { if (viewGroup == null) { return null; } ImageView overflow = null; for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) { View v = viewGroup.getChildAt(i); if (v instanceof ImageView && (v.getClass().getSimpleName().equals("OverflowMenuButton") || v instanceof ActionMenuView.ActionMenuChildView)) { overflow = (ImageView) v; } else if (v instanceof ViewGroup) { overflow = findOverflowMenuButton(activity, (ViewGroup) v); } if (overflow != null) { break; } } return overflow; } private static ViewGroup findActionBar(Activity activity) { int id = activity.getResources().getIdentifier("action_bar", "id", "android"); ViewGroup actionBar = null; if (id != 0) { actionBar = (ViewGroup) activity.findViewById(id); } if (actionBar == null) { actionBar = findToolbar((ViewGroup) activity.findViewById(android.R.id.content).getRootView()); } return actionBar; } private static ViewGroup findToolbar(ViewGroup viewGroup) { ViewGroup toolbar = null; for (int i = 0, len = viewGroup.getChildCount(); i < len; i++) { View view = viewGroup.getChildAt(i); if (view.getClass() == android.support.v7.widget.Toolbar.class || view.getClass().getName().equals("android.widget.Toolbar")) { toolbar = (ViewGroup) view; } else if (view instanceof ViewGroup) { toolbar = findToolbar((ViewGroup) view); } if (toolbar != null) { break; } } return toolbar; } /** * <p>Sets a ColorFilter and/or alpha on all the {@link MenuItem}s in the menu, including the * OverflowMenuButton.</p> * <p/> * <p>Call this method after inflating/creating your menu in * {@link Activity#onCreateOptionsMenu(Menu)}.</p> * <p/> * <p>Note: This is targeted for the native ActionBar/Toolbar, not AppCompat.</p> * * @param activity the activity to apply the menu tinting on. */ public void apply(final Activity activity) { if (forceIcons) { forceMenuIcons(menu); } for (int i = 0, size = menu.size(); i < size; i++) { MenuItem item = menu.getItem(i); colorMenuItem(item, menuItemIconColor, menuItemIconAlpha); if (reApplyOnChange) { View view = item.getActionView(); if (view != null) { if (item instanceof MenuItemImpl) { ((MenuItemImpl) item) .setSupportOnActionExpandListener(new SupportActionExpandListener(this)); } else { item.setOnActionExpandListener(new NativeActionExpandListener(this)); } } } } actionBarView = findActionBar(activity); if (actionBarView == null) { Log.w(LOG_TAG, "Could not find the ActionBar"); return; } // We must wait for the view to be created to set a color filter on the drawables. actionBarView.post(new Runnable() { @Override public void run() { for (int i = 0, size = menu.size(); i < size; i++) { MenuItem menuItem = menu.getItem(i); if (isInOverflow(menuItem)) { colorMenuItem(menuItem, subMenuIconColor, subMenuIconAlpha); } if (menuItem.hasSubMenu()) { SubMenu subMenu = menuItem.getSubMenu(); for (int j = 0; j < subMenu.size(); j++) { colorMenuItem(subMenu.getItem(j), subMenuIconColor, subMenuIconAlpha); } } } if (menuItemIconColor != null || menuItemIconAlpha != null) { overflowButton = findOverflowMenuButton(activity, actionBarView); colorOverflowMenuItem(overflowButton); } } }); } /** * <p>Sets a ColorFilter and/or alpha on all the {@link MenuItem}s in the menu, including the * OverflowMenuButton.</p> * <p/> * <p>This should only be called after calling {@link #apply(Activity)}. It is useful for when * {@link MenuItem}s might be re-arranged due to an action view being collapsed or expanded.</p> */ public void reapply() { for (int i = 0, size = menu.size(); i < size; i++) { MenuItem item = menu.getItem(i); if (isActionButton(item)) { colorMenuItem(menu.getItem(i), menuItemIconColor, menuItemIconAlpha); } } if (actionBarView == null) { return; } actionBarView.post(new Runnable() { @Override public void run() { for (int i = 0, size = menu.size(); i < size; i++) { MenuItem menuItem = menu.getItem(i); if (isInOverflow(menuItem)) { colorMenuItem(menuItem, subMenuIconColor, subMenuIconAlpha); } else { colorMenuItem(menu.getItem(i), menuItemIconColor, menuItemIconAlpha); } if (menuItem.hasSubMenu()) { SubMenu subMenu = menuItem.getSubMenu(); for (int j = 0; j < subMenu.size(); j++) { colorMenuItem(subMenu.getItem(j), subMenuIconColor, subMenuIconAlpha); } } } if (menuItemIconColor != null || menuItemIconAlpha != null) { colorOverflowMenuItem(overflowButton); } } }); } private void colorOverflowMenuItem(ImageView overflow) { if (overflow != null) { if (overflowDrawableId != null) { overflow.setImageResource(overflowDrawableId); } if (menuItemIconColor != null) { overflow.setColorFilter(menuItemIconColor); } if (menuItemIconAlpha != null) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { overflow.setImageAlpha(menuItemIconAlpha); } else { overflow.setAlpha(menuItemIconAlpha); } } } } public Menu getMenu() { return menu; } public ImageView getOverflowMenuButton() { return overflowButton; } public void setMenuItemIconColor(Integer color) { menuItemIconColor = color; } public static class NativeActionExpandListener implements OnActionExpandListener { private final MenuTint menuTint; public NativeActionExpandListener(MenuTint menuTint) { this.menuTint = menuTint; } @Override public boolean onMenuItemActionExpand(MenuItem item) { int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor : menuTint.originalMenuItemIconColor; menuTint.setMenuItemIconColor(color); menuTint.reapply(); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor : menuTint.originalMenuItemIconColor; menuTint.setMenuItemIconColor(color); menuTint.reapply(); return true; } } public static class SupportActionExpandListener implements MenuItemCompat.OnActionExpandListener { private final MenuTint menuTint; public SupportActionExpandListener(MenuTint menuTint) { this.menuTint = menuTint; } @Override public boolean onMenuItemActionExpand(MenuItem item) { int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor : menuTint.originalMenuItemIconColor; menuTint.setMenuItemIconColor(color); menuTint.reapply(); return true; } @Override public boolean onMenuItemActionCollapse(MenuItem item) { int color = menuTint.originalMenuItemIconColor == null ? menuTint.menuItemIconColor : menuTint.originalMenuItemIconColor; menuTint.setMenuItemIconColor(color); menuTint.reapply(); return true; } } // -------------------------------------------------------------------------------------------- public static final class Builder { private final Menu menu; private Integer menuItemIconColor; private Integer menuItemIconAlpha; private Integer subMenuIconColor; private Integer subMenuIconAlpha; private Integer overflowDrawableId; private Integer originalMenuItemIconColor; private boolean reApplyOnChange; private boolean forceIcons; private Builder(Menu menu) { this.menu = menu; } /** * <p>Sets an {@link OnActionExpandListener} on all {@link MenuItem}s with views, so when the * menu is updated, the colors will be also.</p> * <p/> * <p>This is useful when the overflow menu is showing icons and {@link MenuItem}s might be * pushed to the overflow menu when a action view is expanded e.g. android.widget.SearchView. * </p> * * @param reapply {@code true} to set the listeners on all {@link MenuItem}s with action views. * @return this Builder object to allow for chaining of calls to set methods */ public Builder reapplyOnChange(boolean reapply) { reApplyOnChange = reapply; return this; } /** * Specify an alpha value for visible MenuItem icons, including the OverflowMenuButton. * * @param alpha the alpha value for the drawable. 0 means fully transparent, and 255 means fully * opaque. * @return this Builder object to allow for chaining of calls to set methods */ public Builder setMenuItemIconAlpha(int alpha) { menuItemIconAlpha = alpha; return this; } /** * Specify a color for visible MenuItem icons, including the OverflowMenuButton. * * @param color the color to apply on visible MenuItem icons, including the OverflowMenuButton. * @return this Builder object to allow for chaining of calls to set methods */ public Builder setMenuItemIconColor(int color) { menuItemIconColor = color; return this; } /** * Specify a color that is applied when an action view is expanded or collapsed. * * @param color the color to apply on MenuItems when an action-view is expanded or collapsed. * @return this Builder object to allow for chaining of calls to set methods */ public Builder setOriginalMenuItemIconColor(int color) { originalMenuItemIconColor = color; return this; } /** * Set the drawable id to set on the OverflowMenuButton. * * @param drawableId the resource identifier of the drawable * @return this Builder object to allow for chaining of calls to set methods */ public Builder setOverflowDrawableId(int drawableId) { overflowDrawableId = drawableId; return this; } /** * Specify an alpha value for MenuItems that are in a SubMenu or in the Overflow menu. * * @param alpha the alpha value for the drawable. 0 means fully transparent, and 255 means fully * opaque. * @return this Builder object to allow for chaining of calls to set methods */ public Builder setSubMenuIconAlpha(int alpha) { subMenuIconAlpha = alpha; return this; } /** * Specify a color for MenuItems that are in a SubMenu or in the Overflow menu. * * @param color the color to apply on visible MenuItem icons, including the OverflowMenuButton. * @return this Builder object to allow for chaining of calls to set methods */ public Builder setSubMenuIconColor(int color) { subMenuIconColor = color; return this; } /** * Set the menu to show MenuItem icons in the overflow window. * * @return this Builder object to allow for chaining of calls to set methods */ public Builder forceIcons() { forceIcons = true; return this; } /** * <p>Sets a ColorFilter and/or alpha on all the MenuItems in the menu, including the * OverflowMenuButton.</p> * <p/> * <p>Call this method after inflating/creating your menu in</p> * {@link Activity#onCreateOptionsMenu(Menu)}.</p> * <p/> * <p>Note: This is targeted for the native ActionBar/Toolbar, not AppCompat.</p> */ public MenuTint apply(Activity activity) { MenuTint theme = new MenuTint(this); theme.apply(activity); return theme; } /** * <p>Creates a {@link MenuTint} with the arguments supplied to this builder.</p> * <p/> * <p>It does not apply the theme. Call {@link MenuTint#apply(Activity)} to do so.</p> * * @see #apply(Activity) */ public MenuTint create() { return new MenuTint(this); } } // -------------------------------------------------------------------------------------------- /** * Auto collapses the SearchView when the soft keyboard is dismissed. */ public static class SearchViewFocusListener implements View.OnFocusChangeListener { private final MenuItem item; public SearchViewFocusListener(MenuItem item) { this.item = item; } @Override public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus && item != null) { item.collapseActionView(); if (v instanceof SearchView) { ((SearchView) v).setQuery("", false); } } } } }