package com.maciekjanusz.compassproject.ui;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.os.Build;
import android.util.AttributeSet;
import android.view.View;
import com.maciekjanusz.compassproject.util.ValueFormatter;
import static java.lang.Math.cos;
import static java.lang.Math.min;
import static java.lang.Math.sin;
import static java.lang.Math.toRadians;
public class CompassView extends View {
private static final int DEFAULT_SCALE_LINES = 144;
private static final int SPIKES = 8;
private static final float STEP_ANGLE = 360f / SPIKES;
private static final float HALF_STEP_ANGLE = STEP_ANGLE / 2;
private int strokeWidth = 2; // default
private int scaleLines;
private final Paint facePaint = new Paint();
private final Paint linePaint = new Paint();
private final Paint northPaint = new Paint();
private final Paint southPaint = new Paint();
private final Paint backgroundPaint = new Paint();
private final Paint navigationPaint = new Paint();
private final Paint textPaint = new Paint();
private final Path path = new Path();
// view center
private float centerX;
private float centerY;
// angle step for drawing scales
private float scaleAngleStep;
// radii, largest to smallest
private float fullRadius;
private float cardinalsRadius;
private float faceRadius;
private float shortSpikeRadius;
private float spikeCornerRadius;
// parameter flags
private boolean compassEnabled;
private boolean navigationEnabled;
private boolean cardinalsEnabled;
private boolean hasBackground;
// bearing
private float compassBearing;
private float navigationBearing;
// value formatter for drawing cardinals
private ValueFormatter valueFormatter;
public CompassView(Context context) {
super(context);
init(context);
}
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CompassView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public CompassView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
initPaint();
setScaleLines(DEFAULT_SCALE_LINES);
path.setFillType(Path.FillType.EVEN_ODD);
valueFormatter = new ValueFormatter(context);
}
private void initPaint() {
linePaint.setColor(Color.BLACK);
linePaint.setStrokeWidth(strokeWidth);
linePaint.setStyle(Paint.Style.STROKE);
linePaint.setAntiAlias(true);
facePaint.setColor(Color.BLACK);
facePaint.setStrokeWidth(strokeWidth);
facePaint.setStyle(Paint.Style.FILL_AND_STROKE);
facePaint.setAntiAlias(true);
northPaint.setColor(Color.RED);
northPaint.setStrokeWidth(strokeWidth);
northPaint.setStyle(Paint.Style.FILL_AND_STROKE);
northPaint.setAntiAlias(true);
southPaint.setColor(Color.BLUE);
southPaint.setStrokeWidth(strokeWidth);
southPaint.setStyle(Paint.Style.FILL_AND_STROKE);
southPaint.setAntiAlias(true);
backgroundPaint.setColor(Color.WHITE);
backgroundPaint.setStrokeWidth(0);
backgroundPaint.setStyle(Paint.Style.FILL);
backgroundPaint.setAntiAlias(true);
navigationPaint.setColor(Color.GREEN);
navigationPaint.setStrokeWidth(0);
navigationPaint.setStyle(Paint.Style.FILL_AND_STROKE);
navigationPaint.setAntiAlias(true);
textPaint.setColor(Color.BLACK);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setLinearText(true);
textPaint.setAntiAlias(true);
textPaint.setStyle(Paint.Style.STROKE);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
float diameter = min(w, h);
this.centerX = w / 2f;
this.centerY = h / 2f;
// fractions are completely arbitrary -> might rewrite for customization
fullRadius = diameter / 2f;
cardinalsRadius = fullRadius * 0.9f;
shortSpikeRadius = cardinalsRadius * 0.7f;
spikeCornerRadius = cardinalsRadius * 0.2f;
faceRadius = cardinalsRadius * 0.9f;
// adjust text size to view size
textPaint.setTextSize(fullRadius / 12f);
}
@Override
protected void onDraw(Canvas canvas) {
if (cardinalsEnabled) {
drawCardinals(canvas);
}
if (navigationEnabled) {
drawNavigation(canvas);
}
if (hasBackground) {
canvas.drawCircle(centerX, centerY, faceRadius, backgroundPaint);
}
if (compassEnabled) {
drawScaleLines(canvas);
drawWindrose(canvas);
}
}
private void drawNavigation(Canvas canvas) {
double degRad = toRadians(compassBearing + navigationBearing - 90);
double ang1 = degRad - toRadians(HALF_STEP_ANGLE / 3);
double ang2 = degRad + toRadians(HALF_STEP_ANGLE / 3);
float endX = (float) (centerX + fullRadius * cos(degRad));
float endY = (float) (centerY + fullRadius * sin(degRad));
float vertex1X = (float) (centerX + faceRadius * cos(ang1));
float vertex1Y = (float) (centerY + faceRadius * sin(ang1));
float vertex2X = (float) (centerX + faceRadius * cos(ang2));
float vertex2Y = (float) (centerY + faceRadius * sin(ang2));
path.moveTo(endX, endY);
path.lineTo(vertex1X, vertex1Y);
path.lineTo(vertex2X, vertex2Y);
path.close();
canvas.drawPath(path, navigationPaint);
canvas.drawLine(endX, endY, vertex1X, vertex1Y, facePaint);
canvas.drawLine(endX, endY, vertex2X, vertex2Y, facePaint);
path.reset();
}
private void drawCardinals(Canvas canvas) {
float y = centerY - cardinalsRadius;
canvas.save();
canvas.rotate(compassBearing, centerX, centerY);
for (int i = 0; i < 8; i++) {
float currentDegrees = i * 45;
String cardinal = valueFormatter.getCardinalDirection(currentDegrees);
canvas.drawText(cardinal, centerX, y, textPaint);
canvas.rotate(45, centerX, centerY);
}
canvas.restore();
}
private void drawScaleLines(Canvas canvas) {
// maybe it's more efficient to do it with canvas rotating and less trigonometry
canvas.drawCircle(centerX, centerY, faceRadius, linePaint);
for (int i = 0; i < scaleLines; i++) {
float fraction = i % (scaleLines / 12) == 0 ? 0.8f : 0.9f;
float fromRadius = faceRadius * fraction;
float degrees = compassBearing + i * scaleAngleStep;
double degRad = toRadians(degrees);
float fromX = (float) (centerX + fromRadius * cos(degRad));
float fromY = (float) (centerY + fromRadius * sin(degRad));
float toX = (float) (centerX + faceRadius * cos(degRad));
float toY = (float) (centerY + faceRadius * sin(degRad));
canvas.drawLine(fromX, fromY, toX, toY, facePaint);
}
}
private void drawWindrose(Canvas canvas) {
// maybe it's more efficient to do it with canvas rotating and less trigonometry
for (int i = 0; i < SPIKES; i++) {
float radius = i % 2 == 0 ? faceRadius : shortSpikeRadius;
float degrees = compassBearing + i * STEP_ANGLE;
float plusDegrees = degrees + HALF_STEP_ANGLE;
float minusDegrees = degrees - HALF_STEP_ANGLE;
double degRad = toRadians(degrees);
double plusDegRad = toRadians(plusDegrees);
double minusDegRad = toRadians(minusDegrees);
float endX = (float) (centerX + radius * cos(degRad));
float vertexX = (float) (centerX + spikeCornerRadius * cos(plusDegRad));
float backVertexX = (float) (centerX + spikeCornerRadius * cos(minusDegRad));
float endY = (float) (centerY + radius * sin(degRad));
float vertexY = (float) (centerY + spikeCornerRadius * sin(plusDegRad));
float backVertexY = (float) (centerY + spikeCornerRadius * sin(minusDegRad));
path.moveTo(centerX, centerY);
path.lineTo(endX, endY);
path.lineTo(vertexX, vertexY);
path.close();
canvas.drawPath(path, facePaint);
path.reset();
// 6 is north, 2 is south - ugly but whatever
if (i == 6 || i == 2) {
path.moveTo(centerX, centerY);
path.lineTo(endX, endY);
path.lineTo(backVertexX, backVertexY);
path.close();
canvas.drawPath(path, i == 6 ? northPaint : southPaint);
path.reset();
// correct line from center to smallradius
canvas.drawLine(centerX, centerY, backVertexX, backVertexY, facePaint);
}
canvas.drawLine(endX, endY, backVertexX, backVertexY, facePaint);
}
}
public boolean isCompassEnabled() {
return compassEnabled;
}
public void setCompassEnabled(boolean compassEnabled) {
this.compassEnabled = compassEnabled;
}
public boolean isNavigationEnabled() {
return navigationEnabled;
}
public void setNavigationEnabled(boolean navigationEnabled) {
this.navigationEnabled = navigationEnabled;
}
public float getCompassBearing() {
return compassBearing;
}
public void setCompassBearing(float compassBearing) {
this.compassBearing = compassBearing;
invalidate();
}
public float getNavigationBearing() {
return navigationBearing;
}
public void setNavigationBearing(float navigationBearing) {
this.navigationBearing = navigationBearing;
invalidate();
}
public void setHasBackground(boolean hasBackground) {
this.hasBackground = hasBackground;
}
public void setStrokeWidth(int strokeWidth) {
this.strokeWidth = strokeWidth;
}
public boolean isCardinalsEnabled() {
return cardinalsEnabled;
}
public void setCardinalsEnabled(boolean cardinalsEnabled) {
this.cardinalsEnabled = cardinalsEnabled;
}
public int getScaleLines() {
return scaleLines;
}
public void setScaleLines(int scaleLines) {
this.scaleLines = scaleLines;
this.scaleAngleStep = (360f / scaleLines);
}
}