app/src/main/java/com/androidstudy/andelatrackchallenge/utils/Easel.java
package com.androidstudy.andelatrackchallenge.utils;
/**
* Created by anonymous on 11/2/17.
*/
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.AppCompatEditText;
import android.support.v7.widget.SwitchCompat;
import android.support.v7.widget.Toolbar;
import android.util.TypedValue;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EdgeEffect;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.Spinner;
import android.widget.TextView;
import com.androidstudy.andelatrackchallenge.R;
import java.lang.reflect.Field;
import java.util.ArrayList;
/**
* Apply tinting to widgets, drawables, all the color things you would ever need!
*/
public class Easel {
/**
* Get a darker version of the specified color (10% darker)
*
* @param color starting color
* @return darker color
*/
public static int getDarkerColor(@ColorInt int color) {
return getDarkerColor(color, 0.9f);
}
/**
* Get a darker version of the color based on the ratio
*
* @param color starting color
* @param darkerAmount value between 0 and 1 to darken the color by
* @return darker color
*/
public static int getDarkerColor(@ColorInt int color, float darkerAmount) {
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] *= darkerAmount;
return Color.HSVToColor(hsv);
}
/**
* Get a color from the attribute theme
*
* @param context theme context
* @param attributeColor the attribute color, ex R.attr.colorPrimary
* @return the color, or {@link Color#TRANSPARENT} if failed to resolve
*/
public static int getThemeAttrColor(@NonNull Context context, @AttrRes int attributeColor) {
int[] attrs = new int[]{attributeColor};
TypedArray ta = context.obtainStyledAttributes(attrs);
int color = ta.getColor(0, Color.TRANSPARENT);
ta.recycle();
return color;
}
/**
* Get a drawable from the attribute theme
*
* @param context theme context
* @param attributeDrawable the attribute drawable, ex R.attr.selectableItemBackground
* @return the drawable, if it exists in the theme context
*/
@Nullable
public static Drawable getThemeAttrDrawable(@NonNull Context context, @AttrRes int attributeDrawable) {
int[] attrs = new int[]{attributeDrawable};
TypedArray ta = context.obtainStyledAttributes(attrs);
Drawable drawableFromTheme = ta.getDrawable(0);
ta.recycle();
return drawableFromTheme;
}
/**
* Get a dimen from the attribute theme
*
* @param context theme context
* @param attributeDimen the attribute dimen, ex R.attr.actionBarSize
* @return the dimen pixel size, if it exists in the theme context. Otherwise, -1
*/
public static float getThemeAttrDimen(@NonNull Context context, @AttrRes int attributeDimen) {
TypedValue tv = new TypedValue();
int value = -1;
if (context.getTheme().resolveAttribute(attributeDimen, tv, true)) {
value = TypedValue.complexToDimensionPixelSize(tv.data, context.getResources().getDisplayMetrics());
}
return value;
}
/**
* Gets the color with the alpha changed by a factor.
*
* @param color original color
* @param factor factor, such as 0.5f for 50%
* @return the color with the adjusted alpha
*/
public static int adjustAlpha(@ColorInt int color, float factor) {
return Color.argb(Math.round(Color.alpha(color) * factor), Color.red(color), Color.green(color), Color.blue(color));
}
public static boolean isColorDark(@ColorInt int color) {
double darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255;
return darkness >= 0.5;
}
/**
* Simplified way of getting a drawable and tinting it to a certain color
*
* @param context context
* @param resId the drawable resource ID
* @param color the color to tint the drawable to
* @return the tinted drawable
*/
public static Drawable tint(@NonNull Context context, @DrawableRes int resId, @ColorInt int color) {
return tint(ContextCompat.getDrawable(context, resId), color);
}
/**
* Simplified way of tinting a drawable to a certain color
*
* @param drawable the drawable to tint
* @param color the color to tint the drawable to
* @return the tinted drawable
*/
public static Drawable tint(@NonNull Drawable drawable, @ColorInt int color) {
drawable = DrawableCompat.wrap(drawable);
DrawableCompat.setTint(drawable, color);
return drawable;
}
/**
* Tint the button
*
* @param button the button
* @param color the color
*/
public static void tint(@NonNull Button button, @ColorInt int color) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{}
}, new int[]{
color
});
ViewCompat.setBackgroundTintList(button, sl);
}
/**
* Tint the checkbox
*
* @param checkBox the checkbox
* @param color the color
*/
public static void tint(@NonNull CheckBox checkBox, @ColorInt int color) {
final int disabledColor = getDisabledColor(checkBox.getContext());
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked},
new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked}
}, new int[]{
getThemeAttrColor(checkBox.getContext(), R.attr.colorControlNormal),
color,
disabledColor,
disabledColor
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
checkBox.setButtonTintList(sl);
} else {
Drawable checkDrawable = ContextCompat.getDrawable(checkBox.getContext(), R.drawable.abc_btn_check_material);
Drawable drawable = DrawableCompat.wrap(checkDrawable);
DrawableCompat.setTintList(drawable, sl);
checkBox.setButtonDrawable(drawable);
}
}
/**
* Tint the edit text
*
* @param editText the edit text
* @param color the color
*/
@SuppressWarnings("RestrictedApi")
public static void tint(@NonNull EditText editText, @ColorInt int color) {
ColorStateList editTextColorStateList = createEditTextColorStateList(editText.getContext(), color);
if (editText instanceof AppCompatEditText) {
((AppCompatEditText) editText).setSupportBackgroundTintList(editTextColorStateList);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
editText.setBackgroundTintList(editTextColorStateList);
}
tint((TextView) editText, color);
}
/**
* Tint all menu items within a menu to be a certain color. Note that this does not tint the
* overflow menu. Call {@link #tintOverflow(Toolbar, int)} for that.
*
* @param menu the menu
* @param color the color
*/
public static void tint(@NonNull Menu menu, @ColorInt int color) {
for (int i = 0; i < menu.size(); i++) {
MenuItem menuItem = menu.getItem(i);
if (menuItem.getIcon() != null) {
tint(menuItem, color);
}
}
}
/**
* Tint a menu item
*
* @param menuItem the menu item
* @param color the color
*/
public static void tint(@NonNull MenuItem menuItem, @ColorInt int color) {
Drawable icon = menuItem.getIcon();
if (icon != null) {
icon.setColorFilter(color, PorterDuff.Mode.MULTIPLY);
} else {
throw new IllegalArgumentException("Menu item does not have an icon");
}
}
/**
* Tint the progressbar
*
* @param progressBar the progress bar
* @param color the color
*/
public static void tint(@NonNull ProgressBar progressBar, @ColorInt int color) {
tint(progressBar, color, false);
}
public static void tint(@NonNull ProgressBar progressBar, @ColorInt int color, boolean skipIndeterminate) {
ColorStateList sl = ColorStateList.valueOf(color);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
progressBar.setProgressTintList(sl);
progressBar.setSecondaryProgressTintList(sl);
if (!skipIndeterminate)
progressBar.setIndeterminateTintList(sl);
} else {
PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
mode = PorterDuff.Mode.MULTIPLY;
}
if (!skipIndeterminate && progressBar.getIndeterminateDrawable() != null)
progressBar.getIndeterminateDrawable().setColorFilter(color, mode);
if (progressBar.getProgressDrawable() != null)
progressBar.getProgressDrawable().setColorFilter(color, mode);
}
}
/**
* Tint the radio button
*
* @param radioButton the radio button
* @param color the color
*/
public static void tint(@NonNull RadioButton radioButton, @ColorInt int color) {
final int disabledColor = getDisabledColor(radioButton.getContext());
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked},
new int[]{-android.R.attr.state_enabled, -android.R.attr.state_checked},
new int[]{-android.R.attr.state_enabled, android.R.attr.state_checked}
}, new int[]{
getThemeAttrColor(radioButton.getContext(), R.attr.colorControlNormal),
color,
disabledColor,
disabledColor
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
radioButton.setButtonTintList(sl);
} else {
Drawable radioDrawable = ContextCompat.getDrawable(radioButton.getContext(), R.drawable.abc_btn_radio_material);
Drawable d = DrawableCompat.wrap(radioDrawable);
DrawableCompat.setTintList(d, sl);
radioButton.setButtonDrawable(d);
}
}
/**
* Tint the {@link SeekBar}
*
* @param seekBar the seekbar
* @param color the color
*/
public static void tint(@NonNull SeekBar seekBar, @ColorInt int color) {
ColorStateList s1 = ColorStateList.valueOf(color);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
seekBar.setThumbTintList(s1);
seekBar.setProgressTintList(s1);
} else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
Drawable progressDrawable = DrawableCompat.wrap(seekBar.getProgressDrawable());
seekBar.setProgressDrawable(progressDrawable);
DrawableCompat.setTintList(progressDrawable, s1);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Drawable thumbDrawable = DrawableCompat.wrap(seekBar.getThumb());
DrawableCompat.setTintList(thumbDrawable, s1);
seekBar.setThumb(thumbDrawable);
}
} else {
PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN;
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
mode = PorterDuff.Mode.MULTIPLY;
}
if (seekBar.getIndeterminateDrawable() != null)
seekBar.getIndeterminateDrawable().setColorFilter(color, mode);
if (seekBar.getProgressDrawable() != null)
seekBar.getProgressDrawable().setColorFilter(color, mode);
}
}
public static void tint(@NonNull Spinner spinner, @ColorInt int color) {
spinner.getBackground().setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
public static void tint(@NonNull SwitchCompat switchCompat, @ColorInt int color) {
tint(switchCompat, color, getThemeAttrColor(switchCompat.getContext(), R.attr.colorSwitchThumbNormal));
}
public static void tint(@NonNull SwitchCompat switchCompat, @ColorInt int color, @ColorInt int unpressedColor) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_checked},
new int[]{android.R.attr.state_checked}
}, new int[]{
unpressedColor,
color
});
DrawableCompat.setTintList(switchCompat.getThumbDrawable(), sl);
DrawableCompat.setTintList(switchCompat.getTrackDrawable(), createSwitchTrackColorStateList(switchCompat.getContext(), color));
}
/**
* Tint the textview. This is a convenience call through to {@link #tintCursor(TextView, int)}, {@link #tintHandles(TextView, int)}
* and {@link #tintSelectionHighlight(TextView, int)}
*
* @param textView text view
* @param color the color
*/
public static void tint(@NonNull TextView textView, @ColorInt int color) {
tintCursor(textView, color);
tintHandles(textView, color);
tintSelectionHighlight(textView, adjustAlpha(color, 0.3f));
}
/**
* Tints the cursor color of the EditText. Kinda hacky using reflection, but it gets the job done.
* http://stackoverflow.com/questions/25996032/how-to-change-programatically-edittext-cursor-color-in-android
*
* @param editText editText to change the cursor color
* @param color color to change the cursor to
* @return true if cursor was successfully tinted, false if Android has changed the field names and reflection has failed us
*/
public static boolean tintCursor(@NonNull TextView editText, @ColorInt int color) {
try {
Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes");
fCursorDrawableRes.setAccessible(true);
int mCursorDrawableRes = fCursorDrawableRes.getInt(editText);
Field fEditor = getEditorField();
Object editor = fEditor.get(editText);
Class<?> clazz = editor.getClass();
Field fCursorDrawable = clazz.getDeclaredField("mCursorDrawable");
fCursorDrawable.setAccessible(true);
Drawable[] drawables = new Drawable[2];
drawables[0] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);
drawables[0].setColorFilter(color, PorterDuff.Mode.SRC_IN);
drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);
drawables[1].setColorFilter(color, PorterDuff.Mode.SRC_IN);
fCursorDrawable.set(editor, drawables);
} catch (Exception e) {
return false;
}
return true;
}
/**
* Tint the handles that appear when you select text
*
* @param textView the text view
* @param color the color
* @return true if properly tinted, false if reflection has failed us
*/
public static boolean tintHandles(@NonNull TextView textView, @ColorInt int color) {
try {
Field fTextSelectHandleLeftRes = getField(TextView.class, "mTextSelectHandleLeftRes");
Field fTextSelectHandleRightRes = getField(TextView.class, "mTextSelectHandleRightRes");
Field fTextSelectHandleCenterRes = getField(TextView.class, "mTextSelectHandleRes");
int leftRes = fTextSelectHandleLeftRes.getInt(textView);
int rightRes = fTextSelectHandleRightRes.getInt(textView);
int centerRes = fTextSelectHandleCenterRes.getInt(textView);
Field fEditor = getEditorField();
Object editor = fEditor.get(textView);
Class<?> clazz = editor.getClass();
Field fHandleLeftDrawable = getField(clazz, "mSelectHandleLeft");
Field fHandleRightDrawable = getField(clazz, "mSelectHandleRight");
Field fHandleCenterDrawable = getField(clazz, "mSelectHandleCenter");
setDrawable(textView, editor, fHandleLeftDrawable, color, leftRes);
setDrawable(textView, editor, fHandleRightDrawable, color, rightRes);
setDrawable(textView, editor, fHandleCenterDrawable, color, centerRes);
} catch (Exception e) {
return false;
}
return true;
}
/**
* Tint the highlight that appears when you select text
*
* @param textView the text view
* @param color the color to tint to
* @return true if success, false if reflection failed
*/
public static boolean tintSelectionHighlight(@NonNull TextView textView, @ColorInt int color) {
//You would think you would modify mHighlightPaint, but no, you need to modify mHighlightColor,
//as it gets set as the color on the paint on each draw call
try {
Field fHighlightColor = getField(TextView.class, "mHighlightColor");
fHighlightColor.set(textView, color);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
private static void setDrawable(TextView textView, Object editor, Field field, @ColorInt int color,
@DrawableRes int drawableRes) throws IllegalAccessException {
Drawable drawable = ContextCompat.getDrawable(textView.getContext(), drawableRes)
.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
field.set(editor, drawable);
}
/**
* Sets the color of the overflow menu item within the Toolbar.
*
* @param toolbar the toolbar
* @param color color to set the overflow icon to
* @return true if tint was set.
*/
public static boolean tintOverflow(@NonNull Toolbar toolbar, @ColorInt int color) {
@SuppressLint("PrivateResource") final String overflowDescription = toolbar.getContext().getString(R.string.abc_action_menu_overflow_description);
final ArrayList<View> outViews = new ArrayList<>();
toolbar.findViewsWithText(outViews, overflowDescription,
View.FIND_VIEWS_WITH_CONTENT_DESCRIPTION);
if (outViews.isEmpty()) {
return false;
}
ImageView overflow = (ImageView) outViews.get(0);
overflow.setColorFilter(color);
return true;
}
/**
* Tint the edge effect when you reach the end of a scroll view. API 21+ only
*
* @param scrollableView the scrollable view, such as a {@link android.widget.ScrollView}
* @param color the color
* @return true if it worked, false if it did not
*/
@TargetApi(21)
public static boolean tintEdgeEffect(@NonNull View scrollableView, @ColorInt int color) {
//http://stackoverflow.com/questions/27104521/android-lollipop-scrollview-edge-effect-color
boolean outcome = false;
final String[] edgeGlows = {"mEdgeGlowTop", "mEdgeGlowBottom", "mEdgeGlowLeft", "mEdgeGlowRight"};
for (String edgeGlow : edgeGlows) {
Class<?> clazz = scrollableView.getClass();
while (clazz != null) {
try {
final Field edgeGlowField = clazz.getDeclaredField(edgeGlow);
edgeGlowField.setAccessible(true);
final EdgeEffect edgeEffect = (EdgeEffect) edgeGlowField.get(scrollableView);
edgeEffect.setColor(color);
outcome = true;
break;
} catch (Exception e) {
clazz = clazz.getSuperclass();
}
}
}
return outcome;
}
private static Field getEditorField() throws NoSuchFieldException {
Field fEditor = TextView.class.getDeclaredField("mEditor");
fEditor.setAccessible(true);
return fEditor;
}
private static ColorStateList createEditTextColorStateList(@NonNull Context context, @ColorInt int color) {
int[][] states = new int[3][];
int[] colors = new int[3];
int i = 0;
states[i] = new int[]{-android.R.attr.state_enabled};
colors[i] = getThemeAttrColor(context, R.attr.colorControlNormal);
i++;
states[i] = new int[]{-android.R.attr.state_pressed, -android.R.attr.state_focused};
colors[i] = getThemeAttrColor(context, R.attr.colorControlNormal);
i++;
states[i] = new int[]{};
colors[i] = color;
return new ColorStateList(states, colors);
}
@ColorInt
private static int getDisabledColor(Context context) {
final int primaryColor = getThemeAttrColor(context, android.R.attr.textColorPrimary);
final int disabledColor = isColorDark(primaryColor) ? Color.BLACK : Color.WHITE;
return adjustAlpha(disabledColor, 0.3f);
}
private static ColorStateList createSwitchTrackColorStateList(Context context, @ColorInt int color) {
final int[][] states = new int[3][];
final int[] colors = new int[3];
int i = 0;
// Disabled state
states[i] = new int[]{-android.R.attr.state_enabled};
colors[i] = adjustAlpha(getThemeAttrColor(context, R.attr.colorControlNormal), 0.1f);
i++;
states[i] = new int[]{android.R.attr.state_checked};
colors[i] = adjustAlpha(color, 0.3f);
i++;
// Default enabled state
states[i] = new int[0];
colors[i] = adjustAlpha(getThemeAttrColor(context, R.attr.colorControlNormal), 0.5f);
return new ColorStateList(states, colors);
}
static Field getField(Class clazz, String fieldName) throws NoSuchFieldException {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field;
}
}