plectrum

Plectrum: instrument tuner for Android
Log | Files | Refs | README | LICENSE

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 }