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;
37 import static net.ktnx.mobileledger.utils.Logger.debug;
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;
45 private int initialHueDegrees;
46 private int color, hueDegrees;
49 private float bandWidth;
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);
61 public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
62 super(context, attrs, defStyleAttr);
63 init(Colors.DEFAULT_HUE_DEG);
65 public HueRing(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
67 super(context, attrs, defStyleAttr, defStyleRes);
68 init(Colors.DEFAULT_HUE_DEG);
70 public HueRing(Context context) {
72 init(Colors.DEFAULT_HUE_DEG);
74 public HueRing(Context context, int initialHueDegrees) {
76 init(initialHueDegrees);
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
87 Shader rainbow = new SweepGradient(0, 0, steps, null);
89 ringPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
90 ringPaint.setShader(rainbow);
91 ringPaint.setStyle(Paint.Style.STROKE);
93 initialPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
94 initialPaint.setStyle(Paint.Style.FILL);
96 currentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
97 currentPaint.setStyle(Paint.Style.FILL);
99 setInitialHue(initialHueDegrees);
100 setHue(initialHueDegrees);
102 markerStrokeWidth = DimensionUtils.dp2px(getContext(), 4);
104 padding = markerStrokeWidth * 2 + 2;
106 markerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
107 markerPaint.setStyle(Paint.Style.STROKE);
108 markerPaint.setColor(0xa0000000);
109 markerPaint.setStrokeWidth(markerStrokeWidth);
111 public int getColor() {
114 public int getHueDegrees() {
117 public void setHue(int hueDegrees) {
118 if (hueDegrees == -1)
119 hueDegrees = Colors.DEFAULT_HUE_DEG;
121 if (hueDegrees != Colors.DEFAULT_HUE_DEG) {
122 // round to 15 degrees
123 int rem = hueDegrees % hueStepDegrees;
124 if (rem < (hueStepDegrees / 2))
127 hueDegrees += hueStepDegrees - rem;
130 this.hueDegrees = hueDegrees;
131 this.color = Colors.getPrimaryColorForHue(hueDegrees);
132 currentPaint.setColor(this.color);
135 private void setHue(float hue) {
136 setHue((int) (360f * hue));
139 protected void onDraw(Canvas canvas) {
140 super.onDraw(canvas);
142 float center = getWidth() / 2f;
143 ringPaint.setStrokeWidth((int) bandWidth);
146 canvas.translate(center, center);
147 canvas.drawOval(ringRect, ringPaint);
149 canvas.drawArc(centerRect, 180, 180, true, initialPaint);
150 canvas.drawArc(centerRect, 0, 180, true, currentPaint);
153 drawMarker(canvas, center);
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;
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,
172 canvas.translate(center, center);
173 canvas.rotate(hueDegrees, 0, 0);
174 canvas.drawPath(p, markerPaint);
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);
184 if ((widthMode == MeasureSpec.AT_MOST) && (heightMode == MeasureSpec.AT_MOST)) {
185 diameter = Math.min(widthSize, heightSize);
188 setMeasuredDimension(MEASURED_STATE_TOO_SMALL, MEASURED_STATE_TOO_SMALL);
192 setMeasuredDimension(diameter, diameter);
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;
201 bandWidth = diameter / 3.5f;
202 ringR = outerR - bandWidth / 2f;
203 innerR = outerR - bandWidth;
205 ringRect = new RectF(-ringR, -ringR, ringR, ringR);
207 innerDiameter = diameter - 2 * bandWidth;
208 centerR = innerDiameter * 0.5f;
209 centerRect = new RectF(-centerR, -centerR, centerR, centerR);
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;
219 float dist = (float) Math.hypot(x, y);
221 if (dist < centerR) {
223 setHue(initialHueDegrees);
228 float angleRad = (float) Math.atan2(y, x);
229 // angleRad is [-𝜋; +𝜋]
230 float hue = (float) (angleRad / (2 * Math.PI));
234 String.format("x=%1.3f, y=%1.3f, angle=%1.3frad, hueDegrees=%1.3f", x, y,
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));