]> git.ktnx.net Git - mobile-ledger.git/blob - app/src/main/java/net/ktnx/mobileledger/ui/HueRing.java
fixed-locale String.format
[mobile-ledger.git] / app / src / main / java / net / ktnx / mobileledger / ui / HueRing.java
1 /*
2  * Copyright © 2019 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.ui;
19
20 import android.content.Context;
21 import android.graphics.Canvas;
22 import android.graphics.Paint;
23 import android.graphics.Path;
24 import android.graphics.RectF;
25 import android.graphics.Shader;
26 import android.graphics.SweepGradient;
27 import android.util.AttributeSet;
28 import android.view.MotionEvent;
29 import android.view.View;
30
31 import androidx.annotation.Nullable;
32
33 import net.ktnx.mobileledger.utils.Colors;
34 import net.ktnx.mobileledger.utils.DimensionUtils;
35
36
37 import static net.ktnx.mobileledger.utils.Logger.debug;
38
39 public class HueRing extends View {
40     public static final int hueStepDegrees = 5;
41     private Paint ringPaint, initialPaint, currentPaint, markerPaint;
42     private int centerX, centerY;
43     private int diameter;
44     private int padding;
45     private int initialHueDegrees;
46     private int color, hueDegrees;
47     private float outerR;
48     private float innerR;
49     private float bandWidth;
50     private float ringR;
51     private float innerDiameter;
52     private float centerR;
53     private RectF centerRect;
54     private RectF ringRect;
55     private int markerOverflow;
56     private int markerStrokeWidth;
57     public HueRing(Context context, @Nullable AttributeSet attrs) {
58         super(context, attrs);
59         init(Colors.DEFAULT_HUE_DEG);
60     }
61     public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
62         super(context, attrs, defStyleAttr);
63         init(Colors.DEFAULT_HUE_DEG);
64     }
65     public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
66                    int defStyleRes) {
67         super(context, attrs, defStyleAttr, defStyleRes);
68         init(Colors.DEFAULT_HUE_DEG);
69     }
70     public HueRing(Context context) {
71         super(context);
72         init(Colors.DEFAULT_HUE_DEG);
73     }
74     public HueRing(Context context, int initialHueDegrees) {
75         super(context);
76         init(initialHueDegrees);
77     }
78     private void init(int initialHueDegrees) {
79         final int[] steps = {Colors.getPrimaryColorForHue(0),      // red
80                              Colors.getPrimaryColorForHue(60),     // yellow
81                              Colors.getPrimaryColorForHue(120),    // green
82                              Colors.getPrimaryColorForHue(180),    // cyan
83                              Colors.getPrimaryColorForHue(240),    // blue
84                              Colors.getPrimaryColorForHue(300),    // magenta
85                              Colors.getPrimaryColorForHue(360),    // red, again
86         };
87         Shader rainbow = new SweepGradient(0, 0, steps, null);
88
89         ringPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
90         ringPaint.setShader(rainbow);
91         ringPaint.setStyle(Paint.Style.STROKE);
92
93         initialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
94         initialPaint.setStyle(Paint.Style.FILL);
95
96         currentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
97         currentPaint.setStyle(Paint.Style.FILL);
98
99         setInitialHue(initialHueDegrees);
100         setHue(initialHueDegrees);
101
102         markerStrokeWidth = DimensionUtils.dp2px(getContext(), 4);
103
104         padding = markerStrokeWidth * 2 + 2;
105
106         markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
107         markerPaint.setStyle(Paint.Style.STROKE);
108         markerPaint.setColor(0xa0000000);
109         markerPaint.setStrokeWidth(markerStrokeWidth);
110     }
111     public int getColor() {
112         return color;
113     }
114     public int getHueDegrees() {
115         return hueDegrees;
116     }
117     public void setHue(int hueDegrees) {
118         if (hueDegrees == -1)
119             hueDegrees = Colors.DEFAULT_HUE_DEG;
120
121         if (hueDegrees != Colors.DEFAULT_HUE_DEG) {
122             // round to 15 degrees
123             int rem = hueDegrees % hueStepDegrees;
124             if (rem < (hueStepDegrees / 2))
125                 hueDegrees -= rem;
126             else
127                 hueDegrees += hueStepDegrees - rem;
128         }
129
130         this.hueDegrees = hueDegrees;
131         this.color = Colors.getPrimaryColorForHue(hueDegrees);
132         currentPaint.setColor(this.color);
133         invalidate();
134     }
135     private void setHue(float hue) {
136         setHue((int) (360f * hue));
137     }
138     @Override
139     protected void onDraw(Canvas canvas) {
140         super.onDraw(canvas);
141
142         float center = getWidth() / 2f;
143         ringPaint.setStrokeWidth((int) bandWidth);
144
145         canvas.save();
146         canvas.translate(center, center);
147         canvas.drawOval(ringRect, ringPaint);
148
149         canvas.drawArc(centerRect, 180, 180, true, initialPaint);
150         canvas.drawArc(centerRect, 0, 180, true, currentPaint);
151
152         canvas.restore();
153         drawMarker(canvas, center);
154     }
155     private void drawMarker(Canvas canvas, float center) {
156         float leftRadians = (float) Math.toRadians(-hueStepDegrees / 2f);
157         float rightRadians = (float) Math.toRadians(hueStepDegrees / 2f);
158         float sl = (float) Math.sin(leftRadians);
159         float sr = (float) Math.sin(rightRadians);
160         float cl = (float) Math.cos(leftRadians);
161         float cr = (float) Math.cos(rightRadians);
162         float innerEdge = innerR - 1.5f * markerStrokeWidth;
163         float outerEdge = outerR + 1.5f + markerStrokeWidth;
164         Path p = new Path();
165 //        p.arcTo(-innerEdge, -innerEdge, innerEdge, innerEdge, -hueStepDegrees / 2f,
166 //                hueStepDegrees, true);
167 //        p.lineTo(outerEdge * cr, outerEdge * sr);
168         p.arcTo(-outerEdge, -outerEdge, outerEdge, outerEdge, hueStepDegrees / 2f, -hueStepDegrees,
169                 false);
170 //        p.close();
171         canvas.save();
172         canvas.translate(center, center);
173         canvas.rotate(hueDegrees, 0, 0);
174         canvas.drawPath(p, markerPaint);
175         canvas.restore();
176     }
177     @Override
178     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
179         int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
180         int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
181         int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
182         int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
183
184         if ((widthMode == MeasureSpec.AT_MOST) && (heightMode == MeasureSpec.AT_MOST)) {
185             diameter = Math.min(widthSize, heightSize);
186         }
187         else {
188             setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
189             return;
190         }
191
192         setMeasuredDimension(diameter, diameter);
193
194 //        padding = DimensionUtils.dp2px(getContext(),
195 //                getContext().getResources().getDimension(R.dimen.activity_horizontal_margin)) / 2;
196         diameter -= 2 * padding;
197         outerR = diameter / 2f;
198         centerX = padding + (int) outerR;
199         centerY = centerX;
200
201         bandWidth = diameter / 3.5f;
202         ringR = outerR - bandWidth / 2f;
203         innerR = outerR - bandWidth;
204
205         ringRect = new RectF(-ringR, -ringR, ringR, ringR);
206
207         innerDiameter = diameter - 2 * bandWidth;
208         centerR = innerDiameter * 0.5f;
209         centerRect = new RectF(-centerR, -centerR, centerR, centerR);
210     }
211     @Override
212     public boolean onTouchEvent(MotionEvent event) {
213         switch (event.getAction()) {
214             case MotionEvent.ACTION_DOWN:
215             case MotionEvent.ACTION_MOVE:
216                 float x = event.getX() - centerX;
217                 float y = event.getY() - centerY;
218
219                 float dist = (float) Math.hypot(x, y);
220
221                 if (dist < centerR) {
222                     if (y < 0) {
223                         setHue(initialHueDegrees);
224                     }
225
226                     break;
227                 }
228                 float angleRad = (float) Math.atan2(y, x);
229                 // angleRad is [-𝜋; +𝜋]
230                 float hue = (float) (angleRad / (2 * Math.PI));
231                 if (hue < 0)
232                     hue += 1;
233                 debug("TMP", String.format(Locale.US,
234                         "x=%1.3f, y=%1.3f, angle=%1.3f rad, hueDegrees=%1.3f", x, y, angleRad,
235                         hue));
236                 setHue(hue);
237                 break;
238         }
239
240         return true;
241     }
242     public void setInitialHue(int initialHue) {
243         if (initialHue == -1)
244             initialHue = Colors.DEFAULT_HUE_DEG;
245         this.initialHueDegrees = initialHue;
246         this.initialPaint.setColor(Colors.getPrimaryColorForHue(initialHue));
247         invalidate();
248     }
249 }