Initial commit
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
package com.wytehat.btlogger;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RadialGradient;
|
||||
import android.graphics.Shader;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
import android.view.View;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
public class BluetoothRadarView extends View {
|
||||
public interface Listener { void onTargetSelected(String address); }
|
||||
|
||||
public static class Target {
|
||||
public String address;
|
||||
public String name;
|
||||
public int rssi;
|
||||
float x, y;
|
||||
public Target(String address, String name, int rssi) {
|
||||
this.address = address; this.name = name; this.rssi = rssi;
|
||||
}
|
||||
}
|
||||
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final ArrayList<Target> targets = new ArrayList<Target>();
|
||||
private final HashMap<String, Float> targetAngles = new HashMap<String, Float>();
|
||||
private final HashMap<String, Float> displayedRadii = new HashMap<String, Float>();
|
||||
private Listener listener;
|
||||
|
||||
private boolean scanning;
|
||||
private final ScaleGestureDetector scaleDetector;
|
||||
private float zoom = 1.0f;
|
||||
private boolean suppressTap;
|
||||
private double sweepAngle = -90.0;
|
||||
public BluetoothRadarView(Context context) {
|
||||
super(context);
|
||||
scaleDetector = new ScaleGestureDetector(context, new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
public boolean onScaleBegin(ScaleGestureDetector detector) { suppressTap = true; return true; }
|
||||
public boolean onScale(ScaleGestureDetector detector) {
|
||||
zoom = clamp(zoom * detector.getScaleFactor(), 1.0f, 3.5f);
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
paint.setTypeface(android.graphics.Typeface.create("sans", android.graphics.Typeface.NORMAL));
|
||||
setBackgroundColor(Color.rgb(4, 24, 29));
|
||||
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
|
||||
}
|
||||
public synchronized void setScanning(boolean value) {
|
||||
if (value && !scanning) zoom = 1.0f;
|
||||
scanning = value;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
|
||||
public void setListener(Listener value) { listener = value; }
|
||||
|
||||
public synchronized void setTargets(List<Target> values) {
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
Target target = values.get(i);
|
||||
if (!targetAngles.containsKey(target.address))
|
||||
targetAngles.put(target.address, Float.valueOf(findOpenAngle(target.address)));
|
||||
}
|
||||
targets.clear(); targets.addAll(values); invalidate();
|
||||
}
|
||||
|
||||
protected synchronized void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
float cx = getWidth() / 2f;
|
||||
float cy = getHeight() / 2f;
|
||||
canvas.save();
|
||||
canvas.scale(zoom, zoom, cx, cy);
|
||||
float radius = Math.min(cx, cy) - 34;
|
||||
float glowRadius = radius + 34f;
|
||||
float edgeStop = radius / glowRadius;
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setShader(new android.graphics.RadialGradient(cx, cy, glowRadius,
|
||||
new int[] { Color.TRANSPARENT, Color.argb(5, 35, 180, 110), Color.argb(24, 45, 225, 140), Color.argb(48, 55, 235, 150), Color.argb(18, 35, 190, 120), Color.TRANSPARENT },
|
||||
new float[] { 0f, 0.55f, edgeStop - 0.05f, edgeStop, edgeStop + 0.10f, 1f }, android.graphics.Shader.TileMode.CLAMP));
|
||||
canvas.drawCircle(cx, cy, glowRadius, paint);
|
||||
paint.setShader(null);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(2.4f);
|
||||
paint.setColor(Color.argb(95, 55, 220, 145));
|
||||
paint.setShadowLayer(12f, 0f, 0f, Color.argb(65, 45, 225, 145));
|
||||
canvas.drawCircle(cx, cy, radius, paint);
|
||||
paint.clearShadowLayer();
|
||||
paint.setColor(Color.argb(22, 65, 225, 155));
|
||||
paint.setStrokeWidth(6f);
|
||||
for (int i = 1; i <= 4; i++) canvas.drawCircle(cx, cy, radius * i / 4f, paint);
|
||||
paint.setColor(Color.rgb(38, 145, 110));
|
||||
paint.setStrokeWidth(2.8f);
|
||||
for (int i = 1; i <= 4; i++) canvas.drawCircle(cx, cy, radius * i / 4f, paint);
|
||||
paint.setColor(Color.argb(28, 55, 205, 145)); paint.setStrokeWidth(3.5f);
|
||||
canvas.drawLine(cx - radius, cy, cx + radius, cy, paint);
|
||||
canvas.drawLine(cx, cy - radius, cx, cy + radius, paint);
|
||||
paint.setColor(Color.rgb(35, 125, 100)); paint.setStrokeWidth(1.8f);
|
||||
canvas.drawLine(cx - radius, cy, cx + radius, cy, paint);
|
||||
canvas.drawLine(cx, cy - radius, cx, cy + radius, paint);
|
||||
|
||||
if (scanning) sweepAngle = (System.currentTimeMillis() % 6000L) * 360.0 / 6000.0 - 90.0;
|
||||
double angle = sweepAngle + 90.0;
|
||||
double radians = Math.toRadians(angle - 90);
|
||||
float sweepX = cx + (float)Math.cos(radians) * radius;
|
||||
float sweepY = cy + (float)Math.sin(radians) * radius;
|
||||
paint.setColor(Color.argb(35, 70, 255, 170)); paint.setStrokeWidth(16);
|
||||
canvas.drawLine(cx, cy, sweepX, sweepY, paint);
|
||||
paint.setColor(Color.argb(85, 70, 255, 170)); paint.setStrokeWidth(8);
|
||||
canvas.drawLine(cx, cy, sweepX, sweepY, paint);
|
||||
paint.setColor(Color.rgb(65, 255, 170)); paint.setStrokeWidth(2.5f);
|
||||
canvas.drawLine(cx, cy, sweepX, sweepY, paint);
|
||||
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(Color.WHITE);
|
||||
canvas.drawCircle(cx, cy, 7, paint);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
paint.setTextSize(12 * getResources().getDisplayMetrics().scaledDensity);
|
||||
canvas.drawText("YOU", cx, cy + 24, paint);
|
||||
ArrayList<Target> ordered = new ArrayList<Target>(targets);
|
||||
Collections.sort(ordered, new Comparator<Target>() {
|
||||
public int compare(Target first, Target second) {
|
||||
float a = angleFor(first.address);
|
||||
float b = angleFor(second.address);
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
}
|
||||
});
|
||||
float density = getResources().getDisplayMetrics().scaledDensity;
|
||||
for (int i = 0; i < ordered.size(); i++) {
|
||||
Target target = ordered.get(i);
|
||||
float degrees = angleFor(target.address);
|
||||
double targetAngle = Math.toRadians(degrees);
|
||||
float desiredRadius = radius * radialPosition(target.rssi);
|
||||
Float previous = displayedRadii.get(target.address);
|
||||
float shownRadius = previous == null ? desiredRadius : previous.floatValue() + (desiredRadius - previous.floatValue()) * 0.055f;
|
||||
displayedRadii.put(target.address, Float.valueOf(shownRadius));
|
||||
float cos = (float)Math.cos(targetAngle);
|
||||
float sin = (float)Math.sin(targetAngle);
|
||||
target.x = cx + cos * shownRadius;
|
||||
target.y = cy + sin * shownRadius;
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setColor(target.rssi >= -60 ? Color.rgb(255, 70, 70) : target.rssi >= -80 ? Color.rgb(255, 180, 40) : Color.rgb(40, 150, 255));
|
||||
canvas.drawCircle(target.x, target.y, 9, paint);
|
||||
|
||||
paint.setTextSize(9.5f * density);
|
||||
paint.setTextAlign(Paint.Align.CENTER);
|
||||
String label = target.name == null ? "Unknown" : target.name;
|
||||
if (label.length() > 14) label = label.substring(0, 14);
|
||||
String signal = target.rssi + " dBm";
|
||||
float textWidth = Math.max(paint.measureText(label), paint.measureText(signal));
|
||||
int lane = target.address == null ? 0 : Math.abs(target.address.hashCode() / 360) % 5 - 2;
|
||||
float shift = lane * 10f * density;
|
||||
float labelX = target.x + cos * (15f + textWidth / 2f) - sin * shift;
|
||||
float labelY = target.y + sin * (17f + 12f * density) + cos * shift;
|
||||
labelX = clamp(labelX, textWidth / 2f + 3f, getWidth() - textWidth / 2f - 3f);
|
||||
labelY = clamp(labelY, 12f * density, getHeight() - 12f * density);
|
||||
paint.setColor(Color.WHITE);
|
||||
paint.setShadowLayer(3f, 1f, 1f, Color.BLACK);
|
||||
canvas.drawText(label, labelX, labelY - 2f, paint);
|
||||
paint.setTextSize(8.5f * density);
|
||||
canvas.drawText(signal, labelX, labelY + 10f * density, paint);
|
||||
paint.clearShadowLayer();
|
||||
}
|
||||
canvas.restore();
|
||||
if (scanning) postInvalidateDelayed(33);
|
||||
}
|
||||
|
||||
public synchronized boolean onTouchEvent(MotionEvent event) {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN && getParent() != null)
|
||||
getParent().requestDisallowInterceptTouchEvent(true);
|
||||
scaleDetector.onTouchEvent(event);
|
||||
if (event.getPointerCount() > 1) { suppressTap = true; return true; }
|
||||
if (event.getAction() != MotionEvent.ACTION_UP) return true;
|
||||
if (suppressTap) { suppressTap = false; return true; }
|
||||
float cx = getWidth() / 2f;
|
||||
float cy = getHeight() / 2f;
|
||||
float touchX = cx + (event.getX() - cx) / zoom;
|
||||
float touchY = cy + (event.getY() - cy) / zoom;
|
||||
Target closest = null;
|
||||
float best = Float.MAX_VALUE;
|
||||
for (int i = 0; i < targets.size(); i++) {
|
||||
Target target = targets.get(i);
|
||||
float dx = touchX - target.x;
|
||||
float dy = touchY - target.y;
|
||||
float distance = dx * dx + dy * dy;
|
||||
if (distance < best) { best = distance; closest = target; }
|
||||
}
|
||||
float limit = 48 * getResources().getDisplayMetrics().density / zoom;
|
||||
if (closest != null && best <= limit * limit && listener != null)
|
||||
listener.onTargetSelected(closest.address);
|
||||
return true;
|
||||
}
|
||||
|
||||
private float angleFor(String address) {
|
||||
Float value = targetAngles.get(address);
|
||||
if (value != null) return value.floatValue();
|
||||
float created = findOpenAngle(address);
|
||||
targetAngles.put(address, Float.valueOf(created));
|
||||
return created;
|
||||
}
|
||||
|
||||
private float findOpenAngle(String address) {
|
||||
if (targetAngles.isEmpty()) return address == null ? -90f : (Math.abs(address.hashCode()) % 360) - 90f;
|
||||
float bestAngle = 0f;
|
||||
float bestGap = -1f;
|
||||
for (int candidate = 0; candidate < 360; candidate += 5) {
|
||||
float nearest = 360f;
|
||||
for (Float used : targetAngles.values()) nearest = Math.min(nearest, circularDistance(candidate, used.floatValue()));
|
||||
if (nearest > bestGap) { bestGap = nearest; bestAngle = candidate; }
|
||||
}
|
||||
return bestAngle;
|
||||
}
|
||||
|
||||
private float circularDistance(float first, float second) {
|
||||
float difference = Math.abs(first - second) % 360f;
|
||||
return Math.min(difference, 360f - difference);
|
||||
}
|
||||
|
||||
private float radialPosition(int rssi) {
|
||||
return clamp((-rssi - 45f) / 60f, 0.12f, 0.94f);
|
||||
}
|
||||
|
||||
private float clamp(float value, float min, float max) {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user