1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
package it.niedermann.nextcloud.deck.util;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.util.Pair;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public final class ColorUtil {
private static final Map<ColorPair, Boolean> CONTRAST_RATIO_SUFFICIENT_CACHE = new HashMap<>();
private static final Map<Integer, Integer> FOREGROUND_CACHE = new HashMap<>();
private static final Map<Integer, Boolean> IS_DARK_COLOR_CACHE = new HashMap<>();
private ColorUtil() {
}
@ColorInt
public static int getForegroundColorForBackgroundColor(@ColorInt int color) {
Integer ret = FOREGROUND_CACHE.get(color);
if (ret == null) {
if (Color.TRANSPARENT == color)
ret = Color.BLACK;
else if (isColorDark(color))
ret = Color.WHITE;
else
ret = Color.BLACK;
FOREGROUND_CACHE.put(color, ret);
}
return ret;
}
/**
* @return well formatted string starting with a hash followed by 6 hex numbers that is parsable by {@link Color#parseColor(String)}.
*/
public static String formatColorToParsableHexString(String input) {
if (input == null) {
throw new IllegalArgumentException("input color string is null");
}
if (isParsableValidHexColorString(input)) {
return input;
}
final char[] chars = input.replaceAll("#", "").toCharArray();
final StringBuilder sb = new StringBuilder(7).append("#");
switch (chars.length) {
case 8: { // Strip alpha channel
sb.append(Arrays.copyOfRange(chars, 0, 6));
break;
}
case 6: { // Default long
sb.append(chars);
break;
}
case 4: { // Strip alpha channel
for (char c : Arrays.copyOfRange(chars, 0, 3)) {
sb.append(c).append(c);
}
break;
}
case 3: { // Default short
for (char c : chars) {
sb.append(c).append(c);
}
break;
}
default: {
throw new IllegalArgumentException("unparsable color string: \"" + input + "\"");
}
}
final String formattedHexColor = sb.toString();
if (isParsableValidHexColorString(formattedHexColor)) {
return formattedHexColor;
} else {
throw new IllegalArgumentException("\"" + input + "\" is not a valid color string. Result of tried normalizing: " + formattedHexColor);
}
}
/**
* Checking for {@link Color#parseColor(String)} being able to parse the input is the important part because we don't know the implementation and rely on it to be able to parse the color.
*
* @return true, if the input starts with a hash followed by 6 characters of hex numbers and is parsable by {@link Color#parseColor(String)}.
*/
private static boolean isParsableValidHexColorString(@NonNull String input) {
try {
Color.parseColor(input);
return input.matches("#[a-fA-F0-9]{6}");
} catch (Exception e) {
return false;
}
}
public static boolean isColorDark(@ColorInt int color) {
Boolean ret = IS_DARK_COLOR_CACHE.get(color);
if (ret == null) {
ret = getBrightness(color) < 200;
IS_DARK_COLOR_CACHE.put(color, ret);
}
return ret;
}
private static int getBrightness(@ColorInt int color) {
final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)};
return (int) Math.sqrt(rgb[0] * rgb[0] * .241 + rgb[1]
* rgb[1] * .691 + rgb[2] * rgb[2] * .068);
}
// ---------------------------------------------------
// Based on https://github.com/LeaVerou/contrast-ratio
// ---------------------------------------------------
public static boolean contrastRatioIsSufficient(@ColorInt int colorOne, @ColorInt int colorTwo) {
ColorPair key = new ColorPair(colorOne, colorTwo);
Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
if (ret == null) {
ret = getContrastRatio(colorOne, colorTwo) > 3d;
CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
return ret;
}
return ret;
}
public static boolean contrastRatioIsSufficientBigAreas(@ColorInt int colorOne, @ColorInt int colorTwo) {
ColorPair key = new ColorPair(colorOne, colorTwo);
Boolean ret = CONTRAST_RATIO_SUFFICIENT_CACHE.get(key);
if (ret == null) {
ret = getContrastRatio(colorOne, colorTwo) > 1.47d;
CONTRAST_RATIO_SUFFICIENT_CACHE.put(key, ret);
return ret;
}
return ret;
}
public static double getContrastRatio(@ColorInt int colorOne, @ColorInt int colorTwo) {
final double lum1 = getLuminanace(colorOne);
final double lum2 = getLuminanace(colorTwo);
final double brightest = Math.max(lum1, lum2);
final double darkest = Math.min(lum1, lum2);
return (brightest + 0.05) / (darkest + 0.05);
}
private static double getLuminanace(@ColorInt int color) {
final int[] rgb = {Color.red(color), Color.green(color), Color.blue(color)};
return getSubcolorLuminance(rgb[0]) * 0.2126 + getSubcolorLuminance(rgb[1]) * 0.7152 + getSubcolorLuminance(rgb[2]) * 0.0722;
}
private static double getSubcolorLuminance(@ColorInt int color) {
final double value = color / 255d;
return value <= 0.03928
? value / 12.92
: Math.pow((value + 0.055) / 1.055, 2.4);
}
private static class ColorPair extends Pair<Integer, Integer> {
private ColorPair(@Nullable Integer first, @Nullable Integer second) {
super(first, second);
}
@SuppressWarnings({"EqualsWhichDoesntCheckParameterClass", "NumberEquality"})
@Override
public boolean equals(Object o) {
final ColorPair colorPair = (ColorPair) o;
if (first != colorPair.first) return false;
return second == colorPair.second;
}
@SuppressWarnings("ConstantConditions")
@Override
public int hashCode() {
int result = first;
result = 31 * result + second;
return result;
}
}
public static String intColorToHexString(int color) {
return String.format("%06X", (0xFFFFFF & color));
}
}
|