diff options
Diffstat (limited to 'platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/MyLocationView.java')
-rw-r--r-- | platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/MyLocationView.java | 710 |
1 files changed, 710 insertions, 0 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/MyLocationView.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/MyLocationView.java new file mode 100644 index 0000000000..430ebfe443 --- /dev/null +++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/maps/widgets/MyLocationView.java @@ -0,0 +1,710 @@ +package com.mapbox.mapboxsdk.maps.widgets; + +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.PointF; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.LayerDrawable; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.location.Location; +import android.os.SystemClock; +import android.support.annotation.ColorInt; +import android.support.annotation.FloatRange; +import android.support.annotation.IntRange; +import android.support.annotation.NonNull; +import android.support.v4.content.ContextCompat; +import android.support.v4.view.animation.FastOutLinearInInterpolator; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +import com.mapbox.mapboxsdk.R; +import com.mapbox.mapboxsdk.camera.CameraPosition; +import com.mapbox.mapboxsdk.camera.CameraUpdateFactory; +import com.mapbox.mapboxsdk.constants.MapboxConstants; +import com.mapbox.mapboxsdk.constants.MyBearingTracking; +import com.mapbox.mapboxsdk.constants.MyLocationTracking; +import com.mapbox.mapboxsdk.geometry.LatLng; +import com.mapbox.mapboxsdk.location.LocationListener; +import com.mapbox.mapboxsdk.location.LocationServices; +import com.mapbox.mapboxsdk.maps.MapboxMap; +import com.mapbox.mapboxsdk.maps.Projection; +import com.mapbox.mapboxsdk.maps.UiSettings; +import com.mapbox.mapboxsdk.utils.ColorUtils; + +import java.lang.ref.WeakReference; + +/** + * UI element overlaid on a map to show the user's location. + */ +public class MyLocationView extends View { + + private MyLocationBehaviour myLocationBehaviour; + private MapboxMap mapboxMap; + private Projection projection; + private int maxSize; + private int[] contentPadding = new int[4]; + + private Location location; + private LatLng latLng; + private LatLng interpolatedLocation; + private LatLng previousLocation; + private long locationUpdateTimestamp; + + private float gpsDirection; + private float compassDirection; + private float previousDirection; + + private float accuracy = 0; + private Paint accuracyPaint = new Paint(); + + private ValueAnimator locationChangeAnimator; + private ValueAnimator accuracyAnimator; + + private Drawable foregroundDrawable; + private Drawable foregroundBearingDrawable; + private Drawable backgroundDrawable; + + private Rect foregroundBounds; + private Rect backgroundBounds; + + private int backgroundOffsetLeft; + private int backgroundOffsetTop; + private int backgroundOffsetRight; + private int backgroundOffsetBottom; + + @MyLocationTracking.Mode + private int myLocationTrackingMode; + + @MyBearingTracking.Mode + private int myBearingTrackingMode; + + private GpsLocationListener userLocationListener; + private CompassListener compassListener; + + public MyLocationView(Context context) { + super(context); + init(context); + } + + public MyLocationView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context); + } + + public MyLocationView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(context); + } + + private void init(Context context) { + setEnabled(false); + + // behavioural model + myLocationBehaviour = new MyLocationBehaviourFactory().getBehaviouralModel(MyLocationTracking.TRACKING_NONE); + compassListener = new CompassListener(context); + maxSize = (int) context.getResources().getDimension(R.dimen.userlocationview_size); + + // default implementation + setShadowDrawable(ContextCompat.getDrawable(context, R.drawable.ic_userlocationview_shadow)); + setForegroundDrawables(ContextCompat.getDrawable(context, R.drawable.ic_userlocationview_normal), ContextCompat.getDrawable(context, R.drawable.ic_userlocationview_bearing)); + setAccuracyTint(ColorUtils.getAccentColor(context)); + setAccuracyAlpha(100); + } + + public final void setForegroundDrawables(Drawable defaultDrawable, Drawable bearingDrawable) { + if (defaultDrawable == null || bearingDrawable == null) { + Log.e(MapboxConstants.TAG, "Not setting foreground drawables MyLocationView: default = " + defaultDrawable + " bearing = " + bearingDrawable); + return; + } + if (defaultDrawable.getIntrinsicWidth() != bearingDrawable.getIntrinsicWidth() || defaultDrawable.getIntrinsicHeight() != bearingDrawable.getIntrinsicHeight()) { + throw new RuntimeException("The dimensions from location and bearing drawables should be match"); + } + foregroundDrawable = defaultDrawable; + foregroundBearingDrawable = bearingDrawable; + invalidateBounds(); + } + + public final void setForegroundDrawableTint(@ColorInt int color) { + if (foregroundDrawable != null) { + foregroundDrawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + if (foregroundBearingDrawable != null) { + foregroundBearingDrawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + } + + public final void setShadowDrawable(Drawable drawable) { + setShadowDrawable(drawable, 0, 0, 0, 0); + } + + public final void setShadowDrawable(Drawable drawable, int left, int top, int right, int bottom) { + if (drawable == null) { + return; + } + backgroundDrawable = drawable; + backgroundOffsetLeft = left; + backgroundOffsetTop = top; + backgroundOffsetRight = right; + backgroundOffsetBottom = bottom; + invalidateBounds(); + } + + public final void setShadowDrawableTint(@ColorInt int color) { + if (backgroundDrawable == null) { + return; + } + + backgroundDrawable.mutate().setColorFilter(color, PorterDuff.Mode.SRC_IN); + } + + public final void setAccuracyTint(@ColorInt int color) { + int alpha = accuracyPaint.getAlpha(); + accuracyPaint.setColor(color); + accuracyPaint.setAlpha(alpha); + } + + public final void setAccuracyAlpha(@IntRange(from = 0, to = 255) int alpha) { + accuracyPaint.setAlpha(alpha); + } + + private void invalidateBounds() { + if (backgroundDrawable == null || foregroundDrawable == null || foregroundBearingDrawable == null) { + return; + } + + int backgroundWidth = backgroundDrawable.getIntrinsicWidth(); + int backgroundHeight = backgroundDrawable.getIntrinsicHeight(); + + int foregroundWidth = foregroundDrawable.getIntrinsicWidth(); + int foregroundHeight = foregroundDrawable.getIntrinsicHeight(); + + int horizontalOffset = backgroundOffsetLeft - backgroundOffsetRight; + int verticalOffset = backgroundOffsetTop - backgroundOffsetBottom; + + int accuracyWidth = 2 * maxSize; + + backgroundBounds = new Rect(accuracyWidth - (backgroundWidth / 2) + horizontalOffset, accuracyWidth + verticalOffset - (backgroundWidth / 2), accuracyWidth + (backgroundWidth / 2) + horizontalOffset, accuracyWidth + (backgroundHeight / 2) + verticalOffset); + foregroundBounds = new Rect(accuracyWidth - (foregroundWidth / 2), accuracyWidth - (foregroundHeight / 2), accuracyWidth + (foregroundWidth / 2), accuracyWidth + (foregroundHeight / 2)); + + // invoke a new measure + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + if (location == null || foregroundBounds == null || backgroundBounds == null || accuracyAnimator == null) { + // Not ready yet + return; + } + + // Draw circle + float metersPerPixel = (float) projection.getMetersPerPixelAtLatitude(location.getLatitude()); + float accuracyPixels = (Float) accuracyAnimator.getAnimatedValue() / metersPerPixel; + float maxRadius = getWidth() / 2; + canvas.drawCircle(foregroundBounds.centerX(), foregroundBounds.centerY(), accuracyPixels <= maxRadius ? accuracyPixels : maxRadius, accuracyPaint); + + // Draw shadow + if (backgroundDrawable != null) { + backgroundDrawable.draw(canvas); + } + + // Draw foreground + if (myBearingTrackingMode == MyBearingTracking.NONE) { + if (foregroundDrawable != null) { + foregroundDrawable.draw(canvas); + } + } else if (foregroundBearingDrawable != null && foregroundBounds != null) { + getRotateDrawable(foregroundBearingDrawable, myBearingTrackingMode == MyBearingTracking.COMPASS ? compassDirection : gpsDirection).draw(canvas); + } + } + + private Drawable getRotateDrawable(final Drawable d, final float angle) { + return new LayerDrawable(new Drawable[]{d}) { + @Override + public void draw(final Canvas canvas) { + canvas.save(); + canvas.rotate(angle, foregroundBounds.centerX(), foregroundBounds.centerY()); + super.draw(canvas); + canvas.restore(); + } + }; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + if (foregroundDrawable != null && foregroundBounds != null) { + foregroundDrawable.setBounds(foregroundBounds); + } + + if (foregroundBearingDrawable != null && foregroundBounds != null) { + foregroundBearingDrawable.setBounds(foregroundBounds); + } + + if (backgroundDrawable != null && backgroundBounds != null) { + backgroundDrawable.setBounds(backgroundBounds); + } + + setMeasuredDimension(4 * maxSize, 4 * maxSize); + } + + public void setTilt(@FloatRange(from = 0, to = 60.0f) double tilt) { + setRotationX((float) tilt); + } + + void updateOnNextFrame() { + mapboxMap.invalidate(); + } + + public void onPause() { + compassListener.onPause(); + toggleGps(false); + } + + public void onResume() { + if (myBearingTrackingMode == MyBearingTracking.COMPASS) { + compassListener.onResume(); + } + if (isEnabled()) { + toggleGps(true); + } + } + + public void update() { + if (isEnabled()) { + myLocationBehaviour.invalidate(); + } else { + setVisibility(View.INVISIBLE); + } + } + + public void setMapboxMap(MapboxMap mapboxMap) { + this.mapboxMap = mapboxMap; + this.projection = mapboxMap.getProjection(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + setVisibility(enabled ? View.VISIBLE : View.INVISIBLE); + toggleGps(enabled); + } + + /** + * Enabled / Disable GPS location updates along with updating the UI + * + * @param enableGps true if GPS is to be enabled, false if GPS is to be disabled + */ + private void toggleGps(boolean enableGps) { + LocationServices locationServices = LocationServices.getLocationServices(getContext()); + if (enableGps) { + // Set an initial location if one available + Location lastLocation = locationServices.getLastLocation(); + + if (lastLocation != null) { + location = lastLocation; + } + + if (userLocationListener == null) { + userLocationListener = new GpsLocationListener(this); + } + + locationServices.addLocationListener(userLocationListener); + } else { + // Disable location and user dot + location = null; + locationServices.removeLocationListener(userLocationListener); + } + + locationServices.toggleGPS(enableGps); + } + + public Location getLocation() { + return location; + } + + public void setLocation(Location location) { + if (location == null) { + this.location = null; + return; + } + + this.location = location; + myLocationBehaviour.updateLatLng(location); + } + + public void setMyBearingTrackingMode(@MyBearingTracking.Mode int myBearingTrackingMode) { + this.myBearingTrackingMode = myBearingTrackingMode; + if (myBearingTrackingMode == MyBearingTracking.COMPASS) { + compassListener.onResume(); + } else { + compassListener.onPause(); + if (myLocationTrackingMode == MyLocationTracking.TRACKING_FOLLOW) { + // always face north + gpsDirection = 0; + } + } + invalidate(); + update(); + } + + public void setMyLocationTrackingMode(@MyLocationTracking.Mode int myLocationTrackingMode) { + this.myLocationTrackingMode = myLocationTrackingMode; + + MyLocationBehaviourFactory factory = new MyLocationBehaviourFactory(); + myLocationBehaviour = factory.getBehaviouralModel(myLocationTrackingMode); + + if (myLocationTrackingMode != MyLocationTracking.TRACKING_NONE && location != null) { + // center map directly if we have a location fix + myLocationBehaviour.updateLatLng(location); + mapboxMap.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(location))); + } + invalidate(); + update(); + } + + private void setCompass(float bearing) { + if (myLocationTrackingMode == MyLocationTracking.TRACKING_NONE) { + float oldDir = compassDirection; + float newDir = bearing; + float diff = oldDir - newDir; + if (diff > 180.0f) { + newDir += 360.0f; + } else if (diff < -180.0f) { + newDir -= 360.f; + } + compassDirection = newDir; + invalidate(); + } else { + compassDirection = 0; + } + } + + public float getCenterX() { + return getX() + getMeasuredWidth() / 2; + } + + public float getCenterY() { + return getY() + getMeasuredHeight() / 2; + } + + public void setContentPadding(int[] padding) { + contentPadding = padding; + } + + private static class GpsLocationListener implements LocationListener { + + private WeakReference<MyLocationView> mUserLocationView; + + public GpsLocationListener(MyLocationView myLocationView) { + mUserLocationView = new WeakReference<>(myLocationView); + } + + /** + * Callback method for receiving location updates from LocationServices. + * + * @param location The new Location data + */ + @Override + public void onLocationChanged(Location location) { + MyLocationView locationView = mUserLocationView.get(); + if (locationView != null) { + locationView.setLocation(location); + } + } + } + + private class CompassListener implements SensorEventListener { + + private boolean paused; + private SensorManager mSensorManager; + private Sensor mAccelerometer; + private Sensor mMagnetometer; + private float[] mLastAccelerometer = new float[3]; + private float[] mLastMagnetometer = new float[3]; + private boolean mLastAccelerometerSet = false; + private boolean mLastMagnetometerSet = false; + private float[] mR = new float[9]; + private float[] mOrientation = new float[3]; + private float mCurrentDegree = 0f; + + // Controls the sensor updateLatLng rate in milliseconds + private static final int UPDATE_RATE_MS = 300; + + // Compass data + private long mCompassUpdateNextTimestamp = 0; + + public CompassListener(Context context) { + mSensorManager = (SensorManager) context.getSystemService(Context.SENSOR_SERVICE); + mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + mMagnetometer = mSensorManager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD); + } + + public void onResume() { + paused = false; + mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_GAME); + mSensorManager.registerListener(this, mMagnetometer, SensorManager.SENSOR_DELAY_GAME); + } + + public void onPause() { + paused = true; + mSensorManager.unregisterListener(this, mAccelerometer); + mSensorManager.unregisterListener(this, mMagnetometer); + } + + @Override + public void onSensorChanged(SensorEvent event) { + if (paused) { + return; + } + + long currentTime = SystemClock.elapsedRealtime(); + if (currentTime < mCompassUpdateNextTimestamp) { + return; + } + + if (event.sensor == mAccelerometer) { + System.arraycopy(event.values, 0, mLastAccelerometer, 0, event.values.length); + mLastAccelerometerSet = true; + } else if (event.sensor == mMagnetometer) { + System.arraycopy(event.values, 0, mLastMagnetometer, 0, event.values.length); + mLastMagnetometerSet = true; + } + + if (mLastAccelerometerSet && mLastMagnetometerSet) { + SensorManager.getRotationMatrix(mR, null, mLastAccelerometer, mLastMagnetometer); + SensorManager.getOrientation(mR, mOrientation); + float azimuthInRadians = mOrientation[0]; + + float compassBearing = (float) (Math.toDegrees(azimuthInRadians) + 360) % 360; + if (compassBearing < 0) { + // only allow positive degrees + compassBearing += 360; + } + + if (compassBearing > mCurrentDegree + 15 || compassBearing < mCurrentDegree - 15) { + mCurrentDegree = compassBearing; + setCompass(mCurrentDegree); + } + } + mCompassUpdateNextTimestamp = currentTime + UPDATE_RATE_MS; + } + + public float getCurrentDegree() { + return mCurrentDegree; + } + + public boolean isPaused() { + return paused; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + } + + private class MarkerCoordinateAnimatorListener implements ValueAnimator.AnimatorUpdateListener { + + private MyLocationBehaviour behaviour; + private double fromLat; + private double fromLng; + private double toLat; + private double toLng; + + private MarkerCoordinateAnimatorListener(MyLocationBehaviour myLocationBehaviour, LatLng from, LatLng to) { + behaviour = myLocationBehaviour; + fromLat = from.getLatitude(); + fromLng = from.getLongitude(); + toLat = to.getLatitude(); + toLng = to.getLongitude(); + } + + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float frac = animation.getAnimatedFraction(); + double latitude = fromLat + (toLat - fromLat) * frac; + double longitude = fromLng + (toLng - fromLng) * frac; + behaviour.updateLatLng(latitude, longitude); + updateOnNextFrame(); + } + } + + private class MyLocationBehaviourFactory { + + public MyLocationBehaviour getBehaviouralModel(@MyLocationTracking.Mode int mode) { + if (mode == MyLocationTracking.TRACKING_NONE) { + return new MyLocationShowBehaviour(); + } else { + return new MyLocationTrackingBehaviour(); + } + } + } + + private abstract class MyLocationBehaviour { + + abstract void updateLatLng(@NonNull Location location); + + public void updateLatLng(double lat, double lon) { + if (latLng != null) { + latLng.setLatitude(lat); + latLng.setLongitude(lon); + } + } + + protected void updateAccuracy(@NonNull Location location) { + if (accuracyAnimator != null && accuracyAnimator.isRunning()) { + // use current accuracy as a starting point + accuracy = (Float) accuracyAnimator.getAnimatedValue(); + accuracyAnimator.end(); + } + + accuracyAnimator = ValueAnimator.ofFloat(accuracy * 10, location.getAccuracy() * 10); + accuracyAnimator.setDuration(750); + accuracyAnimator.start(); + accuracy = location.getAccuracy(); + } + + abstract void invalidate(); + } + + private class MyLocationTrackingBehaviour extends MyLocationBehaviour { + + @Override + void updateLatLng(@NonNull Location location) { + if (latLng == null) { + // first location fix + latLng = new LatLng(location); + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + } + + // updateLatLng timestamp + long previousUpdateTimeStamp = locationUpdateTimestamp; + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + + // calculate animation duration + long locationUpdateDuration; + if (previousUpdateTimeStamp == 0) { + locationUpdateDuration = 0; + } else { + // TODO remove 10 * hack to multiply duration to workaround easing interpolation (easeCamera) + locationUpdateDuration = 10 * (locationUpdateTimestamp - previousUpdateTimeStamp); + } + + // calculate interpolated location + previousLocation = latLng; + latLng = new LatLng(location); + interpolatedLocation = new LatLng((latLng.getLatitude() + previousLocation.getLatitude()) / 2, (latLng.getLongitude() + previousLocation.getLongitude()) / 2); + + // build new camera + CameraPosition.Builder builder = new CameraPosition.Builder().target(interpolatedLocation); + + // add direction + if (myBearingTrackingMode == MyBearingTracking.GPS) { + if (location.hasBearing()) { + builder.bearing(location.getBearing()); + gpsDirection = 0; + } + } else if (myBearingTrackingMode == MyBearingTracking.COMPASS) { + if (!compassListener.isPaused()) { + builder.bearing(compassListener.getCurrentDegree()); + compassDirection = 0; + } + } + + updateAccuracy(location); + + // animate to new camera + mapboxMap.easeCamera(CameraUpdateFactory.newCameraPosition(builder.build()), (int) locationUpdateDuration, null); + } + + @Override + void invalidate() { + int[] mapPadding = mapboxMap.getPadding(); + UiSettings uiSettings = mapboxMap.getUiSettings(); + setX((uiSettings.getWidth() - getWidth() + mapPadding[0] - mapPadding[2]) / 2 + (contentPadding[0] - contentPadding[2]) / 2); + setY((uiSettings.getHeight() - getHeight() - mapPadding[3] + mapPadding[1]) / 2 + (contentPadding[1] - contentPadding[3]) / 2); + MyLocationView.this.invalidate(); + } + } + + private class MyLocationShowBehaviour extends MyLocationBehaviour { + + @Override + void updateLatLng(@NonNull final Location location) { + if (latLng == null) { + // first location update + latLng = new LatLng(location); + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + } + + // update LatLng location + previousLocation = latLng; + latLng = new LatLng(location); + + // update LatLng direction + if (location.hasBearing()) { + gpsDirection = clamp(location.getBearing() - (float) mapboxMap.getCameraPosition().bearing); + } + + // update LatLng accuracy + updateAccuracy(location); + + // calculate updateLatLng time + add some extra offset to improve animation + long previousUpdateTimeStamp = locationUpdateTimestamp; + locationUpdateTimestamp = SystemClock.elapsedRealtime(); + long locationUpdateDuration = (long) ((locationUpdateTimestamp - previousUpdateTimeStamp) * 1.2); + + // calculate interpolated entity + interpolatedLocation = new LatLng((latLng.getLatitude() + previousLocation.getLatitude()) / 2, (latLng.getLongitude() + previousLocation.getLongitude()) / 2); + + // animate changes + if (locationChangeAnimator != null) { + locationChangeAnimator.end(); + locationChangeAnimator = null; + } + + locationChangeAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); + locationChangeAnimator.setDuration((long) (locationUpdateDuration * 1.2)); + locationChangeAnimator.addUpdateListener(new MarkerCoordinateAnimatorListener(this, + previousLocation, interpolatedLocation + )); + locationChangeAnimator.setInterpolator(new FastOutLinearInInterpolator()); + locationChangeAnimator.start(); + + // use interpolated location as current location + latLng = interpolatedLocation; + } + + private float clamp(float direction) { + float diff = previousDirection - direction; + if (diff > 180.0f) { + direction += 360.0f; + } else if (diff < -180.0f) { + direction -= 360.f; + } + previousDirection = direction; + return direction; + } + + @Override + void invalidate() { + PointF screenLocation = projection.toScreenLocation(latLng); + if (screenLocation != null) { + setX((screenLocation.x - getWidth() / 2)); + setY((screenLocation.y - getHeight() / 2)); + } + MyLocationView.this.invalidate(); + } + } +} |