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.
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.
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/>.
18 package net.ktnx.mobileledger.ui;
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;
31 import androidx.annotation.Nullable;
33 import net.ktnx.mobileledger.utils.Colors;
34 import net.ktnx.mobileledger.utils.DimensionUtils;
36 import java.util.Locale;
38 import static net.ktnx.mobileledger.utils.Logger.debug;
40 public class HueRing extends View {
41 public static final int hueStepDegrees = 5;
42 private Paint ringPaint, initialPaint, currentPaint, markerPaint;
45 private int initialHueDegrees;
46 private int color, hueDegrees;
49 private float bandWidth;
50 private float centerR;
51 private RectF centerRect = new RectF();
52 private RectF ringRect = new RectF();
53 private int markerOverflow;
54 private int markerStrokeWidth;
55 public HueRing(Context context, @Nullable AttributeSet attrs) {
56 super(context, attrs);
57 init(Colors.DEFAULT_HUE_DEG);
59 public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
60 super(context, attrs, defStyleAttr);
61 init(Colors.DEFAULT_HUE_DEG);
63 public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
65 super(context, attrs, defStyleAttr, defStyleRes);
66 init(Colors.DEFAULT_HUE_DEG);
68 public HueRing(Context context) {
70 init(Colors.DEFAULT_HUE_DEG);
72 public HueRing(Context context, int initialHueDegrees) {
74 init(initialHueDegrees);
76 private void init(int initialHueDegrees) {
77 final int[] steps = {Colors.getPrimaryColorForHue(0), // red
78 Colors.getPrimaryColorForHue(60), // yellow
79 Colors.getPrimaryColorForHue(120), // green
80 Colors.getPrimaryColorForHue(180), // cyan
81 Colors.getPrimaryColorForHue(240), // blue
82 Colors.getPrimaryColorForHue(300), // magenta
83 Colors.getPrimaryColorForHue(360), // red, again
85 Shader rainbow = new SweepGradient(0, 0, steps, null);
87 ringPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
88 ringPaint.setShader(rainbow);
89 ringPaint.setStyle(Paint.Style.STROKE);
91 initialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
92 initialPaint.setStyle(Paint.Style.FILL);
94 currentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
95 currentPaint.setStyle(Paint.Style.FILL);
97 setInitialHue(initialHueDegrees);
98 setHue(initialHueDegrees);
100 markerStrokeWidth = DimensionUtils.dp2px(getContext(), 4);
102 padding = markerStrokeWidth * 2 + 2;
104 markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
105 markerPaint.setStyle(Paint.Style.STROKE);
106 markerPaint.setColor(0xa0000000);
107 markerPaint.setStrokeWidth(markerStrokeWidth);
109 public int getColor() {
112 public int getHueDegrees() {
115 public void setHue(int hueDegrees) {
116 if (hueDegrees == -1)
117 hueDegrees = Colors.DEFAULT_HUE_DEG;
119 if (hueDegrees != Colors.DEFAULT_HUE_DEG) {
120 // round to 15 degrees
121 int rem = hueDegrees % hueStepDegrees;
122 if (rem < (hueStepDegrees / 2))
125 hueDegrees += hueStepDegrees - rem;
128 this.hueDegrees = hueDegrees;
129 this.color = Colors.getPrimaryColorForHue(hueDegrees);
130 currentPaint.setColor(this.color);
133 private void setHue(float hue) {
134 setHue((int) (360f * hue));
137 protected void onDraw(Canvas canvas) {
138 super.onDraw(canvas);
140 float center = getWidth() / 2f;
141 ringPaint.setStrokeWidth((int) bandWidth);
144 canvas.translate(center, center);
145 canvas.drawOval(ringRect, ringPaint);
147 canvas.drawArc(centerRect, 180, 180, true, initialPaint);
148 canvas.drawArc(centerRect, 0, 180, true, currentPaint);
151 drawMarker(canvas, center);
153 private void drawMarker(Canvas canvas, float center) {
154 float leftRadians = (float) Math.toRadians(-hueStepDegrees / 2f);
155 float rightRadians = (float) Math.toRadians(hueStepDegrees / 2f);
156 float sl = (float) Math.sin(leftRadians);
157 float sr = (float) Math.sin(rightRadians);
158 float cl = (float) Math.cos(leftRadians);
159 float cr = (float) Math.cos(rightRadians);
160 float innerEdge = innerR - 1.5f * markerStrokeWidth;
161 float outerEdge = outerR + 1.5f + markerStrokeWidth;
163 // p.arcTo(-innerEdge, -innerEdge, innerEdge, innerEdge, -hueStepDegrees / 2f,
164 // hueStepDegrees, true);
165 // p.lineTo(outerEdge * cr, outerEdge * sr);
166 p.arcTo(-outerEdge, -outerEdge, outerEdge, outerEdge, hueStepDegrees / 2f, -hueStepDegrees,
170 canvas.translate(center, center);
171 canvas.rotate(hueDegrees, 0, 0);
172 canvas.drawPath(p, markerPaint);
176 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
177 int widthMode = View.MeasureSpec.getMode(widthMeasureSpec);
178 int widthSize = View.MeasureSpec.getSize(widthMeasureSpec);
179 int heightMode = View.MeasureSpec.getMode(heightMeasureSpec);
180 int heightSize = View.MeasureSpec.getSize(heightMeasureSpec);
183 if ((widthMode == MeasureSpec.AT_MOST) && (heightMode == MeasureSpec.AT_MOST)) {
184 diameter = Math.min(widthSize, heightSize);
187 setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
191 setMeasuredDimension(diameter, diameter);
193 // padding = DimensionUtils.dp2px(getContext(),
194 // getContext().getResources().getDimension(R.dimen.activity_horizontal_margin)) / 2;
195 diameter -= 2 * padding;
196 outerR = diameter / 2f;
197 center = padding + (int) outerR;
199 bandWidth = diameter / 3.5f;
200 float ringR = outerR - bandWidth / 2f;
201 innerR = outerR - bandWidth;
203 ringRect.set(-ringR, -ringR, ringR, ringR);
205 float innerDiameter = diameter - 2 * bandWidth;
206 centerR = innerDiameter * 0.5f;
207 centerRect.set(-centerR, -centerR, centerR, centerR);
210 public boolean performClick() {
211 return super.performClick();
214 public boolean onTouchEvent(MotionEvent event) {
215 switch (event.getAction()) {
216 case MotionEvent.ACTION_DOWN:
217 case MotionEvent.ACTION_MOVE:
218 float x = event.getX() - center;
219 float y = event.getY() - center;
221 float dist = (float) Math.hypot(x, y);
223 if (dist < centerR) {
225 setHue(initialHueDegrees);
230 float angleRad = (float) Math.atan2(y, x);
231 // angleRad is [-𝜋; +𝜋]
232 float hue = (float) (angleRad / (2 * Math.PI));
235 debug("TMP", String.format(Locale.US,
236 "x=%1.3f, y=%1.3f, angle=%1.3f rad, hueDegrees=%1.3f", x, y, angleRad,
240 case MotionEvent.ACTION_UP:
246 public void setInitialHue(int initialHue) {
247 if (initialHue == -1)
248 initialHue = Colors.DEFAULT_HUE_DEG;
249 this.initialHueDegrees = initialHue;
250 this.initialPaint.setColor(Colors.getPrimaryColorForHue(initialHue));