PlayerAndroid/appthemehelper/src/main/java/code/name/monkey/appthemehelper/util/TintHelper.java

490 lines
23 KiB
Java
Executable File

package code.name.monkey.appthemehelper.util;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Build;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RadioButton;
import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import androidx.annotation.CheckResult;
import androidx.annotation.ColorInt;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import androidx.appcompat.widget.SwitchCompat;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.drawable.DrawableCompat;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import java.lang.reflect.Field;
import code.name.monkey.appthemehelper.R;
/**
* @author afollestad, plusCubed
*/
public final class TintHelper {
@SuppressWarnings("JavaReflectionMemberAccess")
public static void colorHandles(@NonNull TextView view, int color) {
try {
Field editorField = TextView.class.getDeclaredField("mEditor");
if (!editorField.isAccessible()) {
editorField.setAccessible(true);
}
Object editor = editorField.get(view);
Class<?> editorClass = editor.getClass();
String[] handleNames = {"mSelectHandleLeft", "mSelectHandleRight", "mSelectHandleCenter"};
String[] resNames = {"mTextSelectHandleLeftRes", "mTextSelectHandleRightRes", "mTextSelectHandleRes"};
for (int i = 0; i < handleNames.length; i++) {
Field handleField = editorClass.getDeclaredField(handleNames[i]);
if (!handleField.isAccessible()) {
handleField.setAccessible(true);
}
Drawable handleDrawable = (Drawable) handleField.get(editor);
if (handleDrawable == null) {
Field resField = TextView.class.getDeclaredField(resNames[i]);
if (!resField.isAccessible()) {
resField.setAccessible(true);
}
int resId = resField.getInt(view);
handleDrawable = view.getResources().getDrawable(resId);
}
if (handleDrawable != null) {
Drawable drawable = handleDrawable.mutate();
drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
handleField.set(editor, drawable);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
@CheckResult
@NonNull
public static Drawable createTintedDrawable(Context context,
@DrawableRes int res, @ColorInt int color) {
Drawable drawable = ContextCompat.getDrawable(context, res);
return createTintedDrawable(drawable, color);
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
@NonNull
public static Drawable createTintedDrawable(@Nullable Drawable drawable, @ColorInt int color) {
if (drawable == null) {
return null;
}
drawable = DrawableCompat.wrap(drawable.mutate());
DrawableCompat.setTintMode(drawable, PorterDuff.Mode.SRC_IN);
DrawableCompat.setTint(drawable, color);
return drawable;
}
// This returns a NEW Drawable because of the mutate() call. The mutate() call is necessary because Drawables with the same resource have shared states otherwise.
@CheckResult
@Nullable
public static Drawable createTintedDrawable(@Nullable Drawable drawable, @NonNull ColorStateList sl) {
if (drawable == null) {
return null;
}
Drawable temp = DrawableCompat.wrap(drawable.mutate());
DrawableCompat.setTintList(temp, sl);
return temp;
}
public static void setCursorTint(@NonNull EditText editText, @ColorInt int color) {
try {
Field fCursorDrawableRes = TextView.class.getDeclaredField("mCursorDrawableRes");
fCursorDrawableRes.setAccessible(true);
int mCursorDrawableRes = fCursorDrawableRes.getInt(editText);
Field fEditor = TextView.class.getDeclaredField("mEditor");
fEditor.setAccessible(true);
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] = createTintedDrawable(drawables[0], color);
drawables[1] = ContextCompat.getDrawable(editText.getContext(), mCursorDrawableRes);
drawables[1] = createTintedDrawable(drawables[1], color);
fCursorDrawable.set(editor, drawables);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void setTint(@NonNull RadioButton radioButton, @ColorInt int color, boolean useDarker) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
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[]{
// Rdio button includes own alpha for disabled state
ColorUtil.INSTANCE.stripAlpha(ContextCompat.getColor(radioButton.getContext(),
useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light)),
ContextCompat.getColor(radioButton.getContext(),
useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
radioButton.setButtonTintList(sl);
} else {
Drawable d = createTintedDrawable(
ContextCompat.getDrawable(radioButton.getContext(), R.drawable.abc_btn_radio_material), sl);
radioButton.setButtonDrawable(d);
}
}
public static void setTint(@NonNull SeekBar seekBar, @ColorInt int color, boolean useDarker) {
final ColorStateList s1 = getDisabledColorStateList(color, ContextCompat.getColor(seekBar.getContext(),
useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light));
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 = createTintedDrawable(seekBar.getProgressDrawable(), s1);
seekBar.setProgressDrawable(progressDrawable);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
Drawable thumbDrawable = createTintedDrawable(seekBar.getThumb(), 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 setTint(@NonNull ProgressBar progressBar, @ColorInt int color) {
setTint(progressBar, color, false);
}
public static void setTint(@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);
}
}
}
public static void setTint(@NonNull EditText editText, @ColorInt int color, boolean useDarker) {
final ColorStateList editTextColorStateList = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_pressed, -android.R.attr.state_focused},
new int[]{}
}, new int[]{
ContextCompat.getColor(editText.getContext(),
useDarker ? R.color.ate_text_disabled_dark : R.color.ate_text_disabled_light),
ContextCompat.getColor(editText.getContext(),
useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
if (editText instanceof AppCompatEditText) {
((AppCompatEditText) editText).setSupportBackgroundTintList(editTextColorStateList);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
editText.setBackgroundTintList(editTextColorStateList);
}
setCursorTint(editText, color);
}
public static void setTint(@NonNull CheckBox box, @ColorInt int color, boolean useDarker) {
ColorStateList sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
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[]{
ContextCompat.getColor(box.getContext(),
useDarker ? R.color.ate_control_disabled_dark : R.color.ate_control_disabled_light),
ContextCompat.getColor(box.getContext(),
useDarker ? R.color.ate_control_normal_dark : R.color.ate_control_normal_light),
color
});
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
box.setButtonTintList(sl);
} else {
Drawable drawable = createTintedDrawable(
ContextCompat.getDrawable(box.getContext(), R.drawable.abc_btn_check_material), sl);
box.setButtonDrawable(drawable);
}
}
public static void setTint(@NonNull ImageView image, @ColorInt int color) {
image.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
}
public static void setTint(@NonNull Switch switchView, @ColorInt int color, boolean useDarker) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
return;
}
if (switchView.getTrackDrawable() != null) {
switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getTrackDrawable(), color, false, false, useDarker));
}
if (switchView.getThumbDrawable() != null) {
switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getThumbDrawable(), color, true, false, useDarker));
}
}
public static void setTint(@NonNull SwitchCompat switchView, @ColorInt int color, boolean useDarker) {
if (switchView.getTrackDrawable() != null) {
switchView.setTrackDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getTrackDrawable(), color, false, true, useDarker));
}
if (switchView.getThumbDrawable() != null) {
switchView.setThumbDrawable(modifySwitchDrawable(switchView.getContext(),
switchView.getThumbDrawable(), color, true, true, useDarker));
}
}
public static void setTintAuto(final @NonNull View view, final @ColorInt int color,
boolean background) {
setTintAuto(view, color, background, ATHUtil.INSTANCE.isWindowBackgroundDark(view.getContext()));
}
@SuppressWarnings("deprecation")
public static void setTintAuto(final @NonNull View view, final @ColorInt int color,
boolean background, final boolean isDark) {
if (!background) {
if (view instanceof FloatingActionButton) {
setTint((FloatingActionButton) view, color, isDark);
} else if (view instanceof RadioButton) {
setTint((RadioButton) view, color, isDark);
} else if (view instanceof SeekBar) {
setTint((SeekBar) view, color, isDark);
} else if (view instanceof ProgressBar) {
setTint((ProgressBar) view, color);
} else if (view instanceof EditText) {
setTint((EditText) view, color, isDark);
} else if (view instanceof CheckBox) {
setTint((CheckBox) view, color, isDark);
} else if (view instanceof ImageView) {
setTint((ImageView) view, color);
} else if (view instanceof Switch) {
setTint((Switch) view, color, isDark);
} else if (view instanceof SwitchCompat) {
setTint((SwitchCompat) view, color, isDark);
} else {
background = true;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
!background && view.getBackground() instanceof RippleDrawable) {
// Ripples for the above views (e.g. when you tap and hold a switch or checkbox)
RippleDrawable rd = (RippleDrawable) view.getBackground();
@SuppressLint("PrivateResource") final int unchecked = ContextCompat.getColor(view.getContext(),
isDark ? R.color.ripple_material_dark : R.color.ripple_material_light);
final int checked = ColorUtil.INSTANCE.adjustAlpha(color, 0.4f);
final ColorStateList sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_activated, -android.R.attr.state_checked},
new int[]{android.R.attr.state_activated},
new int[]{android.R.attr.state_checked}
},
new int[]{
unchecked,
checked,
checked
}
);
rd.setColor(sl);
}
}
if (background) {
// Need to tint the background of a view
if (view instanceof FloatingActionButton || view instanceof Button) {
setTintSelector(view, color, false, isDark);
} else if (view.getBackground() != null) {
Drawable drawable = view.getBackground();
if (drawable != null) {
drawable = createTintedDrawable(drawable, color);
ViewUtil.INSTANCE.setBackgroundCompat(view, drawable);
}
}
}
}
@SuppressWarnings("deprecation")
public static void setTintSelector(@NonNull View view, @ColorInt final int color, final boolean darker,
final boolean useDarkTheme) {
final boolean isColorLight = ColorUtil.INSTANCE.isColorLight(color);
final int disabled = ContextCompat.getColor(view.getContext(),
useDarkTheme ? R.color.ate_button_disabled_dark : R.color.ate_button_disabled_light);
final int pressed = ColorUtil.INSTANCE.shiftColor(color, darker ? 0.9f : 1.1f);
final int activated = ColorUtil.INSTANCE.shiftColor(color, darker ? 1.1f : 0.9f);
final int rippleColor = getDefaultRippleColor(view.getContext(), isColorLight);
final int textColor = ContextCompat.getColor(view.getContext(),
isColorLight ? R.color.ate_primary_text_light : R.color.ate_primary_text_dark);
final ColorStateList sl;
if (view instanceof Button) {
sl = getDisabledColorStateList(color, disabled);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP &&
view.getBackground() instanceof RippleDrawable) {
RippleDrawable rd = (RippleDrawable) view.getBackground();
rd.setColor(ColorStateList.valueOf(rippleColor));
}
// Disabled text color state for buttons, may get overridden later by ATE tags
final Button button = (Button) view;
button.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(),
useDarkTheme ? R.color.ate_button_text_disabled_dark : R.color.ate_button_text_disabled_light)));
} else if (view instanceof FloatingActionButton) {
// FloatingActionButton doesn't support disabled state?
sl = new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_pressed},
new int[]{android.R.attr.state_pressed}
}, new int[]{
color,
pressed
});
final FloatingActionButton fab = (FloatingActionButton) view;
fab.setRippleColor(rippleColor);
fab.setBackgroundTintList(sl);
if (fab.getDrawable() != null) {
fab.setImageDrawable(createTintedDrawable(fab.getDrawable(), textColor));
}
return;
} else {
sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, android.R.attr.state_pressed},
new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
},
new int[]{
disabled,
color,
pressed,
activated,
activated
}
);
}
Drawable drawable = view.getBackground();
if (drawable != null) {
drawable = createTintedDrawable(drawable, sl);
ViewUtil.INSTANCE.setBackgroundCompat(view, drawable);
}
if (view instanceof TextView && !(view instanceof Button)) {
final TextView tv = (TextView) view;
tv.setTextColor(getDisabledColorStateList(textColor, ContextCompat.getColor(view.getContext(),
isColorLight ? R.color.ate_text_disabled_light : R.color.ate_text_disabled_dark)));
}
}
@SuppressLint("PrivateResource")
@ColorInt
private static int getDefaultRippleColor(@NonNull Context context, boolean useDarkRipple) {
// Light ripple is actually translucent black, and vice versa
return ContextCompat.getColor(context, useDarkRipple ?
R.color.ripple_material_light : R.color.ripple_material_dark);
}
@NonNull
private static ColorStateList getDisabledColorStateList(@ColorInt int normal, @ColorInt int disabled) {
return new ColorStateList(new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled}
}, new int[]{
disabled,
normal
});
}
private static Drawable modifySwitchDrawable(@NonNull Context context, @NonNull Drawable from, @ColorInt int tint,
boolean thumb, boolean compatSwitch, boolean useDarker) {
if (useDarker) {
tint = ColorUtil.INSTANCE.shiftColor(tint, 1.1f);
}
tint = ColorUtil.INSTANCE.adjustAlpha(tint, (compatSwitch && !thumb) ? 0.5f : 1.0f);
int disabled;
int normal;
if (thumb) {
disabled = ContextCompat.getColor(context,
useDarker ? R.color.ate_switch_thumb_disabled_dark : R.color.ate_switch_thumb_disabled_light);
normal = ContextCompat.getColor(context,
useDarker ? R.color.ate_switch_thumb_normal_dark : R.color.ate_switch_thumb_normal_light);
} else {
disabled = ContextCompat.getColor(context,
useDarker ? R.color.ate_switch_track_disabled_dark : R.color.ate_switch_track_disabled_light);
normal = ContextCompat.getColor(context,
useDarker ? R.color.ate_switch_track_normal_dark : R.color.ate_switch_track_normal_light);
}
// Stock switch includes its own alpha
if (!compatSwitch) {
normal = ColorUtil.INSTANCE.stripAlpha(normal);
}
final ColorStateList sl = new ColorStateList(
new int[][]{
new int[]{-android.R.attr.state_enabled},
new int[]{android.R.attr.state_enabled, -android.R.attr.state_activated,
-android.R.attr.state_checked},
new int[]{android.R.attr.state_enabled, android.R.attr.state_activated},
new int[]{android.R.attr.state_enabled, android.R.attr.state_checked}
},
new int[]{
disabled,
normal,
tint,
tint
}
);
return createTintedDrawable(from, sl);
}
private static void setTint(final FloatingActionButton view, final int color, final boolean isDark) {
view.setImageTintList(ColorStateList.valueOf(color));
}
}