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