]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/utils/Colors.java
adefa6f30e701d538b4f6c02a452f15a4873adff
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / utils / Colors.java
1 /*
2  * Copyright © 2021 Damyan Ivanov.
3  * This file is part of MoLe.
4  * MoLe is free software: you can distribute it and/or modify it
5  * under the term of the GNU General Public License as published by
6  * the Free Software Foundation, either version 3 of the License, or
7  * (at your opinion), any later version.
8  *
9  * MoLe is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License terms for details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with MoLe. If not, see <https://www.gnu.org/licenses/>.
16  */
17
18 package net.ktnx.mobileledger.utils;
19
20 import android.app.Activity;
21 import android.content.res.ColorStateList;
22 import android.content.res.Resources;
23 import android.util.TypedValue;
24
25 import androidx.annotation.ColorInt;
26 import androidx.annotation.NonNull;
27 import androidx.lifecycle.MutableLiveData;
28
29 import net.ktnx.mobileledger.BuildConfig;
30 import net.ktnx.mobileledger.R;
31 import net.ktnx.mobileledger.model.MobileLedgerProfile;
32 import net.ktnx.mobileledger.ui.HueRing;
33
34 import java.util.ArrayList;
35 import java.util.Collections;
36 import java.util.HashMap;
37 import java.util.Locale;
38 import java.util.Objects;
39
40 import static net.ktnx.mobileledger.utils.Logger.debug;
41
42 public class Colors {
43     public static final int DEFAULT_HUE_DEG = 261;
44     public static final MutableLiveData<Integer> themeWatch = new MutableLiveData<>(0);
45     private static final int[][] EMPTY_STATES = new int[][]{new int[0]};
46     private static final int SWIPE_COLOR_COUNT = 6;
47     private static final int[] themeIDs =
48             {R.style.AppTheme_default, R.style.AppTheme_000, R.style.AppTheme_005,
49              R.style.AppTheme_010, R.style.AppTheme_015, R.style.AppTheme_020, R.style.AppTheme_025,
50              R.style.AppTheme_030, R.style.AppTheme_035, R.style.AppTheme_040, R.style.AppTheme_045,
51              R.style.AppTheme_050, R.style.AppTheme_055, R.style.AppTheme_060, R.style.AppTheme_065,
52              R.style.AppTheme_070, R.style.AppTheme_075, R.style.AppTheme_080, R.style.AppTheme_085,
53              R.style.AppTheme_090, R.style.AppTheme_095, R.style.AppTheme_100, R.style.AppTheme_105,
54              R.style.AppTheme_110, R.style.AppTheme_115, R.style.AppTheme_120, R.style.AppTheme_125,
55              R.style.AppTheme_130, R.style.AppTheme_135, R.style.AppTheme_140, R.style.AppTheme_145,
56              R.style.AppTheme_150, R.style.AppTheme_155, R.style.AppTheme_160, R.style.AppTheme_165,
57              R.style.AppTheme_170, R.style.AppTheme_175, R.style.AppTheme_180, R.style.AppTheme_185,
58              R.style.AppTheme_190, R.style.AppTheme_195, R.style.AppTheme_200, R.style.AppTheme_205,
59              R.style.AppTheme_210, R.style.AppTheme_215, R.style.AppTheme_220, R.style.AppTheme_225,
60              R.style.AppTheme_230, R.style.AppTheme_235, R.style.AppTheme_240, R.style.AppTheme_245,
61              R.style.AppTheme_250, R.style.AppTheme_255, R.style.AppTheme_260, R.style.AppTheme_265,
62              R.style.AppTheme_270, R.style.AppTheme_275, R.style.AppTheme_280, R.style.AppTheme_285,
63              R.style.AppTheme_290, R.style.AppTheme_295, R.style.AppTheme_300, R.style.AppTheme_305,
64              R.style.AppTheme_310, R.style.AppTheme_315, R.style.AppTheme_320, R.style.AppTheme_325,
65              R.style.AppTheme_330, R.style.AppTheme_335, R.style.AppTheme_340, R.style.AppTheme_345,
66              R.style.AppTheme_350, R.style.AppTheme_355,
67              };
68     private static final HashMap<Integer, Integer> themePrimaryColor = new HashMap<>();
69     public static @ColorInt
70     int secondary;
71     @ColorInt
72     public static int tableRowDarkBG;
73     public static int profileThemeId = DEFAULT_HUE_DEG;
74     public static void refreshColors(Resources.Theme theme) {
75         TypedValue tv = new TypedValue();
76         theme.resolveAttribute(R.attr.table_row_dark_bg, tv, true);
77         tableRowDarkBG = tv.data;
78         theme.resolveAttribute(R.attr.colorSecondary, tv, true);
79         secondary = tv.data;
80
81         if (themePrimaryColor.size() == 0) {
82             for (int themeId : themeIDs) {
83                 Resources.Theme tmpTheme = theme.getResources()
84                                                 .newTheme();
85                 tmpTheme.applyStyle(themeId, true);
86                 tmpTheme.resolveAttribute(R.attr.colorPrimary, tv, false);
87                 themePrimaryColor.put(themeId, tv.data);
88             }
89         }
90
91         // trigger theme observers
92         themeWatch.postValue(themeWatch.getValue() + 1);
93     }
94     public static @ColorInt
95     int getPrimaryColorForHue(int hueDegrees) {
96         if (hueDegrees == DEFAULT_HUE_DEG)
97             return Objects.requireNonNull(themePrimaryColor.get(R.style.AppTheme_default));
98         int mod = hueDegrees % HueRing.hueStepDegrees;
99         if (mod == 0) {
100             int themeId = getThemeIdForHue(hueDegrees);
101             Integer result = Objects.requireNonNull(themePrimaryColor.get(themeId));
102             debug("colors",
103                     String.format(Locale.US, "getPrimaryColorForHue(%d) = %x", hueDegrees, result));
104             return result;
105         }
106         else {
107             int x0 = hueDegrees - mod;
108             int x1 = (x0 + HueRing.hueStepDegrees) % 360;
109             float y0 = Objects.requireNonNull(themePrimaryColor.get(getThemeIdForHue(x0)));
110             float y1 = Objects.requireNonNull(themePrimaryColor.get(getThemeIdForHue(x1)));
111             return Math.round(y0 + hueDegrees * (y1 - y0) / (x1 - x0));
112         }
113     }
114     public static int getThemeIdForHue(int themeHue) {
115         int themeIndex = -1;
116         if (themeHue == 360)
117             themeHue = 0;
118         if ((themeHue >= 0) && (themeHue < 360) && (themeHue != DEFAULT_HUE_DEG)) {
119             if ((themeHue % HueRing.hueStepDegrees) != 0) {
120                 Logger.warn("profiles",
121                         String.format(Locale.US, "Adjusting unexpected hue %d", themeHue));
122                 themeIndex = Math.round(1f * themeHue / HueRing.hueStepDegrees);
123             }
124             else
125                 themeIndex = themeHue / HueRing.hueStepDegrees;
126         }
127
128         return themeIDs[themeIndex + 1];    // 0 is the default theme
129     }
130     public static void setupTheme(Activity activity, int themeHue) {
131         int themeId = getThemeIdForHue(themeHue);
132         activity.setTheme(themeId);
133
134         refreshColors(activity.getTheme());
135     }
136     public static @NonNull
137     ColorStateList getColorStateList() {
138         return getColorStateList(profileThemeId);
139     }
140     public static @NonNull
141     ColorStateList getColorStateList(int hue) {
142         return new ColorStateList(EMPTY_STATES, getSwipeCircleColors(hue));
143     }
144     public static int[] getSwipeCircleColors() {
145         return getSwipeCircleColors(profileThemeId);
146     }
147     public static int[] getSwipeCircleColors(int hue) {
148         int[] colors = new int[SWIPE_COLOR_COUNT];
149         for (int i = 0; i < SWIPE_COLOR_COUNT; i++, hue = (hue + 360 / SWIPE_COLOR_COUNT) % 360) {
150             colors[i] = getPrimaryColorForHue(hue);
151         }
152         return colors;
153     }
154     public static int getNewProfileThemeHue(ArrayList<MobileLedgerProfile> profiles) {
155         if ((profiles == null) || (profiles.size() == 0))
156             return DEFAULT_HUE_DEG;
157
158         int chosenHue;
159
160         if (profiles.size() == 1) {
161             int opposite = profiles.get(0)
162                                    .getThemeHue() + 180;
163             opposite %= 360;
164             chosenHue = opposite;
165         }
166         else {
167             ArrayList<Integer> hues = new ArrayList<>();
168             for (MobileLedgerProfile p : profiles) {
169                 int hue = p.getThemeHue();
170                 if (hue == -1)
171                     hue = DEFAULT_HUE_DEG;
172                 hues.add(hue);
173             }
174             Collections.sort(hues);
175             if (BuildConfig.DEBUG) {
176                 StringBuilder huesSB = new StringBuilder();
177                 for (int h : hues) {
178                     if (huesSB.length() > 0)
179                         huesSB.append(", ");
180                     huesSB.append(h);
181                 }
182                 debug("profiles", String.format("used hues: %s", huesSB.toString()));
183             }
184             hues.add(hues.get(0));
185
186             int lastHue = -1;
187             int largestInterval = 0;
188             ArrayList<Integer> largestIntervalStarts = new ArrayList<>();
189
190             for (int h : hues) {
191                 if (lastHue == -1) {
192                     lastHue = h;
193                     continue;
194                 }
195
196                 int interval;
197                 if (h > lastHue)
198                     interval = h - lastHue;     // 10 -> 20 is a step of 10
199                 else
200                     interval = h + (360 - lastHue);    // 350 -> 20 is a step of 30
201
202                 if (interval > largestInterval) {
203                     largestInterval = interval;
204                     largestIntervalStarts.clear();
205                     largestIntervalStarts.add(lastHue);
206                 }
207                 else if (interval == largestInterval) {
208                     largestIntervalStarts.add(lastHue);
209                 }
210
211                 lastHue = h;
212             }
213
214             final int chosenIndex = (int) (Math.random() * largestIntervalStarts.size());
215             int chosenIntervalStart = largestIntervalStarts.get(chosenIndex);
216
217             debug("profiles",
218                     String.format(Locale.US, "Choosing the middle colour between %d and %d",
219                             chosenIntervalStart, chosenIntervalStart + largestInterval));
220
221             if (largestInterval % 2 != 0)
222                 largestInterval++;    // round up the middle point
223
224             chosenHue = (chosenIntervalStart + (largestInterval / 2)) % 360;
225         }
226
227         final int mod = chosenHue % HueRing.hueStepDegrees;
228         if (mod != 0) {
229             if (mod > HueRing.hueStepDegrees / 2)
230                 chosenHue += (HueRing.hueStepDegrees - mod); // 13 += (5-3) = 15
231             else
232                 chosenHue -= mod;       // 12 -= 2 = 10
233         }
234
235         debug("profiles", String.format(Locale.US, "New profile hue: %d", chosenHue));
236
237         return chosenHue;
238     }
239 }