[android] Implement UserLocationView

Draws animated and synced GPS marker.
Inlcudes direction arrow and accuracy ring.
Fade out the compass after a delay when reset to north.
Lots of other performance and memory improvements.
add OnMyLocationChangeListener
Move FAB with snackbar
Fix getMetersPerPixelAtLatitude
Fix some incorrect annotations
Fixes #2668
Fixes #2411
Fixes #2678
Fixes #2675
Fixes #2566
Fixes #2549
Fixes #2692
Fixes #2572
This commit is contained in:
Leith Bade
2015-10-20 22:37:50 +11:00
parent 729a793b0e
commit 8b79a8d6e6
19 changed files with 796 additions and 281 deletions

View File

@@ -2,6 +2,8 @@ package com.mapbox.mapboxsdk.annotations;
import com.mapbox.mapboxsdk.views.MapView;
import java.lang.ref.WeakReference;
public abstract class Annotation implements Comparable<Annotation> {
/**
@@ -10,7 +12,7 @@ public abstract class Annotation implements Comparable<Annotation> {
* Internal C++ id is stored as unsigned int.
*/
private long id = -1; // -1 unless added to a MapView
private MapView mapView;
private WeakReference<MapView> mapView;
private float alpha = 1.0f;
private boolean visible = true;
@@ -33,8 +35,10 @@ public abstract class Annotation implements Comparable<Annotation> {
}
public void remove() {
if (mapView == null) return;
mapView.removeAnnotation(this);
if ((mapView == null) || (mapView.get() == null)) {
return;
}
mapView.get().removeAnnotation(this);
}
void setAlpha(float alpha) {
@@ -52,11 +56,14 @@ public abstract class Annotation implements Comparable<Annotation> {
* Do not use this method. Used internally by the SDK.
*/
public void setMapView(MapView mapView) {
this.mapView = mapView;
this.mapView = new WeakReference<MapView>(mapView);
}
protected MapView getMapView() {
return mapView;
if (mapView == null) {
return null;
}
return mapView.get();
}
void setVisible(boolean visible) {

View File

@@ -13,13 +13,15 @@ import com.mapbox.mapboxsdk.R;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.views.MapView;
import java.lang.ref.WeakReference;
/**
* A tooltip view
*/
final class InfoWindow {
private Marker boundMarker;
private MapView mMapView;
private WeakReference<Marker> mBoundMarker;
private WeakReference<MapView> mMapView;
private boolean mIsVisible;
protected View mView;
@@ -29,7 +31,7 @@ final class InfoWindow {
static int mImageId = 0;
public InfoWindow(int layoutResId, MapView mapView) {
mMapView = mapView;
mMapView = new WeakReference<MapView>(mapView);
mIsVisible = false;
mView = LayoutInflater.from(mapView.getContext()).inflate(layoutResId, mapView, false);
@@ -50,7 +52,7 @@ final class InfoWindow {
}
public InfoWindow(View view, MapView mapView) {
mMapView = mapView;
mMapView = new WeakReference<MapView>(mapView);
mIsVisible = false;
mView = view;
@@ -80,7 +82,7 @@ final class InfoWindow {
mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
// Calculate default Android x,y coordinate
PointF coords = mMapView.toScreenLocation(position);
PointF coords = mMapView.get().toScreenLocation(position);
float x = coords.x - (mView.getMeasuredWidth() / 2) + offsetX;
float y = coords.y - mView.getMeasuredHeight() + offsetY;
@@ -89,25 +91,25 @@ final class InfoWindow {
float left = x;
// get right/left map view
float mapRight = mMapView.getRight();
float mapLeft = mMapView.getLeft();
float mapRight = mMapView.get().getRight();
float mapLeft = mMapView.get().getLeft();
if (mView instanceof InfoWindowView) {
// only apply repositioning/margin for InfoWindowView
Resources resources = mMapView.getContext().getResources();
Resources resources = mMapView.get().getContext().getResources();
float margin = resources.getDimension(R.dimen.infowindow_margin);
float tipViewOffset = resources.getDimension(R.dimen.infowindow_tipview_width) / 2;
float tipViewMarginLeft = mView.getMeasuredWidth() / 2 - tipViewOffset;
// fit screen on right
if (right > mMapView.getRight()) {
if (right > mMapView.get().getRight()) {
x -= right - mapRight;
tipViewMarginLeft += right - mapRight + tipViewOffset;
right = x + mView.getMeasuredWidth();
}
// fit screen left
if (left < mMapView.getLeft()) {
if (left < mMapView.get().getLeft()) {
x += mapLeft - left;
tipViewMarginLeft -= mapLeft - left + tipViewOffset;
left = x;
@@ -136,7 +138,7 @@ final class InfoWindow {
mView.setY(y);
close(); //if it was already opened
mMapView.addView(mView, lp);
mMapView.get().addView(mView, lp);
mIsVisible = true;
return this;
}
@@ -172,7 +174,7 @@ final class InfoWindow {
* @return the mapView
*/
public MapView getMapView() {
return mMapView;
return mMapView.get();
}
/**
@@ -204,13 +206,16 @@ final class InfoWindow {
//by default, do nothing
}
public InfoWindow setBoundMarker(Marker aBoundMarker) {
this.boundMarker = aBoundMarker;
public InfoWindow setBoundMarker(Marker boundMarker) {
mBoundMarker = new WeakReference<Marker>(boundMarker);
return this;
}
public Marker getBoundMarker() {
return boundMarker;
if (mBoundMarker == null) {
return null;
}
return mBoundMarker.get();
}
/**

View File

@@ -71,16 +71,28 @@ public class LatLng implements ILatLng, Parcelable, Serializable {
altitude = in.readDouble();
}
public void setLatitude(double latitude) {
this.latitude = latitude;
}
@Override
public double getLatitude() {
return latitude;
}
public void setLongitude(double longitude) {
this.longitude = longitude;
}
@Override
public double getLongitude() {
return longitude;
}
public void setAltitude(double altitude) {
this.altitude = altitude;
}
@Override
public double getAltitude() {
return altitude;

View File

@@ -1,23 +1,27 @@
package com.mapbox.mapboxsdk.views;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.hardware.GeomagneticField;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.location.Location;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.ImageView;
import com.mapbox.mapboxsdk.R;
import java.lang.ref.WeakReference;
import java.util.Timer;
import java.util.TimerTask;
final class CompassView extends ImageView {
private Timer mNorthTimer;
private double mDirection = 0.0f;
private ViewPropertyAnimator mFadeAnimator;
public CompassView(Context context) {
super(context);
initialize(context);
@@ -34,20 +38,94 @@ final class CompassView extends ImageView {
}
private void initialize(Context context) {
// View configuration
setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.compass));
setContentDescription(getResources().getString(R.string.compassContentDescription));
setEnabled(false);
// Layout params
float mScreenDensity = context.getResources().getDisplayMetrics().density;
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) (48 * mScreenDensity), (int) (48 * mScreenDensity));
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams((int) (48 * mScreenDensity), (int) (48 * mScreenDensity));
setLayoutParams(lp);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
if (enabled) {
if (mDirection != 0.0) {
if (mNorthTimer != null){
mNorthTimer.cancel();
mNorthTimer = null;
}
if (mFadeAnimator != null) {
mFadeAnimator.cancel();
}
mFadeAnimator = null;
setAlpha(1.0f);
setVisibility(View.VISIBLE);
}
} else {
if (mNorthTimer != null){
mNorthTimer.cancel();
mNorthTimer = null;
}
if (mFadeAnimator != null) {
mFadeAnimator.cancel();
}
mFadeAnimator = null;
setVisibility(View.INVISIBLE);
}
}
public void update(double direction) {
mDirection = direction;
setRotation((float) direction);
if (direction == 0.0) {
if (getVisibility() == View.INVISIBLE) {
return;
}
if (mNorthTimer == null) {
if (mFadeAnimator != null) {
mFadeAnimator.cancel();
}
mFadeAnimator = null;
mNorthTimer = new Timer("CompassView North timer");
mNorthTimer.schedule(new TimerTask() {
@Override
public void run() {
post(new Runnable() {
@Override
public void run() {
setAlpha(1.0f);
mFadeAnimator = animate().alpha(0.0f).setDuration(1000);
mFadeAnimator.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setVisibility(View.INVISIBLE);
mNorthTimer = null;
}
});
}
});
}
}, 1000);
}
} else {
if (mNorthTimer != null){
mNorthTimer.cancel();
mNorthTimer = null;
}
if (mFadeAnimator != null) {
mFadeAnimator.cancel();
}
setAlpha(1.0f);
setVisibility(View.VISIBLE);
}
}
public static class CompassClickListener implements View.OnClickListener {

View File

@@ -1,6 +1,5 @@
package com.mapbox.mapboxsdk.views;
import android.animation.Animator;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.Dialog;
@@ -13,7 +12,7 @@ import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Canvas;
import android.graphics.PointF;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
@@ -48,7 +47,6 @@ import android.view.TextureView;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.widget.ArrayAdapter;
import android.widget.FrameLayout;
import android.widget.ImageView;
@@ -73,10 +71,6 @@ import com.mapbox.mapboxsdk.geometry.BoundingBox;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngZoom;
import com.mapbox.mapboxsdk.utils.ApiAccess;
import com.mapzen.android.lost.api.LocationListener;
import com.mapzen.android.lost.api.LocationRequest;
import com.mapzen.android.lost.api.LocationServices;
import com.mapzen.android.lost.api.LostApiClient;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -170,7 +164,7 @@ public final class MapView extends FrameLayout {
private NativeMapView mNativeMapView;
// Used to track rendering
private Boolean mDirty = false;
private TextureView mTextureView;
// Used to handle DPI scaling
private float mScreenDensity = 1.0f;
@@ -194,14 +188,7 @@ public final class MapView extends FrameLayout {
private Context mContext;
// Used for user location
private LostApiClient mLocationClient;
private LocationRequest mLocationRequest;
private ImageView mGpsMarker;
private float mGpsMarkerOffset;
private Location mGpsLocation;
private MyLocationListener mLocationListener;
private ViewPropertyAnimator mGpsMarkerAnimatorX;
private ViewPropertyAnimator mGpsMarkerAnimatorY;
private UserLocationView mUserLocationView;
// Used for the compass
private CompassView mCompassView;
@@ -222,7 +209,7 @@ public final class MapView extends FrameLayout {
private ImageView mAttributionsView;
// Used to manage MapChange event listeners
private ArrayList<OnMapChangedListener> mOnMapChangedListener;
private ArrayList<OnMapChangedListener> mOnMapChangedListener = new ArrayList<>();
// Used to manage map click event listeners
private OnMapClickListener mOnMapClickListener;
@@ -247,7 +234,6 @@ public final class MapView extends FrameLayout {
private boolean mScrollEnabled = true;
private boolean mRotateEnabled = true;
private String mStyleUrl;
private boolean mIsMyLocationEnabled = false;
//
// Inner classes
@@ -479,10 +465,26 @@ public final class MapView extends FrameLayout {
* @return View to be shown as a {@code InfoWindow}. If null is returned the default
* {@code InfoWindow} will be shown.
*/
@NonNull
@Nullable
View getInfoWindow(@NonNull Marker marker);
}
/**
* Interface definition for a callback to be invoked when the the My Location dot
* (which signifies the user's location) changes location.
*
* @see MapView#setOnMyLocationChangeListener(OnMyLocationChangeListener)
*/
public interface OnMyLocationChangeListener {
/**
* Called when the location of the My Location dot has changed
* (be it latitude/longitude, bearing or accuracy).
*
* @param location The current location of the My Location dot The type of map change event.
*/
void onMyLocationChange(@Nullable Location location);
}
//
// Constructors
//
@@ -563,9 +565,12 @@ public final class MapView extends FrameLayout {
// Save the context
mContext = context;
setWillNotDraw(false);
// Create the TextureView
TextureView textureView = new TextureView(mContext);
addView(textureView);
mTextureView = new TextureView(mContext);
addView(mTextureView);
mTextureView.setSurfaceTextureListener(new SurfaceTextureListener());
// Check if we are in Android Studio UI editor to avoid error in layout preview
if (isInEditMode()) {
@@ -600,9 +605,6 @@ public final class MapView extends FrameLayout {
setFocusableInTouchMode(true);
requestFocus();
// Register the TextureView callbacks
textureView.setSurfaceTextureListener(new SurfaceTextureListener());
// Touch gesture detectors
mGestureDetector = new GestureDetectorCompat(context, new GestureListener());
mGestureDetector.setIsLongpressEnabled(true);
@@ -628,26 +630,12 @@ public final class MapView extends FrameLayout {
onConnectivityChanged(isConnected);
}
// Setup location services
mLocationClient = new LostApiClient.Builder(getContext()).build();
mLocationRequest = LocationRequest.create()
.setFastestInterval(1250l)
.setSmallestDisplacement(2.0f)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
// Setup user location UI
mGpsMarker = new ImageView(getContext());
mGpsMarker.setImageResource(R.drawable.location_marker);
mGpsMarker.setVisibility(View.INVISIBLE);
float iconSize = 27.0f * mScreenDensity;
mGpsMarkerOffset = iconSize/2;
FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams((int) iconSize, (int) iconSize);
mGpsMarker.setLayoutParams(lp);
addView(mGpsMarker);
mUserLocationView = new UserLocationView(this, getContext());
addView(mUserLocationView);
// Setup compass
mCompassView = new CompassView(mContext);
mCompassView.setVisibility(View.INVISIBLE);
mCompassView.setOnClickListener(new CompassView.CompassClickListener(this));
addView(mCompassView);
@@ -655,7 +643,7 @@ public final class MapView extends FrameLayout {
mLogoView = new ImageView(mContext);
mLogoView.setImageDrawable(ContextCompat.getDrawable(mContext, R.drawable.ic_logo_mapbox));
mLogoView.setContentDescription(getResources().getString(R.string.mapboxIconContentDescription));
LayoutParams logoParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams logoParams = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
mLogoView.setLayoutParams(logoParams);
addView(mLogoView);
@@ -675,10 +663,6 @@ public final class MapView extends FrameLayout {
addView(mAttributionsView);
mAttributionsView.setOnClickListener(new AttributionOnClickListener(this));
// Setup Support For Listener Tracking
// MapView's internal listener is setup in onCreate()
mOnMapChangedListener = new ArrayList<>();
// Load the attributes
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.MapView, 0, 0);
try {
@@ -887,6 +871,9 @@ public final class MapView extends FrameLayout {
public void onDestroy() {
mNativeMapView.terminateContext();
mNativeMapView.terminateDisplay();
mNativeMapView.destroySurface();
mNativeMapView.destroy();
mNativeMapView = null;
}
/**
@@ -901,6 +888,7 @@ public final class MapView extends FrameLayout {
*/
@UiThread
public void onStop() {
mUserLocationView.cancelAnimations();
}
/**
@@ -912,10 +900,7 @@ public final class MapView extends FrameLayout {
getContext().unregisterReceiver(mConnectivityReceiver);
mConnectivityReceiver = null;
if (mIsMyLocationEnabled) {
toggleGps(false);
}
mUserLocationView.pause();
mNativeMapView.pause();
}
@@ -928,11 +913,9 @@ public final class MapView extends FrameLayout {
mConnectivityReceiver = new ConnectivityReceiver();
mContext.registerReceiver(mConnectivityReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
if (mIsMyLocationEnabled) {
toggleGps(true);
}
mUserLocationView.resume();
mNativeMapView.resume();
mNativeMapView.update();
}
/**
@@ -1360,7 +1343,6 @@ public final class MapView extends FrameLayout {
* @param style The bundled style. Accepts one of the values from {@link Style}.
*/
@UiThread
@NonNull
public void setStyle(@Style.StyleUrl String style) {
setStyleUrl(style);
}
@@ -1694,8 +1676,10 @@ public final class MapView extends FrameLayout {
throw new NullPointerException("markerOptionsList is null");
}
List<Marker> markers = new ArrayList<>(markerOptionsList.size());
for (MarkerOptions markerOptions : markerOptionsList) {
int count = markerOptionsList.size();
List<Marker> markers = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
MarkerOptions markerOptions = markerOptionsList.get(i);
Marker marker = prepareMarker(markerOptions);
markers.add(marker);
}
@@ -1703,7 +1687,6 @@ public final class MapView extends FrameLayout {
long[] ids = mNativeMapView.addMarkers(markers);
Marker m;
int count = markers.size();
for (int i = 0; i < count; i++) {
m = markers.get(i);
m.setId(ids[i]);
@@ -1749,8 +1732,10 @@ public final class MapView extends FrameLayout {
}
// TODO make faster in JNI
List<Polyline> polylines = new ArrayList<>(polylineOptionsList.size());
for (PolylineOptions polylineOptions : polylineOptionsList) {
int count = polylineOptionsList.size();
List<Polyline> polylines = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
PolylineOptions polylineOptions = polylineOptionsList.get(i);
polylines.add(addPolyline(polylineOptions));
}
@@ -1842,7 +1827,9 @@ public final class MapView extends FrameLayout {
}
// TODO make faster in JNI
for (Annotation annotation : annotationList) {
int count = annotationList.size();
for (int i = 0; i < count; i++) {
Annotation annotation = annotationList.get(i);
removeAnnotation(annotation);
}
}
@@ -1887,8 +1874,8 @@ public final class MapView extends FrameLayout {
long[] ids = mNativeMapView.getAnnotationsInBounds(bbox);
List<Long> idsList = new ArrayList<>(ids.length);
for (long id : ids) {
idsList.add(id);
for (int i = 0; i < ids.length; i++) {
idsList.add(ids[i]);
}
List<Marker> annotations = new ArrayList<>(ids.length);
@@ -1924,7 +1911,7 @@ public final class MapView extends FrameLayout {
*/
@UiThread
public double getMetersPerPixelAtLatitude(@FloatRange(from = -180, to = 180) double latitude) {
return mNativeMapView.getMetersPerPixelAtLatitude(latitude, getZoomLevel());
return mNativeMapView.getMetersPerPixelAtLatitude(latitude, getZoomLevel()) / mScreenDensity;
}
private void selectMarker(Marker marker) {
@@ -1977,7 +1964,9 @@ public final class MapView extends FrameLayout {
}
private void adjustTopOffsetPixels() {
for (Annotation annotation : mAnnotations) {
int count = mAnnotations.size();
for (int i = 0; i < count; i++) {
Annotation annotation = mAnnotations.get(i);
if (annotation instanceof Marker) {
Marker marker = (Marker) annotation;
marker.setTopOffsetPixels(
@@ -2012,29 +2001,16 @@ public final class MapView extends FrameLayout {
// Called when the map needs to be rerendered
// Called via JNI from NativeMapView
synchronized protected void onInvalidate() {
if (!mDirty) {
mDirty = true;
postRender();
}
protected void onInvalidate() {
postInvalidate();
}
private void postRender() {
Runnable mRunnable = new Runnable() {
@Override
public void run() {
updateCompass();
updateGpsMarker();
mNativeMapView.renderSync();
mDirty = false;
}
};
if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
postOnAnimation(mRunnable);
} else {
postDelayed(mRunnable, 1000 / 60);
@Override
public void onDraw(Canvas canvas) {
if (!mNativeMapView.isPaused()) {
mNativeMapView.renderSync();
}
super.onDraw(canvas);
}
@Override
@@ -2059,7 +2035,9 @@ public final class MapView extends FrameLayout {
// Must do all EGL/GL ES destruction here
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
mNativeMapView.destroySurface();
if (mNativeMapView != null) {
mNativeMapView.destroySurface();
}
return true;
}
@@ -2070,13 +2048,20 @@ public final class MapView extends FrameLayout {
mNativeMapView.resizeFramebuffer(width, height);
}
// Not used
// Called when the SurfaceTexure frame is drawn to screen
// Must sync with UI here
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// Do nothing
mCompassView.update(getDirection());
mUserLocationView.update();
}
}
// Used by UserLocationView
void update() {
mNativeMapView.update();
}
//
// View events
//
@@ -2111,7 +2096,7 @@ public final class MapView extends FrameLayout {
// Called when user touches the screen, all positions are absolute
@Override
public boolean onTouchEvent(MotionEvent event) {
public boolean onTouchEvent(@NonNull MotionEvent event) {
// Check and ignore non touch or left clicks
if ((event.getButtonState() != 0) && (event.getButtonState() != MotionEvent.BUTTON_PRIMARY)) {
@@ -2260,7 +2245,9 @@ public final class MapView extends FrameLayout {
if (newSelectedMarkerId >= 0) {
for (Annotation annotation : mAnnotations) {
int count = mAnnotations.size();
for (int i = 0; i < count; i++) {
Annotation annotation = mAnnotations.get(i);
if (annotation instanceof Marker) {
if (annotation.getId() == newSelectedMarkerId) {
if (mSelectedMarker == null || annotation.getId() != mSelectedMarker.getId()) {
@@ -2512,7 +2499,7 @@ public final class MapView extends FrameLayout {
// Called when the user presses a key, also called for repeating keys held
// down
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
// If the user has held the scroll key down for a while then accelerate
// the scroll speed
double scrollDist = event.getRepeatCount() >= 5 ? 50.0 : 10.0;
@@ -2761,7 +2748,7 @@ public final class MapView extends FrameLayout {
// Called when the mouse pointer enters or exits the view
// or when it fades in or out due to movement
@Override
public boolean onHoverEvent(MotionEvent event) {
public boolean onHoverEvent(@NonNull MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE:
@@ -2840,18 +2827,12 @@ public final class MapView extends FrameLayout {
// Called when the map view transformation has changed
// Called via JNI from NativeMapView
// Forward to any listeners
protected void onMapChanged(int rawChange) {
final int mapChange = rawChange;
protected void onMapChanged(int mapChange) {
if (mOnMapChangedListener != null) {
post(new Runnable() {
@Override
public void run() {
for (OnMapChangedListener listener : mOnMapChangedListener) {
//noinspection ResourceType
listener.onMapChanged(mapChange);
}
}
});
int count = mOnMapChangedListener.size();
for (int i = 0; i < count; i++) {
mOnMapChangedListener.get(i).onMapChanged(mapChange);
}
}
}
@@ -2883,14 +2864,14 @@ public final class MapView extends FrameLayout {
// Called via JNI from NativeMapView
// Forward to any listener
protected void onFpsChanged(final double fps) {
if (mOnFpsChangedListener != null) {
post(new Runnable() {
@Override
public void run() {
post(new Runnable() {
@Override
public void run() {
if (mOnFpsChangedListener != null) {
mOnFpsChangedListener.onFpsChanged(fps);
}
});
}
}
});
}
/**
@@ -2959,13 +2940,13 @@ public final class MapView extends FrameLayout {
*/
@UiThread
public boolean isMyLocationEnabled() {
return mIsMyLocationEnabled;
return mUserLocationView.isEnabled();
}
/**
* Enables or disables the my-location layer.
* While enabled, the my-location layer continuously draws an indication of a user's current
* location.
* location and bearing.
* <p/>
* In order to use the my-location-layer feature you need to request permission for either
* {@link android.Manifest.permission#ACCESS_COARSE_LOCATION}
@@ -2975,8 +2956,7 @@ public final class MapView extends FrameLayout {
*/
@UiThread
public void setMyLocationEnabled(boolean enabled) {
mIsMyLocationEnabled = enabled;
toggleGps(enabled);
mUserLocationView.setEnabled(enabled);
}
/**
@@ -2987,74 +2967,19 @@ public final class MapView extends FrameLayout {
@UiThread
@Nullable
public Location getMyLocation() {
return mGpsLocation;
return mUserLocationView.getLocation();
}
/**
* Enabled / Disable GPS location updates along with updating the UI
* Sets a callback that's invoked when the the My Location dot
* (which signifies the user's location) changes location.
*
* @param enableGps true if GPS is to be enabled, false if GPS is to be disabled
* @param listener The callback that's invoked when the user clicks on a marker.
* To unset the callback, use null.
*/
private void toggleGps(boolean enableGps) {
if (enableGps) {
if (!mLocationClient.isConnected()) {
mGpsLocation = null;
mLocationClient.connect();
updateLocation(LocationServices.FusedLocationApi.getLastLocation());
mLocationListener = new MyLocationListener();
LocationServices.FusedLocationApi.requestLocationUpdates(mLocationRequest, mLocationListener);
}
} else {
if (mLocationClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(mLocationListener);
mLocationListener = null;
mLocationClient.disconnect();
mGpsLocation = null;
}
}
onInvalidate();
}
private class MyLocationListener implements LocationListener {
@Override
public void onLocationChanged(Location location) {
updateLocation(location);
}
}
// Handles location updates from GPS
private void updateLocation(Location location) {
if (location != null) {
mGpsLocation = location;
updateGpsMarker();
}
}
private void updateGpsMarker() {
if (mIsMyLocationEnabled && mGpsLocation != null) {
mGpsMarker.setVisibility(View.VISIBLE);
LatLng coordinate = new LatLng(mGpsLocation);
PointF screenLocation = toScreenLocation(coordinate);
if (!mDirty) {
// Map is idle, animate change of location
mGpsMarkerAnimatorX = mGpsMarker.animate().x(screenLocation.x - mGpsMarkerOffset);
mGpsMarkerAnimatorY = mGpsMarker.animate().y(screenLocation.y - mGpsMarkerOffset);
} else {
// Map is not idle, set value, don't animate
if (mGpsMarkerAnimatorX != null) {
mGpsMarkerAnimatorX.cancel();
mGpsMarkerAnimatorY.cancel();
}
// Reposition correctly
mGpsMarker.setX(screenLocation.x - mGpsMarkerOffset);
mGpsMarker.setY(screenLocation.y - mGpsMarkerOffset);
}
} else {
if (mGpsMarker != null) {
mGpsMarker.setVisibility(View.INVISIBLE);
}
}
@UiThread
public void setOnMyLocationChangeListener(@Nullable OnMyLocationChangeListener listener) {
mUserLocationView.setOnMyLocationChangeListener(listener);
}
//
@@ -3084,7 +3009,6 @@ public final class MapView extends FrameLayout {
@UiThread
public void setCompassEnabled(boolean compassEnabled) {
mCompassView.setEnabled(compassEnabled);
onInvalidate();
}
/**
@@ -3096,8 +3020,8 @@ public final class MapView extends FrameLayout {
* @param gravity One of the values from {@link Gravity}.
* @see Gravity
*/
@UiThread
public void setCompassGravity(int gravity) {
@UiThread
public void setCompassGravity(int gravity) {
setWidgetGravity(mCompassView, gravity);
}
@@ -3115,25 +3039,6 @@ public final class MapView extends FrameLayout {
setWidgetMargins(mCompassView, left, top, right, bottom);
}
// Rotates an ImageView - does not work if the ImageView has padding, use margins
private void rotateImageView(ImageView imageView, float angle) {
Matrix matrix = new Matrix();
matrix.setScale((float) imageView.getWidth() / (float) imageView.getDrawable().getIntrinsicWidth(), (float) imageView.getHeight() / (float) imageView.getDrawable().getIntrinsicHeight());
matrix.postRotate(angle, (float) imageView.getWidth() / 2.0f, (float) imageView.getHeight() / 2.0f);
imageView.setImageMatrix(matrix);
imageView.setScaleType(ImageView.ScaleType.MATRIX);
}
// Updates the UI to match the current map's position
private void updateCompass() {
if (isCompassEnabled()) {
mCompassView.setVisibility(VISIBLE);
rotateImageView(mCompassView, (float) getDirection());
} else {
mCompassView.setVisibility(INVISIBLE);
}
}
//
// Logo
//

View File

@@ -10,6 +10,8 @@ import com.mapbox.mapboxsdk.geometry.BoundingBox;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapbox.mapboxsdk.geometry.LatLngZoom;
import com.mapbox.mapboxsdk.geometry.ProjectedMeters;
import java.lang.ref.WeakReference;
import java.util.List;
// Class that wraps the native methods for convenience
@@ -23,11 +25,13 @@ final class NativeMapView {
// Instance members
//
boolean mDestroyed = false;
// Holds the pointer to JNI NativeMapView
private long mNativeMapViewPtr = 0;
// Used for callbacks
private MapView mMapView;
private WeakReference<MapView> mMapView;
//
// Static methods
@@ -50,7 +54,7 @@ final class NativeMapView {
throw new IllegalArgumentException("totalMemory cannot be negative.");
}
mMapView = mapView;
mMapView = new WeakReference<>(mapView);
// Create the NativeMapView
mNativeMapViewPtr = nativeCreate(cachePath, dataPath, apkPath, pixelRatio, availableProcessors, totalMemory);
@@ -60,6 +64,17 @@ final class NativeMapView {
// Methods
//
public void destroy() {
nativeDestroy(mNativeMapViewPtr);
mNativeMapViewPtr = 0;
mMapView = null;
mDestroyed = true;
}
public boolean wasDestroyed() {
return mDestroyed;
}
public void initializeDisplay() {
nativeInitializeDisplay(mNativeMapViewPtr);
}
@@ -434,28 +449,21 @@ final class NativeMapView {
//
protected void onInvalidate() {
mMapView.onInvalidate();
mMapView.get().onInvalidate();
}
protected void onMapChanged(int rawChange) {
mMapView.onMapChanged(rawChange);
mMapView.get().onMapChanged(rawChange);
}
protected void onFpsChanged(double fps) {
mMapView.onFpsChanged(fps);
mMapView.get().onFpsChanged(fps);
}
//
// JNI methods
//
@Override
protected void finalize() throws Throwable {
nativeDestroy(mNativeMapViewPtr);
mNativeMapViewPtr = 0;
super.finalize();
}
private native long nativeCreate(String cachePath, String dataPath, String apkPath, float pixelRatio, int availableProcessors, long totalMemory);
private native void nativeDestroy(long nativeMapViewPtr);

View File

@@ -0,0 +1,462 @@
package com.mapbox.mapboxsdk.views;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.location.Location;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import com.mapbox.mapboxsdk.geometry.LatLng;
import com.mapzen.android.lost.api.LocationListener;
import com.mapzen.android.lost.api.LocationRequest;
import com.mapzen.android.lost.api.LocationServices;
import com.mapzen.android.lost.api.LostApiClient;
import java.lang.ref.WeakReference;
final class UserLocationView extends View {
private WeakReference<MapView> mMapView;
private float mDensity;
private PointF mMarkerScreenPoint;
private Matrix mMarkerScreenMatrix;
private Paint mOuterRingPaint;
private Path mOuterRingPath;
private RectF mOuterRingBounds;
private Paint mInnerRingPaintFill;
private Paint mInnerRingPaintStroke;
private Path mInnerRingPath;
private boolean mShowDirection;
private Paint mDirectionArrowPaint;
private Path mDirectionArrowPath;
private boolean mShowAccuracy;
private Paint mAccuracyPaintFill;
private Paint mAccuracyPaintStroke;
private Path mAccuracyPath;
private RectF mAccuracyBounds;
private Rect mDirtyRect;
private RectF mDirtyRectF;
private boolean mShowMarker;
private LatLng mMarkerCoordinate;
private ValueAnimator mMarkerCoordinateAnimator;
private float mMarkerDirection;
private ObjectAnimator mMarkerDirectionAnimator;
private float mMarkerAccuracy;
private ObjectAnimator mMarkerAccuracyAnimator;
private boolean mPaused = false;
private LostApiClient mLocationClient;
private LocationRequest mLocationRequest;
private Location mUserLocation;
private MyLocationListener mLocationListener;
MapView.OnMyLocationChangeListener mOnMyLocationChangeListener;
public UserLocationView(MapView mapView, Context context) {
super(context);
initialize(mapView, context);
}
public UserLocationView(MapView mapView, Context context, AttributeSet attrs) {
super(context, attrs);
initialize(mapView, context);
}
public UserLocationView(MapView mapView, Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(mapView, context);
}
private void initialize(MapView mapView, Context context) {
mMapView = new WeakReference<MapView>(mapView);
// View configuration
setEnabled(false);
// Layout params
ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT);
setLayoutParams(lp);
// Setup location services
mLocationClient = new LostApiClient.Builder(getContext()).build();
mLocationRequest = LocationRequest.create()
.setFastestInterval(1000)
.setSmallestDisplacement(3.0f)
.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY);
// Setup the custom paint
Resources resources = context.getResources();
mDensity = resources.getDisplayMetrics().density;
mMarkerScreenPoint = new PointF();
mMarkerScreenMatrix = new Matrix();
mOuterRingPaint = new Paint();
mOuterRingPaint.setAntiAlias(true);
mOuterRingPaint.setStyle(Paint.Style.FILL);
mOuterRingPaint.setColor(Color.BLACK);
mOuterRingPaint.setAlpha((int) (255 * 0.5f));
mOuterRingPath = new Path();
mOuterRingPath.addCircle(0.0f, 0.0f, 15.0f * mDensity, Path.Direction.CW);
mOuterRingBounds = new RectF();
mOuterRingPath.computeBounds(mOuterRingBounds, false);
mInnerRingPaintFill = new Paint();
mInnerRingPaintFill.setAntiAlias(true);
mInnerRingPaintFill.setStyle(Paint.Style.FILL);
mInnerRingPaintFill.setColor(Color.WHITE);
mInnerRingPaintStroke = new Paint();
mInnerRingPaintStroke.setAntiAlias(true);
mInnerRingPaintStroke.setStyle(Paint.Style.STROKE);
mInnerRingPaintStroke.setStrokeWidth(1.0f * mDensity);
mInnerRingPaintStroke.setColor(Color.BLACK);
mInnerRingPath = new Path();
mInnerRingPath.addCircle(0.0f, 0.0f, 7.5f * mDensity, Path.Direction.CW);
mDirectionArrowPaint = mInnerRingPaintFill;
mDirectionArrowPath = new Path();
mDirectionArrowPath.moveTo(0.0f, -15.0f * mDensity);
mDirectionArrowPath.arcTo(
new RectF(
-9.0f * mDensity, -9.0f * mDensity,
9.0f * mDensity, 9.0f * mDensity),
225.0f, 90.0f, false);
mDirectionArrowPath.close();
mAccuracyPaintFill = new Paint();
mAccuracyPaintFill.setAntiAlias(true);
mAccuracyPaintFill.setStyle(Paint.Style.FILL);
mAccuracyPaintFill.setColor(Color.BLACK);
mAccuracyPaintFill.setAlpha((int) (255 * 0.25f));
mAccuracyPaintStroke = new Paint();
mAccuracyPaintStroke.setAntiAlias(true);
mAccuracyPaintStroke.setStyle(Paint.Style.STROKE);
mAccuracyPaintStroke.setStrokeWidth(0.5f * mDensity);
mAccuracyPaintStroke.setColor(Color.BLACK);
mAccuracyPaintStroke.setAlpha((int) (255 * 0.5f));
mAccuracyPath = new Path();
mAccuracyBounds = new RectF();
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mShowMarker) {
return;
}
canvas.concat(mMarkerScreenMatrix);
if (!canvas.quickReject(mOuterRingPath, Canvas.EdgeType.AA)) {
if (mShowAccuracy) {
canvas.drawPath(mAccuracyPath, mAccuracyPaintFill);
canvas.drawPath(mAccuracyPath, mAccuracyPaintStroke);
}
canvas.drawPath(mOuterRingPath, mOuterRingPaint);
canvas.drawPath(mInnerRingPath, mInnerRingPaintFill);
canvas.drawPath(mInnerRingPath, mInnerRingPaintStroke);
if (mShowDirection) {
canvas.drawPath(mDirectionArrowPath, mDirectionArrowPaint);
}
}
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
toggleGps(enabled);
}
public void update() {
if (isEnabled() && mShowMarker) {
setVisibility(View.VISIBLE);
// compute new marker position
// TODO add JNI method that takes existing pointf
mMarkerScreenPoint = mMapView.get().toScreenLocation(mMarkerCoordinate);
mMarkerScreenMatrix.reset();
mMarkerScreenMatrix.setTranslate(
mMarkerScreenPoint.x,
mMarkerScreenPoint.y);
// rotate so arrow in points to bearing
if (mShowDirection) {
mMarkerScreenMatrix.preRotate(mMarkerDirection +
(float) mMapView.get().getDirection());
}
// adjust accuracy circle
if (mShowAccuracy) {
mAccuracyPath.reset();
mAccuracyPath.addCircle(0.0f, 0.0f,
(float) (mMarkerAccuracy / mMapView.get().getMetersPerPixelAtLatitude(
mMarkerCoordinate.getLatitude())),
Path.Direction.CW);
mAccuracyPath.computeBounds(mAccuracyBounds, false);
}
// invalidate changed pixels
if (mDirtyRect == null) {
mDirtyRect = new Rect();
mDirtyRectF = new RectF();
} else {
// the old marker location
// TODO need to take into account accuracy stroke
//invalidate(mDirtyRect);
invalidate();
}
RectF largerBounds;
if (mAccuracyBounds.contains(mOuterRingBounds)) {
largerBounds = mAccuracyBounds;
} else {
largerBounds = mOuterRingBounds;
}
mMarkerScreenMatrix.mapRect(mDirtyRectF, largerBounds);
mDirtyRectF.roundOut(mDirtyRect);
// TODO need to take into account accuracy stroke
//invalidate(mDirtyRect); // the new marker location
invalidate();
} else {
setVisibility(View.INVISIBLE);
}
}
public Location getLocation() {
return mUserLocation;
}
/**
* 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) {
if (mLocationClient == null) {
return;
}
if (enableGps) {
if (!mLocationClient.isConnected()) {
mUserLocation = null;
mLocationClient.connect();
setLocation(LocationServices.FusedLocationApi.getLastLocation());
mLocationListener = new MyLocationListener();
LocationServices.FusedLocationApi.requestLocationUpdates(mLocationRequest, mLocationListener);
}
} else {
if (mLocationClient.isConnected()) {
LocationServices.FusedLocationApi.removeLocationUpdates(mLocationListener);
mLocationListener = null;
mLocationClient.disconnect();
mUserLocation = null;
}
}
}
private class MyLocationListener implements LocationListener {
@Override
public void onLocationChanged(Location location) {
if (mPaused) {
return;
}
setLocation(location);
}
}
// Handles location updates from GPS
private void setLocation(Location location) {
// if null we should hide the marker
if (location == null) {
mShowMarker = false;
mShowDirection = false;
mShowAccuracy = false;
cancelAnimations();
mUserLocation = null;
mMarkerCoordinate = null;
return;
}
if (mMarkerCoordinateAnimator != null) {
mMarkerCoordinateAnimator.end();
mMarkerCoordinateAnimator = null;
}
if (mMarkerDirectionAnimator != null) {
mMarkerDirectionAnimator.end();
mMarkerDirectionAnimator = null;
}
if (mMarkerAccuracyAnimator != null) {
mMarkerAccuracyAnimator.end();
mMarkerAccuracyAnimator = null;
}
mShowMarker = true;
LatLng previousCoordinate;
if (mUserLocation == null) {
previousCoordinate = new LatLng(location);
} else {
previousCoordinate = new LatLng(mUserLocation);
}
mMarkerCoordinateAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
mMarkerCoordinateAnimator.setDuration(1000);
mMarkerCoordinateAnimator.addUpdateListener(new MarkerCoordinateAnimatorListener(
previousCoordinate, new LatLng(location)
));
mMarkerCoordinateAnimator.start();
mShowDirection = location.hasBearing();
if (mShowDirection) {
if (mUserLocation != null && mUserLocation.hasBearing()) {
mMarkerDirection = mUserLocation.getBearing();
}
float oldDir = mMarkerDirection;
float newDir = location.getBearing();
float diff = oldDir - newDir;
if (diff > 180.0f) {
newDir += 360.0f;
} else if (diff < -180.0f) {
newDir -= 360.f;
}
mMarkerDirectionAnimator = ObjectAnimator.ofFloat(this, "direction", oldDir, newDir);
mMarkerDirectionAnimator.setDuration(1000);
mMarkerDirectionAnimator.start();
}
mShowAccuracy = location.hasAccuracy();
if (mShowAccuracy) {
if (mUserLocation != null && mUserLocation.hasAccuracy()) {
mMarkerAccuracy = mUserLocation.getAccuracy();
}
mMarkerAccuracyAnimator = ObjectAnimator.ofFloat(this, "accuracy", location.getAccuracy());
mMarkerAccuracyAnimator.setDuration(1000);
mMarkerAccuracyAnimator.start();
}
mUserLocation = location;
updateOnNextFrame();
if (mOnMyLocationChangeListener != null) {
mOnMyLocationChangeListener.onMyLocationChange(location);
}
}
void updateOnNextFrame() {
mMapView.get().update();
}
public void pause() {
mPaused = true;
toggleGps(false);
}
public void resume() {
mPaused = false;
if (isEnabled()) {
toggleGps(true);
}
}
public void setOnMyLocationChangeListener(@Nullable MapView.OnMyLocationChangeListener listener) {
mOnMyLocationChangeListener = listener;
}
// public for animator only
public float getDirection() {
return mMarkerDirection;
}
// public for animator only
public void setDirection(float direction) {
mMarkerDirection = direction % 360.0f;
updateOnNextFrame();
}
// public for animator only
public float getAccuracy() {
return mMarkerAccuracy;
}
// public for animator only
public void setAccuracy(float accuracy) {
mMarkerAccuracy = accuracy;
updateOnNextFrame();
}
private class MarkerCoordinateAnimatorListener implements ValueAnimator.AnimatorUpdateListener {
private double mFromLat;
private double mFromLng;
private double mToLat;
private double mToLng;
private MarkerCoordinateAnimatorListener(LatLng from, LatLng to) {
mFromLat = from.getLatitude();
mFromLng = from.getLongitude();
mToLat = to.getLatitude();
mToLng = to.getLongitude();
}
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float frac = animation.getAnimatedFraction();
double latitude = mFromLat + (mToLat - mFromLat) * frac;
double longitude = mFromLng + (mToLng - mFromLng) * frac;
mMarkerCoordinate.setLatitude(latitude);
mMarkerCoordinate.setLongitude(longitude);
updateOnNextFrame();
}
}
public void cancelAnimations() {
if (mMarkerCoordinateAnimator != null) {
mMarkerCoordinateAnimator.cancel();
mMarkerCoordinateAnimator = null;
}
if (mMarkerDirectionAnimator != null) {
mMarkerDirectionAnimator.cancel();
mMarkerDirectionAnimator = null;
}
if (mMarkerAccuracyAnimator != null) {
mMarkerAccuracyAnimator.cancel();
mMarkerAccuracyAnimator = null;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB