Skip to content

Instantly share code, notes, and snippets.

@tbruyelle
Created September 9, 2014 12:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tbruyelle/5627d1a9a2e0e7ea1721 to your computer and use it in GitHub Desktop.
Save tbruyelle/5627d1a9a2e0e7ea1721 to your computer and use it in GitHub Desktop.
RadarView
/*
* Copyright (C) 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.radar;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.drawable.BitmapDrawable;
import android.hardware.SensorListener;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.LocationProvider;
import android.os.Bundle;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
public class RadarView extends View implements SensorListener, LocationListener {
private static final long RETAIN_GPS_MILLIS = 10000L;
private Paint mGridPaint;
private Paint mErasePaint;
private float mOrientation;
private double mTargetLat;
private double mTargetLon;
private double mMyLocationLat;
private double mMyLocationLon;
private int mLastScale = -1;
private String[] mDistanceScale = new String[4];
private static float KM_PER_METERS = 0.001f;
private static float METERS_PER_KM = 1000f;
/**
* These are the list of choices for the radius of the outer circle on the
* screen when using metric units. All items are in kilometers. This array is
* used to choose the scale of the radar display.
*/
private static double mMetricScaleChoices[] = {
100 * KM_PER_METERS,
200 * KM_PER_METERS,
400 * KM_PER_METERS,
1,
2,
4,
8,
20,
40,
100,
200,
400,
1000,
2000,
4000,
10000,
20000,
40000,
80000 };
/**
* Once the scale is chosen, this array is used to convert the number of
* kilometers on the screen to an integer. (Note that for short distances we
* use meters, so we multiply the distance by {@link #METERS_PER_KM}. (This
* array is for metric measurements.)
*/
private static float mMetricDisplayUnitsPerKm[] = {
METERS_PER_KM,
METERS_PER_KM,
METERS_PER_KM,
METERS_PER_KM,
METERS_PER_KM,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f,
1.0f };
/**
* This array holds the formatting string used to display the distance to
* the target. (This array is for metric measurements.)
*/
private static String mMetricDisplayFormats[] = {
"%.0fm",
"%.0fm",
"%.0fm",
"%.0fm",
"%.0fm",
"%.1fkm",
"%.1fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm" };
/**
* This array holds the formatting string used to display the distance on
* each ring of the radar screen. (This array is for metric measurements.)
*/
private static String mMetricScaleFormats[] = {
"%.0fm",
"%.0fm",
"%.0fm",
"%.0fm",
"%.0fm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm",
"%.0fkm" };
private static float KM_PER_YARDS = 0.0009144f;
private static float KM_PER_MILES = 1.609344f;
private static float YARDS_PER_KM = 1093.6133f;
private static float MILES_PER_KM = 0.621371192f;
/**
* These are the list of choices for the radius of the outer circle on the
* screen when using standard units. All items are in kilometers. This array is
* used to choose the scale of the radar display.
*/
private static double mEnglishScaleChoices[] = {
100 * KM_PER_YARDS,
200 * KM_PER_YARDS,
400 * KM_PER_YARDS,
1000 * KM_PER_YARDS,
1 * KM_PER_MILES,
2 * KM_PER_MILES,
4 * KM_PER_MILES,
8 * KM_PER_MILES,
20 * KM_PER_MILES,
40 * KM_PER_MILES,
100 * KM_PER_MILES,
200 * KM_PER_MILES,
400 * KM_PER_MILES,
1000 * KM_PER_MILES,
2000 * KM_PER_MILES,
4000 * KM_PER_MILES,
10000 * KM_PER_MILES,
20000 * KM_PER_MILES,
40000 * KM_PER_MILES,
80000 * KM_PER_MILES };
/**
* Once the scale is chosen, this array is used to convert the number of
* kilometers on the screen to an integer. (Note that for short distances we
* use meters, so we multiply the distance by {@link #YARDS_PER_KM}. (This
* array is for standard measurements.)
*/
private static float mEnglishDisplayUnitsPerKm[] = {
YARDS_PER_KM,
YARDS_PER_KM,
YARDS_PER_KM,
YARDS_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM,
MILES_PER_KM };
/**
* This array holds the formatting string used to display the distance to
* the target. (This array is for standard measurements.)
*/
private static String mEnglishDisplayFormats[] = {
"%.0fyd",
"%.0fyd",
"%.0fyd",
"%.0fyd",
"%.1fmi",
"%.1fmi",
"%.1fmi",
"%.1fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi" };
/**
* This array holds the formatting string used to display the distance on
* each ring of the radar screen. (This array is for standard measurements.)
*/
private static String mEnglishScaleFormats[] = {
"%.0fyd",
"%.0fyd",
"%.0fyd",
"%.0fyd",
"%.2fmi",
"%.1fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi",
"%.0fmi" };
/**
* True when we have know our own location
*/
private boolean mHaveLocation = false;
/**
* The view that will display the distance text
*/
private TextView mDistanceView;
/**
* Distance to target, in KM
*/
private double mDistance;
/**
* Bearing to target, in degrees
*/
private double mBearing;
/**
* Ratio of the distance to the target to the radius of the outermost ring on the radar screen
*/
private float mDistanceRatio;
/**
* Utility rect for calculating the ring labels
*/
private Rect mTextBounds = new Rect();
/**
* The bitmap used to draw the target
*/
private Bitmap mBlip;
/**
* Used to draw the animated ring that sweeps out from the center
*/
private Paint mSweepPaint0;
/**
* Used to draw the animated ring that sweeps out from the center
*/
private Paint mSweepPaint1;
/**
* Used to draw the animated ring that sweeps out from the center
*/
private Paint mSweepPaint2;
/**
* Time in millis when the most recent sweep began
*/
private long mSweepTime;
/**
* True if the sweep has not yet intersected the blip
*/
private boolean mSweepBefore;
/**
* Time in millis when the sweep last crossed the blip
*/
private long mBlipTime;
/**
* True if the display should use metric units; false if the display should use standard
* units
*/
private boolean mUseMetric;
/**
* Time in millis for the last time GPS reported a location
*/
private long mLastGpsFixTime = 0L;
/**
* The last location reported by the network provider. Use this if we can't get a location from
* GPS
*/
private Location mNetworkLocation;
/**
* True if GPS is reporting a location
*/
private boolean mGpsAvailable;
/**
* True if the network provider is reporting a location
*/
private boolean mNetworkAvailable;
public RadarView(Context context) {
this(context, null);
}
public RadarView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public RadarView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
// Paint used for the rings and ring text
mGridPaint = new Paint();
mGridPaint.setColor(0xFF00FF00);
mGridPaint.setAntiAlias(true);
mGridPaint.setStyle(Style.STROKE);
mGridPaint.setStrokeWidth(1.0f);
mGridPaint.setTextSize(10.0f);
mGridPaint.setTextAlign(Align.CENTER);
// Paint used to erase the rectangle behing the ring text
mErasePaint = new Paint();
mErasePaint.setColor(0xFF191919);
mErasePaint.setStyle(Style.FILL);
// Outer ring of the sweep
mSweepPaint0 = new Paint();
mSweepPaint0.setColor(0xFF33FF33);
mSweepPaint0.setAntiAlias(true);
mSweepPaint0.setStyle(Style.STROKE);
mSweepPaint0.setStrokeWidth(2f);
// Middle ring of the sweep
mSweepPaint1 = new Paint();
mSweepPaint1.setColor(0x7733FF33);
mSweepPaint1.setAntiAlias(true);
mSweepPaint1.setStyle(Style.STROKE);
mSweepPaint1.setStrokeWidth(2f);
// Inner ring of the sweep
mSweepPaint2 = new Paint();
mSweepPaint2.setColor(0x3333FF33);
mSweepPaint2.setAntiAlias(true);
mSweepPaint2.setStyle(Style.STROKE);
mSweepPaint2.setStrokeWidth(2f);
mBlip = ((BitmapDrawable)getResources().getDrawable(R.drawable.blip)).getBitmap();
}
/**
* Sets the target to track on the radar
* @param latE6 Latitude of the target, multiplied by 1,000,000
* @param lonE6 Longitude of the target, multiplied by 1,000,000
*/
public void setTarget(int latE6, int lonE6) {
mTargetLat = latE6 / (double) GeoUtils.MILLION;
mTargetLon = lonE6 / (double) GeoUtils.MILLION;
}
/**
* Sets the view that we will use to report distance
*
* @param t The text view used to report distance
*/
public void setDistanceView(TextView t) {
mDistanceView = t;
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int center = getWidth() / 2;
int radius = center - 8;
// Draw the rings
final Paint gridPaint = mGridPaint;
canvas.drawCircle(center, center, radius, gridPaint);
canvas.drawCircle(center, center, radius * 3 / 4, gridPaint);
canvas.drawCircle(center, center, radius >> 1, gridPaint);
canvas.drawCircle(center, center, radius >> 2, gridPaint);
int blipRadius = (int) (mDistanceRatio * radius);
final long now = SystemClock.uptimeMillis();
if (mSweepTime > 0 && mHaveLocation) {
// Draw the sweep. Radius is determined by how long ago it started
long sweepDifference = now - mSweepTime;
if (sweepDifference < 512L) {
int sweepRadius = (int) (((radius + 6) * sweepDifference) >> 9);
canvas.drawCircle(center, center, sweepRadius, mSweepPaint0);
canvas.drawCircle(center, center, sweepRadius - 2, mSweepPaint1);
canvas.drawCircle(center, center, sweepRadius - 4, mSweepPaint2);
// Note when the sweep has passed the blip
boolean before = sweepRadius < blipRadius;
if (!before && mSweepBefore) {
mSweepBefore = false;
mBlipTime = now;
}
} else {
mSweepTime = now + 1000;
mSweepBefore = true;
}
postInvalidate();
}
// Draw horizontal and vertical lines
canvas.drawLine(center, center - (radius >> 2) + 6, center, center - radius - 6, gridPaint);
canvas.drawLine(center, center + (radius >> 2) - 6 , center, center + radius + 6, gridPaint);
canvas.drawLine(center - (radius >> 2) + 6, center, center - radius - 6, center, gridPaint);
canvas.drawLine(center + (radius >> 2) - 6, center, center + radius + 6, center, gridPaint);
// Draw X in the center of the screen
canvas.drawLine(center - 4, center - 4, center + 4, center + 4, gridPaint);
canvas.drawLine(center - 4, center + 4, center + 4, center - 4, gridPaint);
if (mHaveLocation) {
double bearingToTarget = mBearing - mOrientation;
double drawingAngle = Math.toRadians(bearingToTarget) - (Math.PI / 2);
float cos = (float) Math.cos(drawingAngle);
float sin = (float) Math.sin(drawingAngle);
// Draw the text for the rings
final String[] distanceScale = mDistanceScale;
addText(canvas, distanceScale[0], center, center + (radius >> 2));
addText(canvas, distanceScale[1], center, center + (radius >> 1));
addText(canvas, distanceScale[2], center, center + radius * 3 / 4);
addText(canvas, distanceScale[3], center, center + radius);
// Draw the blip. Alpha is based on how long ago the sweep crossed the blip
long blipDifference = now - mBlipTime;
gridPaint.setAlpha(255 - (int)((128 * blipDifference) >> 10));
canvas.drawBitmap(mBlip, center + (cos * blipRadius) - 8 ,
center + (sin * blipRadius) - 8, gridPaint);
gridPaint.setAlpha(255);
}
}
private void addText(Canvas canvas, String str, int x, int y) {
mGridPaint.getTextBounds(str, 0, str.length(), mTextBounds);
mTextBounds.offset(x - (mTextBounds.width() >> 1), y);
mTextBounds.inset(-2, -2);
canvas.drawRect(mTextBounds, mErasePaint);
canvas.drawText(str, x, y, mGridPaint);
}
public void onAccuracyChanged(int sensor, int accuracy) {
}
/**
* Called when we get a new value from the compass
*
* @see android.hardware.SensorListener#onSensorChanged(int, float[])
*/
public void onSensorChanged(int sensor, float[] values) {
mOrientation = values[0];
postInvalidate();
}
/**
* Called when a location provider has a new location to report
*
* @see android.location.LocationListener#onLocationChanged(android.location.Location)
*/
public void onLocationChanged(Location location) {
if (!mHaveLocation) {
mHaveLocation = true;
}
final long now = SystemClock.uptimeMillis();
boolean useLocation = false;
final String provider = location.getProvider();
if (LocationManager.GPS_PROVIDER.equals(provider)) {
// Use GPS if available
mLastGpsFixTime = SystemClock.uptimeMillis();
useLocation = true;
} else if (LocationManager.NETWORK_PROVIDER.equals(provider)) {
// Use network provider if GPS is getting stale
useLocation = now - mLastGpsFixTime > RETAIN_GPS_MILLIS;
if (mNetworkLocation == null) {
mNetworkLocation = new Location(location);
} else {
mNetworkLocation.set(location);
}
mLastGpsFixTime = 0L;
}
if (useLocation) {
mMyLocationLat = location.getLatitude();
mMyLocationLon = location.getLongitude();
mDistance = GeoUtils.distanceKm(mMyLocationLat, mMyLocationLon, mTargetLat,
mTargetLon);
mBearing = GeoUtils.bearing(mMyLocationLat, mMyLocationLon, mTargetLat,
mTargetLon);
updateDistance(mDistance);
}
}
public void onProviderDisabled(String provider) {
}
public void onProviderEnabled(String provider) {
}
/**
* Called when a location provider has changed its availability.
*
* @see android.location.LocationListener#onStatusChanged(java.lang.String, int, android.os.Bundle)
*/
public void onStatusChanged(String provider, int status, Bundle extras) {
if (LocationManager.GPS_PROVIDER.equals(provider)) {
switch (status) {
case LocationProvider.AVAILABLE:
mGpsAvailable = true;
break;
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
mGpsAvailable = false;
if (mNetworkLocation != null && mNetworkAvailable) {
// Fallback to network location
mLastGpsFixTime = 0L;
onLocationChanged(mNetworkLocation);
} else {
handleUnknownLocation();
}
break;
}
} else if (LocationManager.NETWORK_PROVIDER.equals(provider)) {
switch (status) {
case LocationProvider.AVAILABLE:
mNetworkAvailable = true;
break;
case LocationProvider.OUT_OF_SERVICE:
case LocationProvider.TEMPORARILY_UNAVAILABLE:
mNetworkAvailable = false;
if (!mGpsAvailable) {
handleUnknownLocation();
}
break;
}
}
}
/**
* Called when we no longer have a valid lcoation.
*/
private void handleUnknownLocation() {
mHaveLocation = false;
mDistanceView.setText(R.string.scanning);
}
/**
* Update state to reflect whether we are using metric or standard units.
*
* @param useMetric True if the display should use metric units
*/
public void setUseMetric(boolean useMetric) {
mUseMetric = useMetric;
mLastScale = -1;
if (mHaveLocation) {
updateDistance(mDistance);
}
invalidate();
}
/**
* Update our state to reflect a new distance to the target. This may require
* choosing a new scale for the radar rings.
*
* @param distanceKm The new distance to the target
*/
private void updateDistance(double distanceKm) {
final double[] scaleChoices;
final float[] displayUnitsPerKm;
final String[] displayFormats;
final String[] scaleFormats;
String distanceStr = null;
if (mUseMetric) {
scaleChoices = mMetricScaleChoices;
displayUnitsPerKm = mMetricDisplayUnitsPerKm;
displayFormats = mMetricDisplayFormats;
scaleFormats = mMetricScaleFormats;
} else {
scaleChoices = mEnglishScaleChoices;
displayUnitsPerKm = mEnglishDisplayUnitsPerKm;
displayFormats = mEnglishDisplayFormats;
scaleFormats = mEnglishScaleFormats;
}
int count = scaleChoices.length;
for (int i = 0; i < count; i++) {
if (distanceKm < scaleChoices[i] || i == (count - 1)) {
String format = displayFormats[i];
double distanceDisplay = distanceKm * displayUnitsPerKm[i];
if (mLastScale != i) {
mLastScale = i;
String scaleFormat = scaleFormats[i];
float scaleDistance = (float) (scaleChoices[i] * displayUnitsPerKm[i]);
mDistanceScale[0] = String.format(scaleFormat, (scaleDistance / 4));
mDistanceScale[1] = String.format(scaleFormat, (scaleDistance / 2));
mDistanceScale[2] = String.format(scaleFormat, (scaleDistance * 3 / 4));
mDistanceScale[3] = String.format(scaleFormat, scaleDistance);
}
mDistanceRatio = (float) (mDistance / scaleChoices[mLastScale]);
distanceStr = String.format(format, distanceDisplay);
break;
}
}
mDistanceView.setText(distanceStr);
}
/**
* Turn on the sweep animation starting with the next draw
*/
public void startSweep() {
mSweepTime = SystemClock.uptimeMillis();
mSweepBefore = true;
}
/**
* Turn off the sweep animation
*/
public void stopSweep() {
mSweepTime = 0L;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment