/**
*
* @author Peter Brinkmann (peter.brinkmann@gmail.com)
*
* For information on usage and redistribution, and for a DISCLAIMER OF ALL
* WARRANTIES, see the file, "LICENSE.txt," in this distribution.
*
*/
package org.puredata.android.fifths;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.Path.FillType;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
public final class CircleView extends View {
private static enum State { UP, MAJOR, MINOR, SHIFT };
private static final String[] notesSharp = { "C", "C\u266f", "D", "D\u266f", "E", "F", "F\u266f", "G", "G\u266f", "A", "A\u266f", "B" };
private static final String[] notesFlat = { "C", "D\u266d", "D", "E\u266d", "E", "F", "G\u266d", "G", "A\u266d", "A", "B\u266d", "B" };
private static final int[] shifts = { 0, -5, 2, -3, 4, -1, 6, 1, -4, 3, -2, 5 };
private static final float R0 = 0.25f;
private static final float R2 = 0.92f;
private static final float R1 = (float) Math.sqrt((R0 * R0 + R2 * R2) / 2); // equal area for major and minor fields
private CircleOfFifths owner;
private int top = 0;
private float xCenter, yCenter, xNorm, yNorm;
private int selectedSegment;
private State currentState = State.UP;
private Bitmap keySigs[];
private Bitmap wheel = null;
private Bitmap grid = null;
private Bitmap shadow = null;
private final Path minorField = new Path();
private final Path majorField = new Path();
private final Path rimField = new Path();
private final Paint labelPaint = new Paint();
private final Paint selectedPaint = new Paint();
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public void setOwner(CircleOfFifths owner) {
this.owner = owner;
}
public void setTopSegment(int top) {
this.top = top;
invalidate();
}
private void init() {
RectF r0 = new RectF(-R0, -R0, R0, R0);
RectF r1 = new RectF(-R1, -R1, R1, R1);
RectF r2 = new RectF(-R2, -R2, R2, R2);
RectF r = new RectF(-1, -1, 1, 1);
float phi = 255, dphi = 30;
minorField.arcTo(r1, phi, dphi, true);
minorField.arcTo(r0, phi+dphi, -dphi);
minorField.close();
minorField.setFillType(FillType.WINDING);
majorField.arcTo(r2, phi, dphi, true);
majorField.arcTo(r1, phi+dphi, -dphi);
majorField.close();
majorField.setFillType(FillType.WINDING);
rimField.arcTo(r, phi, dphi, true);
rimField.arcTo(r2, phi+dphi, -dphi);
rimField.close();
rimField.setFillType(FillType.WINDING);
selectedPaint.setAntiAlias(true);
selectedPaint.setColor(Color.RED);
selectedPaint.setStyle(Paint.Style.FILL);
labelPaint.setAntiAlias(true);
labelPaint.setColor(Color.BLACK);
labelPaint.setTextAlign(Paint.Align.CENTER);
labelPaint.setTypeface(Typeface.MONOSPACE);
labelPaint.setTextSize(0.16f);
Resources res = getResources();
keySigs = new Bitmap[] {
BitmapFactory.decodeResource(res, R.drawable.ks00), BitmapFactory.decodeResource(res, R.drawable.ks01),
BitmapFactory.decodeResource(res, R.drawable.ks02), BitmapFactory.decodeResource(res, R.drawable.ks03),
BitmapFactory.decodeResource(res, R.drawable.ks04), BitmapFactory.decodeResource(res, R.drawable.ks05),
BitmapFactory.decodeResource(res, R.drawable.ks06), BitmapFactory.decodeResource(res, R.drawable.ks07),
BitmapFactory.decodeResource(res, R.drawable.ks08), BitmapFactory.decodeResource(res, R.drawable.ks09),
BitmapFactory.decodeResource(res, R.drawable.ks10), BitmapFactory.decodeResource(res, R.drawable.ks11)
};
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int xDim = getDim(widthMeasureSpec);
int yDim = getDim(heightMeasureSpec);
int dim = Math.min(xDim, yDim);
setMeasuredDimension(dim, dim);
}
private int getDim(int widthMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
return (mode == MeasureSpec.UNSPECIFIED) ? 320 : size;
}
@Override
protected void onDraw(Canvas canvas) {
canvas.translate(xCenter, yCenter);
canvas.scale(xCenter, yCenter);
canvas.save(Canvas.MATRIX_SAVE_FLAG);
canvas.rotate(-top * 30);
canvas.drawBitmap(wheel, null, new RectF(-1, -1, 1, 1), null);
canvas.restore();
int c = (top * 7) % 12;
float dy = R0 / 1.8f;
float dx = dy * 1.38f;
canvas.drawBitmap(keySigs[c], null, new RectF(-dx, -dy, dx, dy), null);
int s0 = shifts[c];
for (int i = 0; i < 12; i++) {
if (i == selectedSegment) {
if (currentState == State.MAJOR) {
canvas.drawPath(majorField, selectedPaint);
}
else if (currentState == State.MINOR) {
canvas.drawPath(minorField, selectedPaint);
}
else if (currentState == State.SHIFT) {
canvas.drawPath(rimField, selectedPaint);
}
}
int s1 = s0 + i;
if (i > 6) s1 -= 12;
String label = (s1 >= 0) ? notesSharp[c] : notesFlat[c];
drawLabel(canvas, label, (R1 + R2) / 2);
c = (c + 9) % 12;
label = (s1 >= 0) ? notesSharp[c] : notesFlat[c];
drawLabel(canvas, label.toLowerCase(), (R0 + R1) / 2);
c = (c + 10) % 12;
canvas.rotate(30);
}
canvas.drawBitmap(grid, null, new RectF(-1, -1, 1, 1), null);
canvas.drawBitmap(shadow, null, new RectF(-1, -1, 1, 1), null);
}
private void drawLabel(Canvas canvas, String label, float r) {
float d = labelPaint.getTextSize() / 3f - r;
if (label.length() > 1) {
// ugly hack to work around unicode spacing problem
canvas.drawText(label.charAt(0) + " ", 0, d, labelPaint);
canvas.drawText(" " + label.charAt(1), 0, d, labelPaint);
} else {
canvas.drawText(label, 0, d, labelPaint);
}
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
xCenter = w / 2;
xNorm = 1 / xCenter;
yCenter = h / 2;
yNorm = 1 / yCenter;
drawBitmaps(w, h);
}
private void drawBitmaps(int w, int h) {
if (wheel != null) {
wheel.recycle();
shadow.recycle();
grid.recycle();
}
wheel = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
shadow = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
grid = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas();
canvas.translate(xCenter, yCenter);
canvas.scale(xCenter, yCenter);
Paint shades[] = new Paint[4];
for (int i = 0; i < 4; i++) {
int c = 0x98 + 0x10 * i;
shades[i] = new Paint();
shades[i].setStyle(Paint.Style.FILL);
shades[i].setColor(Color.argb(0xff, c, c, c));
}
Paint centerShade = new Paint(shades[0]);
centerShade.setColor(Color.argb(0xff, 0x78, 0x78, 0x78));
Paint shadowPaint = new Paint();
shadowPaint.setShader(new LinearGradient(0, -1, 0, 1,
new int[] { 0x00ffffff, 0x77000000 }, null, TileMode.CLAMP));
Paint gridPaint = new Paint();
gridPaint.setAntiAlias(true);
gridPaint.setStrokeWidth(0.016f);
gridPaint.setStyle(Paint.Style.STROKE);
gridPaint.setColor(Color.DKGRAY);
canvas.setBitmap(shadow);
canvas.drawCircle(0, 0, 1, shadowPaint);
canvas.setBitmap(wheel);
canvas.drawCircle(0, 0, R0, centerShade);
for (int i = 0; i < 12; i++) {
Paint shade = shades[i % 4];
canvas.drawPath(minorField, shade);
canvas.drawPath(majorField, shade);
canvas.drawPath(rimField, shade);
canvas.rotate(30);
}
canvas.setBitmap(grid);
canvas.drawCircle(0, 0, R0, gridPaint);
canvas.drawCircle(0, 0, R1, gridPaint);
canvas.drawCircle(0, 0, R2, gridPaint);
canvas.drawCircle(0, 0, 1, gridPaint);
canvas.rotate(15);
for (int i = 0; i < 12; i++) {
canvas.drawLine(0, R0, 0, 1, gridPaint);
canvas.rotate(30);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float x = (event.getX() - xCenter) * xNorm;
float y = (event.getY() - yCenter) * yNorm;
float radiusSquared = x * x + y * y;
float angle = (float) (Math.atan2(x, -y) * 6 / Math.PI);
int segment = (int) (angle + 12.5f) % 12;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (radiusSquared >= R0 * R0) {
selectedSegment = segment;
if (radiusSquared >= R2 * R2) {
currentState = State.SHIFT;
owner.setTop(top);
} else {
int note = (top * 7 + segment * 7) % 12;
if (radiusSquared >= R1 * R1) {
currentState = State.MAJOR;
owner.playChord(true, note);
} else {
currentState = State.MINOR;
note = (note + 9) % 12;
owner.playChord(false, note);
}
}
invalidate();
}
break;
case MotionEvent.ACTION_MOVE:
if (currentState == State.SHIFT && radiusSquared >= R0 * R0) {
int step = (selectedSegment - segment + 12) % 12;
if (step > 0) {
top = (top + step) % 12;
invalidate();
owner.setTop(top);
}
selectedSegment = segment;
}
break;
case MotionEvent.ACTION_UP:
default:
if (currentState == State.MAJOR || currentState == State.MINOR) {
owner.endChord();
}
currentState = State.UP;
invalidate();
break;
}
return true;
}
}