CanvasPainter.java (10895B)
1 package com.github.cythara; 2 3 import android.content.Context; 4 import android.content.SharedPreferences; 5 import android.graphics.Canvas; 6 import android.graphics.Color; 7 import android.graphics.Matrix; 8 import android.graphics.Paint; 9 import android.graphics.Path; 10 import android.graphics.Rect; 11 import android.graphics.drawable.Drawable; 12 import android.text.TextPaint; 13 14 import java.util.Locale; 15 import java.util.Objects; 16 17 import androidx.core.content.ContextCompat; 18 19 import static android.graphics.Paint.ANTI_ALIAS_FLAG; 20 import static com.github.cythara.MainActivity.*; 21 22 class CanvasPainter { 23 24 private static final double TOLERANCE = 10D; 25 private static final int MAX_DEVIATION = 60; 26 private static final int NUMBER_OF_MARKS_PER_SIDE = 6; 27 private final Context context; 28 29 private Canvas canvas; 30 31 private TextPaint textPaint = new TextPaint(ANTI_ALIAS_FLAG); 32 private TextPaint numbersPaint = new TextPaint(ANTI_ALIAS_FLAG); 33 private Paint gaugePaint = new Paint(ANTI_ALIAS_FLAG); 34 private Paint symbolPaint = new TextPaint(ANTI_ALIAS_FLAG); 35 36 private int redBackground; 37 private int greenBackground; 38 private int textColor; 39 40 private PitchDifference pitchDifference; 41 42 private float gaugeWidth; 43 private float x; 44 private float y; 45 private boolean useScientificNotation; 46 private int referencePitch; 47 48 private CanvasPainter(Context context) { 49 this.context = context; 50 } 51 52 static CanvasPainter with(Context context) { 53 return new CanvasPainter(context); 54 } 55 56 CanvasPainter paint(PitchDifference pitchDifference) { 57 this.pitchDifference = pitchDifference; 58 59 return this; 60 } 61 62 void on(Canvas canvas) { 63 SharedPreferences preferences = context.getSharedPreferences( 64 PREFS_FILE, Context.MODE_PRIVATE); 65 66 useScientificNotation = preferences.getBoolean( 67 USE_SCIENTIFIC_NOTATION, true); 68 69 referencePitch = preferences.getInt( 70 REFERENCE_PITCH, 440); 71 72 this.canvas = canvas; 73 74 redBackground = R.color.red_light; 75 greenBackground = R.color.green_light; 76 textColor = Color.BLACK; 77 if (isDarkModeEnabled()) { 78 int color = context.getResources().getColor(R.color.colorPrimaryDark); 79 this.canvas.drawColor(color); 80 81 redBackground = R.color.red_dark; 82 greenBackground = R.color.green_dark; 83 textColor = context.getResources().getColor(R.color.colorTextDarkCanvas); 84 } 85 86 gaugeWidth = 0.45F * canvas.getWidth(); 87 x = canvas.getWidth() / 2F; 88 y = canvas.getHeight() / 2F; 89 90 textPaint.setColor(textColor); 91 int textSize = context.getResources().getDimensionPixelSize(R.dimen.noteTextSize); 92 textPaint.setTextSize(textSize); 93 94 drawGauge(); 95 96 if (!isAutoModeEnabled()) { 97 Note[] tuningNotes = getCurrentTuning().getNotes(); 98 Note note = tuningNotes[getReferencePosition()]; 99 drawText(x, y / 4F, note, symbolPaint); 100 } 101 102 if (pitchDifference != null) { 103 int abs = Math.abs(getNearestDeviation()); 104 boolean shouldDraw = abs <= MAX_DEVIATION || 105 (abs <= MAX_DEVIATION * 2 && !isAutoModeEnabled()); 106 if (shouldDraw) { 107 setBackground(); 108 109 drawGauge(); 110 111 drawIndicator(); 112 113 if (!isAutoModeEnabled()) { 114 drawDeviation(); 115 } 116 117 float x = canvas.getWidth() / 2F; 118 float y = canvas.getHeight() * 0.75f; 119 120 drawText(x, y, pitchDifference.closest, textPaint); 121 } else { 122 drawListeningIndicator(); 123 } 124 } else { 125 drawListeningIndicator(); 126 } 127 } 128 129 private void drawDeviation() { 130 long rounded = Math.round(pitchDifference.deviation); 131 String text = String.valueOf(rounded); 132 133 Rect bounds = new Rect(); 134 symbolPaint.getTextBounds(text, 0, text.length(), bounds); 135 int width = bounds.width(); 136 137 float xPos = x - width / 2F; 138 float yPos = canvas.getHeight() / 3F; 139 140 canvas.drawText(text, xPos, yPos, symbolPaint); 141 } 142 143 private void drawGauge() { 144 gaugePaint.setColor(textColor); 145 146 int gaugeSize = context.getResources().getDimensionPixelSize(R.dimen.gaugeSize); 147 gaugePaint.setStrokeWidth(gaugeSize); 148 149 int textSize = context.getResources().getDimensionPixelSize(R.dimen.numbersTextSize); 150 numbersPaint.setTextSize(textSize); 151 numbersPaint.setColor(textColor); 152 153 canvas.drawLine(x - gaugeWidth, y, x + gaugeWidth, y, gaugePaint); 154 155 float spaceWidth = gaugeWidth / NUMBER_OF_MARKS_PER_SIDE; 156 157 int stepWidth = MAX_DEVIATION / NUMBER_OF_MARKS_PER_SIDE; 158 for (int i = 0; i <= MAX_DEVIATION; i = i + stepWidth) { 159 float factor = i / stepWidth; 160 drawMark(y, x + factor * spaceWidth, i); 161 drawMark(y, x - factor * spaceWidth, -i); 162 } 163 164 drawSymbols(spaceWidth); 165 166 displayReferencePitch(); 167 } 168 169 private void displayReferencePitch() { 170 float y = canvas.getHeight() * 0.9f; 171 172 Note note = new Note() { 173 @Override 174 public NoteName getName() { 175 return NoteName.A; 176 } 177 178 @Override 179 public int getOctave() { 180 return 4; 181 } 182 183 @Override 184 public String getSign() { 185 return ""; 186 } 187 }; 188 189 TextPaint paint = new TextPaint(ANTI_ALIAS_FLAG); 190 paint.setColor(textColor); 191 int size = (int) (textPaint.getTextSize() / 2); 192 paint.setTextSize(size); 193 194 float offset = paint.measureText(getNote(note.getName()) + getOctave(4)) * 0.75f; 195 196 drawText(x - gaugeWidth, y, note, paint); 197 canvas.drawText(String.format(Locale.ENGLISH, "= %d Hz", referencePitch), 198 x - gaugeWidth + offset, y, paint); 199 } 200 201 private void drawListeningIndicator() { 202 int resourceId = R.drawable.ic_line_style_icons_mic; 203 204 if (ListenerFragment.IS_RECORDING) { 205 resourceId = R.drawable.ic_line_style_icons_mic_active; 206 } 207 208 Drawable drawable = ContextCompat.getDrawable(context, resourceId); 209 210 int x = (int) (canvas.getWidth() / 2F); 211 int y = (int) (canvas.getHeight() - canvas.getHeight() / 3F); 212 213 int width = Objects.requireNonNull(drawable).getIntrinsicWidth() * 2; 214 int height = drawable.getIntrinsicHeight() * 2; 215 drawable.setBounds( 216 x - width / 2, y, 217 x + width / 2, 218 y + height); 219 220 221 drawable.draw(canvas); 222 } 223 224 private void drawSymbols(float spaceWidth) { 225 String sharp = "♯"; 226 String flat = "♭"; 227 228 int symbolsTextSize = context.getResources().getDimensionPixelSize(R.dimen.symbolsTextSize); 229 symbolPaint.setTextSize(symbolsTextSize); 230 symbolPaint.setColor(textColor); 231 232 float yPos = canvas.getHeight() / 4F; 233 canvas.drawText(sharp, 234 x + NUMBER_OF_MARKS_PER_SIDE * spaceWidth - symbolPaint.measureText(sharp) / 2F, 235 yPos, symbolPaint); 236 237 canvas.drawText(flat, 238 x - NUMBER_OF_MARKS_PER_SIDE * spaceWidth - symbolPaint.measureText(flat) / 2F, 239 yPos, 240 symbolPaint); 241 } 242 243 private void drawIndicator() { 244 float xPos = x + (getNearestDeviation() * gaugeWidth / MAX_DEVIATION); 245 float yPosition = y * 1.15f; 246 247 Matrix matrix = new Matrix(); 248 float scalingFactor = numbersPaint.getTextSize() / 3; 249 matrix.setScale(scalingFactor, scalingFactor); 250 251 Path indicator = new Path(); 252 indicator.moveTo(0, -2); 253 indicator.lineTo(1, 0); 254 indicator.lineTo(-1, 0); 255 indicator.close(); 256 257 indicator.transform(matrix); 258 259 indicator.offset(xPos, yPosition); 260 canvas.drawPath(indicator, gaugePaint); 261 } 262 263 private void drawMark(float y, float xPos, int mark) { 264 String prefix = ""; 265 if (mark > 0) { 266 prefix = "+"; 267 } 268 String text = prefix + mark; 269 270 int yOffset = (int) (numbersPaint.getTextSize() / 6); 271 if (mark % 10 == 0) { 272 yOffset *= 2; 273 } 274 if (mark % 20 == 0) { 275 canvas.drawText(text, xPos - numbersPaint.measureText(text) / 2F, 276 y - numbersPaint.getTextSize(), numbersPaint); 277 yOffset *= 2; 278 } 279 280 canvas.drawLine(xPos, y - yOffset, xPos, y + yOffset, gaugePaint); 281 } 282 283 private void drawText(float x, float y, Note note, Paint textPaint) { 284 String noteText = getNote(note.getName()); 285 float offset = textPaint.measureText(noteText) / 2F; 286 287 String sign = note.getSign(); 288 String octave = String.valueOf(getOctave(note.getOctave())); 289 290 TextPaint paint = new TextPaint(ANTI_ALIAS_FLAG); 291 paint.setColor(textColor); 292 int textSize = (int) (textPaint.getTextSize() / 2); 293 paint.setTextSize(textSize); 294 295 float factor = 0.75f; 296 if (useScientificNotation) { 297 factor = 1.5f; 298 } 299 300 canvas.drawText(sign, x + offset * 1.25f, y - offset * factor, paint); 301 canvas.drawText(octave, x + offset * 1.25f, y + offset * 0.5f, paint); 302 303 canvas.drawText(noteText, x - offset, y, textPaint); 304 } 305 306 private int getOctave(int octave) { 307 if (useScientificNotation) { 308 return octave; 309 } 310 311 /* 312 The octave number in the (French notation) of Solfège is one less than the 313 corresponding octave number in the scientific pitch notation. 314 There is also no octave with the number zero 315 (see https://fr.wikipedia.org/wiki/Octave_(musique)#Solf%C3%A8ge). 316 */ 317 if (octave <= 1) { 318 return octave - 2; 319 } 320 321 return octave - 1; 322 } 323 324 private String getNote(NoteName name) { 325 if (useScientificNotation) { 326 return name.getScientific(); 327 } 328 329 return name.getSol(); 330 } 331 332 private void setBackground() { 333 int color = redBackground; 334 String text = "✗"; 335 if (Math.abs(getNearestDeviation()) <= TOLERANCE) { 336 color = greenBackground; 337 text = "✓"; 338 } 339 340 canvas.drawColor(context.getResources().getColor(color)); 341 342 canvas.drawText(text, 343 x + gaugeWidth - symbolPaint.measureText(text), 344 canvas.getHeight() * 0.9f, symbolPaint); 345 } 346 347 private int getNearestDeviation() { 348 float deviation = (float) pitchDifference.deviation; 349 int rounded = Math.round(deviation); 350 351 return Math.round(rounded / 10f) * 10; 352 } 353 }