summaryrefslogtreecommitdiff
path: root/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps')
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/FeatureOverviewActivity.java168
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/AnimatedSymbolLayerActivity.java438
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/BulkMarkerActivity.java285
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/DynamicMarkerChangeActivity.java117
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewActivity.java483
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewsInRectangleActivity.java110
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolygonActivity.java218
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolylineActivity.java223
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PressForMarkerActivity.java140
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimationTypeActivity.java182
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimatorActivity.java278
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraPositionActivity.java254
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/GestureDetectorActivity.java422
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/LatLngBoundsActivity.java162
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ManualZoomActivity.java121
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/MaxMinZoomActivity.java82
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ScrollByActivity.java157
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/customlayer/CustomLayerActivity.java140
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/DeviceIndependentTestActivity.java76
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/EspressoTestActivity.java80
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxCountActivity.java125
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxHighlightActivity.java125
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxSymbolCountActivity.java129
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesPropertiesActivity.java234
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QuerySourceFeaturesActivity.java109
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MapFragmentActivity.java89
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MultiMapActivity.java18
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/SupportMapFragmentActivity.java90
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/ViewPagerActivity.java79
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/PrintActivity.java88
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/SnapshotActivity.java113
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/DynamicInfoWindowAdapterActivity.java142
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowActivity.java190
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowAdapterActivity.java125
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/BottomSheetActivity.java273
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DebugModeActivity.java271
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DoubleMapActivity.java155
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LatLngBoundsForCameraActivity.java109
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LocalGlyphActivity.java85
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapChangeActivity.java102
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapInDialogActivity.java119
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapPaddingActivity.java123
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/SimpleMapActivity.java66
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/VisibilityChangeActivity.java142
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/DeleteRegionActivity.java157
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/OfflineActivity.java321
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/UpdateMetadataActivity.java184
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestActivity.java365
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestDefinition.java91
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestSnapshotter.java18
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestStyleDefinition.java131
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterActivity.java126
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterLocalStyleActivity.java71
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterMarkerActivity.java96
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterReuseActivity.java106
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/storage/UrlTransformActivity.java100
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/AnimatedImageSourceActivity.java147
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/BuildingFillExtrusionActivity.java148
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CircleLayerActivity.java240
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CustomSpriteActivity.java135
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/DataDrivenStyleActivity.java471
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionActivity.java122
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionStyleTestActivity.java74
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GeoJsonClusteringActivity.java210
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GridSourceActivity.java152
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HeatmapLayerActivity.java226
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HillshadeLayerActivity.java84
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RealTimeGeoJsonActivity.java125
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleActivity.java587
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTestActivity.java74
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTimingTestActivity.java96
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/StyleFileActivity.java177
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolGeneratorActivity.java352
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolLayerActivity.java214
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/ZoomFunctionSymbolLayerActivity.java191
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewAnimationActivity.java145
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewDebugModeActivity.java265
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewResizeActivity.java98
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewTransparentBackgroundActivity.java94
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/FontCache.java26
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/GeoParseUtil.java51
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/IconUtils.java31
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ItemClickSupport.java95
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/OfflineUtils.java36
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ResourceUtils.java39
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TimingLogger.java160
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TokenUtils.java37
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ViewToBitmapUtil.java22
88 files changed, 13927 insertions, 0 deletions
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/FeatureOverviewActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/FeatureOverviewActivity.java
new file mode 100644
index 0000000000..74cb904dd9
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/FeatureOverviewActivity.java
@@ -0,0 +1,168 @@
+package com.mapbox.mapboxsdk.maps.activity;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.StringRes;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.adapter.FeatureAdapter;
+import com.mapbox.mapboxsdk.testapp.adapter.FeatureSectionAdapter;
+import com.mapbox.mapboxsdk.testapp.model.activity.Feature;
+import com.mapbox.mapboxsdk.maps.utils.ItemClickSupport;
+import timber.log.Timber;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Activity shown when application is started
+ * <p>
+ * This activity will generate data for RecyclerView based on the AndroidManifest entries.
+ * It uses tags as category and description to order the different entries.
+ * </p>
+ */
+public class FeatureOverviewActivity extends AppCompatActivity {
+
+ private static final String KEY_STATE_FEATURES = "featureList";
+
+ private RecyclerView recyclerView;
+ private FeatureSectionAdapter sectionAdapter;
+ private List<Feature> features;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_feature_overview);
+
+ recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+ recyclerView.addOnItemTouchListener(new RecyclerView.SimpleOnItemTouchListener());
+ recyclerView.setHasFixedSize(true);
+
+ ItemClickSupport.addTo(recyclerView).setOnItemClickListener((recyclerView, position, view) -> {
+ if (!sectionAdapter.isSectionHeaderPosition(position)) {
+ int itemPosition = sectionAdapter.getConvertedPosition(position);
+ Feature feature = features.get(itemPosition);
+ startFeature(feature);
+ }
+ });
+
+ if (savedInstanceState == null) {
+ loadFeatures();
+ } else {
+ features = savedInstanceState.getParcelableArrayList(KEY_STATE_FEATURES);
+ onFeaturesLoaded(features);
+ }
+ }
+
+ private void loadFeatures() {
+ try {
+ new LoadFeatureTask().execute(
+ getPackageManager().getPackageInfo(getPackageName(),
+ PackageManager.GET_ACTIVITIES | PackageManager.GET_META_DATA));
+ } catch (PackageManager.NameNotFoundException exception) {
+ Timber.e(exception, "Could not resolve package info");
+ }
+ }
+
+ private void onFeaturesLoaded(List<Feature> featuresList) {
+ features = featuresList;
+ if (featuresList == null || featuresList.isEmpty()) {
+ return;
+ }
+
+ List<FeatureSectionAdapter.Section> sections = new ArrayList<>();
+ String currentCat = "";
+ for (int i = 0; i < features.size(); i++) {
+ String category = features.get(i).getCategory();
+ if (!currentCat.equals(category)) {
+ sections.add(new FeatureSectionAdapter.Section(i, category));
+ currentCat = category;
+ }
+ }
+
+ FeatureSectionAdapter.Section[] dummy = new FeatureSectionAdapter.Section[sections.size()];
+ sectionAdapter = new FeatureSectionAdapter(
+ this, R.layout.section_main_layout, R.id.section_text, new FeatureAdapter(features));
+ sectionAdapter.setSections(sections.toArray(dummy));
+ recyclerView.setAdapter(sectionAdapter);
+ }
+
+ private void startFeature(Feature feature) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(getPackageName(), feature.getName()));
+ startActivity(intent);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putParcelableArrayList(KEY_STATE_FEATURES, (ArrayList<Feature>) features);
+ }
+
+ private class LoadFeatureTask extends AsyncTask<PackageInfo, Void, List<Feature>> {
+
+ @Override
+ protected List<Feature> doInBackground(PackageInfo... params) {
+ List<Feature> features = new ArrayList<>();
+ PackageInfo app = params[0];
+
+ String packageName = getApplicationContext().getPackageName();
+ String metaDataKey = getString(R.string.category);
+ for (ActivityInfo info : app.activities) {
+ if (info.labelRes != 0 && info.name.startsWith(packageName)
+ && !info.name.equals(FeatureOverviewActivity.class.getName())) {
+ String label = getString(info.labelRes);
+ String description = resolveString(info.descriptionRes);
+ String category = resolveMetaData(info.metaData, metaDataKey);
+ features.add(new Feature(info.name, label, description, category));
+ }
+ }
+
+ if (!features.isEmpty()) {
+ Comparator<Feature> comparator = (lhs, rhs) -> {
+ int result = lhs.getCategory().compareToIgnoreCase(rhs.getCategory());
+ if (result == 0) {
+ result = lhs.getLabel().compareToIgnoreCase(rhs.getLabel());
+ }
+ return result;
+ };
+ Collections.sort(features, comparator);
+ }
+
+ return features;
+ }
+
+ private String resolveMetaData(Bundle bundle, String key) {
+ String category = null;
+ if (bundle != null) {
+ category = bundle.getString(key);
+ }
+ return category;
+ }
+
+ private String resolveString(@StringRes int stringRes) {
+ try {
+ return getString(stringRes);
+ } catch (Resources.NotFoundException exception) {
+ return "-";
+ }
+ }
+
+ @Override
+ protected void onPostExecute(List<Feature> features) {
+ super.onPostExecute(features);
+ onFeaturesLoaded(features);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/AnimatedSymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/AnimatedSymbolLayerActivity.java
new file mode 100644
index 0000000000..c293c0cd24
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/AnimatedSymbolLayerActivity.java
@@ -0,0 +1,438 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+
+import com.google.gson.JsonObject;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.turf.TurfMeasurement;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconRotate;
+
+/**
+ * Test activity showcasing animating MarkerViews.
+ */
+public class AnimatedSymbolLayerActivity extends AppCompatActivity {
+
+ private static final String PASSENGER = "passenger";
+ private static final String PASSENGER_LAYER = "passenger-layer";
+ private static final String PASSENGER_SOURCE = "passenger-source";
+ private static final String TAXI = "taxi";
+ private static final String TAXI_LAYER = "taxi-layer";
+ private static final String TAXI_SOURCE = "taxi-source";
+ private static final String RANDOM_CAR_LAYER = "random-car-layer";
+ private static final String RANDOM_CAR_SOURCE = "random-car-source";
+ private static final String RANDOM_CAR_IMAGE_ID = "random-car";
+ private static final String PROPERTY_BEARING = "bearing";
+ private static final String WATERWAY_LAYER_ID = "waterway-label";
+ private static final int DURATION_RANDOM_MAX = 1500;
+ private static final int DURATION_BASE = 3000;
+
+ private final Random random = new Random();
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private List<Car> randomCars = new ArrayList<>();
+ private GeoJsonSource randomCarSource;
+ private Car taxi;
+ private GeoJsonSource taxiSource;
+ private LatLng passenger;
+
+ private List<Animator> animators = new ArrayList<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_animated_marker);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ AnimatedSymbolLayerActivity.this.mapboxMap = mapboxMap;
+ setupCars();
+ animateRandomRoutes();
+ animateTaxi();
+ });
+ }
+
+ private void setupCars() {
+ addRandomCars();
+ addPassenger();
+ addMainCar();
+ }
+
+ private void animateRandomRoutes() {
+ final Car longestDrive = getLongestDrive();
+ final Random random = new Random();
+ for (final Car car : randomCars) {
+ final boolean isLongestDrive = longestDrive.equals(car);
+ ValueAnimator valueAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), car.current, car.next);
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+
+ private LatLng latLng;
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ latLng = (LatLng) animation.getAnimatedValue();
+ car.current = latLng;
+ if (isLongestDrive) {
+ updateRandomCarSource();
+ }
+ }
+ });
+
+ if (isLongestDrive) {
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ updateRandomDestinations();
+ animateRandomRoutes();
+ }
+ });
+ }
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ car.feature.properties().addProperty("bearing", Car.getBearing(car.current, car.next));
+ }
+ });
+
+ int offset = random.nextInt(2) == 0 ? 0 : random.nextInt(1000) + 250;
+ valueAnimator.setStartDelay(offset);
+ valueAnimator.setDuration(car.duration - offset);
+ valueAnimator.setInterpolator(new LinearInterpolator());
+ valueAnimator.start();
+
+ animators.add(valueAnimator);
+ }
+ }
+
+ private void animateTaxi() {
+ ValueAnimator valueAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), taxi.current, taxi.next);
+ valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+
+ private LatLng latLng;
+
+ @Override
+ public void onAnimationUpdate(ValueAnimator animation) {
+ latLng = (LatLng) animation.getAnimatedValue();
+ taxi.current = latLng;
+ updateTaxiSource();
+ }
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ updatePassenger();
+ animateTaxi();
+ }
+ });
+
+ valueAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationStart(Animator animation) {
+ super.onAnimationStart(animation);
+ taxi.feature.properties().addProperty("bearing", Car.getBearing(taxi.current, taxi.next));
+ }
+ });
+
+ valueAnimator.setDuration((long) (7 * taxi.current.distanceTo(taxi.next)));
+ valueAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
+ valueAnimator.start();
+
+ animators.add(valueAnimator);
+ }
+
+ private void updatePassenger() {
+ passenger = getLatLngInBounds();
+ updatePassengerSource();
+ taxi.setNext(passenger);
+ }
+
+ private void updatePassengerSource() {
+ GeoJsonSource source = mapboxMap.getSourceAs(PASSENGER_SOURCE);
+ FeatureCollection featureCollection = FeatureCollection.fromFeatures(new Feature[] {
+ Feature.fromGeometry(
+ Point.fromLngLat(
+ passenger.getLongitude(),
+ passenger.getLatitude()
+ )
+ )
+ });
+ source.setGeoJson(featureCollection);
+ }
+
+ private void updateTaxiSource() {
+ taxi.updateFeature();
+ taxiSource.setGeoJson(taxi.feature);
+ }
+
+ private void updateRandomDestinations() {
+ for (Car randomCar : randomCars) {
+ randomCar.setNext(getLatLngInBounds());
+ }
+ }
+
+ private Car getLongestDrive() {
+ Car longestDrive = null;
+ for (Car randomCar : randomCars) {
+ if (longestDrive == null) {
+ longestDrive = randomCar;
+ } else if (longestDrive.duration < randomCar.duration) {
+ longestDrive = randomCar;
+ }
+ }
+ return longestDrive;
+ }
+
+ private void updateRandomCarSource() {
+ for (Car randomCarsRoute : randomCars) {
+ randomCarsRoute.updateFeature();
+ }
+ randomCarSource.setGeoJson(featuresFromRoutes());
+ }
+
+ private FeatureCollection featuresFromRoutes() {
+ List<Feature> features = new ArrayList<>();
+ for (Car randomCarsRoute : randomCars) {
+ features.add(randomCarsRoute.feature);
+ }
+ return FeatureCollection.fromFeatures(features);
+ }
+
+ private long getDuration() {
+ return random.nextInt(DURATION_RANDOM_MAX) + DURATION_BASE;
+ }
+
+ private void addRandomCars() {
+ LatLng latLng;
+ LatLng next;
+ for (int i = 0; i < 10; i++) {
+ latLng = getLatLngInBounds();
+ next = getLatLngInBounds();
+
+ JsonObject properties = new JsonObject();
+ properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, next));
+
+ Feature feature = Feature.fromGeometry(
+ Point.fromLngLat(
+ latLng.getLongitude(),
+ latLng.getLatitude()
+ ), properties);
+
+ randomCars.add(
+ new Car(feature, next, getDuration())
+ );
+ }
+
+ randomCarSource = new GeoJsonSource(RANDOM_CAR_SOURCE, featuresFromRoutes());
+ mapboxMap.addSource(randomCarSource);
+ mapboxMap.addImage(RANDOM_CAR_IMAGE_ID,
+ ((BitmapDrawable) getResources().getDrawable(R.drawable.ic_car_top)).getBitmap());
+
+ SymbolLayer symbolLayer = new SymbolLayer(RANDOM_CAR_LAYER, RANDOM_CAR_SOURCE);
+ symbolLayer.withProperties(
+ iconImage(RANDOM_CAR_IMAGE_ID),
+ iconAllowOverlap(true),
+ iconRotate(get(PROPERTY_BEARING)),
+ iconIgnorePlacement(true)
+ );
+
+ mapboxMap.addLayerBelow(symbolLayer, WATERWAY_LAYER_ID);
+ }
+
+ private void addPassenger() {
+ passenger = getLatLngInBounds();
+ FeatureCollection featureCollection = FeatureCollection.fromFeatures(new Feature[] {
+ Feature.fromGeometry(
+ Point.fromLngLat(
+ passenger.getLongitude(),
+ passenger.getLatitude()
+ )
+ )
+ });
+
+ mapboxMap.addImage(PASSENGER,
+ ((BitmapDrawable) getResources().getDrawable(R.drawable.icon_burned)).getBitmap());
+
+ GeoJsonSource geoJsonSource = new GeoJsonSource(PASSENGER_SOURCE, featureCollection);
+ mapboxMap.addSource(geoJsonSource);
+
+ SymbolLayer symbolLayer = new SymbolLayer(PASSENGER_LAYER, PASSENGER_SOURCE);
+ symbolLayer.withProperties(
+ iconImage(PASSENGER),
+ iconIgnorePlacement(true),
+ iconAllowOverlap(true)
+ );
+ mapboxMap.addLayerBelow(symbolLayer, RANDOM_CAR_LAYER);
+ }
+
+ private void addMainCar() {
+ LatLng latLng = getLatLngInBounds();
+ JsonObject properties = new JsonObject();
+ properties.addProperty(PROPERTY_BEARING, Car.getBearing(latLng, passenger));
+ Feature feature = Feature.fromGeometry(
+ Point.fromLngLat(
+ latLng.getLongitude(),
+ latLng.getLatitude()), properties);
+ FeatureCollection featureCollection = FeatureCollection.fromFeatures(new Feature[] {feature});
+
+ taxi = new Car(feature, passenger, getDuration());
+ mapboxMap.addImage(TAXI,
+ ((BitmapDrawable) getResources().getDrawable(R.drawable.ic_taxi_top)).getBitmap());
+ taxiSource = new GeoJsonSource(TAXI_SOURCE, featureCollection);
+ mapboxMap.addSource(taxiSource);
+
+ SymbolLayer symbolLayer = new SymbolLayer(TAXI_LAYER, TAXI_SOURCE);
+ symbolLayer.withProperties(
+ iconImage(TAXI),
+ iconRotate(get(PROPERTY_BEARING)),
+ iconAllowOverlap(true),
+ iconIgnorePlacement(true)
+
+ );
+ mapboxMap.addLayer(symbolLayer);
+ }
+
+ private LatLng getLatLngInBounds() {
+ LatLngBounds bounds = mapboxMap.getProjection().getVisibleRegion().latLngBounds;
+ Random generator = new Random();
+ double randomLat = bounds.getLatSouth() + generator.nextDouble()
+ * (bounds.getLatNorth() - bounds.getLatSouth());
+ double randomLon = bounds.getLonWest() + generator.nextDouble()
+ * (bounds.getLonEast() - bounds.getLonWest());
+ return new LatLng(randomLat, randomLon);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ for (Animator animator : animators) {
+ if (animator != null) {
+ animator.removeAllListeners();
+ animator.cancel();
+ }
+ }
+
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ /**
+ * Evaluator for LatLng pairs
+ */
+ private static class LatLngEvaluator implements TypeEvaluator<LatLng> {
+
+ private LatLng latLng = new LatLng();
+
+ @Override
+ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
+ latLng.setLatitude(startValue.getLatitude()
+ + ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
+ latLng.setLongitude(startValue.getLongitude()
+ + ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
+ return latLng;
+ }
+ }
+
+
+ private static class Car {
+ private Feature feature;
+ private LatLng next;
+ private LatLng current;
+ private long duration;
+
+ Car(Feature feature, LatLng next, long duration) {
+ this.feature = feature;
+ Point point = ((Point) feature.geometry());
+ this.current = new LatLng(point.latitude(), point.longitude());
+ this.duration = duration;
+ this.next = next;
+ }
+
+ void setNext(LatLng next) {
+ this.next = next;
+ }
+
+ void updateFeature() {
+ feature = Feature.fromGeometry(Point.fromLngLat(
+ current.getLongitude(),
+ current.getLatitude())
+ );
+ feature.properties().addProperty("bearing", getBearing(current, next));
+ }
+
+ private static float getBearing(LatLng from, LatLng to) {
+ return (float) TurfMeasurement.bearing(
+ Point.fromLngLat(from.getLongitude(), from.getLatitude()),
+ Point.fromLngLat(to.getLongitude(), to.getLatitude())
+ );
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/BulkMarkerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/BulkMarkerActivity.java
new file mode 100644
index 0000000000..1623ddf9d3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/BulkMarkerActivity.java
@@ -0,0 +1,285 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.app.ProgressDialog;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.view.MenuItemCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.annotations.MarkerViewOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.GeoParseUtil;
+import com.mapbox.mapboxsdk.maps.utils.IconUtils;
+import timber.log.Timber;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Random;
+
+/**
+ * Test activity showcasing adding a large amount of Markers or MarkerViews.
+ */
+public class BulkMarkerActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private boolean customMarkerView;
+ private List<LatLng> locations;
+ private ProgressDialog progressDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_marker_bulk);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> BulkMarkerActivity.this.mapboxMap = mapboxMap);
+
+ final View fab = findViewById(R.id.fab);
+ if (fab != null) {
+ fab.setOnClickListener(new FabClickListener());
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ ArrayAdapter<CharSequence> spinnerAdapter = ArrayAdapter.createFromResource(
+ this, R.array.bulk_marker_list, android.R.layout.simple_spinner_item);
+ spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ getMenuInflater().inflate(R.menu.menu_bulk_marker, menu);
+ MenuItem item = menu.findItem(R.id.spinner);
+ Spinner spinner = (Spinner) MenuItemCompat.getActionView(item);
+ spinner.setAdapter(spinnerAdapter);
+ spinner.setOnItemSelectedListener(BulkMarkerActivity.this);
+ return true;
+ }
+
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ int amount = Integer.valueOf(getResources().getStringArray(R.array.bulk_marker_list)[position]);
+ if (locations == null) {
+ progressDialog = ProgressDialog.show(this, "Loading", "Fetching markers", false);
+ new LoadLocationTask(this, amount).execute();
+ } else {
+ showMarkers(amount);
+ }
+ }
+
+ private void onLatLngListLoaded(List<LatLng> latLngs, int amount) {
+ progressDialog.hide();
+ locations = latLngs;
+ showMarkers(amount);
+ }
+
+ private void showMarkers(int amount) {
+ if (mapboxMap == null || locations == null) {
+ return;
+ }
+
+ mapboxMap.clear();
+
+ if (locations.size() < amount) {
+ amount = locations.size();
+ }
+
+ if (customMarkerView) {
+ showViewMarkers(amount);
+ } else {
+ showGlMarkers(amount);
+ }
+ }
+
+ private void showViewMarkers(int amount) {
+ DecimalFormat formatter = new DecimalFormat("#.#####");
+ Random random = new Random();
+ int randomIndex;
+
+ int color = ResourcesCompat.getColor(getResources(), R.color.redAccent, getTheme());
+ Icon icon = IconUtils.drawableToIcon(this, R.drawable.ic_droppin, color);
+
+ List<MarkerViewOptions> markerOptionsList = new ArrayList<>();
+ for (int i = 0; i < amount; i++) {
+ randomIndex = random.nextInt(locations.size());
+ LatLng latLng = locations.get(randomIndex);
+ MarkerViewOptions markerOptions = new MarkerViewOptions()
+ .position(latLng)
+ .icon(icon)
+ .title(String.valueOf(i))
+ .snippet(formatter.format(latLng.getLatitude()) + ", " + formatter.format(latLng.getLongitude()));
+ markerOptionsList.add(markerOptions);
+ }
+ mapboxMap.addMarkerViews(markerOptionsList);
+ }
+
+ private void showGlMarkers(int amount) {
+ List<MarkerOptions> markerOptionsList = new ArrayList<>();
+ DecimalFormat formatter = new DecimalFormat("#.#####");
+ Random random = new Random();
+ int randomIndex;
+
+ for (int i = 0; i < amount; i++) {
+ randomIndex = random.nextInt(locations.size());
+ LatLng latLng = locations.get(randomIndex);
+ markerOptionsList.add(new MarkerOptions()
+ .position(latLng)
+ .title(String.valueOf(i))
+ .snippet(formatter.format(latLng.getLatitude()) + ", " + formatter.format(latLng.getLongitude())));
+ }
+
+ mapboxMap.addMarkers(markerOptionsList);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {
+ // nothing selected, nothing to do!
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ private class FabClickListener implements View.OnClickListener {
+
+ private TextView viewCountView;
+
+ @Override
+ public void onClick(final View view) {
+ if (mapboxMap != null) {
+ customMarkerView = true;
+
+ // remove fab
+ view.animate().alpha(0).setListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ view.setVisibility(View.GONE);
+ }
+ }).start();
+
+ // reload markers
+ Spinner spinner = (Spinner) findViewById(R.id.spinner);
+ if (spinner != null) {
+ int amount = Integer.valueOf(
+ getResources().getStringArray(R.array.bulk_marker_list)[spinner.getSelectedItemPosition()]);
+ showMarkers(amount);
+ }
+
+ viewCountView = (TextView) findViewById(R.id.countView);
+
+ mapView.addOnMapChangedListener(change -> {
+ if (change == MapView.REGION_IS_CHANGING || change == MapView.REGION_DID_CHANGE) {
+ if (!mapboxMap.getMarkerViewManager().getMarkerViewAdapters().isEmpty()) {
+ viewCountView.setText(String.format(Locale.getDefault(), "ViewCache size %d",
+ mapboxMap.getMarkerViewManager().getMarkerViewContainer().getChildCount()));
+ }
+ }
+ });
+
+ mapboxMap.getMarkerViewManager().setOnMarkerViewClickListener(
+ (marker, view1, adapter) -> {
+ Toast.makeText(
+ BulkMarkerActivity.this,
+ "Hello " + marker.getId(),
+ Toast.LENGTH_SHORT).show();
+ return false;
+ });
+ }
+ }
+ }
+
+ private static class LoadLocationTask extends AsyncTask<Void, Integer, List<LatLng>> {
+
+ private WeakReference<BulkMarkerActivity> activity;
+ private int amount;
+
+ private LoadLocationTask(BulkMarkerActivity activity, int amount) {
+ this.amount = amount;
+ this.activity = new WeakReference<>(activity);
+ }
+
+ @Override
+ protected List<LatLng> doInBackground(Void... params) {
+ BulkMarkerActivity activity = this.activity.get();
+ if (activity != null) {
+ String json = null;
+ try {
+ json = GeoParseUtil.loadStringFromAssets(activity.getApplicationContext(), "points.geojson");
+ } catch (IOException exception) {
+ Timber.e(exception, "Could not add markers");
+ }
+
+ if (json != null) {
+ return GeoParseUtil.parseGeoJsonCoordinates(json);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(List<LatLng> locations) {
+ super.onPostExecute(locations);
+ BulkMarkerActivity activity = this.activity.get();
+ if (activity != null) {
+ activity.onLatLngListLoaded(locations, amount);
+ }
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/DynamicMarkerChangeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/DynamicMarkerChangeActivity.java
new file mode 100644
index 0000000000..bb9531c0d3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/DynamicMarkerChangeActivity.java
@@ -0,0 +1,117 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.IconUtils;
+
+/**
+ * Test activity showcasing updating a Marker position, title, icon and snippet.
+ */
+public class DynamicMarkerChangeActivity extends AppCompatActivity {
+
+ private static final LatLng LAT_LNG_CHELSEA = new LatLng(51.481670, -0.190849);
+ private static final LatLng LAT_LNG_ARSENAL = new LatLng(51.555062, -0.108417);
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private Marker marker;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dynamic_marker);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.setTag(false);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ DynamicMarkerChangeActivity.this.mapboxMap = mapboxMap;
+ // Create marker
+ MarkerOptions markerOptions = new MarkerOptions()
+ .position(LAT_LNG_CHELSEA)
+ .icon(IconUtils.drawableToIcon(DynamicMarkerChangeActivity.this, R.drawable.ic_stars,
+ ResourcesCompat.getColor(getResources(), R.color.blueAccent, getTheme())))
+ .title(getString(R.string.dynamic_marker_chelsea_title))
+ .snippet(getString(R.string.dynamic_marker_chelsea_snippet));
+ marker = mapboxMap.addMarker(markerOptions);
+ });
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setColorFilter(ContextCompat.getColor(this, R.color.primary));
+ fab.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ updateMarker();
+ }
+ });
+ }
+
+ private void updateMarker() {
+ // update model
+ boolean first = (boolean) mapView.getTag();
+ mapView.setTag(!first);
+
+ // update marker
+ marker.setPosition(first ? LAT_LNG_CHELSEA : LAT_LNG_ARSENAL);
+ marker.setIcon(IconUtils.drawableToIcon(this, R.drawable.ic_stars, first
+ ? ResourcesCompat.getColor(getResources(), R.color.blueAccent, getTheme()) :
+ ResourcesCompat.getColor(getResources(), R.color.redAccent, getTheme())
+ ));
+
+ marker.setTitle(first
+ ? getString(R.string.dynamic_marker_chelsea_title) : getString(R.string.dynamic_marker_arsenal_title));
+ marker.setSnippet(first
+ ? getString(R.string.dynamic_marker_chelsea_snippet) : getString(R.string.dynamic_marker_arsenal_snippet));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewActivity.java
new file mode 100644
index 0000000000..910538a803
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewActivity.java
@@ -0,0 +1,483 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.FloatEvaluator;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.IconFactory;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.annotations.MarkerView;
+import com.mapbox.mapboxsdk.annotations.MarkerViewManager;
+import com.mapbox.mapboxsdk.annotations.MarkerViewOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.model.annotations.CountryMarkerView;
+import com.mapbox.mapboxsdk.testapp.model.annotations.CountryMarkerViewOptions;
+import com.mapbox.mapboxsdk.testapp.model.annotations.TextMarkerView;
+import com.mapbox.mapboxsdk.testapp.model.annotations.TextMarkerViewOptions;
+
+import java.util.Locale;
+import java.util.Random;
+
+/**
+ * Test activity showcasing multiple MarkerViews above Washington D.C.
+ * <p>
+ * Shows a couple of open InfoWindows out of current Viewport.
+ * Updates the rotation and location of a couple of MarkerViews.
+ * </p>
+ */
+public class MarkerViewActivity extends AppCompatActivity {
+
+ private static final LatLng[] LAT_LNGS = new LatLng[] {
+ new LatLng(38.897424, -77.036508),
+ new LatLng(38.909698, -77.029642),
+ new LatLng(38.907227, -77.036530),
+ new LatLng(38.905607, -77.031916),
+ new LatLng(38.889441, -77.050134),
+ new LatLng(38.888000, -77.050000) // Slight overlap to show re-ordering on selection
+ };
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+
+ // MarkerView location updates
+ private MarkerView movingMarkerOne;
+ private MarkerView movingMarkerTwo;
+ private Random randomAnimator = new Random();
+ private Handler locationUpdateHandler = new Handler();
+ private Runnable moveMarkerRunnable = new MoveMarkerRunnable();
+
+ // MarkerView rotate updates
+ private MarkerView rotateMarker;
+ private Handler rotateUpdateHandler = new Handler();
+ private Runnable rotateMarkerRunnable = new RotateMarkerRunnable();
+ private int rotation = 360;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_marker_view);
+
+ final TextView viewCountView = (TextView) findViewById(R.id.countView);
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ MarkerViewActivity.this.mapboxMap = mapboxMap;
+
+ final MarkerViewManager markerViewManager = mapboxMap.getMarkerViewManager();
+
+ Icon usFlag = IconFactory.getInstance(MarkerViewActivity.this)
+ .fromResource(R.drawable.ic_us);
+
+ // add default ViewMarker markers
+ for (int i = 0; i < LAT_LNGS.length; i++) {
+ MarkerViewActivity.this.mapboxMap.addMarker(new MarkerViewOptions()
+ .position(LAT_LNGS[i])
+ .title(String.valueOf(i))
+ .alpha(0.5f)
+ .icon(usFlag)
+ );
+ }
+
+ // add custom ViewMarker
+ CountryMarkerViewOptions options = new CountryMarkerViewOptions();
+ options.flagRes(R.drawable.icon_burned);
+ options.abbrevName("Mapbox");
+ options.title("Hello");
+ options.position(new LatLng(38.899774, -77.023237));
+ options.flat(true);
+ MarkerView markerView = mapboxMap.addMarker(options);
+
+ // Use object animator to rotate MarkerView
+ ValueAnimator markerAnimator = ObjectAnimator.ofObject(markerView, "rotation", new FloatEvaluator(), -90, 90);
+ markerAnimator.setDuration(5000);
+ markerAnimator.start();
+
+ MarkerViewActivity.this.mapboxMap.addMarker(new MarkerOptions()
+ .title("United States")
+ .position(new LatLng(38.902580, -77.050102))
+ );
+
+ rotateMarker = MarkerViewActivity.this.mapboxMap.addMarker(new TextMarkerViewOptions()
+ .text("A")
+ .rotation(rotation = 270)
+ .position(new LatLng(38.889876, -77.008849))
+ );
+ loopMarkerRotate();
+
+
+ MarkerViewActivity.this.mapboxMap.addMarker(new TextMarkerViewOptions()
+ .text("B")
+ .position(new LatLng(38.907327, -77.041293))
+ );
+
+ MarkerViewActivity.this.mapboxMap.addMarker(new TextMarkerViewOptions()
+ .text("C")
+ .position(new LatLng(38.897642, -77.041980))
+ );
+
+ // if you want to customise a ViewMarker you need to extend ViewMarker and provide an adapter implementation
+ // set adapters for child classes of ViewMarker
+ markerViewManager.addMarkerViewAdapter(new CountryAdapter(MarkerViewActivity.this, mapboxMap));
+ markerViewManager.addMarkerViewAdapter(new TextAdapter(MarkerViewActivity.this, mapboxMap));
+
+ final ViewGroup markerViewContainer = markerViewManager.getMarkerViewContainer();
+
+ // add a change listener to validate the size of amount of child views
+ mapView.addOnMapChangedListener(change -> {
+ if (change == MapView.REGION_IS_CHANGING || change == MapView.REGION_DID_CHANGE) {
+ if (!markerViewManager.getMarkerViewAdapters().isEmpty() && viewCountView != null) {
+ viewCountView.setText(String.format(
+ Locale.getDefault(),
+ getString(R.string.viewcache_size),
+ markerViewContainer.getChildCount())
+ );
+ }
+ }
+ });
+
+ // add a OnMarkerView click listener
+ MarkerViewActivity.this.mapboxMap.getMarkerViewManager().setOnMarkerViewClickListener(
+ (marker, view, adapter) -> {
+ Toast.makeText(MarkerViewActivity.this, "Hello " + marker.getId(), Toast.LENGTH_SHORT).show();
+ return false;
+ });
+
+ movingMarkerOne = MarkerViewActivity.this.mapboxMap.addMarker(new MarkerViewOptions()
+ .position(CarLocation.CAR_0_LNGS[0])
+ .icon(IconFactory.getInstance(mapView.getContext())
+ .fromResource(R.drawable.ic_android))
+ );
+
+ movingMarkerTwo = mapboxMap.addMarker(new MarkerViewOptions()
+ .position(CarLocation.CAR_1_LNGS[0])
+ .icon(IconFactory.getInstance(mapView.getContext())
+ .fromResource(R.drawable.ic_android_2))
+ );
+
+ // allow more open infowindows at the same time
+ mapboxMap.setAllowConcurrentMultipleOpenInfoWindows(true);
+
+ // add offscreen markers
+ Marker markerRightOffScreen = mapboxMap.addMarker(new MarkerOptions()
+ .setPosition(new LatLng(38.892846, -76.909399))
+ .title("InfoWindow")
+ .snippet("Offscreen, to the right of the Map."));
+
+ Marker markerRightBottomOffScreen = mapboxMap.addMarker(new MarkerOptions()
+ .setPosition(new LatLng(38.791645, -77.039006))
+ .title("InfoWindow")
+ .snippet("Offscreen, to the bottom of the Map"));
+
+ // open infowindow offscreen markers
+ mapboxMap.selectMarker(markerRightOffScreen);
+ mapboxMap.selectMarker(markerRightBottomOffScreen);
+ });
+ }
+
+ private void loopMarkerRotate() {
+ rotateUpdateHandler.postDelayed(rotateMarkerRunnable, 800);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ loopMarkerMove();
+ }
+
+ private void loopMarkerMove() {
+ locationUpdateHandler.postDelayed(moveMarkerRunnable, randomAnimator.nextInt(3000) + 1000);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ locationUpdateHandler.removeCallbacks(moveMarkerRunnable);
+ rotateUpdateHandler.removeCallbacks(rotateMarkerRunnable);
+ }
+
+ /**
+ * Updates the position of a Marker
+ */
+ private class MoveMarkerRunnable implements Runnable {
+ @Override
+ public void run() {
+ int randomInteger = randomAnimator.nextInt(9);
+ if (randomAnimator.nextInt() % 2 == 0) {
+ movingMarkerOne.setPosition(CarLocation.CAR_0_LNGS[randomInteger]);
+ } else {
+ movingMarkerTwo.setPosition(CarLocation.CAR_1_LNGS[randomInteger]);
+ }
+ loopMarkerMove();
+ }
+ }
+
+ /**
+ * Updates the rotation of a Marker
+ */
+ private class RotateMarkerRunnable implements Runnable {
+
+ private static final int ROTATION_INCREASE_VALUE = 9;
+
+ @Override
+ public void run() {
+ rotation -= ROTATION_INCREASE_VALUE;
+ if (rotation >= 0) {
+ rotation += 360;
+ }
+ rotateMarker.setRotation(rotation);
+ loopMarkerRotate();
+ }
+ }
+
+ /**
+ * Adapts a MarkerView to display an abbreviated name in a TextView and a flag in an ImageView.
+ */
+ private static class CountryAdapter extends MapboxMap.MarkerViewAdapter<CountryMarkerView> {
+
+ private LayoutInflater inflater;
+ private MapboxMap mapboxMap;
+
+ CountryAdapter(@NonNull Context context, @NonNull MapboxMap mapboxMap) {
+ super(context, CountryMarkerView.class);
+ this.inflater = LayoutInflater.from(context);
+ this.mapboxMap = mapboxMap;
+ }
+
+ @Nullable
+ @Override
+ public View getView(@NonNull CountryMarkerView marker, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder viewHolder;
+ if (convertView == null) {
+ viewHolder = new ViewHolder();
+ convertView = inflater.inflate(R.layout.view_custom_marker, parent, false);
+ viewHolder.flag = (ImageView) convertView.findViewById(R.id.imageView);
+ viewHolder.abbrev = (TextView) convertView.findViewById(R.id.textView);
+ convertView.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) convertView.getTag();
+ }
+ viewHolder.flag.setImageResource(marker.getFlagRes());
+ viewHolder.abbrev.setText(marker.getAbbrevName());
+ return convertView;
+ }
+
+ @Override
+ public boolean onSelect(
+ @NonNull final CountryMarkerView marker, @NonNull final View convertView, boolean reselectionForViewReuse) {
+ convertView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(convertView, View.ROTATION, 0, 360);
+ rotateAnimator.setDuration(reselectionForViewReuse ? 0 : 350);
+ rotateAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ convertView.setLayerType(View.LAYER_TYPE_NONE, null);
+ mapboxMap.selectMarker(marker);
+ }
+ });
+ rotateAnimator.start();
+
+ // false indicates that we are calling selectMarker after our animation ourselves
+ // true will let the system call it for you, which will result in showing an InfoWindow instantly
+ return false;
+ }
+
+ @Override
+ public void onDeselect(@NonNull CountryMarkerView marker, @NonNull final View convertView) {
+ convertView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ ObjectAnimator rotateAnimator = ObjectAnimator.ofFloat(convertView, View.ROTATION, 360, 0);
+ rotateAnimator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ convertView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ });
+ rotateAnimator.start();
+ }
+
+ private static class ViewHolder {
+ ImageView flag;
+ TextView abbrev;
+ }
+ }
+
+ /**
+ * Adapts a MarkerView to display text in a TextView.
+ */
+ public static class TextAdapter extends MapboxMap.MarkerViewAdapter<TextMarkerView> {
+
+ private LayoutInflater inflater;
+ private MapboxMap mapboxMap;
+
+ public TextAdapter(@NonNull Context context, @NonNull MapboxMap mapboxMap) {
+ super(context, TextMarkerView.class);
+ this.inflater = LayoutInflater.from(context);
+ this.mapboxMap = mapboxMap;
+ }
+
+ @Nullable
+ @Override
+ public View getView(@NonNull TextMarkerView marker, @Nullable View convertView, @NonNull ViewGroup parent) {
+ ViewHolder viewHolder;
+ if (convertView == null) {
+ viewHolder = new ViewHolder();
+ convertView = inflater.inflate(R.layout.view_text_marker, parent, false);
+ viewHolder.textView = (TextView) convertView.findViewById(R.id.textView);
+ convertView.setTag(viewHolder);
+ } else {
+ viewHolder = (ViewHolder) convertView.getTag();
+ }
+ viewHolder.textView.setText(marker.getText());
+ return convertView;
+ }
+
+ @Override
+ public boolean onSelect(
+ @NonNull final TextMarkerView marker, @NonNull final View convertView, boolean reselectionForViewReuse) {
+ animateGrow(marker, convertView, 0);
+
+ // false indicates that we are calling selectMarker after our animation ourselves
+ // true will let the system call it for you, which will result in showing an InfoWindow instantly
+ return false;
+ }
+
+ @Override
+ public void onDeselect(@NonNull TextMarkerView marker, @NonNull final View convertView) {
+ animateShrink(convertView, 350);
+ }
+
+ @Override
+ public boolean prepareViewForReuse(@NonNull MarkerView marker, @NonNull View convertView) {
+ // this method is called before a view will be reused, we need to restore view state
+ // as we have scaled the view in onSelect. If not correctly applied other MarkerView will
+ // become large since these have been recycled
+
+ // cancel ongoing animation
+ convertView.animate().cancel();
+
+ if (marker.isSelected()) {
+ // shrink view to be able to be reused
+ animateShrink(convertView, 0);
+ }
+
+ // true if you want reuse to occur automatically, false if you want to manage this yourself
+ return true;
+ }
+
+ private void animateGrow(@NonNull final MarkerView marker, @NonNull final View convertView, int duration) {
+ convertView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ Animator animator = AnimatorInflater.loadAnimator(convertView.getContext(), R.animator.scale_up);
+ animator.setDuration(duration);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ convertView.setLayerType(View.LAYER_TYPE_NONE, null);
+ mapboxMap.selectMarker(marker);
+ }
+ });
+ animator.setTarget(convertView);
+ animator.start();
+ }
+
+ private void animateShrink(@NonNull final View convertView, int duration) {
+ convertView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
+ Animator animator = AnimatorInflater.loadAnimator(convertView.getContext(), R.animator.scale_down);
+ animator.setDuration(duration);
+ animator.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ convertView.setLayerType(View.LAYER_TYPE_NONE, null);
+ }
+ });
+ animator.setTarget(convertView);
+ animator.start();
+ }
+
+ private static class ViewHolder {
+ TextView textView;
+ }
+ }
+
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ private static class CarLocation {
+
+ static LatLng[] CAR_0_LNGS = new LatLng[] {
+ new LatLng(38.92334425495122, -77.0533673443786),
+ new LatLng(38.9234737236897, -77.05389484528261),
+ new LatLng(38.9257094658146, -76.98819752280579),
+ new LatLng(38.8324328369647, -77.00690648325929),
+ new LatLng(38.87540698725855, -77.0093148713099),
+ new LatLng(38.96499498141065, -77.07707916040054),
+ new LatLng(38.90794910679896, -76.99695304153806),
+ new LatLng(38.86234025281626, -76.9950528034839),
+ new LatLng(38.862930274733635, -76.99647808241964)
+ };
+
+ static LatLng[] CAR_1_LNGS = new LatLng[] {
+ new LatLng(38.94237975070426, -76.98324549005675),
+ new LatLng(38.941520236084486, -76.98234257804742),
+ new LatLng(38.85972219720714, -76.98955808483929),
+ new LatLng(38.944289166113776, -76.98584257252891),
+ new LatLng(38.94375860578053, -76.98470344318412),
+ new LatLng(38.943167431929645, -76.98373163938666),
+ new LatLng(38.882834728904605, -77.02862535635137),
+ new LatLng(38.882869724926245, -77.02992539231113),
+ new LatLng(38.9371988177896, -76.97786740676564)
+ };
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewsInRectangleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewsInRectangleActivity.java
new file mode 100644
index 0000000000..4ac2a25520
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/MarkerViewsInRectangleActivity.java
@@ -0,0 +1,110 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.annotations.MarkerView;
+import com.mapbox.mapboxsdk.annotations.MarkerViewOptions;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing counting MarkerViews in a rectangle.
+ */
+public class MarkerViewsInRectangleActivity extends AppCompatActivity implements OnMapReadyCallback,
+ View.OnClickListener {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+ private View selectionBox;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_marker_view_in_rect);
+
+ selectionBox = findViewById(R.id.selection_box);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ MarkerViewsInRectangleActivity.this.mapboxMap = mapboxMap;
+ selectionBox.setOnClickListener(this);
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(52.0907, 5.1214), 16));
+ mapboxMap.addMarker(new MarkerViewOptions().position(new LatLng(52.0907, 5.1214)));
+ }
+
+ @Override
+ public void onClick(View view) {
+ // Query
+ int top = selectionBox.getTop() - mapView.getTop();
+ int left = selectionBox.getLeft() - mapView.getLeft();
+ RectF box = new RectF(left, top, left + selectionBox.getWidth(), top + selectionBox.getHeight());
+ Timber.i("Querying box %s", box);
+ List<MarkerView> markers = mapboxMap.getMarkerViewsInRect(box);
+
+ // Show count
+ Toast.makeText(
+ MarkerViewsInRectangleActivity.this,
+ String.format("%s markers inside box", markers.size()),
+ Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolygonActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolygonActivity.java
new file mode 100644
index 0000000000..fe830e819a
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolygonActivity.java
@@ -0,0 +1,218 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.annotations.Polygon;
+import com.mapbox.mapboxsdk.annotations.PolygonOptions;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.BLUE_COLOR;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.BROKEN_SHAPE_POINTS;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.FULL_ALPHA;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.NO_ALPHA;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.PARTIAL_ALPHA;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.RED_COLOR;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.STAR_SHAPE_HOLES;
+import static com.mapbox.mapboxsdk.maps.activity.annotation.PolygonActivity.Config.STAR_SHAPE_POINTS;
+
+/**
+ * Test activity to showcase the Polygon annotation API & programmatically creating a MapView.
+ * <p>
+ * Shows how to change Polygon features as visibility, alpha, color and points.
+ * </p>
+ */
+public class PolygonActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private Polygon polygon;
+ private boolean fullAlpha = true;
+ private boolean visible = true;
+ private boolean color = true;
+ private boolean allPoints = true;
+ private boolean holes = false;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // configure inital map state
+ MapboxMapOptions options = new MapboxMapOptions()
+ .attributionTintColor(RED_COLOR)
+ .compassFadesWhenFacingNorth(false)
+ .styleUrl(Style.MAPBOX_STREETS)
+ .camera(new CameraPosition.Builder()
+ .target(new LatLng(45.520486, -122.673541))
+ .zoom(12)
+ .tilt(40)
+ .build());
+
+ // create map
+ mapView = new MapView(this, options);
+ mapView.setId(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+
+ setContentView(mapView);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+
+ map.setOnPolygonClickListener(polygon -> Toast.makeText(
+ PolygonActivity.this,
+ "You clicked on polygon with id = " + polygon.getId(),
+ Toast.LENGTH_SHORT
+ ).show());
+
+ polygon = mapboxMap.addPolygon(new PolygonOptions()
+ .addAll(STAR_SHAPE_POINTS)
+ .fillColor(BLUE_COLOR));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_id_alpha:
+ fullAlpha = !fullAlpha;
+ polygon.setAlpha(fullAlpha ? FULL_ALPHA : PARTIAL_ALPHA);
+ return true;
+ case R.id.action_id_visible:
+ visible = !visible;
+ polygon.setAlpha(visible ? (fullAlpha ? FULL_ALPHA : PARTIAL_ALPHA) : NO_ALPHA);
+ return true;
+ case R.id.action_id_points:
+ allPoints = !allPoints;
+ polygon.setPoints(allPoints ? STAR_SHAPE_POINTS : BROKEN_SHAPE_POINTS);
+ return true;
+ case R.id.action_id_color:
+ color = !color;
+ polygon.setFillColor(color ? BLUE_COLOR : RED_COLOR);
+ return true;
+ case R.id.action_id_holes:
+ holes = !holes;
+ polygon.setHoles(holes ? STAR_SHAPE_HOLES : Collections.<List<LatLng>>emptyList());
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_polygon, menu);
+ return true;
+ }
+
+ static final class Config {
+ static final int BLUE_COLOR = Color.parseColor("#3bb2d0");
+ static final int RED_COLOR = Color.parseColor("#AF0000");
+
+ static final float FULL_ALPHA = 1.0f;
+ static final float PARTIAL_ALPHA = 0.5f;
+ static final float NO_ALPHA = 0.0f;
+
+ static final List<LatLng> STAR_SHAPE_POINTS = new ArrayList<LatLng>() {
+ {
+ add(new LatLng(45.522585, -122.685699));
+ add(new LatLng(45.534611, -122.708873));
+ add(new LatLng(45.530883, -122.678833));
+ add(new LatLng(45.547115, -122.667503));
+ add(new LatLng(45.530643, -122.660121));
+ add(new LatLng(45.533529, -122.636260));
+ add(new LatLng(45.521743, -122.659091));
+ add(new LatLng(45.510677, -122.648792));
+ add(new LatLng(45.515008, -122.664070));
+ add(new LatLng(45.502496, -122.669048));
+ add(new LatLng(45.515369, -122.678489));
+ add(new LatLng(45.506346, -122.702007));
+ add(new LatLng(45.522585, -122.685699));
+ }
+ };
+
+ static final List<LatLng> BROKEN_SHAPE_POINTS =
+ STAR_SHAPE_POINTS.subList(0, STAR_SHAPE_POINTS.size() - 3);
+
+ static final List<? extends List<LatLng>> STAR_SHAPE_HOLES = new ArrayList<List<LatLng>>() {
+ {
+ add(new ArrayList<>(new ArrayList<LatLng>() {
+ {
+ add(new LatLng(45.521743, -122.669091));
+ add(new LatLng(45.530483, -122.676833));
+ add(new LatLng(45.520483, -122.676833));
+ add(new LatLng(45.521743, -122.669091));
+ }
+ }));
+ add(new ArrayList<>(new ArrayList<LatLng>() {
+ {
+ add(new LatLng(45.529743, -122.662791));
+ add(new LatLng(45.525543, -122.662791));
+ add(new LatLng(45.525543, -122.660));
+ add(new LatLng(45.527743, -122.660));
+ add(new LatLng(45.529743, -122.662791));
+ }
+ }));
+ }
+ };
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolylineActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolylineActivity.java
new file mode 100644
index 0000000000..30adf3604d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PolylineActivity.java
@@ -0,0 +1,223 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.annotations.Polyline;
+import com.mapbox.mapboxsdk.annotations.PolylineOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Test activity showcasing the Polyline annotations API.
+ * <p>
+ * Shows how to add and remove polylines.
+ * </p>
+ */
+public class PolylineActivity extends AppCompatActivity {
+
+ private static final String STATE_POLYLINE_OPTIONS = "polylineOptions";
+
+ private static final LatLng ANDORRA = new LatLng(42.505777, 1.52529);
+ private static final LatLng LUXEMBOURG = new LatLng(49.815273, 6.129583);
+ private static final LatLng MONACO = new LatLng(43.738418, 7.424616);
+ private static final LatLng VATICAN_CITY = new LatLng(41.902916, 12.453389);
+ private static final LatLng SAN_MARINO = new LatLng(43.942360, 12.457777);
+ private static final LatLng LIECHTENSTEIN = new LatLng(47.166000, 9.555373);
+
+ private static final float FULL_ALPHA = 1.0f;
+ private static final float PARTIAL_ALPHA = 0.5f;
+ private static final float NO_ALPHA = 0.0f;
+
+ private List<Polyline> polylines;
+ private ArrayList<PolylineOptions> polylineOptions = new ArrayList<>();
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private boolean fullAlpha = true;
+ private boolean visible = true;
+ private boolean width = true;
+ private boolean color = true;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_polyline);
+
+ if (savedInstanceState != null) {
+ polylineOptions = savedInstanceState.getParcelableArrayList(STATE_POLYLINE_OPTIONS);
+ } else {
+ polylineOptions.addAll(getAllPolylines());
+ }
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ PolylineActivity.this.mapboxMap = mapboxMap;
+
+ mapboxMap.setOnPolylineClickListener(polyline -> Toast.makeText(
+ PolylineActivity.this,
+ "You clicked on polygon with id = " + polyline.getId(),
+ Toast.LENGTH_SHORT
+ ).show());
+
+ polylines = mapboxMap.addPolylines(polylineOptions);
+ });
+
+ View fab = findViewById(R.id.fab);
+ if (fab != null) {
+ fab.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ if (polylines != null && polylines.size() > 0) {
+ if (polylines.size() == 1) {
+ // test for removing annotation
+ mapboxMap.removeAnnotation(polylines.get(0));
+ } else {
+ // test for removing annotations
+ mapboxMap.removeAnnotations(polylines);
+ }
+ }
+ polylineOptions.clear();
+ polylineOptions.addAll(getRandomLine());
+ polylines = mapboxMap.addPolylines(polylineOptions);
+
+ }
+ });
+ }
+ }
+
+ private List<PolylineOptions> getAllPolylines() {
+ List<PolylineOptions> options = new ArrayList<>();
+ options.add(generatePolyline(ANDORRA, LUXEMBOURG, "#F44336"));
+ options.add(generatePolyline(ANDORRA, MONACO, "#FF5722"));
+ options.add(generatePolyline(MONACO, VATICAN_CITY, "#673AB7"));
+ options.add(generatePolyline(VATICAN_CITY, SAN_MARINO, "#009688"));
+ options.add(generatePolyline(SAN_MARINO, LIECHTENSTEIN, "#795548"));
+ options.add(generatePolyline(LIECHTENSTEIN, LUXEMBOURG, "#3F51B5"));
+ return options;
+ }
+
+ private PolylineOptions generatePolyline(LatLng start, LatLng end, String color) {
+ PolylineOptions line = new PolylineOptions();
+ line.add(start);
+ line.add(end);
+ line.color(Color.parseColor(color));
+ return line;
+ }
+
+ public List<PolylineOptions> getRandomLine() {
+ final List<PolylineOptions> randomLines = getAllPolylines();
+ Collections.shuffle(randomLines);
+ return new ArrayList<PolylineOptions>() {
+ {
+ add(randomLines.get(0));
+ }
+ };
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ outState.putParcelableArrayList(STATE_POLYLINE_OPTIONS, polylineOptions);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_polyline, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (polylines.size() <= 0) {
+ Toast.makeText(PolylineActivity.this, "No polylines on map", Toast.LENGTH_LONG).show();
+ return super.onOptionsItemSelected(item);
+ }
+ switch (item.getItemId()) {
+ case R.id.action_id_remove:
+ // test to remove all annotations
+ polylineOptions.clear();
+ mapboxMap.clear();
+ polylines.clear();
+ return true;
+
+ case R.id.action_id_alpha:
+ fullAlpha = !fullAlpha;
+ for (Polyline p : polylines) {
+ p.setAlpha(fullAlpha ? FULL_ALPHA : PARTIAL_ALPHA);
+ }
+ return true;
+
+ case R.id.action_id_color:
+ color = !color;
+ for (Polyline p : polylines) {
+ p.setColor(color ? Color.RED : Color.BLUE);
+ }
+ return true;
+
+ case R.id.action_id_width:
+ width = !width;
+ for (Polyline p : polylines) {
+ p.setWidth(width ? 3.0f : 5.0f);
+ }
+ return true;
+
+ case R.id.action_id_visible:
+ visible = !visible;
+ for (Polyline p : polylines) {
+ p.setAlpha(visible ? (fullAlpha ? FULL_ALPHA : PARTIAL_ALPHA) : NO_ALPHA);
+ }
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PressForMarkerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PressForMarkerActivity.java
new file mode 100644
index 0000000000..cb5ee9e65b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/annotation/PressForMarkerActivity.java
@@ -0,0 +1,140 @@
+package com.mapbox.mapboxsdk.maps.activity.annotation;
+
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+
+/**
+ * Test activity showcasing to add a Marker on click.
+ * <p>
+ * Shows how to use a OnMapClickListener and a OnMapLongClickListener
+ * </p>
+ */
+public class PressForMarkerActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private ArrayList<MarkerOptions> markerList = new ArrayList<>();
+
+ private static final DecimalFormat LAT_LON_FORMATTER = new DecimalFormat("#.#####");
+
+ private static String STATE_MARKER_LIST = "markerList";
+
+ @Override
+ protected void onCreate(@Nullable final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_press_for_marker);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ resetMap();
+
+ mapboxMap.setOnMapLongClickListener(point -> addMarker(point));
+
+ mapboxMap.setOnMapClickListener(point -> addMarker(point));
+
+ if (savedInstanceState != null) {
+ markerList = savedInstanceState.getParcelableArrayList(STATE_MARKER_LIST);
+ mapboxMap.addMarkers(markerList);
+ }
+ });
+ }
+
+ private void addMarker(LatLng point) {
+ final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
+
+ String title = LAT_LON_FORMATTER.format(point.getLatitude()) + ", "
+ + LAT_LON_FORMATTER.format(point.getLongitude());
+ String snippet = "X = " + (int) pixel.x + ", Y = " + (int) pixel.y;
+
+ MarkerOptions marker = new MarkerOptions()
+ .position(point)
+ .title(title)
+ .snippet(snippet);
+
+ markerList.add(marker);
+ mapboxMap.addMarker(marker);
+ }
+
+ private void resetMap() {
+ if (mapboxMap == null) {
+ return;
+ }
+ mapboxMap.removeAnnotations();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_press_for_marker, menu);
+ return true;
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ mapView.onSaveInstanceState(outState);
+ outState.putParcelableArrayList(STATE_MARKER_LIST, markerList);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menuItemReset:
+ resetMap();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimationTypeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimationTypeActivity.java
new file mode 100644
index 0000000000..e462315021
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimationTypeActivity.java
@@ -0,0 +1,182 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing the Camera API and listen to camera updates by animating the camera above London.
+ * <p>
+ * Shows how to use animate, ease and move camera update factory methods.
+ * </p>
+ */
+public class CameraAnimationTypeActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final LatLng LAT_LNG_LONDON_EYE = new LatLng(51.50325, -0.11968);
+ private static final LatLng LAT_LNG_TOWER_BRIDGE = new LatLng(51.50550, -0.07520);
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private boolean cameraState;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_animation_types);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ if (mapView != null) {
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ mapboxMap.getUiSettings().setAttributionEnabled(false);
+ mapboxMap.getUiSettings().setLogoEnabled(false);
+ mapboxMap.setOnCameraChangeListener(position -> Timber.w(position.toString()));
+
+ // handle move button clicks
+ View moveButton = findViewById(R.id.cameraMoveButton);
+ if (moveButton != null) {
+ moveButton.setOnClickListener(view -> {
+ CameraPosition cameraPosition = new CameraPosition.Builder()
+ .target(getNextLatLng())
+ .zoom(14)
+ .tilt(30)
+ .tilt(0)
+ .build();
+ mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
+ });
+ }
+
+ // handle ease button clicks
+ View easeButton = findViewById(R.id.cameraEaseButton);
+ if (easeButton != null) {
+ easeButton.setOnClickListener(view -> {
+ CameraPosition cameraPosition = new CameraPosition.Builder()
+ .target(getNextLatLng())
+ .zoom(15)
+ .bearing(180)
+ .tilt(30)
+ .build();
+
+ MapboxMap.CancelableCallback callback = new MapboxMap.CancelableCallback() {
+ @Override
+ public void onCancel() {
+ Timber.i("Duration onCancel Callback called.");
+ Toast.makeText(
+ CameraAnimationTypeActivity.this,
+ "Ease onCancel Callback called.",
+ Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFinish() {
+ Timber.i("Duration onFinish Callback called.");
+ Toast.makeText(
+ CameraAnimationTypeActivity.this,
+ "Ease onFinish Callback called.",
+ Toast.LENGTH_LONG).show();
+ }
+ };
+
+ mapboxMap.easeCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 7500, callback);
+ });
+ }
+
+ // handle animate button clicks
+ View animateButton = findViewById(R.id.cameraAnimateButton);
+ if (animateButton != null) {
+ animateButton.setOnClickListener(view -> {
+ CameraPosition cameraPosition = new CameraPosition.Builder()
+ .target(getNextLatLng())
+ .bearing(270)
+ .tilt(20)
+ .build();
+
+ MapboxMap.CancelableCallback callback = new MapboxMap.CancelableCallback() {
+ @Override
+ public void onCancel() {
+ Timber.i("Duration onCancel Callback called.");
+ Toast.makeText(
+ CameraAnimationTypeActivity.this,
+ "Duration onCancel Callback called.",
+ Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onFinish() {
+ Timber.i("Duration onFinish Callback called.");
+ Toast.makeText(
+ CameraAnimationTypeActivity.this,
+ "Duration onFinish Callback called.",
+ Toast.LENGTH_LONG).show();
+ }
+ };
+
+ mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 7500, callback);
+ });
+ }
+ }
+
+ private LatLng getNextLatLng() {
+ cameraState = !cameraState;
+ return cameraState ? LAT_LNG_TOWER_BRIDGE : LAT_LNG_LONDON_EYE;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimatorActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimatorActivity.java
new file mode 100644
index 0000000000..d6a6593e72
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraAnimatorActivity.java
@@ -0,0 +1,278 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.TypeEvaluator;
+import android.animation.ValueAnimator;
+import android.os.Bundle;
+import android.support.v4.util.LongSparseArray;
+import android.support.v4.view.animation.FastOutLinearInInterpolator;
+import android.support.v4.view.animation.FastOutSlowInInterpolator;
+import android.support.v4.view.animation.PathInterpolatorCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.animation.AnticipateOvershootInterpolator;
+import android.view.animation.BounceInterpolator;
+import android.view.animation.Interpolator;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using Android SDK animators to animate camera position changes.
+ */
+public class CameraAnimatorActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final double ANIMATION_DELAY_FACTOR = 1.5;
+ private static final LatLng START_LAT_LNG = new LatLng(37.787947, -122.407432);
+
+ private final LongSparseArray<AnimatorBuilder> animators = new LongSparseArray<AnimatorBuilder>() {
+ {
+ put(R.id.menu_action_accelerate_decelerate_interpolator, () -> {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ createLatLngAnimator(START_LAT_LNG, new LatLng(37.826715, -122.422795)),
+ obtainExampleInterpolator(new FastOutSlowInInterpolator(), 2500)
+ );
+ return animatorSet;
+ });
+
+ put(R.id.menu_action_bounce_interpolator, () -> {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.playTogether(
+ createLatLngAnimator(START_LAT_LNG, new LatLng(37.787947, -122.407432)),
+ obtainExampleInterpolator(new BounceInterpolator(), 3750)
+ );
+ return animatorSet;
+ });
+
+ put(R.id.menu_action_anticipate_overshoot_interpolator, () ->
+ obtainExampleInterpolator(new AnticipateOvershootInterpolator(), 2500)
+ );
+
+ put(R.id.menu_action_path_interpolator, () -> obtainExampleInterpolator(
+ PathInterpolatorCompat.create(.22f, .68f, 0, 1.71f), 2500));
+ }
+ };
+
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_animator);
+ mapView = (MapView) findViewById(R.id.mapView);
+ if (mapView != null) {
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+ }
+
+ @Override
+ public void onMapReady(final MapboxMap map) {
+ mapboxMap = map;
+ initFab();
+ }
+
+ private void initFab() {
+ findViewById(R.id.fab).setOnClickListener(view -> {
+ view.setVisibility(View.GONE);
+
+ CameraPosition animatedPosition = new CameraPosition.Builder()
+ .target(new LatLng(37.789992, -122.402214))
+ .tilt(60)
+ .zoom(14.5f)
+ .bearing(135)
+ .build();
+
+ createExampleAnimator(mapboxMap.getCameraPosition(), animatedPosition).start();
+ });
+ }
+
+ //
+ // Animator API used for the animation on the FAB
+ //
+
+ private Animator createExampleAnimator(CameraPosition currentPosition, CameraPosition targetPosition) {
+ AnimatorSet animatorSet = new AnimatorSet();
+ animatorSet.play(createLatLngAnimator(currentPosition.target, targetPosition.target));
+ animatorSet.play(createZoomAnimator(currentPosition.zoom, targetPosition.zoom));
+ animatorSet.play(createBearingAnimator(currentPosition.bearing, targetPosition.bearing));
+ animatorSet.play(createTiltAnimator(currentPosition.tilt, targetPosition.tilt));
+ return animatorSet;
+ }
+
+ private Animator createLatLngAnimator(LatLng currentPosition, LatLng targetPosition) {
+ ValueAnimator latLngAnimator = ValueAnimator.ofObject(new LatLngEvaluator(), currentPosition, targetPosition);
+ latLngAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR));
+ latLngAnimator.setInterpolator(new FastOutSlowInInterpolator());
+ latLngAnimator.addUpdateListener(animation -> mapboxMap.moveCamera(
+ CameraUpdateFactory.newLatLng((LatLng) animation.getAnimatedValue()))
+ );
+ return latLngAnimator;
+ }
+
+ private Animator createZoomAnimator(double currentZoom, double targetZoom) {
+ ValueAnimator zoomAnimator = ValueAnimator.ofFloat((float) currentZoom, (float) targetZoom);
+ zoomAnimator.setDuration((long) (2200 * ANIMATION_DELAY_FACTOR));
+ zoomAnimator.setStartDelay((long) (600 * ANIMATION_DELAY_FACTOR));
+ zoomAnimator.setInterpolator(new AnticipateOvershootInterpolator());
+ zoomAnimator.addUpdateListener(animation -> mapboxMap.moveCamera(
+ CameraUpdateFactory.zoomTo((Float) animation.getAnimatedValue()))
+ );
+ return zoomAnimator;
+ }
+
+ private Animator createBearingAnimator(double currentBearing, double targetBearing) {
+ ValueAnimator bearingAnimator = ValueAnimator.ofFloat((float) currentBearing, (float) targetBearing);
+ bearingAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR));
+ bearingAnimator.setStartDelay((long) (1000 * ANIMATION_DELAY_FACTOR));
+ bearingAnimator.setInterpolator(new FastOutLinearInInterpolator());
+ bearingAnimator.addUpdateListener(animation -> mapboxMap.moveCamera(
+ CameraUpdateFactory.bearingTo((Float) animation.getAnimatedValue()))
+ );
+ return bearingAnimator;
+ }
+
+ private Animator createTiltAnimator(double currentTilt, double targetTilt) {
+ ValueAnimator tiltAnimator = ValueAnimator.ofFloat((float) currentTilt, (float) targetTilt);
+ tiltAnimator.setDuration((long) (1000 * ANIMATION_DELAY_FACTOR));
+ tiltAnimator.setStartDelay((long) (1500 * ANIMATION_DELAY_FACTOR));
+ tiltAnimator.addUpdateListener(animation -> mapboxMap.moveCamera(
+ CameraUpdateFactory.tiltTo((Float) animation.getAnimatedValue()))
+ );
+ return tiltAnimator;
+ }
+
+ //
+ // Interpolator examples
+ //
+
+ private Animator obtainExampleInterpolator(int menuItemId) {
+ return animators.get(menuItemId).build();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_animator, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mapboxMap == null) {
+ return false;
+ }
+
+ if (item.getItemId() != android.R.id.home) {
+ findViewById(R.id.fab).setVisibility(View.GONE);
+ resetCameraPosition();
+ playAnimation(item.getItemId());
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void resetCameraPosition() {
+ mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
+ new CameraPosition.Builder()
+ .target(START_LAT_LNG)
+ .zoom(11)
+ .bearing(0)
+ .tilt(0)
+ .build()
+ ));
+ }
+
+ private void playAnimation(int itemId) {
+ Animator animator = obtainExampleInterpolator(itemId);
+ if (animator != null) {
+ animator.start();
+ }
+ }
+
+ private Animator obtainExampleInterpolator(Interpolator interpolator, long duration) {
+ ValueAnimator zoomAnimator = ValueAnimator.ofFloat(11.0f, 16.0f);
+ zoomAnimator.setDuration((long) (duration * ANIMATION_DELAY_FACTOR));
+ zoomAnimator.setInterpolator(interpolator);
+ zoomAnimator.addUpdateListener(animation -> mapboxMap.moveCamera(
+ CameraUpdateFactory.zoomTo((Float) animation.getAnimatedValue()))
+ );
+ return zoomAnimator;
+ }
+
+ //
+ // MapView lifecycle
+ //
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ /**
+ * Helper class to evaluate LatLng objects with a ValueAnimator
+ */
+ private static class LatLngEvaluator implements TypeEvaluator<LatLng> {
+
+ private final LatLng latLng = new LatLng();
+
+ @Override
+ public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
+ latLng.setLatitude(startValue.getLatitude()
+ + ((endValue.getLatitude() - startValue.getLatitude()) * fraction));
+ latLng.setLongitude(startValue.getLongitude()
+ + ((endValue.getLongitude() - startValue.getLongitude()) * fraction));
+ return latLng;
+ }
+ }
+
+ interface AnimatorBuilder {
+ Animator build();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraPositionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraPositionActivity.java
new file mode 100644
index 0000000000..1c4b022416
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/CameraPositionActivity.java
@@ -0,0 +1,254 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.SeekBar;
+import android.widget.TextView;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing how to listen to camera change events.
+ */
+public class CameraPositionActivity extends AppCompatActivity implements OnMapReadyCallback, View.OnClickListener,
+ MapboxMap.OnMapLongClickListener {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private FloatingActionButton fab;
+ private boolean logCameraChanges;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_position);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(@NonNull final MapboxMap map) {
+ mapboxMap = map;
+ toggleLogCameraChanges();
+
+ // add a listener to FAB
+ fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, R.color.primary));
+ fab.setOnClickListener(this);
+
+ // listen to long click events to toggle logging camera changes
+ mapboxMap.setOnMapLongClickListener(this);
+ }
+
+ @Override
+ public void onMapLongClick(@NonNull LatLng point) {
+ toggleLogCameraChanges();
+ }
+
+ @Override
+ public void onClick(View view) {
+ Context context = view.getContext();
+ final View dialogContent = LayoutInflater.from(context).inflate(R.layout.dialog_camera_position, null);
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.dialog_camera_position);
+ builder.setView(onInflateDialogContent(dialogContent));
+ builder.setPositiveButton("Animate", new DialogClickListener(mapboxMap, dialogContent));
+ builder.setNegativeButton("Cancel", null);
+ builder.setCancelable(false);
+ builder.show();
+ }
+
+ private void toggleLogCameraChanges() {
+ logCameraChanges = !logCameraChanges;
+ if (logCameraChanges) {
+ mapboxMap.addOnCameraIdleListener(idleListener);
+ mapboxMap.addOnCameraMoveCancelListener(moveCanceledListener);
+ mapboxMap.addOnCameraMoveListener(moveListener);
+ mapboxMap.addOnCameraMoveStartedListener(moveStartedListener);
+ } else {
+ mapboxMap.removeOnCameraIdleListener(idleListener);
+ mapboxMap.removeOnCameraMoveCancelListener(moveCanceledListener);
+ mapboxMap.removeOnCameraMoveListener(moveListener);
+ mapboxMap.removeOnCameraMoveStartedListener(moveStartedListener);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ private View onInflateDialogContent(View view) {
+ linkTextView(view, R.id.value_lat, R.id.seekbar_lat, new LatLngChangeListener(), 180 + 38);
+ linkTextView(view, R.id.value_lon, R.id.seekbar_lon, new LatLngChangeListener(), 180 - 77);
+ linkTextView(view, R.id.value_zoom, R.id.seekbar_zoom, new ValueChangeListener(), 6);
+ linkTextView(view, R.id.value_bearing, R.id.seekbar_bearing, new ValueChangeListener(), 90);
+ linkTextView(view, R.id.value_tilt, R.id.seekbar_tilt, new ValueChangeListener(), 40);
+ return view;
+ }
+
+ private void linkTextView(
+ View view, @IdRes int textViewRes, @IdRes int seekBarRes, ValueChangeListener listener, int defaultValue) {
+ final TextView value = (TextView) view.findViewById(textViewRes);
+ SeekBar seekBar = (SeekBar) view.findViewById(seekBarRes);
+ listener.setLinkedValueView(value);
+ seekBar.setOnSeekBarChangeListener(listener);
+ seekBar.setProgress(defaultValue);
+ }
+
+ private MapboxMap.OnCameraIdleListener idleListener = new MapboxMap.OnCameraIdleListener() {
+ @Override
+ public void onCameraIdle() {
+ Timber.e("OnCameraIdle");
+ fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_green_dark));
+ }
+ };
+
+ private MapboxMap.OnCameraMoveListener moveListener = new MapboxMap.OnCameraMoveListener() {
+ @Override
+ public void onCameraMove() {
+ Timber.e("OnCameraMove");
+ fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_orange_dark));
+ }
+ };
+
+ private MapboxMap.OnCameraMoveCanceledListener moveCanceledListener = () -> Timber.e("OnCameraMoveCanceled");
+
+ private MapboxMap.OnCameraMoveStartedListener moveStartedListener = new MapboxMap.OnCameraMoveStartedListener() {
+
+ private final String[] REASONS = {"REASON_API_GESTURE", "REASON_DEVELOPER_ANIMATION", "REASON_API_ANIMATION"};
+
+ @Override
+ public void onCameraMoveStarted(int reason) {
+ // reason ranges from 1 <-> 3
+ fab.setColorFilter(ContextCompat.getColor(CameraPositionActivity.this, android.R.color.holo_red_dark));
+ Timber.e("OnCameraMoveStarted: %s", REASONS[reason - 1]);
+ }
+ };
+
+ private class ValueChangeListener implements SeekBar.OnSeekBarChangeListener {
+
+ protected TextView textView;
+
+ public void setLinkedValueView(TextView textView) {
+ this.textView = textView;
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ textView.setText(String.valueOf(progress));
+ }
+ }
+
+ private class LatLngChangeListener extends ValueChangeListener {
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ super.onProgressChanged(seekBar, progress - 180, fromUser);
+ }
+ }
+
+ private static class DialogClickListener implements DialogInterface.OnClickListener {
+
+ private MapboxMap mapboxMap;
+ private View dialogContent;
+
+ public DialogClickListener(MapboxMap mapboxMap, View view) {
+ this.mapboxMap = mapboxMap;
+ this.dialogContent = view;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ double latitude = Double.parseDouble(
+ ((TextView) dialogContent.findViewById(R.id.value_lat)).getText().toString());
+ double longitude = Double.parseDouble(
+ ((TextView) dialogContent.findViewById(R.id.value_lon)).getText().toString());
+ double zoom = Double.parseDouble(
+ ((TextView) dialogContent.findViewById(R.id.value_zoom)).getText().toString());
+ double bearing = Double.parseDouble(
+ ((TextView) dialogContent.findViewById(R.id.value_bearing)).getText().toString());
+ double tilt = Double.parseDouble(
+ ((TextView) dialogContent.findViewById(R.id.value_tilt)).getText().toString());
+
+ CameraPosition cameraPosition = new CameraPosition.Builder()
+ .target(new LatLng(latitude, longitude))
+ .zoom(zoom)
+ .bearing(bearing)
+ .tilt(tilt)
+ .build();
+
+ mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 5000,
+ new MapboxMap.CancelableCallback() {
+ @Override
+ public void onCancel() {
+ Timber.v("OnCancel called");
+ }
+
+ @Override
+ public void onFinish() {
+ Timber.v("OnFinish called");
+ }
+ });
+ Timber.v(cameraPosition.toString());
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/GestureDetectorActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/GestureDetectorActivity.java
new file mode 100644
index 0000000000..89270880d0
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/GestureDetectorActivity.java
@@ -0,0 +1,422 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.ColorInt;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.mapbox.android.gestures.AndroidGesturesManager;
+import com.mapbox.android.gestures.MoveGestureDetector;
+import com.mapbox.android.gestures.RotateGestureDetector;
+import com.mapbox.android.gestures.ShoveGestureDetector;
+import com.mapbox.android.gestures.StandardScaleGestureDetector;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.FontCache;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+/**
+ * Test activity showcasing APIs around gestures implementation.
+ */
+public class GestureDetectorActivity extends AppCompatActivity {
+
+ private static final int MAX_NUMBER_OF_ALERTS = 30;
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private RecyclerView recyclerView;
+ private GestureAlertsAdapter gestureAlertsAdapter;
+
+ private AndroidGesturesManager gesturesManager;
+
+ @Nullable
+ private Marker marker;
+ @Nullable
+ private LatLng focalPointLatLng;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_gesture_detector);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(new OnMapReadyCallback() {
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ GestureDetectorActivity.this.mapboxMap = mapboxMap;
+ initializeMap();
+ }
+ });
+
+ recyclerView = (RecyclerView) findViewById(R.id.alerts_recycler);
+ recyclerView.setLayoutManager(new LinearLayoutManager(this));
+
+ gestureAlertsAdapter = new GestureAlertsAdapter();
+ recyclerView.setAdapter(gestureAlertsAdapter);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ gestureAlertsAdapter.cancelUpdates();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ private void initializeMap() {
+ gesturesManager = mapboxMap.getGesturesManager();
+
+ RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) recyclerView.getLayoutParams();
+ layoutParams.height = (int) (mapView.getHeight() / 1.75);
+ layoutParams.width = (mapView.getWidth() / 3);
+ recyclerView.setLayoutParams(layoutParams);
+
+ attachListeners();
+ }
+
+ public void attachListeners() {
+ mapboxMap.addOnMoveListener(new MapboxMap.OnMoveListener() {
+ @Override
+ public void onMoveBegin(MoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_START, "MOVE START"));
+ }
+
+ @Override
+ public void onMove(MoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_PROGRESS, "MOVE PROGRESS"));
+ }
+
+ @Override
+ public void onMoveEnd(MoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_END, "MOVE END"));
+ }
+ });
+
+ mapboxMap.addOnRotateListener(new MapboxMap.OnRotateListener() {
+ @Override
+ public void onRotateBegin(RotateGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_START, "ROTATE START"));
+ }
+
+ @Override
+ public void onRotate(RotateGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_PROGRESS, "ROTATE PROGRESS"));
+ recalculateFocalPoint();
+ }
+
+ @Override
+ public void onRotateEnd(RotateGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_END, "ROTATE END"));
+ }
+ });
+
+ mapboxMap.addOnScaleListener(new MapboxMap.OnScaleListener() {
+ @Override
+ public void onScaleBegin(StandardScaleGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_START, "SCALE START"));
+ if (focalPointLatLng != null) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_OTHER, "INCREASING MOVE THRESHOLD"));
+ gesturesManager.getMoveGestureDetector().setMoveThreshold(
+ ResourceUtils.convertDpToPx(GestureDetectorActivity.this, 175));
+
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_OTHER, "MANUALLY INTERRUPTING MOVE"));
+ gesturesManager.getMoveGestureDetector().interrupt();
+ }
+ recalculateFocalPoint();
+ }
+
+ @Override
+ public void onScale(StandardScaleGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_PROGRESS, "SCALE PROGRESS"));
+ }
+
+ @Override
+ public void onScaleEnd(StandardScaleGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_END, "SCALE END"));
+
+ if (focalPointLatLng != null) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_OTHER, "REVERTING MOVE THRESHOLD"));
+ gesturesManager.getMoveGestureDetector().setMoveThreshold(0f);
+ }
+ }
+ });
+
+ mapboxMap.addOnShoveListener(new MapboxMap.OnShoveListener() {
+ @Override
+ public void onShoveBegin(ShoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_START, "SHOVE START"));
+ }
+
+ @Override
+ public void onShove(ShoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_PROGRESS, "SHOVE PROGRESS"));
+ }
+
+ @Override
+ public void onShoveEnd(ShoveGestureDetector detector) {
+ gestureAlertsAdapter.addAlert(new GestureAlert(GestureAlert.TYPE_END, "SHOVE END"));
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_gestures, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ resetModes();
+ switch (item.getItemId()) {
+ case R.id.menu_gesture_none:
+ return true;
+ case R.id.menu_gesture_focus_point:
+ focalPointLatLng = new LatLng(51.50325, -0.12968);
+ marker = mapboxMap.addMarker(new MarkerOptions().position(focalPointLatLng));
+ mapboxMap.easeCamera(CameraUpdateFactory.newLatLngZoom(focalPointLatLng, 16));
+ mapboxMap.getUiSettings().setFocalPoint(mapboxMap.getProjection().toScreenLocation(focalPointLatLng));
+ return true;
+ case R.id.menu_gesture_animation:
+ mapboxMap.getUiSettings().setAllVelocityAnimationsEnabled(false);
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ private void resetModes() {
+ focalPointLatLng = null;
+ mapboxMap.getUiSettings().setFocalPoint(null);
+ gesturesManager.getMoveGestureDetector().setMoveThreshold(0f);
+ mapboxMap.getUiSettings().setAllVelocityAnimationsEnabled(true);
+
+ if (marker != null) {
+ mapboxMap.removeMarker(marker);
+ marker = null;
+ }
+ }
+
+ private void recalculateFocalPoint() {
+ if (focalPointLatLng != null) {
+ mapboxMap.getUiSettings().setFocalPoint(
+ mapboxMap.getProjection().toScreenLocation(focalPointLatLng)
+ );
+ }
+ }
+
+ private static class GestureAlertsAdapter extends RecyclerView.Adapter<GestureAlertsAdapter.ViewHolder> {
+
+ private boolean isUpdating;
+ private final Handler updateHandler = new Handler();
+ private final List<GestureAlert> alerts = new ArrayList<>();
+
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ TextView alertMessageTv;
+
+ @ColorInt
+ public int textColor;
+
+ ViewHolder(View view) {
+ super(view);
+ Typeface typeface = FontCache.get("Roboto-Regular.ttf", view.getContext());
+ alertMessageTv = (TextView) view.findViewById(R.id.alert_message);
+ alertMessageTv.setTypeface(typeface);
+ }
+ }
+
+
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_gesture_alert, parent, false);
+ return new ViewHolder(view);
+ }
+
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ GestureAlert alert = alerts.get(position);
+ holder.alertMessageTv.setText(alert.getMessage());
+ holder.alertMessageTv.setTextColor(
+ ContextCompat.getColor(holder.alertMessageTv.getContext(), alert.getColor()));
+ }
+
+ @Override
+ public int getItemCount() {
+ return alerts.size();
+ }
+
+ void addAlert(GestureAlert alert) {
+ for (GestureAlert gestureAlert : alerts) {
+ if (gestureAlert.getAlertType() != GestureAlert.TYPE_PROGRESS) {
+ break;
+ }
+
+ if (alert.getAlertType() == GestureAlert.TYPE_PROGRESS && gestureAlert.equals(alert)) {
+ return;
+ }
+ }
+
+ if (getItemCount() >= MAX_NUMBER_OF_ALERTS) {
+ alerts.remove(getItemCount() - 1);
+ }
+
+ alerts.add(0, alert);
+ if (!isUpdating) {
+ isUpdating = true;
+ updateHandler.postDelayed(updateRunnable, 250);
+ }
+ }
+
+ private Runnable updateRunnable = new Runnable() {
+ @Override
+ public void run() {
+ notifyDataSetChanged();
+ isUpdating = false;
+ }
+ };
+
+ void cancelUpdates() {
+ updateHandler.removeCallbacksAndMessages(null);
+ }
+ }
+
+ private static class GestureAlert {
+ @Retention(SOURCE)
+ @IntDef( {TYPE_NONE, TYPE_START, TYPE_PROGRESS, TYPE_END, TYPE_OTHER})
+ @interface Type {
+ }
+
+ static final int TYPE_NONE = 0;
+ static final int TYPE_START = 1;
+ static final int TYPE_END = 2;
+ static final int TYPE_PROGRESS = 3;
+ static final int TYPE_OTHER = 4;
+
+ @Type
+ private int alertType;
+
+ private String message;
+
+ @ColorInt
+ private int color;
+
+ GestureAlert(@Type int alertType, String message) {
+ this.alertType = alertType;
+ this.message = message;
+
+ switch (alertType) {
+ case TYPE_NONE:
+ color = android.R.color.black;
+ break;
+ case TYPE_END:
+ color = android.R.color.holo_red_dark;
+ break;
+ case TYPE_OTHER:
+ color = android.R.color.holo_purple;
+ break;
+ case TYPE_PROGRESS:
+ color = android.R.color.holo_orange_dark;
+ break;
+ case TYPE_START:
+ color = android.R.color.holo_green_dark;
+ break;
+ }
+ }
+
+ int getAlertType() {
+ return alertType;
+ }
+
+ String getMessage() {
+ return message;
+ }
+
+ int getColor() {
+ return color;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ GestureAlert that = (GestureAlert) o;
+
+ if (alertType != that.alertType) {
+ return false;
+ }
+ return message != null ? message.equals(that.message) : that.message == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = alertType;
+ result = 31 * result + (message != null ? message.hashCode() : 0);
+ return result;
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/LatLngBoundsActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/LatLngBoundsActivity.java
new file mode 100644
index 0000000000..fd8f9afa01
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/LatLngBoundsActivity.java
@@ -0,0 +1,162 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.http.HttpRequestUtil;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.view.LockableBottomSheetBehavior;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Test activity showcasing using the LatLngBounds camera API.
+ */
+public class LatLngBoundsActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private static final List<LatLng> LOCATIONS = new ArrayList<LatLng>() {
+ {
+ add(new LatLng(37.806866, -122.422502));
+ add(new LatLng(37.812905, -122.477605));
+ add(new LatLng(37.826944, -122.423188));
+ add(new LatLng(37.752676, -122.447736));
+ add(new LatLng(37.769305, -122.479322));
+ add(new LatLng(37.749834, -122.417867));
+ add(new LatLng(37.756149, -122.405679));
+ add(new LatLng(37.751403, -122.387397));
+ add(new LatLng(37.793064, -122.391517));
+ add(new LatLng(37.769122, -122.427394));
+ }
+ };
+ private static final LatLngBounds BOUNDS = new LatLngBounds.Builder().includes(LOCATIONS).build();
+ private static final int ANIMATION_DURATION_LONG = 450;
+ private static final int ANIMATION_DURATION_SHORT = 250;
+ private static final int BOUNDS_PADDING_DIVIDER_SMALL = 3;
+ private static final int BOUNDS_PADDING_DIVIDER_LARGE = 9;
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private View bottomSheet;
+ private LockableBottomSheetBehavior bottomSheetBehavior;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ HttpRequestUtil.setLogEnabled(false);
+ setContentView(R.layout.activity_latlngbounds);
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ initMap();
+ });
+ }
+
+ private void initMap() {
+ addMarkers();
+ initFab();
+ initBottomSheet();
+ moveToBounds(bottomSheet.getMeasuredHeight(), BOUNDS_PADDING_DIVIDER_SMALL, 0);
+ }
+
+ private void addMarkers() {
+ for (LatLng location : LOCATIONS) {
+ mapboxMap.addMarker(new MarkerOptions().position(location));
+ }
+ }
+
+ private void initFab() {
+ findViewById(R.id.fab).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
+ v.animate().alpha(0.0f).setDuration(ANIMATION_DURATION_SHORT);
+ }
+
+ private void initBottomSheet() {
+ bottomSheet = findViewById(R.id.bottom_sheet);
+ bottomSheetBehavior = (LockableBottomSheetBehavior) BottomSheetBehavior.from(bottomSheet);
+ bottomSheetBehavior.setLocked(true);
+ bottomSheetBehavior.setBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() {
+ @Override
+ public void onStateChanged(@NonNull View bottomSheet, int newState) {
+ if (newState == BottomSheetBehavior.STATE_SETTLING) {
+ moveToBounds(0, BOUNDS_PADDING_DIVIDER_LARGE, ANIMATION_DURATION_LONG);
+ }
+ }
+
+ @Override
+ public void onSlide(@NonNull View bottomSheet, float slideOffset) {
+
+ }
+ });
+ }
+
+ private void moveToBounds(int verticalOffset, int boundsPaddingDivider, int duration) {
+ int paddingHorizontal = mapView.getMeasuredWidth() / boundsPaddingDivider;
+ int paddingVertical = (mapView.getMeasuredHeight() - verticalOffset) / boundsPaddingDivider;
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngBounds(
+ BOUNDS,
+ paddingHorizontal,
+ paddingVertical,
+ paddingHorizontal,
+ paddingVertical + verticalOffset),
+ duration
+ );
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ HttpRequestUtil.setLogEnabled(true);
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ManualZoomActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ManualZoomActivity.java
new file mode 100644
index 0000000000..7288850636
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ManualZoomActivity.java
@@ -0,0 +1,121 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.graphics.Point;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.UiSettings;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing the zoom Camera API.
+ * <p>
+ * This includes zoomIn, zoomOut, zoomTo, zoomBy (center and custom focal point).
+ * </p>
+ */
+public class ManualZoomActivity extends AppCompatActivity {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_manual_zoom);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.setStyleUrl(Style.SATELLITE);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ ManualZoomActivity.this.mapboxMap = mapboxMap;
+
+ UiSettings uiSettings = ManualZoomActivity.this.mapboxMap.getUiSettings();
+ uiSettings.setAllGesturesEnabled(false);
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_zoom, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.action_zoom_in:
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomIn());
+ return true;
+
+ case R.id.action_zoom_out:
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomOut());
+ return true;
+
+ case R.id.action_zoom_by:
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomBy(2));
+ return true;
+ case R.id.action_zoom_to:
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(2));
+ return true;
+
+ case R.id.action_zoom_to_point:
+ View view = getWindow().getDecorView();
+ mapboxMap.animateCamera(
+ CameraUpdateFactory.zoomBy(1, new Point(view.getMeasuredWidth() / 4, view.getMeasuredHeight() / 4)));
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/MaxMinZoomActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/MaxMinZoomActivity.java
new file mode 100644
index 0000000000..26daed7f15
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/MaxMinZoomActivity.java
@@ -0,0 +1,82 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing using maximum and minimum zoom levels to restrict camera movement.
+ */
+public class MaxMinZoomActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_maxmin_zoom);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(final MapboxMap map) {
+ mapboxMap = map;
+ mapboxMap.setMinZoomPreference(3);
+ mapboxMap.setMaxZoomPreference(5);
+ mapboxMap.setOnMapClickListener(point -> map.setStyle(Style.OUTDOORS, style -> Timber.d("Style Loaded %s", style)));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ScrollByActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ScrollByActivity.java
new file mode 100644
index 0000000000..5a85ff0f44
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/camera/ScrollByActivity.java
@@ -0,0 +1,157 @@
+package com.mapbox.mapboxsdk.maps.activity.camera;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.StringRes;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.support.v7.widget.Toolbar;
+import android.view.MenuItem;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.UiSettings;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the scrollBy Camera API by moving x,y pixels above Grenada, Spain.
+ */
+public class ScrollByActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ public static final int MULTIPLIER_PER_PIXEL = 50;
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private SeekBar seekBarX;
+ private SeekBar seekBarY;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_scroll_by);
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setDisplayShowHomeEnabled(true);
+ }
+
+ seekBarX = (SeekBar) findViewById(R.id.seekbar_move_x);
+ TextView textViewX = (TextView) findViewById(R.id.textview_x);
+ seekBarX.setOnSeekBarChangeListener(new PixelBarChangeListener(textViewX, R.string.scrollby_x_value));
+
+ seekBarY = (SeekBar) findViewById(R.id.seekbar_move_y);
+ TextView textViewY = (TextView) findViewById(R.id.textview_y);
+ seekBarY.setOnSeekBarChangeListener(new PixelBarChangeListener(textViewY, R.string.scrollby_y_value));
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.setTag(true);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ UiSettings uiSettings = mapboxMap.getUiSettings();
+ uiSettings.setLogoEnabled(false);
+ uiSettings.setAttributionEnabled(false);
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setColorFilter(ContextCompat.getColor(ScrollByActivity.this, R.color.primary));
+ fab.setOnClickListener(view -> mapboxMap.easeCamera(CameraUpdateFactory.scrollBy(
+ seekBarX.getProgress() * MULTIPLIER_PER_PIXEL,
+ seekBarY.getProgress() * MULTIPLIER_PER_PIXEL)
+ ));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private static class PixelBarChangeListener implements SeekBar.OnSeekBarChangeListener {
+
+ @StringRes
+ private int prefixTextResource;
+ private TextView valueView;
+
+ public PixelBarChangeListener(@NonNull TextView textView, @StringRes int textRes) {
+ valueView = textView;
+ prefixTextResource = textRes;
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ int value = progress * ScrollByActivity.MULTIPLIER_PER_PIXEL;
+ valueView.setText(String.format(seekBar.getResources().getString(prefixTextResource), value));
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/customlayer/CustomLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/customlayer/CustomLayerActivity.java
new file mode 100644
index 0000000000..d10d4b28b4
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/customlayer/CustomLayerActivity.java
@@ -0,0 +1,140 @@
+package com.mapbox.mapboxsdk.maps.activity.customlayer;
+
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.CustomLayer;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.model.customlayer.ExampleCustomLayer;
+
+/**
+ * Test activity showcasing the Custom Layer API
+ * <p>
+ * Note: experimental API, do not use.
+ * </p>
+ */
+public class CustomLayerActivity extends AppCompatActivity {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private CustomLayer customLayer;
+
+ private FloatingActionButton fab;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_custom_layer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(39.91448, -243.60947), 10));
+
+ });
+
+ fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setColorFilter(ContextCompat.getColor(this, R.color.primary));
+ fab.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ swapCustomLayer();
+ }
+ });
+ }
+
+ private void swapCustomLayer() {
+ if (customLayer != null) {
+ mapboxMap.removeLayer(customLayer);
+ customLayer = null;
+ fab.setImageResource(R.drawable.ic_layers);
+ } else {
+ customLayer = new CustomLayer("custom",
+ ExampleCustomLayer.createContext());
+ mapboxMap.addLayerBelow(customLayer, "building");
+ fab.setImageResource(R.drawable.ic_layers_clear);
+ }
+ }
+
+ private void updateLayer() {
+ if (customLayer != null) {
+ customLayer.update();
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_custom_layer, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_update_layer:
+ updateLayer();
+ return true;
+ case R.id.action_set_color_red:
+ ExampleCustomLayer.setColor(1, 0, 0, 1);
+ return true;
+ case R.id.action_set_color_green:
+ ExampleCustomLayer.setColor(0, 1, 0, 1);
+ return true;
+ case R.id.action_set_color_blue:
+ ExampleCustomLayer.setColor(0, 0, 1, 1);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/DeviceIndependentTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/DeviceIndependentTestActivity.java
new file mode 100644
index 0000000000..83a9c8c891
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/DeviceIndependentTestActivity.java
@@ -0,0 +1,76 @@
+package com.mapbox.mapboxsdk.maps.activity.espresso;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+public class DeviceIndependentTestActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ public MapView mapView;
+ protected MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_camera_test);
+
+ // Initialize map as normal
+ mapView = findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/EspressoTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/EspressoTestActivity.java
new file mode 100644
index 0000000000..92d4327802
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/espresso/EspressoTestActivity.java
@@ -0,0 +1,80 @@
+package com.mapbox.mapboxsdk.maps.activity.espresso;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Base activity for instrumentation testing.
+ */
+public class EspressoTestActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ public MapView mapView;
+ protected MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_espresso_test);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxCountActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxCountActivity.java
new file mode 100644
index 0000000000..75c5acec74
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxCountActivity.java
@@ -0,0 +1,125 @@
+package com.mapbox.mapboxsdk.maps.activity.feature;
+
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Toast;
+
+import com.google.gson.JsonElement;
+import com.mapbox.geojson.Feature;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+import java.util.Map;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing using the query rendered features API to count features in a rectangle.
+ */
+public class QueryRenderedFeaturesBoxCountActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_query_features_box);
+
+ final View selectionBox = findViewById(R.id.selection_box);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ QueryRenderedFeaturesBoxCountActivity.this.mapboxMap = mapboxMap;
+ selectionBox.setOnClickListener(view -> {
+ // Query
+ int top = selectionBox.getTop() - mapView.getTop();
+ int left = selectionBox.getLeft() - mapView.getLeft();
+ RectF box = new RectF(left, top, left + selectionBox.getWidth(), top + selectionBox.getHeight());
+ Timber.i("Querying box %s", box);
+ List<Feature> features = mapboxMap.queryRenderedFeatures(box);
+
+ // Show count
+ Toast.makeText(
+ QueryRenderedFeaturesBoxCountActivity.this,
+ String.format("%s features in box", features.size()),
+ Toast.LENGTH_SHORT).show();
+
+ // Debug output
+ debugOutput(features);
+ });
+ });
+ }
+
+ private void debugOutput(List<Feature> features) {
+ Timber.i("Got %s features", features.size());
+ for (Feature feature : features) {
+ if (feature != null) {
+ Timber.i("Got feature %s with %s properties and Geometry %s",
+ feature.id(),
+ feature.properties() != null ? feature.properties().entrySet().size() : "<null>",
+ feature.geometry() != null ? feature.geometry().getClass().getSimpleName() : "<null>"
+ );
+ if (feature.properties() != null) {
+ for (Map.Entry<String, JsonElement> entry : feature.properties().entrySet()) {
+ Timber.i("Prop %s - %s", entry.getKey(), entry.getValue());
+ }
+ }
+ } else {
+ Timber.i("Got 0 features");
+ }
+ }
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxHighlightActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxHighlightActivity.java
new file mode 100644
index 0000000000..2fb119bd02
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxHighlightActivity.java
@@ -0,0 +1,125 @@
+package com.mapbox.mapboxsdk.maps.activity.feature;
+
+import android.graphics.Color;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Toast;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.expressions.Expression;
+import com.mapbox.mapboxsdk.style.layers.FillLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.lt;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.toNumber;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor;
+
+/**
+ * Demo's query rendered features
+ */
+public class QueryRenderedFeaturesBoxHighlightActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_query_features_box);
+
+ final View selectionBox = findViewById(R.id.selection_box);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ QueryRenderedFeaturesBoxHighlightActivity.this.mapboxMap = mapboxMap;
+
+ // Add layer / source
+ final GeoJsonSource source = new GeoJsonSource("highlighted-shapes-source");
+ mapboxMap.addSource(source);
+ mapboxMap.addLayer(
+ new FillLayer("highlighted-shapes-layer", "highlighted-shapes-source")
+ .withProperties(fillColor(Color.RED))
+ );
+
+ selectionBox.setOnClickListener(view -> {
+ // Query
+ int top = selectionBox.getTop() - mapView.getTop();
+ int left = selectionBox.getLeft() - mapView.getLeft();
+ RectF box = new RectF(left, top, left + selectionBox.getWidth(), top + selectionBox.getHeight());
+ Timber.i("Querying box %s for buildings", box);
+
+ Expression filter = lt(toNumber(get("height")), literal(10));
+ List<Feature> features = mapboxMap.queryRenderedFeatures(box, filter, "building");
+
+ // Show count
+ Toast.makeText(
+ QueryRenderedFeaturesBoxHighlightActivity.this,
+ String.format("%s features in box", features.size()),
+ Toast.LENGTH_SHORT).show();
+
+ // Update source data
+ source.setGeoJson(FeatureCollection.fromFeatures(features));
+ });
+ });
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxSymbolCountActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxSymbolCountActivity.java
new file mode 100644
index 0000000000..efef5cc39e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesBoxSymbolCountActivity.java
@@ -0,0 +1,129 @@
+package com.mapbox.mapboxsdk.maps.activity.feature;
+
+import android.graphics.BitmapFactory;
+import android.graphics.RectF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.Toast;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.IOException;
+import java.util.List;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+
+/**
+ * Test activity showcasing using the query rendered features API to count Symbols in a rectangle.
+ */
+public class QueryRenderedFeaturesBoxSymbolCountActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private Toast toast;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_query_features_box);
+
+ final View selectionBox = findViewById(R.id.selection_box);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ QueryRenderedFeaturesBoxSymbolCountActivity.this.mapboxMap = mapboxMap;
+
+ // Add a symbol layer (also works with annotations)
+ try {
+ mapboxMap.addSource(new GeoJsonSource("symbols-source", ResourceUtils.readRawResource(
+ QueryRenderedFeaturesBoxSymbolCountActivity.this, R.raw.test_points_utrecht)));
+ } catch (IOException ioException) {
+ Timber.e(ioException, "Could not load geojson");
+ return;
+ }
+ mapboxMap.addImage(
+ "test-icon",
+ BitmapFactory.decodeResource(getResources(),
+ R.drawable.mapbox_marker_icon_default)
+ );
+ mapboxMap.addLayer(new SymbolLayer("symbols-layer", "symbols-source").withProperties(iconImage("test-icon")));
+
+ selectionBox.setOnClickListener(view -> {
+ // Query
+ int top = selectionBox.getTop() - mapView.getTop();
+ int left = selectionBox.getLeft() - mapView.getLeft();
+ RectF box = new RectF(left, top, left + selectionBox.getWidth(), top + selectionBox.getHeight());
+ Timber.i("Querying box %s", box);
+ List<Feature> features = mapboxMap.queryRenderedFeatures(box, "symbols-layer");
+
+ // Show count
+ if (toast != null) {
+ toast.cancel();
+ }
+ toast = Toast.makeText(
+ QueryRenderedFeaturesBoxSymbolCountActivity.this,
+ String.format("%s features in box", features.size()),
+ Toast.LENGTH_SHORT);
+ toast.show();
+ });
+ });
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesPropertiesActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesPropertiesActivity.java
new file mode 100644
index 0000000000..1d5e00c55a
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QueryRenderedFeaturesPropertiesActivity.java
@@ -0,0 +1,234 @@
+package com.mapbox.mapboxsdk.maps.activity.feature;
+
+import android.graphics.Color;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.google.gson.JsonElement;
+import com.mapbox.geojson.Feature;
+import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+
+import java.util.List;
+import java.util.Map;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing using the query rendered features API to query feature properties on Map click.
+ */
+public class QueryRenderedFeaturesPropertiesActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+ private Marker marker;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_query_features_point);
+
+ final float density = getResources().getDisplayMetrics().density;
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ QueryRenderedFeaturesPropertiesActivity.this.mapboxMap = mapboxMap;
+
+ // Add custom window adapter
+ addCustomInfoWindowAdapter(mapboxMap);
+
+ // Add a click listener
+ mapboxMap.setOnMapClickListener(point -> {
+ // Query
+ final PointF pixel = mapboxMap.getProjection().toScreenLocation(point);
+ Timber.i(
+ "Requesting features for %sx%s (%sx%s adjusted for density)",
+ pixel.x, pixel.y, pixel.x / density, pixel.y / density
+ );
+ List<Feature> features = mapboxMap.queryRenderedFeatures(pixel);
+
+ // Debug output
+ debugOutput(features);
+
+ // Remove any previous markers
+ if (marker != null) {
+ mapboxMap.removeMarker(marker);
+ }
+
+ // Add a marker on the clicked point
+ marker = mapboxMap.addMarker(new CustomMarkerOptions().position(point).features(features));
+ mapboxMap.selectMarker(marker);
+ });
+ });
+
+ }
+
+ private void debugOutput(List<Feature> features) {
+ Timber.i("Got %s features", features.size());
+ for (Feature feature : features) {
+ if (feature != null) {
+ Timber.i("Got feature %s with %s properties and Geometry %s",
+ feature.id(),
+ feature.properties() != null ? feature.properties().entrySet().size() : "<null>",
+ feature.geometry() != null ? feature.geometry().getClass().getSimpleName() : "<null>"
+ );
+ if (feature.properties() != null) {
+ for (Map.Entry<String, JsonElement> entry : feature.properties().entrySet()) {
+ Timber.i("Prop %s - %s", entry.getKey(), entry.getValue());
+ }
+ }
+ } else {
+ Timber.i("Got NULL feature");
+ }
+ }
+ }
+
+ private void addCustomInfoWindowAdapter(MapboxMap mapboxMap) {
+ mapboxMap.setInfoWindowAdapter(new MapboxMap.InfoWindowAdapter() {
+
+ private TextView row(String text) {
+ TextView view = new TextView(QueryRenderedFeaturesPropertiesActivity.this);
+ view.setText(text);
+ return view;
+ }
+
+ @Override
+ public View getInfoWindow(@NonNull Marker marker) {
+ CustomMarker customMarker = (CustomMarker) marker;
+ LinearLayout view = new LinearLayout(QueryRenderedFeaturesPropertiesActivity.this);
+ view.setOrientation(LinearLayout.VERTICAL);
+ view.setBackgroundColor(Color.WHITE);
+
+ if (customMarker.features.size() > 0) {
+ view.addView(row(String.format("Found %s features", customMarker.features.size())));
+ Feature feature = customMarker.features.get(0);
+ for (Map.Entry<String, JsonElement> prop : feature.properties().entrySet()) {
+ view.addView(row(String.format("%s: %s", prop.getKey(), prop.getValue())));
+ }
+ } else {
+ view.addView(row("No features here"));
+ }
+
+ return view;
+ }
+ });
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ private static class CustomMarker extends Marker {
+
+ private final List<Feature> features;
+
+ CustomMarker(BaseMarkerOptions baseMarkerOptions, List<Feature> features) {
+ super(baseMarkerOptions);
+ this.features = features;
+ }
+ }
+
+ private static class CustomMarkerOptions extends BaseMarkerOptions<CustomMarker, CustomMarkerOptions> {
+
+
+ private List<Feature> features;
+
+ public CustomMarkerOptions features(List<Feature> features) {
+ this.features = features;
+ return this;
+ }
+
+ CustomMarkerOptions() {
+ }
+
+ private CustomMarkerOptions(Parcel in) {
+ // Should implement this
+ }
+
+ @Override
+ public CustomMarkerOptions getThis() {
+ return this;
+ }
+
+ @Override
+ public CustomMarker getMarker() {
+ return new CustomMarker(this, features);
+ }
+
+ public static final Parcelable.Creator<CustomMarkerOptions> CREATOR =
+ new Parcelable.Creator<CustomMarkerOptions>() {
+ public CustomMarkerOptions createFromParcel(Parcel in) {
+ return new CustomMarkerOptions(in);
+ }
+
+ public CustomMarkerOptions[] newArray(int size) {
+ return new CustomMarkerOptions[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ // Should implement this
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QuerySourceFeaturesActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QuerySourceFeaturesActivity.java
new file mode 100644
index 0000000000..f1f63b6b59
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/feature/QuerySourceFeaturesActivity.java
@@ -0,0 +1,109 @@
+package com.mapbox.mapboxsdk.maps.activity.feature;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.Toast;
+
+import com.google.gson.JsonObject;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.neq;
+
+/**
+ * Test activity showcasing using the query source features API to query feature counts
+ */
+public class QuerySourceFeaturesActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_query_source_features);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ QuerySourceFeaturesActivity.this.mapboxMap = mapboxMap;
+
+ JsonObject properties = new JsonObject();
+ properties.addProperty("key1", "value1");
+ final GeoJsonSource source = new GeoJsonSource("test-source",
+ FeatureCollection.fromFeatures(new Feature[] {
+ Feature.fromGeometry(Point.fromLngLat(0, 0), properties)
+ }));
+ mapboxMap.addSource(source);
+
+ mapboxMap.addLayer(new CircleLayer("test-layer", source.getId()).withFilter(neq(get("key1"), literal("value1"))));
+
+ // Add a click listener
+ mapboxMap.setOnMapClickListener(point -> {
+ // Query
+ List<Feature> features = source.querySourceFeatures(eq(get("key1"), literal("value1")));
+ Toast.makeText(QuerySourceFeaturesActivity.this, String.format("Found %s features",
+ features.size()), Toast.LENGTH_SHORT).show();
+ });
+ });
+
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MapFragmentActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MapFragmentActivity.java
new file mode 100644
index 0000000000..25a5c649fb
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MapFragmentActivity.java
@@ -0,0 +1,89 @@
+package com.mapbox.mapboxsdk.maps.activity.fragment;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapFragment;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the MapFragment API using SDK Fragments.
+ * <p>
+ * Uses MapboxMapOptions to initialise the Fragment.
+ * </p>
+ */
+public class MapFragmentActivity extends AppCompatActivity implements MapFragment.OnMapViewReadyCallback,
+ OnMapReadyCallback, MapView.OnMapChangedListener {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private boolean initialCameraAnimation = true;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_fragment);
+ if (savedInstanceState == null) {
+ MapFragment mapFragment = MapFragment.newInstance(createFragmentOptions());
+ getFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container,mapFragment, "com.mapbox.map")
+ .commit();
+ mapFragment.getMapAsync(this);
+ }
+ }
+
+ private MapboxMapOptions createFragmentOptions() {
+ MapboxMapOptions options = new MapboxMapOptions();
+ options.styleUrl(Style.OUTDOORS);
+
+ options.scrollGesturesEnabled(false);
+ options.zoomGesturesEnabled(false);
+ options.tiltGesturesEnabled(false);
+ options.rotateGesturesEnabled(false);
+ options.debugActive(false);
+
+ LatLng dc = new LatLng(38.90252, -77.02291);
+
+ options.minZoomPreference(9);
+ options.maxZoomPreference(11);
+ options.camera(new CameraPosition.Builder()
+ .target(dc)
+ .zoom(11)
+ .build());
+ return options;
+ }
+
+ @Override
+ public void onMapViewReady(MapView map) {
+ mapView = map;
+ mapView.addOnMapChangedListener(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ }
+
+ @Override
+ public void onMapChanged(int change) {
+ if (initialCameraAnimation && change == MapView.DID_FINISH_RENDERING_MAP_FULLY_RENDERED) {
+ mapboxMap.animateCamera(
+ CameraUpdateFactory.newCameraPosition(new CameraPosition.Builder().tilt(45.0).build()), 5000);
+ initialCameraAnimation = false;
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.removeOnMapChangedListener(this);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MultiMapActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MultiMapActivity.java
new file mode 100644
index 0000000000..3de1adfd7d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/MultiMapActivity.java
@@ -0,0 +1,18 @@
+package com.mapbox.mapboxsdk.maps.activity.fragment;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test Activity showcasing using multiple static map fragments in one layout.
+ */
+public class MultiMapActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_multi_map);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/SupportMapFragmentActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/SupportMapFragmentActivity.java
new file mode 100644
index 0000000000..6d294eac61
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/SupportMapFragmentActivity.java
@@ -0,0 +1,90 @@
+package com.mapbox.mapboxsdk.maps.activity.fragment;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapFragment;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.maps.SupportMapFragment;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the MapFragment API using Support Library Fragments.
+ * <p>
+ * Uses MapboxMapOptions to initialise the Fragment.
+ * </p>
+ */
+public class SupportMapFragmentActivity extends AppCompatActivity implements MapFragment.OnMapViewReadyCallback,
+ OnMapReadyCallback, MapView.OnMapChangedListener {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private boolean initialCameraAnimation = true;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_fragment);
+ if (savedInstanceState == null) {
+ SupportMapFragment mapFragment = SupportMapFragment.newInstance(createFragmentOptions());
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(R.id.fragment_container, mapFragment, "com.mapbox.map")
+ .commit();
+ mapFragment.getMapAsync(this);
+ }
+ }
+
+ private MapboxMapOptions createFragmentOptions() {
+ MapboxMapOptions options = new MapboxMapOptions();
+ options.styleUrl(Style.MAPBOX_STREETS);
+
+ options.scrollGesturesEnabled(false);
+ options.zoomGesturesEnabled(false);
+ options.tiltGesturesEnabled(false);
+ options.rotateGesturesEnabled(false);
+ options.debugActive(false);
+
+ LatLng dc = new LatLng(38.90252, -77.02291);
+
+ options.minZoomPreference(9);
+ options.maxZoomPreference(11);
+ options.camera(new CameraPosition.Builder()
+ .target(dc)
+ .zoom(11)
+ .build());
+ return options;
+ }
+
+ @Override
+ public void onMapViewReady(MapView map) {
+ mapView = map;
+ mapView.addOnMapChangedListener(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ }
+
+ @Override
+ public void onMapChanged(int change) {
+ if (initialCameraAnimation && change == MapView.DID_FINISH_RENDERING_MAP_FULLY_RENDERED) {
+ mapboxMap.animateCamera(
+ CameraUpdateFactory.newCameraPosition(new CameraPosition.Builder().tilt(45.0).build()), 5000);
+ initialCameraAnimation = false;
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.removeOnMapChangedListener(this);
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/ViewPagerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/ViewPagerActivity.java
new file mode 100644
index 0000000000..d1e4af97d4
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/fragment/ViewPagerActivity.java
@@ -0,0 +1,79 @@
+package com.mapbox.mapboxsdk.maps.activity.fragment;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
+import com.mapbox.mapboxsdk.maps.SupportMapFragment;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the Android SDK ViewPager API to show MapFragments.
+ */
+public class ViewPagerActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_viewpager);
+
+ ViewPager viewPager = (ViewPager) findViewById(R.id.viewpager);
+ if (viewPager != null) {
+ MapFragmentAdapter adapter = new MapFragmentAdapter(getSupportFragmentManager());
+ viewPager.setAdapter(adapter);
+ }
+ }
+
+ static class MapFragmentAdapter extends FragmentPagerAdapter {
+
+ private static int NUM_ITEMS = 3;
+
+ MapFragmentAdapter(FragmentManager fragmentManager) {
+ super(fragmentManager);
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_ITEMS;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ SupportMapFragment fragment = null;
+ MapboxMapOptions options = new MapboxMapOptions();
+ options.textureMode(true);
+
+ switch (position) {
+ case 0:
+ options.styleUrl(Style.MAPBOX_STREETS);
+ options.camera(new CameraPosition.Builder().target(new LatLng(34.920526, 102.634774)).zoom(3).build());
+ fragment = SupportMapFragment.newInstance(options);
+ break;
+ case 1:
+ options.styleUrl(Style.DARK);
+ options.camera(new CameraPosition.Builder().target(new LatLng(62.326440, 92.764913)).zoom(3).build());
+ fragment = SupportMapFragment.newInstance(options);
+ break;
+ case 2:
+ options.styleUrl(Style.SATELLITE);
+ options.camera(new CameraPosition.Builder().target(new LatLng(-25.007786, 133.623852)).zoom(3).build());
+ fragment = SupportMapFragment.newInstance(options);
+ break;
+ }
+ return fragment;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return "Page " + position;
+ }
+ }
+}
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/PrintActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/PrintActivity.java
new file mode 100644
index 0000000000..4c4e4540e7
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/PrintActivity.java
@@ -0,0 +1,88 @@
+package com.mapbox.mapboxsdk.maps.activity.imagegenerator;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.support.v4.print.PrintHelper;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the Snaphot API to print a Map.
+ */
+public class PrintActivity extends AppCompatActivity implements MapboxMap.SnapshotReadyCallback {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_print);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> PrintActivity.this.mapboxMap = mapboxMap);
+
+ final View fab = findViewById(R.id.fab);
+ if (fab != null) {
+ fab.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ mapboxMap.snapshot(PrintActivity.this);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onSnapshotReady(Bitmap snapshot) {
+ PrintHelper photoPrinter = new PrintHelper(this);
+ photoPrinter.setScaleMode(PrintHelper.SCALE_MODE_FIT);
+ photoPrinter.printBitmap("map.jpg - mapbox print job", snapshot);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/SnapshotActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/SnapshotActivity.java
new file mode 100644
index 0000000000..dedc47c691
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/imagegenerator/SnapshotActivity.java
@@ -0,0 +1,113 @@
+package com.mapbox.mapboxsdk.maps.activity.imagegenerator;
+
+import android.graphics.Bitmap;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.Locale;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing the Snapshot API to create and display a bitmap of the current shown Map.
+ */
+public class SnapshotActivity extends AppCompatActivity implements OnMapReadyCallback, View.OnClickListener {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_snapshot);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ mapboxMap.setStyleUrl(Style.OUTDOORS);
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ if (fab != null) {
+ fab.setColorFilter(ContextCompat.getColor(SnapshotActivity.this, R.color.primary));
+ fab.setOnClickListener(this);
+ }
+ }
+
+ @Override
+ public void onClick(View view) {
+ final long startTime = System.nanoTime();
+ mapboxMap.snapshot(snapshot -> {
+ long endTime = System.nanoTime();
+ long duration = (long) ((endTime - startTime) / 1e6);
+ ImageView snapshotView = (ImageView) findViewById(R.id.imageView);
+ snapshotView.setImageBitmap(snapshot);
+ Toast.makeText(
+ SnapshotActivity.this,
+ String.format(Locale.getDefault(), "Snapshot taken in %d ms", duration),
+ Toast.LENGTH_LONG).show();
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapboxMap.snapshot(new MapboxMap.SnapshotReadyCallback() {
+ @Override
+ public void onSnapshotReady(Bitmap snapshot) {
+ Timber.e("Regression test for https://github.com/mapbox/mapbox-gl-native/pull/11358");
+ }
+ });
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/DynamicInfoWindowAdapterActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/DynamicInfoWindowAdapterActivity.java
new file mode 100644
index 0000000000..a19d626028
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/DynamicInfoWindowAdapterActivity.java
@@ -0,0 +1,142 @@
+package com.mapbox.mapboxsdk.maps.activity.infowindow;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.annotations.InfoWindow;
+import com.mapbox.mapboxsdk.annotations.MarkerView;
+import com.mapbox.mapboxsdk.annotations.MarkerViewOptions;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.IconUtils;
+
+import java.util.Locale;
+
+/**
+ * Test activity showcasing how to dynamically update InfoWindow when Using an MapboxMap.InfoWindowAdapter.
+ */
+public class DynamicInfoWindowAdapterActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final LatLng PARIS = new LatLng(48.864716, 2.349014);
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_infowindow_adapter);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+
+ // Add info window adapter
+ addCustomInfoWindowAdapter(mapboxMap);
+
+ // Keep info windows open on click
+ mapboxMap.getUiSettings().setDeselectMarkersOnTap(false);
+
+ // Add a marker
+ final MarkerView marker = addMarker(mapboxMap);
+ mapboxMap.selectMarker(marker);
+
+ // On map click, change the info window contents
+ mapboxMap.setOnMapClickListener(point -> {
+ // Distance from click to marker
+ double distanceKm = marker.getPosition().distanceTo(point) / 1000;
+
+ // Get the info window
+ final InfoWindow infoWindow = marker.getInfoWindow();
+
+ // Get the view from the info window
+ if (infoWindow != null && infoWindow.getView() != null) {
+ // Set the new text on the text view in the info window
+ TextView textView = (TextView) infoWindow.getView();
+ textView.setText(String.format(Locale.getDefault(), "%.2fkm", distanceKm));
+ textView.post(() -> {
+ // Update the info window position (as the text length changes)
+ infoWindow.update();
+ });
+ }
+ });
+
+ // Focus on Paris
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLng(PARIS));
+ }
+
+ private MarkerView addMarker(MapboxMap mapboxMap) {
+ return mapboxMap.addMarker(
+ new MarkerViewOptions()
+ .position(PARIS)
+ .icon(IconUtils.drawableToIcon(this, R.drawable.ic_location_city,
+ ResourcesCompat.getColor(getResources(), R.color.mapbox_blue, getTheme()))
+ ));
+ }
+
+ private void addCustomInfoWindowAdapter(final MapboxMap mapboxMap) {
+ final int padding = (int) getResources().getDimension(R.dimen.attr_margin);
+ mapboxMap.setInfoWindowAdapter(marker -> {
+ TextView textView = new TextView(DynamicInfoWindowAdapterActivity.this);
+ textView.setText(marker.getTitle());
+ textView.setBackgroundColor(Color.WHITE);
+ textView.setText(R.string.action_calculate_distance);
+ textView.setPadding(padding, padding, padding, padding);
+ return textView;
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowActivity.java
new file mode 100644
index 0000000000..1a391598cc
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowActivity.java
@@ -0,0 +1,190 @@
+package com.mapbox.mapboxsdk.maps.activity.infowindow;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.text.DecimalFormat;
+
+/**
+ * Test activity showcasing using the InfoWindow API above Washington D.C.
+ * <p>
+ * Allows to test mulitple concurrently open InfoWindows.
+ * </p>
+ */
+public class InfoWindowActivity extends AppCompatActivity
+ implements OnMapReadyCallback, MapboxMap.OnInfoWindowCloseListener, MapboxMap.OnMapLongClickListener,
+ MapboxMap.OnInfoWindowClickListener, MapboxMap.OnInfoWindowLongClickListener {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private Marker customMarker;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_infowindow);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(@NonNull MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+ addMarkers();
+ addInfoWindowListeners();
+ }
+
+ private void addMarkers() {
+ mapboxMap.addMarker(new MarkerOptions()
+ .title("Intersection")
+ .snippet("H St NW with 15th St NW")
+ .position(new LatLng(38.9002073, -77.03364419)));
+
+ mapboxMap.addMarker(new MarkerOptions().title("Intersection")
+ .snippet("E St NW with 17th St NW")
+ .position(new LatLng(38.8954236, -77.0394623)));
+
+ mapboxMap.addMarker(new MarkerOptions().title("The Ellipse").position(new LatLng(38.89393, -77.03654)));
+
+ mapboxMap.addMarker(new MarkerOptions().position(new LatLng(38.89596, -77.03434)));
+
+ mapboxMap.addMarker(new MarkerOptions().snippet("Lafayette Square").position(new LatLng(38.89949, -77.03656)));
+
+ Marker marker = mapboxMap.addMarker(new MarkerOptions()
+ .title("White House")
+ .snippet("The official residence and principal workplace of the President of the United States, "
+ + "located at 1600 Pennsylvania Avenue NW in Washington, D.C. It has been the residence of every"
+ + "U.S. president since John Adams in 1800.")
+ .position(new LatLng(38.897705003219784, -77.03655168667463)));
+
+ // open InfoWindow at startup
+ mapboxMap.selectMarker(marker);
+ }
+
+ private void addInfoWindowListeners() {
+ mapboxMap.setOnInfoWindowCloseListener(this);
+ mapboxMap.setOnMapLongClickListener(this);
+ mapboxMap.setOnInfoWindowClickListener(this);
+ mapboxMap.setOnInfoWindowLongClickListener(this);
+ }
+
+ private void toggleConcurrentInfoWindow(boolean allowConcurrentInfoWindow) {
+ mapboxMap.deselectMarkers();
+ mapboxMap.setAllowConcurrentMultipleOpenInfoWindows(allowConcurrentInfoWindow);
+ }
+
+ private void toggleDeselectMarkersOnTap(boolean deselectMarkersOnTap) {
+ mapboxMap.getUiSettings().setDeselectMarkersOnTap(deselectMarkersOnTap);
+ }
+
+ @Override
+ public boolean onInfoWindowClick(@NonNull Marker marker) {
+ Toast.makeText(getApplicationContext(), "OnClick: " + marker.getTitle(), Toast.LENGTH_LONG).show();
+ // returning true will leave the info window open
+ return false;
+ }
+
+ @Override
+ public void onInfoWindowClose(Marker marker) {
+ Toast.makeText(getApplicationContext(), "OnClose: " + marker.getTitle(), Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onInfoWindowLongClick(Marker marker) {
+ Toast.makeText(getApplicationContext(), "OnLongClick: " + marker.getTitle(), Toast.LENGTH_LONG).show();
+ }
+
+ @Override
+ public void onMapLongClick(@NonNull LatLng point) {
+ if (customMarker != null) {
+ // Remove previous added marker
+ mapboxMap.removeAnnotation(customMarker);
+ customMarker = null;
+ }
+
+ // Add marker on long click location with default marker image
+ customMarker = mapboxMap.addMarker(new MarkerOptions()
+ .title("Custom Marker")
+ .snippet(new DecimalFormat("#.#####").format(point.getLatitude()) + ", "
+ + new DecimalFormat("#.#####").format(point.getLongitude()))
+ .position(point));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_infowindow, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_toggle_concurrent_infowindow:
+ toggleConcurrentInfoWindow(!item.isChecked());
+ item.setChecked(!item.isChecked());
+ return true;
+ case R.id.action_toggle_deselect_markers_on_tap:
+ toggleDeselectMarkersOnTap(!item.isChecked());
+ item.setChecked(!item.isChecked());
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowAdapterActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowAdapterActivity.java
new file mode 100644
index 0000000000..f5e866ada9
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/infowindow/InfoWindowAdapterActivity.java
@@ -0,0 +1,125 @@
+package com.mapbox.mapboxsdk.maps.activity.infowindow;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.Marker;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.model.annotations.CityStateMarker;
+import com.mapbox.mapboxsdk.testapp.model.annotations.CityStateMarkerOptions;
+import com.mapbox.mapboxsdk.maps.utils.IconUtils;
+
+/**
+ * Test activity showcasing using an InfoWindowAdapter to provide a custom InfoWindow content.
+ */
+public class InfoWindowAdapterActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_infowindow_adapter);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ addMarkers();
+ addCustomInfoWindowAdapter();
+ });
+ }
+
+ private void addMarkers() {
+ mapboxMap.addMarker(generateCityStateMarker("Andorra", 42.505777, 1.52529, "#F44336"));
+ mapboxMap.addMarker(generateCityStateMarker("Luxembourg", 49.815273, 6.129583, "#3F51B5"));
+ mapboxMap.addMarker(generateCityStateMarker("Monaco", 43.738418, 7.424616, "#673AB7"));
+ mapboxMap.addMarker(generateCityStateMarker("Vatican City", 41.902916, 12.453389, "#009688"));
+ mapboxMap.addMarker(generateCityStateMarker("San Marino", 43.942360, 12.457777, "#795548"));
+ mapboxMap.addMarker(generateCityStateMarker("Liechtenstein", 47.166000, 9.555373, "#FF5722"));
+ }
+
+ private CityStateMarkerOptions generateCityStateMarker(String title, double lat, double lng, String color) {
+ CityStateMarkerOptions marker = new CityStateMarkerOptions();
+ marker.title(title);
+ marker.position(new LatLng(lat, lng));
+ marker.infoWindowBackground(color);
+
+ Icon icon = IconUtils.drawableToIcon(this, R.drawable.ic_location_city, Color.parseColor(color));
+ marker.icon(icon);
+ return marker;
+ }
+
+ private void addCustomInfoWindowAdapter() {
+ mapboxMap.setInfoWindowAdapter(new MapboxMap.InfoWindowAdapter() {
+
+ private int tenDp = (int) getResources().getDimension(R.dimen.attr_margin);
+
+ @Override
+ public View getInfoWindow(@NonNull Marker marker) {
+ TextView textView = new TextView(InfoWindowAdapterActivity.this);
+ textView.setText(marker.getTitle());
+ textView.setTextColor(Color.WHITE);
+
+ if (marker instanceof CityStateMarker) {
+ CityStateMarker cityStateMarker = (CityStateMarker) marker;
+ textView.setBackgroundColor(Color.parseColor(cityStateMarker.getInfoWindowBackgroundColor()));
+ }
+
+ textView.setPadding(tenDp, tenDp, tenDp, tenDp);
+ return textView;
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/BottomSheetActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/BottomSheetActivity.java
new file mode 100644
index 0000000000..f076197c08
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/BottomSheetActivity.java
@@ -0,0 +1,273 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.design.widget.BottomSheetBehavior;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.MapboxMapOptions;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.utils.MapFragmentUtils;
+
+/**
+ * Test activity showcasing using a bottomView with a MapView and stacking map fragments below.
+ */
+public class BottomSheetActivity extends AppCompatActivity {
+
+ private static final String TAG_MAIN_FRAGMENT = "com.mapbox.mapboxsdk.fragment.tag.main";
+ private static final String TAG_BOTTOM_FRAGMENT = "com.mapbox.mapboxsdk.fragment.tag.bottom";
+
+ private boolean bottomSheetFragmentAdded;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_bottom_sheet);
+
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ }
+
+ findViewById(R.id.fabFragment).setOnClickListener(v -> addMapFragment());
+
+ findViewById(R.id.fabBottomSheet).setOnClickListener(v -> toggleBottomSheetMapFragment());
+
+ BottomSheetBehavior bottomSheetBehavior = BottomSheetBehavior.from(findViewById(R.id.bottom_sheet));
+ bottomSheetBehavior.setPeekHeight((int) (64 * getResources().getDisplayMetrics().density));
+ bottomSheetBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+ toggleBottomSheetMapFragment();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+
+ if (fragmentManager.getBackStackEntryCount() > 0) {
+ fragmentManager.popBackStack();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ private void addMapFragment() {
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ int fragmentCount = fragmentManager.getBackStackEntryCount();
+
+ FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
+ MainMapFragment mainMapFragment = MainMapFragment.newInstance(fragmentCount);
+ if (fragmentCount == 0) {
+ fragmentTransaction.add(R.id.fragment_container, mainMapFragment, TAG_MAIN_FRAGMENT);
+ } else {
+ fragmentTransaction.replace(R.id.fragment_container, mainMapFragment, TAG_MAIN_FRAGMENT);
+ }
+ fragmentTransaction.addToBackStack(String.valueOf(mainMapFragment.hashCode()));
+ fragmentTransaction.commit();
+ Toast.makeText(this, "Amount of main map fragments: " + (fragmentCount + 1), Toast.LENGTH_SHORT).show();
+ }
+
+ private void toggleBottomSheetMapFragment() {
+ if (!bottomSheetFragmentAdded) {
+ addBottomSheetMapFragment();
+ } else {
+ removeBottomSheetFragment();
+ }
+ bottomSheetFragmentAdded = !bottomSheetFragmentAdded;
+ }
+
+ private void addBottomSheetMapFragment() {
+ FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+ fragmentTransaction.add(R.id.fragment_container_bottom, BottomSheetFragment.newInstance(), TAG_BOTTOM_FRAGMENT);
+ fragmentTransaction.commit();
+ }
+
+ private void removeBottomSheetFragment() {
+ Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_BOTTOM_FRAGMENT);
+ if (fragment != null) {
+ getSupportFragmentManager().beginTransaction().remove(fragment).commit();
+ }
+ }
+
+ public static class MainMapFragment extends Fragment implements OnMapReadyCallback {
+
+ private static final String[] STYLES = new String[] {
+ Style.MAPBOX_STREETS,
+ Style.SATELLITE_STREETS,
+ Style.LIGHT,
+ Style.DARK,
+ Style.SATELLITE,
+ Style.OUTDOORS
+ };
+
+ private MapView map;
+
+ public static MainMapFragment newInstance(int mapCounter) {
+ MainMapFragment mapFragment = new MainMapFragment();
+ MapboxMapOptions mapboxMapOptions = new MapboxMapOptions();
+ mapboxMapOptions.styleUrl(STYLES[Math.min(Math.max(mapCounter, 0), STYLES.length - 1)]);
+ mapFragment.setArguments(MapFragmentUtils.createFragmentArgs(mapboxMapOptions));
+ return mapFragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ Context context = inflater.getContext();
+ return map = new MapView(context, MapFragmentUtils.resolveArgs(context, getArguments()));
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ map.onCreate(savedInstanceState);
+ map.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(37.760545, -122.436055), 15));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ map.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ map.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ map.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ map.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ map.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ map.onLowMemory();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ map.onDestroy();
+ }
+ }
+
+ public static class BottomSheetFragment extends Fragment implements OnMapReadyCallback {
+
+ private MapView map;
+
+ public static BottomSheetFragment newInstance() {
+ BottomSheetFragment mapFragment = new BottomSheetFragment();
+ MapboxMapOptions mapboxMapOptions = new MapboxMapOptions();
+ mapboxMapOptions.renderSurfaceOnTop(true);
+ mapboxMapOptions.styleUrl(Style.LIGHT);
+ mapFragment.setArguments(MapFragmentUtils.createFragmentArgs(mapboxMapOptions));
+ return mapFragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ Context context = inflater.getContext();
+ return map = new MapView(context, MapFragmentUtils.resolveArgs(context, getArguments()));
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ map.onCreate(savedInstanceState);
+ map.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(37.760545, -122.436055), 15));
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ map.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ map.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ map.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ map.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ map.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ map.onLowMemory();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ map.onDestroy();
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DebugModeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DebugModeActivity.java
new file mode 100644
index 0000000000..ca29c3dd81
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DebugModeActivity.java
@@ -0,0 +1,271 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.http.HttpRequestUtil;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+import java.util.Locale;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+/**
+ * Test activity showcasing the different debug modes and allows to cycle between the default map styles.
+ */
+public class DebugModeActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private ActionBarDrawerToggle actionBarDrawerToggle;
+ private int currentStyleIndex = 0;
+
+ private static final String[] STYLES = new String[] {
+ Style.MAPBOX_STREETS,
+ Style.OUTDOORS,
+ Style.LIGHT,
+ Style.DARK,
+ Style.SATELLITE,
+ Style.SATELLITE_STREETS,
+ Style.TRAFFIC_DAY,
+ Style.TRAFFIC_NIGHT
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ HttpRequestUtil.setPrintRequestUrlOnFailure(true);
+ setContentView(R.layout.activity_debug_mode);
+ setupToolbar();
+ setupMapView(savedInstanceState);
+ setupDebugChangeView();
+ setupStyleChangeView();
+ }
+
+ private void setupToolbar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ actionBarDrawerToggle = new ActionBarDrawerToggle(this,
+ drawerLayout,
+ R.string.navigation_drawer_open,
+ R.string.navigation_drawer_close
+ );
+ actionBarDrawerToggle.setDrawerIndicatorEnabled(true);
+ actionBarDrawerToggle.syncState();
+ }
+ }
+
+ private void setupMapView(Bundle savedInstanceState) {
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.addOnMapChangedListener(change -> {
+ if (change == MapView.DID_FINISH_LOADING_STYLE && mapboxMap != null) {
+ Timber.v("New style loaded with JSON: %s", mapboxMap.getStyleJson());
+ setupNavigationView(mapboxMap.getLayers());
+ }
+ });
+
+ mapView.setTag(true);
+ mapView.setStyleUrl(STYLES[currentStyleIndex]);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ mapboxMap.getUiSettings().setZoomControlsEnabled(true);
+
+ setupNavigationView(mapboxMap.getLayers());
+ setupZoomView();
+ setFpsView();
+ }
+
+ private void setFpsView() {
+ final TextView fpsView = (TextView) findViewById(R.id.fpsView);
+ mapboxMap.setOnFpsChangedListener(fps ->
+ fpsView.setText(String.format(Locale.US, "FPS: %4.2f", fps))
+ );
+ }
+
+ private void setupNavigationView(List<Layer> layerList) {
+ final LayerListAdapter adapter = new LayerListAdapter(this, layerList);
+ ListView listView = (ListView) findViewById(R.id.listView);
+ listView.setAdapter(adapter);
+ listView.setOnItemClickListener((parent, view, position, id) -> {
+ Layer clickedLayer = adapter.getItem(position);
+ toggleLayerVisibility(clickedLayer);
+ closeNavigationView();
+ });
+ }
+
+ private void toggleLayerVisibility(Layer layer) {
+ boolean isVisible = layer.getVisibility().getValue().equals(Property.VISIBLE);
+ layer.setProperties(
+ visibility(
+ isVisible ? Property.NONE : Property.VISIBLE
+ )
+ );
+ }
+
+ private void closeNavigationView() {
+ DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ drawerLayout.closeDrawers();
+ }
+
+ private void setupZoomView() {
+ final TextView textView = (TextView) findViewById(R.id.textZoom);
+ mapboxMap.setOnCameraChangeListener(position ->
+ textView.setText(String.format(getString(R.string.debug_zoom), position.zoom))
+ );
+ }
+
+ private void setupDebugChangeView() {
+ FloatingActionButton fabDebug = (FloatingActionButton) findViewById(R.id.fabDebug);
+ fabDebug.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ Timber.d("Debug FAB: isDebug Active? %s", mapboxMap.isDebugActive());
+ mapboxMap.cycleDebugOptions();
+ }
+ });
+ }
+
+ private void setupStyleChangeView() {
+ FloatingActionButton fabStyles = (FloatingActionButton) findViewById(R.id.fabStyles);
+ fabStyles.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ currentStyleIndex++;
+ if (currentStyleIndex == STYLES.length) {
+ currentStyleIndex = 0;
+ }
+ mapboxMap.setStyleUrl(STYLES[currentStyleIndex], style -> Timber.d("Style loaded %s", style));
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return actionBarDrawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ HttpRequestUtil.setPrintRequestUrlOnFailure(false);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ private static class LayerListAdapter extends BaseAdapter {
+
+ private LayoutInflater layoutInflater;
+ private List<Layer> layers;
+
+ LayerListAdapter(Context context, List<Layer> layers) {
+ this.layoutInflater = LayoutInflater.from(context);
+ this.layers = layers;
+ }
+
+ @Override
+ public int getCount() {
+ return layers.size();
+ }
+
+ @Override
+ public Layer getItem(int position) {
+ return layers.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Layer layer = layers.get(position);
+ View view = convertView;
+ if (view == null) {
+ view = layoutInflater.inflate(android.R.layout.simple_list_item_2, parent, false);
+ ViewHolder holder = new ViewHolder(
+ (TextView) view.findViewById(android.R.id.text1),
+ (TextView) view.findViewById(android.R.id.text2)
+ );
+ view.setTag(holder);
+ }
+ ViewHolder holder = (ViewHolder) view.getTag();
+ holder.text.setText(layer.getClass().getSimpleName());
+ holder.subText.setText(layer.getId());
+ return view;
+ }
+
+ private static class ViewHolder {
+ final TextView text;
+ final TextView subText;
+
+ ViewHolder(TextView text, TextView subText) {
+ this.text = text;
+ this.subText = subText;
+ }
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DoubleMapActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DoubleMapActivity.java
new file mode 100644
index 0000000000..c4879c08cb
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/DoubleMapActivity.java
@@ -0,0 +1,155 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.UiSettings;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing having 2 maps on top of each other.
+ * <p>
+ * The small map is using the `mapbox_enableZMediaOverlay="true"` configuration
+ * </p>
+ */
+public class DoubleMapActivity extends AppCompatActivity {
+
+ private static final String TAG_FRAGMENT = "map";
+
+ // used for ui tests
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_fragment);
+
+ if (savedInstanceState == null) {
+ FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
+ transaction.add(R.id.fragment_container, new DoubleMapFragment(), TAG_FRAGMENT);
+ transaction.commit();
+ }
+ }
+
+ public void setMapboxMap(MapboxMap map) {
+ // we need to set mapboxmap on the parent activity,
+ // for auto-generated ui tests
+ mapboxMap = map;
+ mapboxMap.setStyleUrl(Style.DARK);
+ mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(18));
+ }
+
+ /**
+ * Custom fragment containing 2 MapViews.
+ */
+ public static class DoubleMapFragment extends Fragment {
+
+ private DoubleMapActivity activity;
+ private MapView mapView;
+ private MapView mapViewMini;
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ activity = (DoubleMapActivity) context;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_double_map, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // MapView large
+ mapView = (MapView) view.findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ if (activity != null) {
+ activity.setMapboxMap(mapboxMap);
+ }
+ });
+
+ // MapView mini
+ mapViewMini = (MapView) view.findViewById(R.id.mini_map);
+ mapViewMini.onCreate(savedInstanceState);
+ mapViewMini.getMapAsync(mapboxMap -> {
+ mapboxMap.setStyleUrl(Style.LIGHT);
+ mapboxMap.moveCamera(CameraUpdateFactory.zoomTo(4));
+
+ UiSettings uiSettings = mapboxMap.getUiSettings();
+ uiSettings.setAllGesturesEnabled(false);
+ uiSettings.setCompassEnabled(false);
+ uiSettings.setAttributionEnabled(false);
+ uiSettings.setLogoEnabled(false);
+
+ mapboxMap.setOnMapClickListener(point -> {
+ // test if we can open 2 activities after each other
+ startActivity(new Intent(mapViewMini.getContext(), DoubleMapActivity.class));
+ });
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ mapViewMini.onResume();
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mapView.onStart();
+ mapViewMini.onStart();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ mapViewMini.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mapView.onStop();
+ mapViewMini.onStop();
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mapView.onDestroy();
+ mapViewMini.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ mapViewMini.onLowMemory();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ mapViewMini.onSaveInstanceState(outState);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LatLngBoundsForCameraActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LatLngBoundsForCameraActivity.java
new file mode 100644
index 0000000000..0afa67deef
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LatLngBoundsForCameraActivity.java
@@ -0,0 +1,109 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.mapbox.mapboxsdk.annotations.PolygonOptions;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing restricting user gestures to a bounds around Iceland.
+ */
+public class LatLngBoundsForCameraActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final LatLngBounds ICELAND_BOUNDS = new LatLngBounds.Builder()
+ .include(new LatLng(66.852863, -25.985652))
+ .include(new LatLng(62.985661, -12.626277))
+ .build();
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_restricted_bounds);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap mapboxMap) {
+ this.mapboxMap = mapboxMap;
+ mapboxMap.setLatLngBoundsForCameraTarget(ICELAND_BOUNDS);
+ mapboxMap.setMinZoomPreference(2);
+ showBoundsArea();
+ showCrosshair();
+ }
+
+ private void showBoundsArea() {
+ PolygonOptions boundsArea = new PolygonOptions()
+ .add(ICELAND_BOUNDS.getNorthWest())
+ .add(ICELAND_BOUNDS.getNorthEast())
+ .add(ICELAND_BOUNDS.getSouthEast())
+ .add(ICELAND_BOUNDS.getSouthWest());
+ boundsArea.alpha(0.25f);
+ boundsArea.fillColor(Color.RED);
+ mapboxMap.addPolygon(boundsArea);
+ }
+
+ private void showCrosshair() {
+ View crosshair = new View(this);
+ crosshair.setLayoutParams(new FrameLayout.LayoutParams(10, 10, Gravity.CENTER));
+ crosshair.setBackgroundColor(Color.BLUE);
+ mapView.addView(crosshair);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LocalGlyphActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LocalGlyphActivity.java
new file mode 100644
index 0000000000..85d08fcd8b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/LocalGlyphActivity.java
@@ -0,0 +1,85 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity that displays the city of Suzhou with a mixture of server-generated
+ * latin glyphs and CJK glyphs generated locally using "Droid Sans" as a font family.
+ */
+public class LocalGlyphActivity extends AppCompatActivity {
+ private MapView mapView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_local_glyph);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(new OnMapReadyCallback() {
+ @Override
+ public void onMapReady(@NonNull MapboxMap mapboxMap) {
+ // Set initial position to Suzhou
+ mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
+ new CameraPosition.Builder()
+ .target(new LatLng(31.3003, 120.7457))
+ .zoom(11)
+ .bearing(0)
+ .tilt(0)
+ .build()));
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapChangeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapChangeActivity.java
new file mode 100644
index 0000000000..4c957e6bfe
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapChangeActivity.java
@@ -0,0 +1,102 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.os.Bundle;
+import android.support.v4.util.LongSparseArray;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing how to listen to map change events.
+ */
+public class MapChangeActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_simple);
+
+ final LongSparseArray<String> mapChangeMap = buildMapChangeStringValueSparseArray();
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.addOnMapChangedListener(change -> Timber.e("OnMapChange: %s, %s", change, mapChangeMap.get(change)));
+
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ new LatLng(55.754020, 37.620948), 12), 9000);
+ });
+ }
+
+ private LongSparseArray<String> buildMapChangeStringValueSparseArray() {
+ LongSparseArray<String> mapChangeArray = new LongSparseArray<>();
+ mapChangeArray.put(MapView.REGION_WILL_CHANGE, "Region will change");
+ mapChangeArray.put(MapView.REGION_WILL_CHANGE_ANIMATED, "Region will change animated");
+ mapChangeArray.put(MapView.REGION_IS_CHANGING, "Region is changing");
+ mapChangeArray.put(MapView.REGION_DID_CHANGE, "Region did change");
+ mapChangeArray.put(MapView.REGION_DID_CHANGE_ANIMATED, "Region did change animated");
+ mapChangeArray.put(MapView.WILL_START_LOADING_MAP, "Will start loading map");
+ mapChangeArray.put(MapView.DID_FINISH_LOADING_MAP, "Did finish loading map");
+ mapChangeArray.put(MapView.DID_FAIL_LOADING_MAP, "Did fail loading map");
+ mapChangeArray.put(MapView.WILL_START_RENDERING_FRAME, "Will start rendering frame");
+ mapChangeArray.put(MapView.DID_FINISH_RENDERING_FRAME, "Did finish rendering frame");
+ mapChangeArray.put(MapView.DID_FINISH_RENDERING_FRAME_FULLY_RENDERED, "Did finish rendering frame fully rendered");
+ mapChangeArray.put(MapView.WILL_START_RENDERING_MAP, "Will start rendering map");
+ mapChangeArray.put(MapView.DID_FINISH_RENDERING_MAP, "Did finish rendering map");
+ mapChangeArray.put(MapView.DID_FINISH_RENDERING_MAP_FULLY_RENDERED, "Did finish rendering map fully rendered");
+ mapChangeArray.put(MapView.DID_FINISH_LOADING_STYLE, "Did finish loading style");
+ mapChangeArray.put(MapView.SOURCE_DID_CHANGE, "Source did change");
+ return mapChangeArray;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapInDialogActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapInDialogActivity.java
new file mode 100644
index 0000000000..4fa33dc60c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapInDialogActivity.java
@@ -0,0 +1,119 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.app.Dialog;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.DialogFragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing showing a Map inside of a DialogFragment.
+ * <p>
+ * Uses the deprecated TextureView API to workaround the issue of seeing a grey background before the gl surface.
+ * </p>
+ */
+public class MapInDialogActivity extends AppCompatActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_in_dialog);
+
+ Button button = (Button) findViewById(R.id.button_open_dialog);
+ button.setOnClickListener(view -> {
+ FragmentManager fm = getSupportFragmentManager();
+ MapDialogFragment editNameDialogFragment = MapDialogFragment.newInstance("Map Dialog");
+ editNameDialogFragment.show(fm, "fragment_dialog_map");
+ });
+ }
+
+ public static class MapDialogFragment extends DialogFragment {
+
+ private MapView mapView;
+
+ public MapDialogFragment() {
+ }
+
+ public static MapDialogFragment newInstance(String title) {
+ MapDialogFragment frag = new MapDialogFragment();
+ Bundle args = new Bundle();
+ args.putString("title", title);
+ frag.setArguments(args);
+ return frag;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_dialog_map, container);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ mapView = (MapView) view.findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ return new Dialog(getActivity(), getTheme()) {
+ boolean destroyed = false;
+ @Override
+ public void dismiss() {
+ if (mapView != null && !destroyed) {
+ mapView.onDestroy();
+ destroyed = true;
+ }
+ super.dismiss();
+ }
+ };
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapPaddingActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapPaddingActivity.java
new file mode 100644
index 0000000000..09632f55ec
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/MapPaddingActivity.java
@@ -0,0 +1,123 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.mapbox.mapboxsdk.annotations.MarkerOptions;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using the map padding API.
+ */
+public class MapPaddingActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_padding);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.setTag(true);
+ mapView.onCreate(savedInstanceState);
+
+ mapView.getMapAsync(mapboxMap -> {
+ MapPaddingActivity.this.mapboxMap = mapboxMap;
+
+ int paddingLeft = (int) getResources().getDimension(R.dimen.map_padding_left);
+ int paddingBottom = (int) getResources().getDimension(R.dimen.map_padding_bottom);
+ int paddingRight = (int) getResources().getDimension(R.dimen.map_padding_right);
+ int paddingTop = (int) getResources().getDimension(R.dimen.map_padding_top);
+ mapboxMap.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
+
+ moveToBangalore();
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_padding, menu);
+ return true;
+ }
+
+ private void moveToBangalore() {
+ LatLng bangalore = new LatLng(12.9810816, 77.6368034);
+ CameraPosition cameraPosition = new CameraPosition.Builder()
+ .zoom(16)
+ .target(bangalore)
+ .bearing(40)
+ .tilt(45)
+ .build();
+
+ mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
+ mapboxMap.addMarker(new MarkerOptions().title("Center map").position(bangalore));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+
+ case R.id.action_bangalore:
+ if (mapboxMap != null) {
+ moveToBangalore();
+ }
+ return true;
+
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+}
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/SimpleMapActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/SimpleMapActivity.java
new file mode 100644
index 0000000000..24cb56925c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/SimpleMapActivity.java
@@ -0,0 +1,66 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing a simple MapView without any MapboxMap interaction.
+ */
+public class SimpleMapActivity extends AppCompatActivity {
+
+ private MapView mapView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_simple);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/VisibilityChangeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/VisibilityChangeActivity.java
new file mode 100644
index 0000000000..35239a96ea
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/maplayout/VisibilityChangeActivity.java
@@ -0,0 +1,142 @@
+package com.mapbox.mapboxsdk.maps.activity.maplayout;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing visibility changes to the mapview.
+ */
+public class VisibilityChangeActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private Handler handler = new Handler();
+ private Runnable runnable;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_visibility);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(
+ new LatLng(55.754020, 37.620948), 12), 9000);
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ handler.post(runnable = new VisibilityRunner(mapView, findViewById(R.id.viewParent), handler));
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ private static class VisibilityRunner implements Runnable {
+
+ private MapView mapView;
+ private View viewParent;
+ private Handler handler;
+ private int currentStep;
+
+ VisibilityRunner(MapView mapView, View viewParent, Handler handler) {
+ this.mapView = mapView;
+ this.viewParent = viewParent;
+ this.handler = handler;
+ }
+
+ @Override
+ public void run() {
+ if (isViewHiearchyReady()) {
+ if (isEvenStep()) {
+ viewParent.setVisibility(View.VISIBLE);
+ mapView.setVisibility(View.VISIBLE);
+ } else if (isFirstOrThirdStep()) {
+ mapView.setVisibility(getVisibilityForStep());
+ } else if (isFifthOrSeventhStep()) {
+ viewParent.setVisibility(getVisibilityForStep());
+ }
+ updateStep();
+ }
+ handler.postDelayed(this, 1500);
+ }
+
+ private void updateStep() {
+ if (currentStep == 7) {
+ currentStep = 0;
+ } else {
+ currentStep++;
+ }
+ }
+
+ private int getVisibilityForStep() {
+ return (currentStep == 1 || currentStep == 5) ? View.GONE : View.INVISIBLE;
+ }
+
+ private boolean isFifthOrSeventhStep() {
+ return currentStep == 5 || currentStep == 7;
+ }
+
+ private boolean isFirstOrThirdStep() {
+ return currentStep == 1 || currentStep == 3;
+ }
+
+ private boolean isEvenStep() {
+ return currentStep == 0 || currentStep % 2 == 0;
+ }
+
+ private boolean isViewHiearchyReady() {
+ return mapView != null && viewParent != null;
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (runnable != null) {
+ handler.removeCallbacks(runnable);
+ runnable = null;
+ }
+ mapView.onStop();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/DeleteRegionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/DeleteRegionActivity.java
new file mode 100644
index 0000000000..389a483ce2
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/DeleteRegionActivity.java
@@ -0,0 +1,157 @@
+package com.mapbox.mapboxsdk.maps.activity.offline;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegion;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.OfflineUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test activity showing integration of deleting an OfflineRegion.
+ */
+public class DeleteRegionActivity extends AppCompatActivity implements AdapterView.OnItemClickListener {
+
+ private OfflineRegionAdapter adapter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_offline_region_delete);
+
+ ListView listView = (ListView) findViewById(R.id.listView);
+ listView.setAdapter(adapter = new OfflineRegionAdapter(this));
+ listView.setEmptyView(findViewById(android.R.id.empty));
+ listView.setOnItemClickListener(this);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final OfflineRegion region = adapter.getItem(position);
+ String metadata = OfflineUtils.convertRegionName(region.getMetadata());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Delete region");
+
+ final TextView input = new TextView(this);
+ input.setText(metadata);
+ builder.setView(input);
+
+ builder.setPositiveButton("OK", (dialog, which) -> delete(region));
+ builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
+
+ builder.show();
+ }
+
+ private void delete(OfflineRegion region) {
+ region.delete(new OfflineRegion.OfflineRegionDeleteCallback() {
+ @Override
+ public void onDelete() {
+ Toast.makeText(
+ DeleteRegionActivity.this,
+ "Region deleted",
+ Toast.LENGTH_SHORT
+ ).show();
+ loadOfflineRegions();
+ }
+
+ @Override
+ public void onError(String error) {
+ Toast.makeText(
+ DeleteRegionActivity.this,
+ "Region deletion failed with " + error,
+ Toast.LENGTH_LONG
+ ).show();
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ loadOfflineRegions();
+ }
+
+ private void loadOfflineRegions() {
+ OfflineManager.getInstance(this).listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
+ @Override
+ public void onList(OfflineRegion[] offlineRegions) {
+ if (offlineRegions != null && offlineRegions.length > 0) {
+ adapter.setOfflineRegions(Arrays.asList(offlineRegions));
+ }
+ }
+
+ @Override
+ public void onError(String error) {
+ Toast.makeText(DeleteRegionActivity.this, "Error loading regions " + error, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ private static class OfflineRegionAdapter extends BaseAdapter {
+
+ private Context context;
+ private List<OfflineRegion> offlineRegions;
+
+ OfflineRegionAdapter(Context ctx) {
+ context = ctx;
+ offlineRegions = new ArrayList<>();
+ }
+
+ void setOfflineRegions(List<OfflineRegion> offlineRegions) {
+ this.offlineRegions = offlineRegions;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return offlineRegions.size();
+ }
+
+ @Override
+ public OfflineRegion getItem(int position) {
+ return offlineRegions.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ holder = new ViewHolder();
+ convertView = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false);
+ holder.text = (TextView) convertView.findViewById(android.R.id.text1);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ holder.text.setText(OfflineUtils.convertRegionName(getItem(position).getMetadata()));
+ return convertView;
+ }
+
+ static class ViewHolder {
+ TextView text;
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/OfflineActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/OfflineActivity.java
new file mode 100644
index 0000000000..ebce41b102
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/OfflineActivity.java
@@ -0,0 +1,321 @@
+package com.mapbox.mapboxsdk.maps.activity.offline;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.Button;
+import android.widget.ProgressBar;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.Mapbox;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegion;
+import com.mapbox.mapboxsdk.offline.OfflineRegionError;
+import com.mapbox.mapboxsdk.offline.OfflineRegionStatus;
+import com.mapbox.mapboxsdk.offline.OfflineTilePyramidRegionDefinition;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.testapp.model.other.OfflineDownloadRegionDialog;
+import com.mapbox.mapboxsdk.testapp.model.other.OfflineListRegionsDialog;
+import com.mapbox.mapboxsdk.maps.utils.OfflineUtils;
+
+import java.util.ArrayList;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing the Offline API.
+ * <p>
+ * Shows a map of Manhattan and allows the user to download and name a region.
+ * </p>
+ */
+public class OfflineActivity extends AppCompatActivity
+ implements OfflineDownloadRegionDialog.DownloadRegionDialogListener {
+
+ // JSON encoding/decoding
+ public static final String JSON_CHARSET = "UTF-8";
+ public static final String JSON_FIELD_REGION_NAME = "FIELD_REGION_NAME";
+
+ // Style URL
+ public static final String STYLE_URL = Style.MAPBOX_STREETS;
+
+ /*
+ * UI elements
+ */
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private ProgressBar progressBar;
+ private Button downloadRegion;
+ private Button listRegions;
+
+ private boolean isEndNotified;
+
+ /*
+ * Offline objects
+ */
+ private OfflineManager offlineManager;
+ private OfflineRegion offlineRegion;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_offline);
+
+ // You can use Mapbox.setConnected(Boolean) to manually set the connectivity
+ // state of your app. This will override any checks performed via the ConnectivityManager.
+ // Mapbox.getInstance().setConnected(false);
+ Boolean connected = Mapbox.isConnected();
+ Timber.d("Mapbox is connected: %s", connected);
+
+ // Set up map
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.setStyleUrl(STYLE_URL);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ Timber.d("Map is ready");
+ OfflineActivity.this.mapboxMap = mapboxMap;
+
+ // Set initial position to UNHQ in NYC
+ mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(
+ new CameraPosition.Builder()
+ .target(new LatLng(40.749851, -73.967966))
+ .zoom(14)
+ .bearing(0)
+ .tilt(0)
+ .build()));
+ });
+
+ // The progress bar
+ progressBar = (ProgressBar) findViewById(R.id.progress_bar);
+
+ // Set up button listeners
+ downloadRegion = (Button) findViewById(R.id.button_download_region);
+ downloadRegion.setOnClickListener(view -> handleDownloadRegion());
+
+ listRegions = (Button) findViewById(R.id.button_list_regions);
+ listRegions.setOnClickListener(view -> handleListRegions());
+
+ // Set up the OfflineManager
+ offlineManager = OfflineManager.getInstance(this);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ /*
+ * Buttons logic
+ */
+ private void handleDownloadRegion() {
+ Timber.d("handleDownloadRegion");
+
+ // Show dialog
+ OfflineDownloadRegionDialog offlineDownloadRegionDialog = new OfflineDownloadRegionDialog();
+ offlineDownloadRegionDialog.show(getSupportFragmentManager(), "download");
+ }
+
+ private void handleListRegions() {
+ Timber.d("handleListRegions");
+
+ // Query the DB asynchronously
+ offlineManager.listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
+ @Override
+ public void onList(OfflineRegion[] offlineRegions) {
+ // Check result
+ if (offlineRegions == null || offlineRegions.length == 0) {
+ Toast.makeText(OfflineActivity.this, "You have no regions yet.", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Get regions info
+ ArrayList<String> offlineRegionsNames = new ArrayList<>();
+ for (OfflineRegion offlineRegion : offlineRegions) {
+ offlineRegionsNames.add(OfflineUtils.convertRegionName(offlineRegion.getMetadata()));
+ }
+
+ // Create args
+ Bundle args = new Bundle();
+ args.putStringArrayList(OfflineListRegionsDialog.ITEMS, offlineRegionsNames);
+
+ // Show dialog
+ OfflineListRegionsDialog offlineListRegionsDialog = new OfflineListRegionsDialog();
+ offlineListRegionsDialog.setArguments(args);
+ offlineListRegionsDialog.show(getSupportFragmentManager(), "list");
+ }
+
+ @Override
+ public void onError(String error) {
+ Timber.e("Error: %s" , error);
+ }
+ });
+ }
+
+ /*
+ * Dialogs
+ */
+ @Override
+ public void onDownloadRegionDialogPositiveClick(final String regionName) {
+ if (TextUtils.isEmpty(regionName)) {
+ Toast.makeText(OfflineActivity.this, "Region name cannot be empty.", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Start progress bar
+ Timber.d("Download started: %s", regionName);
+ startProgress();
+
+ // Definition
+ LatLngBounds bounds = mapboxMap.getProjection().getVisibleRegion().latLngBounds;
+ double minZoom = mapboxMap.getCameraPosition().zoom;
+ double maxZoom = mapboxMap.getMaxZoomLevel();
+ float pixelRatio = this.getResources().getDisplayMetrics().density;
+ OfflineTilePyramidRegionDefinition definition = new OfflineTilePyramidRegionDefinition(
+ STYLE_URL, bounds, minZoom, maxZoom, pixelRatio);
+
+ // Sample way of encoding metadata from a JSONObject
+ byte[] metadata = OfflineUtils.convertRegionName(regionName);
+
+ // Create region
+ offlineManager.createOfflineRegion(definition, metadata, new OfflineManager.CreateOfflineRegionCallback() {
+ @Override
+ public void onCreate(OfflineRegion offlineRegion) {
+ Timber.d("Offline region created: %s" , regionName);
+ OfflineActivity.this.offlineRegion = offlineRegion;
+ launchDownload();
+ }
+
+ @Override
+ public void onError(String error) {
+ Timber.e("Error: %s", error);
+ }
+ });
+ }
+
+ private void launchDownload() {
+ // Set an observer
+ offlineRegion.setObserver(new OfflineRegion.OfflineRegionObserver() {
+ @Override
+ public void onStatusChanged(OfflineRegionStatus status) {
+ // Compute a percentage
+ double percentage = status.getRequiredResourceCount() >= 0
+ ? (100.0 * status.getCompletedResourceCount() / status.getRequiredResourceCount()) :
+ 0.0;
+
+ if (status.isComplete()) {
+ // Download complete
+ endProgress("Region downloaded successfully.");
+ offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE);
+ offlineRegion.setObserver(null);
+ return;
+ } else if (status.isRequiredResourceCountPrecise()) {
+ // Switch to determinate state
+ setPercentage((int) Math.round(percentage));
+ }
+
+ // Debug
+ Timber.d("%s/%s resources; %s bytes downloaded.",
+ String.valueOf(status.getCompletedResourceCount()),
+ String.valueOf(status.getRequiredResourceCount()),
+ String.valueOf(status.getCompletedResourceSize()));
+ }
+
+ @Override
+ public void onError(OfflineRegionError error) {
+ Timber.e("onError: %s, %s", error.getReason(), error.getMessage());
+ offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE);
+ }
+
+ @Override
+ public void mapboxTileCountLimitExceeded(long limit) {
+ Timber.e("Mapbox tile count limit exceeded: %s", limit);
+ offlineRegion.setDownloadState(OfflineRegion.STATE_INACTIVE);
+ }
+ });
+
+ // Change the region state
+ offlineRegion.setDownloadState(OfflineRegion.STATE_ACTIVE);
+ }
+
+ /*
+ * Progress bar
+ */
+ private void startProgress() {
+ // Disable buttons
+ downloadRegion.setEnabled(false);
+ listRegions.setEnabled(false);
+
+ // Start and show the progress bar
+ isEndNotified = false;
+ progressBar.setIndeterminate(true);
+ progressBar.setVisibility(View.VISIBLE);
+ }
+
+ private void setPercentage(final int percentage) {
+ progressBar.setIndeterminate(false);
+ progressBar.setProgress(percentage);
+ }
+
+ private void endProgress(final String message) {
+ // Don't notify more than once
+ if (isEndNotified) {
+ return;
+ }
+
+ // Enable buttons
+ downloadRegion.setEnabled(true);
+ listRegions.setEnabled(true);
+
+ // Stop and hide the progress bar
+ isEndNotified = true;
+ progressBar.setIndeterminate(false);
+ progressBar.setVisibility(View.GONE);
+
+ // Show a toast
+ Toast.makeText(OfflineActivity.this, message, Toast.LENGTH_LONG).show();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/UpdateMetadataActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/UpdateMetadataActivity.java
new file mode 100644
index 0000000000..b6ed05ade0
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/offline/UpdateMetadataActivity.java
@@ -0,0 +1,184 @@
+package com.mapbox.mapboxsdk.maps.activity.offline;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AppCompatActivity;
+import android.text.InputType;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.EditText;
+import android.widget.ListView;
+import android.widget.TextView;
+import android.widget.Toast;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.offline.OfflineManager;
+import com.mapbox.mapboxsdk.offline.OfflineRegion;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.OfflineUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Test activity showing integration of updating metadata of an OfflineRegion.
+ */
+public class UpdateMetadataActivity extends AppCompatActivity implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
+
+ private OfflineRegionMetadataAdapter adapter;
+
+ private MapView mapView;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_metadata_update);
+
+ ListView listView = (ListView) findViewById(R.id.listView);
+ listView.setAdapter(adapter = new OfflineRegionMetadataAdapter(this));
+ listView.setEmptyView(findViewById(android.R.id.empty));
+ listView.setOnItemClickListener(this);
+ listView.setOnItemLongClickListener(this);
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final OfflineRegion region = adapter.getItem(position);
+ String metadata = OfflineUtils.convertRegionName(region.getMetadata());
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle("Rename metadata");
+
+ final EditText input = new EditText(this);
+ input.setText(metadata);
+ input.setInputType(InputType.TYPE_CLASS_TEXT);
+ input.setSelection(metadata.length());
+ builder.setView(input);
+
+ builder.setPositiveButton("OK", (dialog, which) ->
+ updateMetadata(region, OfflineUtils.convertRegionName(input.getText().toString()))
+ );
+ builder.setNegativeButton("Cancel", (dialog, which) -> dialog.cancel());
+
+ builder.show();
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ ViewGroup container = (ViewGroup) findViewById(R.id.container);
+ container.removeAllViews();
+ container.addView(mapView = new MapView(view.getContext()));
+ mapView.setOfflineRegionDefinition(adapter.getItem(position).getDefinition());
+ mapView.onCreate(null);
+ mapView.onStart();
+ mapView.onResume();
+ return true;
+ }
+
+ private void updateMetadata(OfflineRegion region, byte[] metadata) {
+ region.updateMetadata(metadata, new OfflineRegion.OfflineRegionUpdateMetadataCallback() {
+ @Override
+ public void onUpdate(byte[] metadata) {
+ adapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onError(String error) {
+ Toast.makeText(
+ UpdateMetadataActivity.this,
+ "Region metadata update failed with " + error,
+ Toast.LENGTH_LONG
+ ).show();
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ loadOfflineRegions();
+ }
+
+ private void loadOfflineRegions() {
+ OfflineManager.getInstance(this).listOfflineRegions(new OfflineManager.ListOfflineRegionsCallback() {
+ @Override
+ public void onList(OfflineRegion[] offlineRegions) {
+ if (offlineRegions != null && offlineRegions.length > 0) {
+ adapter.setOfflineRegions(Arrays.asList(offlineRegions));
+ }
+ }
+
+ @Override
+ public void onError(String error) {
+ Toast.makeText(UpdateMetadataActivity.this, "Error loading regions " + error, Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (mapView != null) {
+ mapView.onPause();
+ mapView.onStop();
+ mapView.onDestroy();
+ }
+ }
+
+ private static class OfflineRegionMetadataAdapter extends BaseAdapter {
+
+ private Context context;
+ private List<OfflineRegion> offlineRegions;
+
+ OfflineRegionMetadataAdapter(Context ctx) {
+ context = ctx;
+ offlineRegions = new ArrayList<>();
+ }
+
+ void setOfflineRegions(List<OfflineRegion> offlineRegions) {
+ this.offlineRegions = offlineRegions;
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public int getCount() {
+ return offlineRegions.size();
+ }
+
+ @Override
+ public OfflineRegion getItem(int position) {
+ return offlineRegions.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ ViewHolder holder;
+
+ if (convertView == null) {
+ holder = new ViewHolder();
+ convertView = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false);
+ holder.text = (TextView) convertView.findViewById(android.R.id.text1);
+ convertView.setTag(holder);
+ } else {
+ holder = (ViewHolder) convertView.getTag();
+ }
+
+ holder.text.setText(OfflineUtils.convertRegionName(getItem(position).getMetadata()));
+ return convertView;
+ }
+
+ static class ViewHolder {
+ TextView text;
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestActivity.java
new file mode 100644
index 0000000000..54e8ae602e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestActivity.java
@@ -0,0 +1,365 @@
+package com.mapbox.mapboxsdk.maps.activity.render;
+
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.Environment;
+import android.support.annotation.Nullable;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Gravity;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+import okio.BufferedSource;
+import okio.Okio;
+import timber.log.Timber;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.WeakReference;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Activity that generates map snapshots based on the node render test suite.
+ */
+public class RenderTestActivity extends AppCompatActivity {
+
+ private static final String TEST_BASE_PATH = "integration";
+ private static final String RENDER_TEST_BASE_PATH = TEST_BASE_PATH + "/render-tests";
+
+ // We additionally read out excluded tests from `/platform/node/test/ignore.json`
+ private static final List<String> EXCLUDED_TESTS = new ArrayList<String>() {
+ {
+ add("overlay,background-opacity");
+ add("collision-lines-pitched,debug");
+ add("1024-circle,extent");
+ add("empty,empty");
+ add("rotation-alignment-map,icon-pitch-scaling");
+ add("rotation-alignment-viewport,icon-pitch-scaling");
+ add("pitch15,line-pitch");
+ add("pitch30,line-pitch");
+ add("line-placement-true-pitched,text-keep-upright");
+ add("180,raster-rotation");
+ add("45,raster-rotation");
+ add("90,raster-rotation");
+ add("overlapping,raster-masking");
+ add("missing,raster-loading");
+ add("pitchAndBearing,line-pitch");
+ }
+ };
+
+ private final Map<RenderTestDefinition, Bitmap> renderResultMap = new HashMap<>();
+ private List<RenderTestDefinition> renderTestDefinitions;
+ private OnRenderTestCompletionListener onRenderTestCompletionListener;
+ private MapSnapshotter mapSnapshotter;
+ private ImageView imageView;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(imageView = new ImageView(RenderTestActivity.this));
+ imageView.setLayoutParams(new FrameLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER)
+ );
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mapSnapshotter != null) {
+ mapSnapshotter.cancel();
+ }
+ }
+
+ //
+ // Loads the ignore tests from assets folder
+ //
+ private static class LoadRenderIgnoreTask extends AsyncTask<Void, Void, List<String>> {
+
+ private WeakReference<RenderTestActivity> renderTestActivityWeakReference;
+
+ LoadRenderIgnoreTask(RenderTestActivity renderTestActivity) {
+ this.renderTestActivityWeakReference = new WeakReference<>(renderTestActivity);
+ }
+
+ @Override
+ protected List<String> doInBackground(Void... voids) {
+ return loadIgnoreList(renderTestActivityWeakReference.get().getAssets());
+ }
+
+ @Override
+ protected void onPostExecute(List<String> strings) {
+ super.onPostExecute(strings);
+ if (renderTestActivityWeakReference.get() != null) {
+ renderTestActivityWeakReference.get().onLoadIgnoreList(strings);
+ }
+ }
+ }
+
+
+ //
+ // Loads the render test definitions from assets folder
+ //
+ private static class LoadRenderDefinitionTask extends AsyncTask<Void, Void, List<RenderTestDefinition>> {
+
+ private WeakReference<RenderTestActivity> renderTestActivityWeakReference;
+
+ LoadRenderDefinitionTask(RenderTestActivity renderTestActivity) {
+ this.renderTestActivityWeakReference = new WeakReference<>(renderTestActivity);
+ }
+
+ @Override
+ protected List<RenderTestDefinition> doInBackground(Void... voids) {
+ List<RenderTestDefinition> definitions = new ArrayList<>();
+ AssetManager assetManager = renderTestActivityWeakReference.get().getAssets();
+ String[] categories = new String[0];
+ try {
+ categories = assetManager.list(RENDER_TEST_BASE_PATH);
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ for (int counter = categories.length - 1; counter >= 0; counter--) {
+ try {
+ String[] tests = assetManager.list(String.format("%s/%s", RENDER_TEST_BASE_PATH, categories[counter]));
+ for (String test : tests) {
+ String styleJson = loadStyleJson(assetManager, categories[counter], test);
+ RenderTestStyleDefinition renderTestStyleDefinition = new Gson()
+ .fromJson(styleJson, RenderTestStyleDefinition.class);
+ RenderTestDefinition definition = new RenderTestDefinition(
+ categories[counter], test, styleJson, renderTestStyleDefinition);
+ if (!definition.hasOperations()) {
+ if (!EXCLUDED_TESTS.contains(definition.getName() + "," + definition.getCategory())) {
+ definitions.add(definition);
+ }
+ } else {
+ Timber.e("could not add test, test requires operations: %s from %s", test, categories[counter]);
+ }
+ }
+ } catch (Exception exception) {
+ Timber.e(exception);
+ }
+ }
+ return definitions;
+ }
+
+ @Override
+ protected void onPostExecute(List<RenderTestDefinition> renderTestDefinitions) {
+ super.onPostExecute(renderTestDefinitions);
+ RenderTestActivity renderTestActivity = renderTestActivityWeakReference.get();
+ if (renderTestActivity != null) {
+ renderTestActivity.startRenderTests(renderTestDefinitions);
+ }
+ }
+ }
+
+ private static List<String> loadIgnoreList(AssetManager assets) {
+ List<String> ignores = new ArrayList<>();
+ try (InputStream input = assets.open(String.format("%s/ignores.json", TEST_BASE_PATH))) {
+ BufferedSource source = Okio.buffer(Okio.source(input));
+ String styleJson = source.readByteString().string(Charset.forName("utf-8"));
+ JsonObject object = new Gson().fromJson(styleJson, JsonObject.class);
+ for (Map.Entry<String, JsonElement> stringJsonElementEntry : object.entrySet()) {
+ String[] parts = stringJsonElementEntry.getKey().split("/");
+ ignores.add(String.format("%s,%s", parts[2], parts[1]));
+ }
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ return ignores;
+ }
+
+ private static String loadStyleJson(AssetManager assets, String category, String test) {
+ String styleJson = null;
+ try (InputStream input = assets.open(String.format("%s/%s/%s/style.json", RENDER_TEST_BASE_PATH, category, test))) {
+ BufferedSource source = Okio.buffer(Okio.source(input));
+ styleJson = source.readByteString().string(Charset.forName("utf-8"));
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ return styleJson;
+ }
+
+ private void startRenderTests(List<RenderTestDefinition> renderTestDefinitions) {
+ this.renderTestDefinitions = renderTestDefinitions;
+ if (!renderTestDefinitions.isEmpty()) {
+ render(renderTestDefinitions.get(0), renderTestDefinitions.size());
+ } else {
+ // no definitions, finish test without rendering
+ onRenderTestCompletionListener.onFinish();
+ }
+ }
+
+ private void render(final RenderTestDefinition renderTestDefinition, final int testSize) {
+ Timber.d("Render test %s,%s", renderTestDefinition.getName(), renderTestDefinition.getCategory());
+ mapSnapshotter = new RenderTestSnapshotter(this, renderTestDefinition.toOptions());
+ mapSnapshotter.start(result -> {
+ Bitmap snapshot = result.getBitmap();
+ imageView.setImageBitmap(snapshot);
+ renderResultMap.put(renderTestDefinition, snapshot);
+ if (renderResultMap.size() != testSize) {
+ continueTesting(renderTestDefinition);
+ } else {
+ finishTesting();
+ }
+ }, error -> Timber.e(error));
+ }
+
+ private void continueTesting(RenderTestDefinition renderTestDefinition) {
+ int next = renderTestDefinitions.indexOf(renderTestDefinition) + 1;
+ Timber.d("Next test: %s / %s", next, renderTestDefinitions.size());
+ render(renderTestDefinitions.get(next), renderTestDefinitions.size());
+ }
+
+ private void finishTesting() {
+ new SaveResultToDiskTask(onRenderTestCompletionListener, renderResultMap).execute();
+ }
+
+ //
+ // Save tests results to disk
+ //
+ private static class SaveResultToDiskTask extends AsyncTask<Void, Void, Void> {
+
+ private OnRenderTestCompletionListener onRenderTestCompletionListener;
+ private Map<RenderTestDefinition, Bitmap> renderResultMap;
+
+ SaveResultToDiskTask(OnRenderTestCompletionListener onRenderTestCompletionListener,
+ Map<RenderTestDefinition, Bitmap> renderResultMap) {
+ this.onRenderTestCompletionListener = onRenderTestCompletionListener;
+ this.renderResultMap = renderResultMap;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ if (isExternalStorageWritable()) {
+ try {
+ File testResultDir = FileUtils.createTestResultRootFolder();
+ String basePath = testResultDir.getAbsolutePath();
+ for (Map.Entry<RenderTestDefinition, Bitmap> testResult : renderResultMap.entrySet()) {
+ writeResultToDisk(basePath, testResult);
+ }
+ } catch (final Exception exception) {
+ Timber.e(exception);
+ }
+ }
+ return null;
+ }
+
+ private void writeResultToDisk(String path, Map.Entry<RenderTestDefinition, Bitmap> result) {
+ RenderTestDefinition definition = result.getKey();
+ String categoryName = definition.getCategory();
+ String categoryPath = String.format("%s/%s", path, categoryName);
+ FileUtils.createCategoryDirectory(categoryPath);
+ String testName = result.getKey().getName();
+ String testDir = FileUtils.createTestDirectory(categoryPath, testName);
+ FileUtils.writeTestResultToDisk(testDir, result.getValue());
+ }
+
+ private boolean isExternalStorageWritable() {
+ return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
+ }
+
+ @Override
+ protected void onPostExecute(Void aVoid) {
+ super.onPostExecute(aVoid);
+ if (onRenderTestCompletionListener != null) {
+ onRenderTestCompletionListener.onFinish();
+ }
+ }
+ }
+
+ //
+ // Callback configuration to notify test executor of test finishing
+ //
+ public interface OnRenderTestCompletionListener {
+ void onFinish();
+ }
+
+ public void setOnRenderTestCompletionListener(OnRenderTestCompletionListener listener) {
+ this.onRenderTestCompletionListener = listener;
+ new LoadRenderIgnoreTask(this).execute();
+ }
+
+ public void onLoadIgnoreList(List<String> ignoreList) {
+ Timber.e("We loaded %s amount of tests to be ignored", ignoreList.size());
+ EXCLUDED_TESTS.addAll(ignoreList);
+ new LoadRenderDefinitionTask(this).execute();
+ }
+
+ //
+ // FileUtils
+ //
+
+ private static class FileUtils {
+
+ private static void createCategoryDirectory(String catPath) {
+ File testResultDir = new File(catPath);
+ if (testResultDir.exists()) {
+ return;
+ }
+
+ if (!testResultDir.mkdirs()) {
+ throw new RuntimeException("can't create root test directory");
+ }
+ }
+
+ private static File createTestResultRootFolder() {
+ File testResultDir = new File(Environment.getExternalStorageDirectory()
+ + File.separator + "mapbox" + File.separator + "render");
+ if (testResultDir.exists()) {
+ // cleanup old files
+ deleteRecursive(testResultDir);
+ }
+
+ if (!testResultDir.mkdirs()) {
+ throw new RuntimeException("can't create root test directory");
+ }
+ return testResultDir;
+ }
+
+ private static void deleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory()) {
+ File[] files = fileOrDirectory.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ deleteRecursive(file);
+ }
+ }
+ }
+
+ if (!fileOrDirectory.delete()) {
+ Timber.e("can't delete directory");
+ }
+ }
+
+ private static String createTestDirectory(String basePath, String testName) {
+ File testDir = new File(basePath + "/" + testName);
+ if (!testDir.exists()) {
+ if (!testDir.mkdir()) {
+ throw new RuntimeException("can't create sub directory for " + testName);
+ }
+ }
+ return testDir.getAbsolutePath();
+ }
+
+ private static void writeTestResultToDisk(String testPath, Bitmap testResult) {
+ String filePath = testPath + "/actual.png";
+ try (FileOutputStream out = new FileOutputStream(filePath)) {
+ testResult.compress(Bitmap.CompressFormat.PNG, 100, out);
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestDefinition.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestDefinition.java
new file mode 100644
index 0000000000..49d0d4220a
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestDefinition.java
@@ -0,0 +1,91 @@
+package com.mapbox.mapboxsdk.maps.activity.render;
+
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+
+public class RenderTestDefinition {
+
+ private static final int DEFAULT_WIDTH = 512;
+ private static final int DEFAULT_HEIGHT = 512;
+
+ private String category; // eg. background-color
+ private String name; // eg. colorSpace-hcl
+ private String styleJson;
+ private RenderTestStyleDefinition definition;
+
+ RenderTestDefinition(String category, String name, String styleJson, RenderTestStyleDefinition definition) {
+ this.category = category;
+ this.name = name;
+ this.styleJson = styleJson;
+ this.definition = definition;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getCategory() {
+ return category;
+ }
+
+ public int getWidth() {
+ RenderTestStyleDefinition.Test test = getTest();
+ if (test != null) {
+ Integer testWidth = test.getWidth();
+ if (testWidth != null && testWidth > 0) {
+ return testWidth;
+ }
+ }
+ return DEFAULT_WIDTH;
+ }
+
+ public int getHeight() {
+ RenderTestStyleDefinition.Test test = getTest();
+ if (test != null) {
+ Integer testHeight = test.getHeight();
+ if (testHeight != null && testHeight > 0) {
+ return testHeight;
+ }
+ }
+ return DEFAULT_HEIGHT;
+ }
+
+ public float getPixelRatio() {
+ RenderTestStyleDefinition.Test test = getTest();
+ if (test != null) {
+ Float pixelRatio = test.getPixelRatio();
+ if (pixelRatio != null && pixelRatio > 0) {
+ return pixelRatio;
+ }
+ }
+ return 1;
+ }
+
+ public String getStyleJson() {
+ return styleJson;
+ }
+
+ public boolean hasOperations() {
+ return getTest().getOperations() != null;
+ }
+
+ public RenderTestStyleDefinition.Test getTest() {
+ return definition.getMetadata().getTest();
+ }
+
+ public MapSnapshotter.Options toOptions() {
+ return new MapSnapshotter
+ .Options(getWidth(), getHeight())
+ .withStyleJson(styleJson)
+ .withPixelRatio(getPixelRatio())
+ .withLogo(false);
+ }
+
+ @Override
+ public String toString() {
+ return "RenderTestDefinition{"
+ + "category='" + category + '\''
+ + ", name='" + name + '\''
+ + ", styleJson='" + styleJson + '\''
+ + '}';
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestSnapshotter.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestSnapshotter.java
new file mode 100644
index 0000000000..32d9453aca
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestSnapshotter.java
@@ -0,0 +1,18 @@
+package com.mapbox.mapboxsdk.maps.activity.render;
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+
+public class RenderTestSnapshotter extends MapSnapshotter {
+
+ RenderTestSnapshotter(@NonNull Context context, @NonNull Options options) {
+ super(context, options);
+ }
+
+ @Override
+ protected void addOverlay(MapSnapshot mapSnapshot) {
+ // don't add an overlay
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestStyleDefinition.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestStyleDefinition.java
new file mode 100644
index 0000000000..d8b64dd2ad
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/render/RenderTestStyleDefinition.java
@@ -0,0 +1,131 @@
+package com.mapbox.mapboxsdk.maps.activity.render;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RenderTestStyleDefinition {
+
+ private Integer version;
+ private Metadata metadata;
+ private Map<String, Object> additionalProperties = new HashMap<String, Object>();
+
+ public Integer getVersion() {
+ return version;
+ }
+
+ public void setVersion(Integer version) {
+ this.version = version;
+ }
+
+ public Metadata getMetadata() {
+ return metadata;
+ }
+
+ public void setMetadata(Metadata metadata) {
+ this.metadata = metadata;
+ }
+
+ public Map<String, Object> getAdditionalProperties() {
+ return this.additionalProperties;
+ }
+
+ public void setAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ }
+
+ public class Metadata {
+
+ private Test test;
+ private Map<String, Object> additionalProperties = new HashMap<String, Object>();
+
+ public Test getTest() {
+ return test;
+ }
+
+ public void setTest(Test test) {
+ this.test = test;
+ }
+
+ public Map<String, Object> getAdditionalProperties() {
+ return this.additionalProperties;
+ }
+
+ public void setAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ }
+ }
+
+ public class Test {
+
+ private Integer width;
+ private Integer height;
+ private Float pixelRatio;
+ private List<Integer> center = null;
+ private Integer zoom;
+ private Double diff;
+ private List<List<String>> operations = null;
+ private Map<String, Object> additionalProperties = new HashMap<String, Object>();
+
+ public Integer getWidth() {
+ return width;
+ }
+
+ public void setWidth(Integer width) {
+ this.width = width;
+ }
+
+ public Integer getHeight() {
+ return height;
+ }
+
+ public void setHeight(Integer height) {
+ this.height = height;
+ }
+
+ public Float getPixelRatio() {
+ return pixelRatio;
+ }
+
+ public List<Integer> getCenter() {
+ return center;
+ }
+
+ public void setCenter(List<Integer> center) {
+ this.center = center;
+ }
+
+ public Integer getZoom() {
+ return zoom;
+ }
+
+ public void setZoom(Integer zoom) {
+ this.zoom = zoom;
+ }
+
+ public Double getDiff() {
+ return diff;
+ }
+
+ public void setDiff(Double diff) {
+ this.diff = diff;
+ }
+
+ public List<List<String>> getOperations() {
+ return operations;
+ }
+
+ public void setOperations(List<List<String>> operations) {
+ this.operations = operations;
+ }
+
+ public Map<String, Object> getAdditionalProperties() {
+ return this.additionalProperties;
+ }
+
+ public void setAdditionalProperty(String name, Object value) {
+ this.additionalProperties.put(name, value);
+ }
+
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterActivity.java
new file mode 100644
index 0000000000..e159878a39
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterActivity.java
@@ -0,0 +1,126 @@
+package com.mapbox.mapboxsdk.maps.activity.snapshot;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.ViewTreeObserver;
+import android.widget.GridLayout;
+import android.widget.ImageView;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Random;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showing how to use a the {@link com.mapbox.mapboxsdk.snapshotter.MapSnapshotter}
+ */
+public class MapSnapshotterActivity extends AppCompatActivity {
+
+ private GridLayout grid;
+ private List<MapSnapshotter> snapshotters = new ArrayList<>();
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_snapshotter);
+
+ // Find the grid view and start snapshotting as soon
+ // as the view is measured
+ grid = (GridLayout) findViewById(R.id.snapshot_grid);
+ grid.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ //noinspection deprecation
+ grid.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ addSnapshots();
+ }
+ });
+ }
+
+ private void addSnapshots() {
+ Timber.i("Creating snapshotters");
+
+ for (int row = 0; row < grid.getRowCount(); row++) {
+ for (int column = 0; column < grid.getColumnCount(); column++) {
+ startSnapShot(row, column);
+ }
+ }
+ }
+
+ private void startSnapShot(final int row, final int column) {
+
+ // Define the dimensions
+ MapSnapshotter.Options options = new MapSnapshotter.Options(
+ grid.getMeasuredWidth() / grid.getColumnCount(),
+ grid.getMeasuredHeight() / grid.getRowCount()
+ )
+ // Optionally the pixel ratio
+ .withPixelRatio(1)
+
+ // Optionally the style
+ .withStyle((column + row) % 2 == 0 ? Style.MAPBOX_STREETS : Style.DARK);
+
+ // Optionally the visible region
+ if (row % 2 == 0) {
+ options.withRegion(new LatLngBounds.Builder()
+ .include(new LatLng(randomInRange(-80, 80), randomInRange(-160, 160)))
+ .include(new LatLng(randomInRange(-80, 80), randomInRange(-160, 160)))
+ .build()
+ );
+ }
+
+ // Optionally the camera options
+ if (column % 2 == 0) {
+ options.withCameraPosition(new CameraPosition.Builder()
+ .target(options.getRegion() != null
+ ? options.getRegion().getCenter()
+ : new LatLng(randomInRange(-80, 80), randomInRange(-160, 160)))
+ .bearing(randomInRange(0, 360))
+ .tilt(randomInRange(0, 60))
+ .zoom(randomInRange(0, 20))
+ .build()
+ );
+ }
+
+ MapSnapshotter snapshotter = new MapSnapshotter(MapSnapshotterActivity.this, options);
+
+ snapshotter.start(snapshot -> {
+ Timber.i("Got the snapshot");
+ ImageView imageView = new ImageView(MapSnapshotterActivity.this);
+ imageView.setImageBitmap(snapshot.getBitmap());
+ grid.addView(
+ imageView,
+ new GridLayout.LayoutParams(GridLayout.spec(row), GridLayout.spec(column))
+ );
+ });
+ snapshotters.add(snapshotter);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ // Make sure to stop the snapshotters on pause
+ for (MapSnapshotter snapshotter : snapshotters) {
+ snapshotter.cancel();
+ }
+ snapshotters.clear();
+ }
+
+ private static Random random = new Random();
+
+ public static float randomInRange(float min, float max) {
+ return (random.nextFloat() * (max - min)) + min;
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterLocalStyleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterLocalStyleActivity.java
new file mode 100644
index 0000000000..e2426c8adf
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterLocalStyleActivity.java
@@ -0,0 +1,71 @@
+package com.mapbox.mapboxsdk.maps.activity.snapshot;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+import timber.log.Timber;
+
+import java.io.IOException;
+
+/**
+ * Test activity showing how to use a the MapSnapshotter with a local style
+ */
+public class MapSnapshotterLocalStyleActivity extends AppCompatActivity
+ implements MapSnapshotter.SnapshotReadyCallback {
+
+ private MapSnapshotter mapSnapshotter;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_snapshotter_marker);
+
+ final View container = findViewById(R.id.container);
+ container.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ //noinspection deprecation
+ container.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+
+ String styleJson;
+ try {
+ styleJson = ResourceUtils.readRawResource(MapSnapshotterLocalStyleActivity.this, R.raw.sat_style);
+ } catch (IOException exception) {
+ throw new RuntimeException(exception);
+ }
+
+ Timber.i("Starting snapshot");
+ mapSnapshotter = new MapSnapshotter(
+ getApplicationContext(),
+ new MapSnapshotter
+ .Options(Math.min(container.getMeasuredWidth(), 1024), Math.min(container.getMeasuredHeight(), 1024))
+ .withStyleJson(styleJson)
+ .withCameraPosition(new CameraPosition.Builder().target(new LatLng(52.090737, 5.121420)).zoom(18).build())
+ );
+ mapSnapshotter.start(MapSnapshotterLocalStyleActivity.this, error -> Timber.e(error));
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapSnapshotter.cancel();
+ }
+
+ @Override
+ public void onSnapshotReady(MapSnapshot snapshot) {
+ Timber.i("Snapshot ready");
+ ImageView imageView = (ImageView) findViewById(R.id.snapshot_image);
+ imageView.setImageBitmap(snapshot.getBitmap());
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterMarkerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterMarkerActivity.java
new file mode 100644
index 0000000000..1ff35fd13c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterMarkerActivity.java
@@ -0,0 +1,96 @@
+package com.mapbox.mapboxsdk.maps.activity.snapshot;
+
+import android.annotation.SuppressLint;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewTreeObserver;
+import android.widget.ImageView;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+import com.mapbox.mapboxsdk.testapp.R;
+import timber.log.Timber;
+
+/**
+ * Test activity showing how to use a the {@link MapSnapshotter} and overlay
+ * {@link android.graphics.Bitmap}s on top.
+ */
+public class MapSnapshotterMarkerActivity extends AppCompatActivity implements MapSnapshotter.SnapshotReadyCallback {
+
+ private MapSnapshotter mapSnapshotter;
+ private MapSnapshot mapSnapshot;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_snapshotter_marker);
+
+ final View container = findViewById(R.id.container);
+ container.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ //noinspection deprecation
+ container.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+
+ Timber.i("Starting snapshot");
+
+ mapSnapshotter = new MapSnapshotter(
+ getApplicationContext(),
+ new MapSnapshotter
+ .Options(Math.min(container.getMeasuredWidth(), 1024), Math.min(container.getMeasuredHeight(), 1024))
+ .withStyle(Style.OUTDOORS)
+ .withCameraPosition(new CameraPosition.Builder().target(new LatLng(52.090737, 5.121420)).zoom(15).build())
+ );
+ mapSnapshotter.start(MapSnapshotterMarkerActivity.this);
+ }
+ });
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapSnapshotter.cancel();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public void onSnapshotReady(MapSnapshot snapshot) {
+ Timber.i("Snapshot ready");
+ ImageView imageView = (ImageView) findViewById(R.id.snapshot_image);
+ Bitmap image = addMarker(snapshot);
+ imageView.setImageBitmap(image);
+ imageView.setOnTouchListener((v, event) -> {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ LatLng latLng = snapshot.latLngForPixel(new PointF(event.getX(), event.getY()));
+ Timber.e("Clicked LatLng is %s", latLng);
+ return true;
+ }
+ return false;
+ });
+ }
+
+ private Bitmap addMarker(MapSnapshot snapshot) {
+ Canvas canvas = new Canvas(snapshot.getBitmap());
+ Bitmap marker = BitmapFactory.decodeResource(getResources(), R.drawable.mapbox_marker_icon_default, null);
+ // Dom toren
+ PointF markerLocation = snapshot.pixelForLatLng(new LatLng(52.090649433011315, 5.121310651302338));
+ canvas.drawBitmap(marker,
+ /* Subtract half of the width so we center the bitmap correctly */
+ markerLocation.x - marker.getWidth() / 2,
+ /* Subtract half of the height so we align the bitmap bottom correctly */
+ markerLocation.y - marker.getHeight() / 2,
+ null
+ );
+ return snapshot.getBitmap();
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterReuseActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterReuseActivity.java
new file mode 100644
index 0000000000..c2b9580b90
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/snapshot/MapSnapshotterReuseActivity.java
@@ -0,0 +1,106 @@
+package com.mapbox.mapboxsdk.maps.activity.snapshot;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+import android.widget.ImageView;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshot;
+import com.mapbox.mapboxsdk.snapshotter.MapSnapshotter;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.Random;
+
+/**
+ * Test activity showing how to use a the {@link MapSnapshotter}
+ */
+public class MapSnapshotterReuseActivity extends AppCompatActivity implements MapSnapshotter.SnapshotReadyCallback {
+
+ private MapSnapshotter mapSnapshotter;
+ private View fab;
+
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_map_snapshotter_reuse);
+
+ fab = findViewById(R.id.fab);
+ fab.setVisibility(View.INVISIBLE);
+ fab.setOnClickListener(v -> {
+ fab.setVisibility(View.INVISIBLE);
+
+ mapSnapshotter.setStyleUrl(getRandomStyle());
+ if (random.nextInt(2) == 0) {
+ mapSnapshotter.setCameraPosition(getRandomCameraPosition());
+ } else {
+ mapSnapshotter.setRegion(getRandomBounds());
+ }
+ if (random.nextInt(2) == 0) {
+ mapSnapshotter.setSize(512, 512);
+ } else {
+ mapSnapshotter.setSize(256, 256);
+ }
+ mapSnapshotter.start(MapSnapshotterReuseActivity.this);
+ });
+
+ mapSnapshotter = new MapSnapshotter(
+ getApplicationContext(),
+ new MapSnapshotter.Options(512, 512)
+ );
+
+ mapSnapshotter.start(MapSnapshotterReuseActivity.this);
+ }
+
+ @Override
+ public void onSnapshotReady(MapSnapshot snapshot) {
+ fab.setVisibility(View.VISIBLE);
+ ImageView imageView = (ImageView) findViewById(R.id.snapshot_image);
+ imageView.setImageBitmap(snapshot.getBitmap());
+ }
+
+ private LatLngBounds getRandomBounds() {
+ return LatLngBounds.from(
+ randomInRange(-5, 5),
+ randomInRange(-5, 5),
+ randomInRange(5, 10),
+ randomInRange(5, 10)
+ );
+ }
+
+ private CameraPosition getRandomCameraPosition() {
+ return new CameraPosition.Builder()
+ .target(new LatLng(randomInRange(-80, 80), randomInRange(-160, 160)))
+ .zoom(randomInRange(2, 10))
+ .bearing(randomInRange(0, 90))
+ .build();
+ }
+
+ public String getRandomStyle() {
+ switch (random.nextInt(5)) {
+ case 0:
+ return Style.DARK;
+ case 1:
+ return Style.LIGHT;
+ case 2:
+ return Style.MAPBOX_STREETS;
+ case 3:
+ return Style.OUTDOORS;
+ case 4:
+ return Style.SATELLITE_STREETS;
+ default:
+ return Style.TRAFFIC_DAY;
+ }
+ }
+
+ private static Random random = new Random();
+
+ public static float randomInRange(float min, float max) {
+ return (random.nextFloat() * (max - min)) + min;
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/storage/UrlTransformActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/storage/UrlTransformActivity.java
new file mode 100644
index 0000000000..831e809cfd
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/storage/UrlTransformActivity.java
@@ -0,0 +1,100 @@
+package com.mapbox.mapboxsdk.maps.activity.storage;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.storage.FileSource;
+import com.mapbox.mapboxsdk.storage.Resource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing the url transform
+ */
+public class UrlTransformActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ /**
+ * Be sure to use an isolated class so the activity is not leaked when
+ * the activity goes out of scope (static class in this case).
+ * <p>
+ * Alternatively, unregister the callback in {@link Activity#onDestroy()}
+ */
+ private static final class Transform implements FileSource.ResourceTransformCallback {
+ @Override
+ public String onURL(@Resource.Kind int kind, String url) {
+ Timber.i("[%s] Could be rewriting %s", Thread.currentThread().getName(), url);
+ return url;
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_data_driven_style);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+
+ // Get a handle to the file source and set the resource transform
+ FileSource.getInstance(UrlTransformActivity.this).setResourceTransform(new Transform());
+
+ mapView.getMapAsync(map -> {
+ Timber.i("Map loaded");
+ mapboxMap = map;
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ // Example of how to reset the transform callback
+ FileSource.getInstance(UrlTransformActivity.this).setResourceTransform(null);
+
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/AnimatedImageSourceActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/AnimatedImageSourceActivity.java
new file mode 100644
index 0000000000..741fa89d23
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/AnimatedImageSourceActivity.java
@@ -0,0 +1,147 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.Mapbox;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.geometry.LatLngQuad;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.layers.RasterLayer;
+import com.mapbox.mapboxsdk.style.sources.ImageSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showing how to use a series of images to create an animation
+ * with an ImageSource
+ * <p>
+ * GL-native equivalent of https://www.mapbox.com/mapbox-gl-js/example/animate-images/
+ * </p>
+ */
+public class AnimatedImageSourceActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final String ID_IMAGE_SOURCE = "animated_image_source";
+ private static final String ID_IMAGE_LAYER = "animated_image_layer";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private Handler handler;
+ private Runnable runnable;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_animated_image_source);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(@NonNull final MapboxMap map) {
+ mapboxMap = map;
+
+ // add source
+ LatLngQuad quad = new LatLngQuad(
+ new LatLng(46.437, -80.425),
+ new LatLng(46.437, -71.516),
+ new LatLng(37.936, -71.516),
+ new LatLng(37.936, -80.425));
+ mapboxMap.addSource(new ImageSource(ID_IMAGE_SOURCE, quad, R.drawable.southeast_radar_0));
+
+ // add layer
+ RasterLayer layer = new RasterLayer(ID_IMAGE_LAYER, ID_IMAGE_SOURCE);
+ mapboxMap.addLayer(layer);
+
+ // loop refresh geojson
+ handler = new Handler();
+ runnable = new RefreshImageRunnable(mapboxMap, handler);
+ handler.postDelayed(runnable, 100);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ handler.removeCallbacks(runnable);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ private static class RefreshImageRunnable implements Runnable {
+
+ private MapboxMap mapboxMap;
+ private Handler handler;
+ private Bitmap[] drawables;
+ private int drawableIndex;
+
+ Bitmap getBitmap(int resourceId) {
+ Context context = Mapbox.getApplicationContext();
+ Drawable drawable = ContextCompat.getDrawable(context, resourceId);
+ if (drawable instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ return bitmapDrawable.getBitmap();
+ }
+ return null;
+ }
+
+ RefreshImageRunnable(MapboxMap mapboxMap, Handler handler) {
+ this.mapboxMap = mapboxMap;
+ this.handler = handler;
+ drawables = new Bitmap[4];
+ drawables[0] = getBitmap(R.drawable.southeast_radar_0);
+ drawables[1] = getBitmap(R.drawable.southeast_radar_1);
+ drawables[2] = getBitmap(R.drawable.southeast_radar_2);
+ drawables[3] = getBitmap(R.drawable.southeast_radar_3);
+ drawableIndex = 1;
+ }
+
+ @Override
+ public void run() {
+ ((ImageSource) mapboxMap.getSource(ID_IMAGE_SOURCE)).setImage(drawables[drawableIndex++]);
+ if (drawableIndex > 3) {
+ drawableIndex = 0;
+ }
+ handler.postDelayed(this, 1000);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/BuildingFillExtrusionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/BuildingFillExtrusionActivity.java
new file mode 100644
index 0000000000..ff138c5025
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/BuildingFillExtrusionActivity.java
@@ -0,0 +1,148 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.expressions.Expression;
+import com.mapbox.mapboxsdk.style.layers.FillExtrusionLayer;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.style.layers.PropertyFactory;
+import com.mapbox.mapboxsdk.style.light.Light;
+import com.mapbox.mapboxsdk.style.light.Position;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionBase;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionHeight;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionOpacity;
+
+/**
+ * Test activity showing 3D buildings with a FillExtrusion Layer
+ */
+public class BuildingFillExtrusionActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private Light light;
+
+ private boolean isMapAnchorLight;
+ private boolean isLowIntensityLight;
+ private boolean isRedColor;
+ private boolean isInitPosition;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_building_layer);
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ setupBuildings();
+ setupLight();
+ });
+ }
+
+ private void setupBuildings() {
+ FillExtrusionLayer fillExtrusionLayer = new FillExtrusionLayer("3d-buildings", "composite");
+ fillExtrusionLayer.setSourceLayer("building");
+ fillExtrusionLayer.setFilter(eq(get("extrude"), literal("true")));
+ fillExtrusionLayer.setMinZoom(15);
+ fillExtrusionLayer.setProperties(
+ fillExtrusionColor(Color.LTGRAY),
+ fillExtrusionHeight(Expression.get("height")),
+ fillExtrusionBase(Expression.get("min_height")),
+ fillExtrusionOpacity(0.9f)
+ );
+ mapboxMap.addLayer(fillExtrusionLayer);
+ }
+
+ private void setupLight() {
+ light = mapboxMap.getLight();
+
+ findViewById(R.id.fabLightPosition).setOnClickListener(v -> {
+ isInitPosition = !isInitPosition;
+ if (isInitPosition) {
+ light.setPosition(new Position(1.5f, 90, 80));
+ } else {
+ light.setPosition(new Position(1.15f, 210, 30));
+ }
+ });
+
+ findViewById(R.id.fabLightColor).setOnClickListener(v -> {
+ isRedColor = !isRedColor;
+ light.setColor(PropertyFactory.colorToRgbaString(isRedColor ? Color.RED : Color.BLUE));
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_building, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (light != null) {
+ int id = item.getItemId();
+ if (id == R.id.menu_action_anchor) {
+ isMapAnchorLight = !isMapAnchorLight;
+ light.setAnchor(isMapAnchorLight ? Property.ANCHOR_MAP : Property.ANCHOR_VIEWPORT);
+ } else if (id == R.id.menu_action_intensity) {
+ isLowIntensityLight = !isLowIntensityLight;
+ light.setIntensity(isLowIntensityLight ? 0.35f : 1.0f);
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CircleLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CircleLayerActivity.java
new file mode 100644
index 0000000000..a26ee9f770
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CircleLayerActivity.java
@@ -0,0 +1,240 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.expressions.Expression;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.layers.LineLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.array;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.has;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+
+/**
+ * Test activity showcasing adding a Circle Layer to the Map
+ * <p>
+ * Uses bus stop data from Singapore as a source and allows to filter into 1 specific route with a line layer.
+ * </p>
+ */
+public class CircleLayerActivity extends AppCompatActivity implements View.OnClickListener {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private FloatingActionButton styleFab;
+ private FloatingActionButton routeFab;
+
+ private CircleLayer layer;
+ private GeoJsonSource source;
+
+ private int currentStyleIndex = 0;
+ private boolean isLoadingStyle = true;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_circle_layer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ addBusStopSource();
+ addBusStopCircleLayer();
+ initFloatingActionButtons();
+ isLoadingStyle = false;
+ });
+ }
+
+ private void addBusStopSource() {
+ try {
+ source = new GeoJsonSource("bus_stop",
+ new URL("https://raw.githubusercontent.com/cheeaun/busrouter-sg/master/data/2/bus-stops.geojson"));
+ } catch (MalformedURLException exception) {
+ Timber.e(exception, "That's not an url... ");
+ }
+ mapboxMap.addSource(source);
+ }
+
+ private void addBusStopCircleLayer() {
+ layer = new CircleLayer("stops_layer", "bus_stop");
+ layer.setProperties(
+ circleColor(Color.parseColor("#FF9800")),
+ circleRadius(2.0f)
+ );
+ mapboxMap.addLayer(layer);
+ }
+
+ private void initFloatingActionButtons() {
+ routeFab = (FloatingActionButton) findViewById(R.id.fab_route);
+ routeFab.setColorFilter(ContextCompat.getColor(CircleLayerActivity.this, R.color.primary));
+ routeFab.setOnClickListener(CircleLayerActivity.this);
+
+ styleFab = (FloatingActionButton) findViewById(R.id.fab_style);
+ styleFab.setOnClickListener(CircleLayerActivity.this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (isLoadingStyle) {
+ return;
+ }
+
+ if (view.getId() == R.id.fab_route) {
+ showBusRoute();
+ } else if (view.getId() == R.id.fab_style) {
+ changeMapStyle();
+ }
+ }
+
+ private void showBusRoute() {
+ removeFabs();
+ applyBusRouteFilterToBusStopSource();
+ addBusRouteSource();
+ addBusRouteLayer();
+ }
+
+ private void removeFabs() {
+ routeFab.setVisibility(View.GONE);
+ styleFab.setVisibility(View.GONE);
+ }
+
+ private void applyBusRouteFilterToBusStopSource() {
+ layer.setFilter(has(Expression.toString(get("number")), array(literal(Data.STOPS_FOR_ROUTE))));
+ }
+
+ private void addBusRouteSource() {
+ try {
+ mapboxMap.addSource(new GeoJsonSource("bus_route",
+ new URL("https://gist.githubusercontent.com/tobrun/7fbc0fe7e9ffea509f7608cda2601d5d/raw/"
+ + "a4b8cc289020f91fa2e1553524820054395e36f5/line.geojson")));
+ } catch (MalformedURLException malformedUrlException) {
+ Timber.e(malformedUrlException, "That's not an url... ");
+ }
+ }
+
+ private void addBusRouteLayer() {
+ LineLayer lineLayer = new LineLayer("route_layer", "bus_route");
+ mapboxMap.addLayerBelow(lineLayer, "stops_layer");
+ mapboxMap.animateCamera(CameraUpdateFactory.newCameraPosition(
+ new CameraPosition.Builder()
+ .target(new LatLng(1.3896777, 103.9874997))
+ .bearing(225)
+ .tilt(45)
+ .zoom(13)
+ .build()
+ ), 1750);
+ }
+
+ private void changeMapStyle() {
+ isLoadingStyle = true;
+ removeBusStop();
+ loadNewStyle();
+ }
+
+ private void removeBusStop() {
+ mapboxMap.removeLayer(layer);
+ mapboxMap.removeSource(source);
+ }
+
+ private void loadNewStyle() {
+ mapboxMap.setStyleUrl(getNextStyle(), style -> {
+ addBusStop();
+ isLoadingStyle = false;
+ });
+ }
+
+ private void addBusStop() {
+ mapboxMap.addLayer(layer);
+ mapboxMap.addSource(source);
+ }
+
+ private String getNextStyle() {
+ currentStyleIndex++;
+ if (currentStyleIndex == Data.STYLES.length) {
+ currentStyleIndex = 0;
+ }
+ return Data.STYLES[currentStyleIndex];
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ private static class Data {
+ private static final String[] STOPS_FOR_ROUTE = new String[] {"99009", "99131", "99049", "99039", "99029", "99019",
+ "98079", "98069", "97099", "97089", "97079", "97069", "97209", "97059", "97049", "97039", "97019", "96069",
+ "96059", "96049", "96099", "96089", "96079", "85079", "85089", "85069", "85059", "85099", "84069", "84059",
+ "84049", "84039", "84029", "84019", "83099", "83079", "83059", "83049", "83029", "82069", "82049", "82029",
+ "82109", "81069", "81049", "81029", "80089", "80069", "80049", "80039", "80029", "01319", "01219", "01129",
+ "01059", "01119", "01019", "04159", "04149", "04229", "04239", "05059", "05049", "05039", "05019", "10589"};
+
+ private static final String[] STYLES = new String[] {
+ Style.MAPBOX_STREETS,
+ Style.OUTDOORS,
+ Style.LIGHT,
+ Style.DARK,
+ Style.SATELLITE,
+ Style.SATELLITE_STREETS
+ };
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CustomSpriteActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CustomSpriteActivity.java
new file mode 100644
index 0000000000..03411dfcc2
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/CustomSpriteActivity.java
@@ -0,0 +1,135 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.BitmapFactory;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+
+/**
+ * Test activity showcasing adding a sprite image and use it in a Symbol Layer
+ */
+public class CustomSpriteActivity extends AppCompatActivity {
+ private static final String CUSTOM_ICON = "custom-icon";
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+ private Layer layer;
+ private GeoJsonSource source;
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_add_sprite);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
+ fab.setColorFilter(ContextCompat.getColor(CustomSpriteActivity.this, R.color.primary));
+ fab.setOnClickListener(new View.OnClickListener() {
+ private Point point;
+
+ @Override
+ public void onClick(View view) {
+ if (point == null) {
+ Timber.i("First click -> Car");
+ // Add an icon to reference later
+ mapboxMap.addImage(CUSTOM_ICON, BitmapFactory.decodeResource(getResources(), R.drawable.ic_car_top));
+
+ // Add a source with a geojson point
+ point = Point.fromLngLat(13.400972d, 52.519003d);
+ source = new GeoJsonSource(
+ "point",
+ FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry(point)})
+ );
+ mapboxMap.addSource(source);
+
+ // Add a symbol layer that references that point source
+ layer = new SymbolLayer("layer", "point");
+ layer.setProperties(
+ // Set the id of the sprite to use
+ iconImage(CUSTOM_ICON)
+ );
+
+ // lets add a circle below labels!
+ mapboxMap.addLayerBelow(layer, "waterway-label");
+
+ fab.setImageResource(R.drawable.ic_directions_car_black);
+ } else {
+ // Update point
+ point = Point.fromLngLat(point.longitude() + 0.001,
+ point.latitude() + 0.001);
+ source.setGeoJson(FeatureCollection.fromFeatures(new Feature[] {Feature.fromGeometry(point)}));
+
+ // Move the camera as well
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLng(new LatLng(
+ point.latitude(), point.longitude())));
+ }
+ }
+ });
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/DataDrivenStyleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/DataDrivenStyleActivity.java
new file mode 100644
index 0000000000..f8644c9bb3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/DataDrivenStyleActivity.java
@@ -0,0 +1,471 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.FillLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.style.sources.Source;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.IOException;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.match;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgba;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.step;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.color;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillAntialias;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOutlineColor;
+
+/**
+ * Test activity showcasing the data driven runtime style API.
+ */
+public class DataDrivenStyleActivity extends AppCompatActivity {
+
+ public static final String AMSTERDAM_PARKS_LAYER = "amsterdam-parks-layer";
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_data_driven_style);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+
+ mapView.getMapAsync(map -> {
+ // Store for later
+ mapboxMap = map;
+
+ // Add a parks layer
+ addParksLayer();
+
+ // Add debug overlay
+ setupDebugZoomView();
+
+ // Center and Zoom (Amsterdam, zoomed to streets)
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(52.379189, 4.899431), 14));
+ });
+ }
+
+ private void setupDebugZoomView() {
+ final TextView textView = (TextView) findViewById(R.id.textZoom);
+ mapboxMap.setOnCameraChangeListener(new MapboxMap.OnCameraChangeListener() {
+ @Override
+ public void onCameraChange(CameraPosition position) {
+ textView.setText(String.format(getString(R.string.debug_zoom), position.zoom));
+ }
+ });
+ }
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_data_driven_style, menu);
+ return true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_add_exponential_zoom_function:
+ addExponentialZoomFunction();
+ return true;
+ case R.id.action_add_interval_zoom_function:
+ addIntervalZoomFunction();
+ return true;
+ case R.id.action_add_categorical_source_function:
+ addCategoricalSourceFunction();
+ return true;
+ case R.id.action_add_exponential_source_function:
+ addExponentialSourceFunction();
+ return true;
+ case R.id.action_add_identity_source_function:
+ addIdentitySourceFunction();
+ return true;
+ case R.id.action_add_interval_source_function:
+ addIntervalSourceFunction();
+ return true;
+ case R.id.action_add_composite_categorical_function:
+ addCompositeCategoricalFunction();
+ return true;
+ case R.id.action_add_composite_exponential_function:
+ addCompositeExponentialFunction();
+ return true;
+ case R.id.action_add_composite_interval_function:
+ addCompositeIntervalFunction();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void addExponentialZoomFunction() {
+ Timber.i("Add exponential zoom function");
+ FillLayer layer = mapboxMap.getLayerAs("water");
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ interpolate(
+ exponential(0.5f), zoom(),
+ stop(1, color(Color.RED)),
+ stop(5, color(Color.BLUE)),
+ stop(10, color(Color.GREEN))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addIntervalZoomFunction() {
+ Timber.i("Add interval zoom function");
+ FillLayer layer = mapboxMap.getLayerAs("water");
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ step(zoom(),
+ rgba(0.0f, 255.0f, 255.0f, 1.0f),
+ stop(1, rgba(255.0f, 0.0f, 0.0f, 1.0f)),
+ stop(5, rgba(0.0f, 0.0f, 255.0f, 1.0f)),
+ stop(10, rgba(0.0f, 255.0f, 0.0f, 1.0f))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addExponentialSourceFunction() {
+ Timber.i("Add exponential source function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ interpolate(
+ exponential(0.5f),
+ get("stroke-width"),
+ stop(1f, rgba(255.0f, 0.0f, 0.0f, 1.0f)),
+ stop(5f, rgba(0.0f, 0.0f, 255.0f, 1.0f)),
+ stop(10f, rgba(0.0f, 255.0f, 0.0f, 1.0f))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addCategoricalSourceFunction() {
+ Timber.i("Add categorical source function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ literal("Jordaan"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ literal("Prinseneiland"), rgba(0.0f, 255.0f, 0.0f, 1.0f),
+ rgba(0.0f, 255.0f, 255.0f, 1.0f)
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addIdentitySourceFunction() {
+ Timber.i("Add identity source function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillOpacity(
+ get("fill-opacity")
+ )
+ );
+
+ Timber.i("Fill opacity: %s", layer.getFillOpacity());
+ }
+
+ private void addIntervalSourceFunction() {
+ Timber.i("Add interval source function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ step(
+ get("stroke-width"),
+ rgba(0.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(255.0f, 0.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(0.0f, 0.0f, 255.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 255.0f, 0.0f, 1.0f))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addCompositeExponentialFunction() {
+ Timber.i("Add composite exponential function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ interpolate(
+ exponential(1f),
+ zoom(),
+ stop(12, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(255.0f, 0.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(0.0f, 0.0f, 0.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 0.0f, 255.0f, 1.0f))
+ )),
+ stop(15, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(255.0f, 255.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(211.0f, 211.0f, 211.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 255.0f, 255.0f, 1.0f))
+ )),
+ stop(18, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(0.0f, 0.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(128.0f, 128.0f, 128.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 255.0f, 0.0f, 1.0f)))
+ )
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addCompositeIntervalFunction() {
+ Timber.i("Add composite interval function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ interpolate(
+ linear(),
+ zoom(),
+ stop(12, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(255.0f, 0.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(0.0f, 0.0f, 0.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 0.0f, 255.0f, 1.0f))
+ )),
+ stop(15, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(255.0f, 255.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(211.0f, 211.0f, 211.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 255.0f, 255.0f, 1.0f))
+ )),
+ stop(18, step(
+ get("stroke-width"),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(1f, rgba(0.0f, 0.0f, 0.0f, 1.0f)),
+ stop(2f, rgba(128.0f, 128.0f, 128.0f, 1.0f)),
+ stop(3f, rgba(0.0f, 255.0f, 0.0f, 1.0f))
+ ))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addCompositeCategoricalFunction() {
+ Timber.i("Add composite categorical function");
+ FillLayer layer = mapboxMap.getLayerAs(AMSTERDAM_PARKS_LAYER);
+ assert layer != null;
+ layer.setProperties(
+ fillColor(
+ step(zoom(),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f),
+ stop(7f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(8f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(9f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(10f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(11f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(12f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(13f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(14f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ literal("Jordaan"), rgba(0.0f, 255.0f, 0.0f, 1.0f),
+ literal("PrinsenEiland"), rgba(0.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(15f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(16f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(17f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(18f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ literal("Jordaan"), rgba(0.0f, 255.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(19f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(20f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(21f, match(
+ get("name"),
+ literal("Westerpark"), rgba(255.0f, 0.0f, 0.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ )),
+ stop(22f, match(
+ get("name"),
+ literal("Westerpark"), rgba(0.0f, 0.0f, 255.0f, 1.0f),
+ rgba(255.0f, 255.0f, 255.0f, 1.0f)
+ ))
+ )
+ )
+ );
+
+ Timber.i("Fill color: %s", layer.getFillColor());
+ }
+
+ private void addParksLayer() {
+ // Add a source
+ Source source;
+ try {
+ source = new GeoJsonSource("amsterdam-parks-source", ResourceUtils.readRawResource(this, R.raw.amsterdam));
+ mapboxMap.addSource(source);
+ } catch (IOException ioException) {
+ Toast.makeText(
+ DataDrivenStyleActivity.this,
+ "Couldn't add source: " + ioException.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ // Add a fill layer
+ mapboxMap.addLayer(new FillLayer(AMSTERDAM_PARKS_LAYER, source.getId())
+ .withProperties(
+ fillColor(color(Color.GREEN)),
+ fillOutlineColor(rgb(0, 0, 255)),
+ fillAntialias(true)
+ )
+ );
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionActivity.java
new file mode 100644
index 0000000000..994394817e
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionActivity.java
@@ -0,0 +1,122 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.camera.CameraPosition;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.FillExtrusionLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.geojson.Polygon;
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionHeight;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillExtrusionOpacity;
+
+/**
+ * Test activity showcasing fill extrusions
+ */
+public class FillExtrusionActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_fill_extrusion_layer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ List<List<Point>> lngLats = Arrays.asList(
+ Arrays.asList(
+ Point.fromLngLat(5.12112557888031, 52.09071040847704),
+ Point.fromLngLat(5.121227502822875, 52.09053901776669),
+ Point.fromLngLat(5.121484994888306, 52.090601641371805),
+ Point.fromLngLat(5.1213884353637695, 52.090766439912635),
+ Point.fromLngLat(5.12112557888031, 52.09071040847704)
+ )
+ );
+
+ Polygon domTower = Polygon.fromLngLats(lngLats);
+
+ GeoJsonSource source = new GeoJsonSource("extrusion-source", domTower);
+ map.addSource(source);
+
+ mapboxMap.addLayer(
+ new FillExtrusionLayer("extrusion-layer", source.getId())
+ .withProperties(
+ fillExtrusionHeight(40f),
+ fillExtrusionOpacity(0.5f),
+ fillExtrusionColor(Color.RED)
+ )
+ );
+
+ mapboxMap.animateCamera(
+ CameraUpdateFactory.newCameraPosition(
+ new CameraPosition.Builder()
+ .target(new LatLng(52.09071040847704, 5.12112557888031))
+ .tilt(45.0)
+ .zoom(18)
+ .build()
+ ),
+ 10000
+ );
+ });
+ }
+
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionStyleTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionStyleTestActivity.java
new file mode 100644
index 0000000000..81a417ad73
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/FillExtrusionStyleTestActivity.java
@@ -0,0 +1,74 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity used for instrumentation tests of fill extrusion.
+ */
+public class FillExtrusionStyleTestActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_extrusion_test);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> FillExtrusionStyleTestActivity.this.mapboxMap = mapboxMap);
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GeoJsonClusteringActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GeoJsonClusteringActivity.java
new file mode 100644
index 0000000000..efcf482eb7
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GeoJsonClusteringActivity.java
@@ -0,0 +1,210 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.view.MenuItem;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.expressions.Expression;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonOptions;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.utils.BitmapUtils;
+import timber.log.Timber;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.division;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.gt;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.gte;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.has;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.lt;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.toNumber;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textIgnorePlacement;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;
+
+/**
+ * Test activity showcasing using a geojson source and visualise that source as a cluster by using filters.
+ */
+public class GeoJsonClusteringActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_geojson_clustering);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ // noinspection ConstantConditions
+ mapView.onCreate(savedInstanceState);
+
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(37.7749, 122.4194), 0));
+ mapboxMap.addImage(
+ "icon-id",
+ BitmapUtils.getBitmapFromDrawable(getResources().getDrawable(R.drawable.ic_hearing_black_24dp)),
+ true
+ );
+ // Add a clustered source with some layers
+ addClusteredGeoJsonSource();
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ onBackPressed();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void addClusteredGeoJsonSource() {
+ // Add a clustered source
+ try {
+ mapboxMap.addSource(
+ new GeoJsonSource("earthquakes",
+ new URL("https://www.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson"),
+ new GeoJsonOptions()
+ .withCluster(true)
+ .withClusterMaxZoom(14)
+ .withClusterRadius(50)
+ )
+ );
+ } catch (MalformedURLException malformedUrlException) {
+ Timber.e(malformedUrlException, "That's not an url... ");
+ }
+
+ // Add unclustered layer
+ int[][] layers = new int[][] {
+ new int[] {150, ResourcesCompat.getColor(getResources(), R.color.redAccent, getTheme())},
+ new int[] {20, ResourcesCompat.getColor(getResources(), R.color.greenAccent, getTheme())},
+ new int[] {0, ResourcesCompat.getColor(getResources(), R.color.blueAccent, getTheme())}
+ };
+
+ SymbolLayer unclustered = new SymbolLayer("unclustered-points", "earthquakes");
+ unclustered.setProperties(
+ iconImage("icon-id"),
+ iconSize(
+ division(
+ get("mag"), literal(4.0f)
+ )
+ ),
+ iconColor(
+ interpolate(exponential(1), get("mag"),
+ stop(2.0, rgb(0, 255, 0)),
+ stop(4.5, rgb(0, 0, 255)),
+ stop(7.0, rgb(255, 0, 0))
+ )
+ )
+ );
+
+ mapboxMap.addLayer(unclustered);
+
+ for (int i = 0; i < layers.length; i++) {
+ // Add some nice circles
+ CircleLayer circles = new CircleLayer("cluster-" + i, "earthquakes");
+ circles.setProperties(
+ circleColor(layers[i][1]),
+ circleRadius(18f)
+ );
+
+ Expression pointCount = toNumber(get("point_count"));
+ circles.setFilter(
+ i == 0
+ ? all(has("point_count"),
+ gte(pointCount, literal(layers[i][0]))
+ ) : all(has("point_count"),
+ gt(pointCount, literal(layers[i][0])),
+ lt(pointCount, literal(layers[i - 1][0]))
+ )
+ );
+ mapboxMap.addLayer(circles);
+ }
+
+ // Add the count labels
+ SymbolLayer count = new SymbolLayer("count", "earthquakes");
+ count.setProperties(
+ textField(Expression.toString(get("point_count"))),
+ textSize(12f),
+ textColor(Color.WHITE),
+ textIgnorePlacement(true),
+ textAllowOverlap(true)
+ );
+ mapboxMap.addLayer(count);
+
+
+ // Zoom out to start
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(1));
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GridSourceActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GridSourceActivity.java
new file mode 100644
index 0000000000..88000cb9eb
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/GridSourceActivity.java
@@ -0,0 +1,152 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.MultiLineString;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.geometry.LatLngBounds;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.layers.LineLayer;
+import com.mapbox.mapboxsdk.style.sources.CustomGeometrySource;
+import com.mapbox.mapboxsdk.style.sources.GeometryTileProvider;
+import com.mapbox.mapboxsdk.testapp.R;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;
+
+/**
+ * Test activity showcasing using CustomGeometrySource to create a grid overlay on the map.
+ */
+public class GridSourceActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final String ID_GRID_SOURCE = "grid_source";
+ private static final String ID_GRID_LAYER = "grid_layer";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ /**
+ * Implementation of GeometryTileProvider that returns features representing a zoom-dependent
+ * grid.
+ */
+ static class GridProvider implements GeometryTileProvider {
+ public FeatureCollection getFeaturesForBounds(LatLngBounds bounds, int zoom) {
+ List<Feature> features = new ArrayList<>();
+ double gridSpacing;
+ if (zoom >= 13) {
+ gridSpacing = 0.01;
+ } else if (zoom >= 11) {
+ gridSpacing = 0.05;
+ } else if (zoom == 10) {
+ gridSpacing = .1;
+ } else if (zoom == 9) {
+ gridSpacing = 0.25;
+ } else if (zoom == 8) {
+ gridSpacing = 0.5;
+ } else if (zoom >= 6) {
+ gridSpacing = 1;
+ } else if (zoom == 5) {
+ gridSpacing = 2;
+ } else if (zoom >= 4) {
+ gridSpacing = 5;
+ } else if (zoom == 2) {
+ gridSpacing = 10;
+ } else {
+ gridSpacing = 20;
+ }
+
+ List gridLines = new ArrayList();
+ for (double y = Math.ceil(bounds.getLatNorth() / gridSpacing) * gridSpacing;
+ y >= Math.floor(bounds.getLatSouth() / gridSpacing) * gridSpacing; y -= gridSpacing) {
+ gridLines.add(Arrays.asList(Point.fromLngLat(bounds.getLonWest(), y),
+ Point.fromLngLat(bounds.getLonEast(), y)));
+ }
+ features.add(Feature.fromGeometry(MultiLineString.fromLngLats(gridLines)));
+
+ gridLines = new ArrayList();
+ for (double x = Math.floor(bounds.getLonWest() / gridSpacing) * gridSpacing;
+ x <= Math.ceil(bounds.getLonEast() / gridSpacing) * gridSpacing; x += gridSpacing) {
+ gridLines.add(Arrays.asList(Point.fromLngLat(x, bounds.getLatSouth()),
+ Point.fromLngLat(x, bounds.getLatNorth())));
+ }
+ features.add(Feature.fromGeometry(MultiLineString.fromLngLats(gridLines)));
+
+ return FeatureCollection.fromFeatures(features);
+ }
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_grid_source);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(@NonNull final MapboxMap map) {
+ mapboxMap = map;
+
+ // add source
+ CustomGeometrySource source = new CustomGeometrySource(ID_GRID_SOURCE, new GridProvider());
+ mapboxMap.addSource(source);
+
+ // add layer
+ LineLayer layer = new LineLayer(ID_GRID_LAYER, ID_GRID_SOURCE);
+ layer.setProperties(
+ lineColor(Color.parseColor("#000000"))
+ );
+
+ mapboxMap.addLayer(layer);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HeatmapLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HeatmapLayerActivity.java
new file mode 100644
index 0000000000..074ff8b036
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HeatmapLayerActivity.java
@@ -0,0 +1,226 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.layers.HeatmapLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.heatmapDensity;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.linear;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgb;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgba;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleStrokeColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleStrokeWidth;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapIntensity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.heatmapWeight;
+
+/**
+ * Test activity showcasing the heatmap layer api.
+ */
+public class HeatmapLayerActivity extends AppCompatActivity {
+
+ private static final String EARTHQUAKE_SOURCE_URL = "https://www.mapbox.com/mapbox-gl-js/assets/earthquakes.geojson";
+ private static final String EARTHQUAKE_SOURCE_ID = "earthquakes";
+ private static final String HEATMAP_LAYER_ID = "earthquakes-heat";
+ private static final String HEATMAP_LAYER_SOURCE = "earthquakes";
+ private static final String CIRCLE_LAYER_ID = "earthquakes-circle";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_heatmaplayer);
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ addEarthquakeSource();
+ addHeatmapLayer();
+ addCircleLayer();
+ });
+ }
+
+ private void addEarthquakeSource() {
+ try {
+ mapboxMap.addSource(new GeoJsonSource(EARTHQUAKE_SOURCE_ID, new URL(EARTHQUAKE_SOURCE_URL)));
+ } catch (MalformedURLException malformedUrlException) {
+ Timber.e(malformedUrlException, "That's not an url... ");
+ }
+ }
+
+ private void addHeatmapLayer() {
+ HeatmapLayer layer = new HeatmapLayer(HEATMAP_LAYER_ID, EARTHQUAKE_SOURCE_ID);
+ layer.setMaxZoom(9);
+ layer.setSourceLayer(HEATMAP_LAYER_SOURCE);
+ layer.setProperties(
+
+ // Color ramp for heatmap. Domain is 0 (low) to 1 (high).
+ // Begin color ramp at 0-stop with a 0-transparancy color
+ // to create a blur-like effect.
+ heatmapColor(
+ interpolate(
+ linear(), heatmapDensity(),
+ literal(0), rgba(33, 102, 172, 0),
+ literal(0.2), rgb(103, 169, 207),
+ literal(0.4), rgb(209, 229, 240),
+ literal(0.6), rgb(253, 219, 199),
+ literal(0.8), rgb(239, 138, 98),
+ literal(1), rgb(178, 24, 43)
+ )
+ ),
+
+ // Increase the heatmap weight based on frequency and property magnitude
+ heatmapWeight(
+ interpolate(
+ linear(), get("mag"),
+ stop(0, 0),
+ stop(6, 1)
+ )
+ ),
+
+ // Increase the heatmap color weight weight by zoom level
+ // heatmap-intensity is a multiplier on top of heatmap-weight
+ heatmapIntensity(
+ interpolate(
+ linear(), zoom(),
+ stop(0, 1),
+ stop(9, 3)
+ )
+ ),
+
+ // Adjust the heatmap radius by zoom level
+ heatmapRadius(
+ interpolate(
+ linear(), zoom(),
+ stop(0, 2),
+ stop(9, 20)
+ )
+ ),
+
+ // Transition from heatmap to circle layer by zoom level
+ heatmapOpacity(
+ interpolate(
+ linear(), zoom(),
+ stop(7, 1),
+ stop(9, 0)
+ )
+ )
+ );
+
+ mapboxMap.addLayerAbove(layer, "waterway-label");
+ }
+
+ private void addCircleLayer() {
+ CircleLayer circleLayer = new CircleLayer(CIRCLE_LAYER_ID, EARTHQUAKE_SOURCE_ID);
+ circleLayer.setProperties(
+
+ // Size circle radius by earthquake magnitude and zoom level
+ circleRadius(
+ interpolate(
+ linear(), zoom(),
+ literal(7), interpolate(
+ linear(), get("mag"),
+ stop(1, 1),
+ stop(6, 4)
+ ),
+ literal(16), interpolate(
+ linear(), get("mag"),
+ stop(1, 5),
+ stop(6, 50)
+ )
+ )
+ ),
+
+ // Color circle by earthquake magnitude
+ circleColor(
+ interpolate(
+ linear(), get("mag"),
+ literal(1), rgba(33, 102, 172, 0),
+ literal(2), rgb(103, 169, 207),
+ literal(3), rgb(209, 229, 240),
+ literal(4), rgb(253, 219, 199),
+ literal(5), rgb(239, 138, 98),
+ literal(6), rgb(178, 24, 43)
+ )
+ ),
+
+ // Transition from heatmap to circle layer by zoom level
+ circleOpacity(
+ interpolate(
+ linear(), zoom(),
+ stop(7, 0),
+ stop(8, 1)
+ )
+ ),
+ circleStrokeColor("white"),
+ circleStrokeWidth(1.0f)
+ );
+
+ mapboxMap.addLayerBelow(circleLayer, HEATMAP_LAYER_ID);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HillshadeLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HillshadeLayerActivity.java
new file mode 100644
index 0000000000..1fab4590b9
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/HillshadeLayerActivity.java
@@ -0,0 +1,84 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.HillshadeLayer;
+import com.mapbox.mapboxsdk.style.sources.RasterDemSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity showcasing using HillshadeLayer.
+ */
+public class HillshadeLayerActivity extends AppCompatActivity {
+
+ private static final String LAYER_ID = "hillshade-layer";
+ private static final String LAYER_BELOW_ID = "waterway-river-canal";
+ private static final String SOURCE_ID = "hillshade-source";
+ private static final String SOURCE_URL = "mapbox://mapbox.terrain-rgb";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_fill_extrusion_layer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+
+ RasterDemSource rasterDemSource = new RasterDemSource(SOURCE_ID, SOURCE_URL);
+ mapboxMap.addSource(rasterDemSource);
+
+ HillshadeLayer hillshadeLayer = new HillshadeLayer(LAYER_ID, SOURCE_ID);
+ mapboxMap.addLayerBelow(hillshadeLayer, LAYER_BELOW_ID);
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RealTimeGeoJsonActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RealTimeGeoJsonActivity.java
new file mode 100644
index 0000000000..c25f97cb7f
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RealTimeGeoJsonActivity.java
@@ -0,0 +1,125 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+
+/**
+ * Test activity showcasing using realtime GeoJSON to move a symbol on your map
+ * <p>
+ * GL-native equivalent of https://www.mapbox.com/mapbox-gl-js/example/live-geojson/
+ * </p>
+ */
+public class RealTimeGeoJsonActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final String ID_GEOJSON_LAYER = "wanderdrone";
+ private static final String ID_GEOJSON_SOURCE = ID_GEOJSON_LAYER;
+ private static final String URL_GEOJSON_SOURCE = "https://wanderdrone.appspot.com/";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ private Handler handler;
+ private Runnable runnable;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_default);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(@NonNull final MapboxMap map) {
+ mapboxMap = map;
+
+ // add source
+ try {
+ mapboxMap.addSource(new GeoJsonSource(ID_GEOJSON_SOURCE, new URL(URL_GEOJSON_SOURCE)));
+ } catch (MalformedURLException malformedUrlException) {
+ Timber.e(malformedUrlException, "Invalid URL");
+ }
+
+ // add layer
+ SymbolLayer layer = new SymbolLayer(ID_GEOJSON_LAYER, ID_GEOJSON_SOURCE);
+ layer.setProperties(iconImage("rocket-15"));
+ mapboxMap.addLayer(layer);
+
+ // loop refresh geojson
+ handler = new Handler();
+ runnable = new RefreshGeoJsonRunnable(mapboxMap, handler);
+ handler.postDelayed(runnable, 2000);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ handler.removeCallbacks(runnable);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ private static class RefreshGeoJsonRunnable implements Runnable {
+
+ private MapboxMap mapboxMap;
+ private Handler handler;
+
+ RefreshGeoJsonRunnable(MapboxMap mapboxMap, Handler handler) {
+ this.mapboxMap = mapboxMap;
+ this.handler = handler;
+ }
+
+ @Override
+ public void run() {
+ ((GeoJsonSource) mapboxMap.getSource(ID_GEOJSON_SOURCE)).setUrl(URL_GEOJSON_SOURCE);
+ handler.postDelayed(this, 2000);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleActivity.java
new file mode 100644
index 0000000000..dcd9db850b
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleActivity.java
@@ -0,0 +1,587 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.widget.Toast;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.layers.FillLayer;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.LineLayer;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.style.layers.PropertyValue;
+import com.mapbox.mapboxsdk.style.layers.RasterLayer;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.layers.TransitionOptions;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.style.sources.RasterSource;
+import com.mapbox.mapboxsdk.style.sources.Source;
+import com.mapbox.mapboxsdk.style.sources.TileSet;
+import com.mapbox.mapboxsdk.style.sources.VectorSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.all;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.color;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.exponential;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.gte;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.interpolate;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.lt;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.toNumber;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.layers.Property.FILL_TRANSLATE_ANCHOR_MAP;
+import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
+import static com.mapbox.mapboxsdk.style.layers.Property.SYMBOL_PLACEMENT_POINT;
+import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.backgroundOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillAntialias;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOutlineColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillTranslateAnchor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineCap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineJoin;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineOpacity;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.symbolPlacement;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+/**
+ * Test activity showcasing the runtime style API.
+ */
+public class RuntimeStyleActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_runtime_style);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+
+
+ mapView.getMapAsync(map -> {
+ // Store for later
+ mapboxMap = map;
+
+ // Center and Zoom (Amsterdam, zoomed to streets)
+ mapboxMap.animateCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(52.379189, 4.899431), 14));
+
+ mapboxMap.setTransitionDuration(250);
+ mapboxMap.setTransitionDelay(50);
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_runtime_style, menu);
+ return true;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_list_layers:
+ listLayers();
+ return true;
+ case R.id.action_list_sources:
+ listSources();
+ return true;
+ case R.id.action_water_color:
+ setWaterColor();
+ return true;
+ case R.id.action_background_opacity:
+ setBackgroundOpacity();
+ return true;
+ case R.id.action_road_avoid_edges:
+ setRoadSymbolPlacement();
+ return true;
+ case R.id.action_layer_visibility:
+ setLayerInvisible();
+ return true;
+ case R.id.action_remove_layer:
+ removeBuildings();
+ return true;
+ case R.id.action_add_parks_layer:
+ addParksLayer();
+ return true;
+ case R.id.action_add_dynamic_parks_layer:
+ addDynamicParksLayer();
+ return true;
+ case R.id.action_add_terrain_layer:
+ addTerrainLayer();
+ return true;
+ case R.id.action_add_satellite_layer:
+ addSatelliteLayer();
+ return true;
+ case R.id.action_update_water_color_on_zoom:
+ updateWaterColorOnZoom();
+ return true;
+ case R.id.action_add_custom_tiles:
+ addCustomTileSource();
+ return true;
+ case R.id.action_fill_filter:
+ styleFillFilterLayer();
+ return true;
+ case R.id.action_line_filter:
+ styleLineFilterLayer();
+ return true;
+ case R.id.action_numeric_filter:
+ styleNumericFillLayer();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void listLayers() {
+ List<Layer> layers = mapboxMap.getLayers();
+ StringBuilder builder = new StringBuilder("Layers:");
+ for (Layer layer : layers) {
+ builder.append("\n");
+ builder.append(layer.getId());
+ }
+ Toast.makeText(this, builder.toString(), Toast.LENGTH_LONG).show();
+ }
+
+ private void listSources() {
+ List<Source> sources = mapboxMap.getSources();
+ StringBuilder builder = new StringBuilder("Sources:");
+ for (Source source : sources) {
+ builder.append("\n");
+ builder.append(source.getId());
+ }
+ Toast.makeText(this, builder.toString(), Toast.LENGTH_LONG).show();
+ }
+
+ private void setLayerInvisible() {
+ String[] roadLayers = new String[] {"water"};
+ for (String roadLayer : roadLayers) {
+ Layer layer = mapboxMap.getLayer(roadLayer);
+ if (layer != null) {
+ layer.setProperties(visibility(NONE));
+ }
+ }
+ }
+
+ private void setRoadSymbolPlacement() {
+ // Zoom so that the labels are visible first
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(14), new DefaultCallback() {
+ @Override
+ public void onFinish() {
+ String[] roadLayers = new String[] {"road-label-small", "road-label-medium", "road-label-large"};
+ for (String roadLayer : roadLayers) {
+ Layer layer = mapboxMap.getLayer(roadLayer);
+ if (layer != null) {
+ layer.setProperties(symbolPlacement(SYMBOL_PLACEMENT_POINT));
+ }
+ }
+ }
+ });
+ }
+
+ private void setBackgroundOpacity() {
+ Layer background = mapboxMap.getLayer("background");
+ if (background != null) {
+ background.setProperties(backgroundOpacity(0.2f));
+ }
+ }
+
+ private void setWaterColor() {
+ FillLayer water = mapboxMap.getLayerAs("water");
+ if (water != null) {
+ water.setFillColorTransition(new TransitionOptions(7500, 1000));
+ water.setProperties(
+ visibility(VISIBLE),
+ fillColor(Color.RED)
+ );
+ } else {
+ Toast.makeText(RuntimeStyleActivity.this, "No water layer in this style", Toast.LENGTH_SHORT).show();
+ }
+ }
+
+ private void removeBuildings() {
+ // Zoom to see buildings first
+ mapboxMap.removeLayer("building");
+ }
+
+ private void addParksLayer() {
+ // Add a source
+ Source source;
+ try {
+ source = new GeoJsonSource("amsterdam-spots", ResourceUtils.readRawResource(this, R.raw.amsterdam));
+ } catch (IOException ioException) {
+ Toast.makeText(
+ RuntimeStyleActivity.this,
+ "Couldn't add source: " + ioException.getMessage(),
+ Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ mapboxMap.addSource(source);
+
+ FillLayer layer = new FillLayer("parksLayer", "amsterdam-spots");
+ layer.setProperties(
+ fillColor(Color.RED),
+ fillOutlineColor(Color.BLUE),
+ fillOpacity(0.3f),
+ fillAntialias(true)
+ );
+
+ // Only show me parks (except westerpark with stroke-width == 3)
+ layer.setFilter(all(eq(get("type"), literal("park")), eq(get("stroke-width"), literal(3))));
+
+ mapboxMap.addLayerBelow(layer, "building");
+ // layer.setPaintProperty(fillColor(Color.RED)); // XXX But not after the object is attached
+
+ // Or get the object later and set it. It's all good.
+ mapboxMap.getLayer("parksLayer").setProperties(fillColor(Color.RED));
+
+ // You can get a typed layer, if you're sure it's of that type. Use with care
+ layer = mapboxMap.getLayerAs("parksLayer");
+ // And get some properties
+ PropertyValue<Boolean> fillAntialias = layer.getFillAntialias();
+ Timber.d("Fill anti alias: %s", fillAntialias.getValue());
+ layer.setProperties(fillTranslateAnchor(FILL_TRANSLATE_ANCHOR_MAP));
+ PropertyValue<String> fillTranslateAnchor = layer.getFillTranslateAnchor();
+ Timber.d("Fill translate anchor: %s", fillTranslateAnchor.getValue());
+ PropertyValue<String> visibility = layer.getVisibility();
+ Timber.d("Visibility: %s", visibility.getValue());
+
+ // Get a good look at it all
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(12));
+ }
+
+ private void addDynamicParksLayer() {
+ // Load some data
+ FeatureCollection parks;
+ try {
+ String json = ResourceUtils.readRawResource(this, R.raw.amsterdam);
+ parks = FeatureCollection.fromJson(json);
+ } catch (IOException ioException) {
+ Toast.makeText(
+ RuntimeStyleActivity.this,
+ "Couldn't add source: " + ioException.getMessage(),
+ Toast.LENGTH_SHORT
+ ).show();
+ return;
+ }
+
+ // Add an empty source
+ mapboxMap.addSource(new GeoJsonSource("dynamic-park-source"));
+
+ FillLayer layer = new FillLayer("dynamic-parks-layer", "dynamic-park-source");
+ layer.setProperties(
+ fillColor(Color.GREEN),
+ fillOutlineColor(Color.GREEN),
+ fillOpacity(0.8f),
+ fillAntialias(true)
+ );
+
+ // Only show me parks
+ layer.setFilter(all(eq(get("type"), literal("park"))));
+
+ mapboxMap.addLayer(layer);
+
+ // Get a good look at it all
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(12));
+
+ // Animate the parks source
+ animateParksSource(parks, 0);
+ }
+
+ private void animateParksSource(final FeatureCollection parks, final int counter) {
+ Handler handler = new Handler(getMainLooper());
+ handler.postDelayed(() -> {
+ if (mapboxMap == null) {
+ return;
+ }
+
+ Timber.d("Updating parks source");
+ // change the source
+ int park = counter < parks.features().size() - 1 ? counter : 0;
+
+ GeoJsonSource source = mapboxMap.getSourceAs("dynamic-park-source");
+
+ if (source == null) {
+ Timber.e("Source not found");
+ Toast.makeText(RuntimeStyleActivity.this, "Source not found", Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ List<Feature> features = new ArrayList<>();
+ features.add(parks.features().get(park));
+ source.setGeoJson(FeatureCollection.fromFeatures(features));
+
+ // Re-post
+ animateParksSource(parks, park + 1);
+ }, counter == 0 ? 100 : 1000);
+ }
+
+ private void addTerrainLayer() {
+ // Add a source
+ Source source = new VectorSource("my-terrain-source", "mapbox://mapbox.mapbox-terrain-v2");
+ mapboxMap.addSource(source);
+
+ LineLayer layer = new LineLayer("terrainLayer", "my-terrain-source");
+ layer.setSourceLayer("contour");
+ layer.setProperties(
+ lineJoin(Property.LINE_JOIN_ROUND),
+ lineCap(Property.LINE_CAP_ROUND),
+ lineColor(Color.RED),
+ lineWidth(20f)
+ );
+
+ // adding layers below "road" layers
+ List<Layer> layers = mapboxMap.getLayers();
+ Layer latestLayer = null;
+ Collections.reverse(layers);
+ for (Layer currentLayer : layers) {
+ if (currentLayer instanceof FillLayer && ((FillLayer) currentLayer).getSourceLayer().equals("road")) {
+ latestLayer = currentLayer;
+ } else if (currentLayer instanceof CircleLayer && ((CircleLayer) currentLayer).getSourceLayer().equals("road")) {
+ latestLayer = currentLayer;
+ } else if (currentLayer instanceof SymbolLayer && ((SymbolLayer) currentLayer).getSourceLayer().equals("road")) {
+ latestLayer = currentLayer;
+ } else if (currentLayer instanceof LineLayer && ((LineLayer) currentLayer).getSourceLayer().equals("road")) {
+ latestLayer = currentLayer;
+ }
+ }
+
+ if (latestLayer != null) {
+ mapboxMap.addLayerBelow(layer, latestLayer.getId());
+ }
+
+ // Need to get a fresh handle
+ layer = mapboxMap.getLayerAs("terrainLayer");
+
+ // Make sure it's also applied after the fact
+ layer.setMinZoom(10);
+ layer.setMaxZoom(15);
+
+ layer = (LineLayer) mapboxMap.getLayer("terrainLayer");
+ Toast.makeText(this, String.format(
+ "Set min/max zoom to %s - %s", layer.getMinZoom(), layer.getMaxZoom()), Toast.LENGTH_SHORT).show();
+ }
+
+ private void addSatelliteLayer() {
+ // Add a source
+ Source source = new RasterSource("my-raster-source", "mapbox://mapbox.satellite", 512);
+ mapboxMap.addSource(source);
+
+ // Add a layer
+ mapboxMap.addLayer(new RasterLayer("satellite-layer", "my-raster-source"));
+ }
+
+ private void updateWaterColorOnZoom() {
+ FillLayer layer = mapboxMap.getLayerAs("water");
+ if (layer == null) {
+ return;
+ }
+
+ // Set a zoom function to update the color of the water
+ layer.setProperties(
+ fillColor(
+ interpolate(
+ exponential(0.8f),
+ zoom(),
+ stop(1, color(Color.GREEN)),
+ stop(4, color(Color.BLUE)),
+ stop(12, color(Color.RED)),
+ stop(20, color(Color.BLACK))
+ )
+ )
+ );
+
+ // do some animations to show it off properly
+ mapboxMap.animateCamera(CameraUpdateFactory.zoomTo(1), 1500);
+ }
+
+ private void addCustomTileSource() {
+ // Add a source
+ TileSet tileSet = new TileSet("2.1.0", "https://d25uarhxywzl1j.cloudfront.net/v0.1/{z}/{x}/{y}.mvt");
+ tileSet.setMinZoom(0);
+ tileSet.setMaxZoom(14);
+ Source source = new VectorSource("custom-tile-source", tileSet);
+ mapboxMap.addSource(source);
+
+ // Add a layer
+ LineLayer lineLayer = new LineLayer("custom-tile-layers", "custom-tile-source");
+ lineLayer.setSourceLayer("mapillary-sequences");
+ lineLayer.setProperties(
+ lineCap(Property.LINE_CAP_ROUND),
+ lineJoin(Property.LINE_JOIN_ROUND),
+ lineOpacity(0.6f),
+ lineWidth(2.0f),
+ lineColor(Color.GREEN)
+ );
+ mapboxMap.addLayer(lineLayer);
+ }
+
+ private void styleFillFilterLayer() {
+ mapboxMap.setStyleUrl("asset://fill_filter_style.json");
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(31, -100), 3));
+
+ Handler handler = new Handler(getMainLooper());
+ handler.postDelayed(() -> {
+ if (mapboxMap == null) {
+ return;
+ }
+
+ Timber.d("Styling filtered fill layer");
+
+ FillLayer states = (FillLayer) mapboxMap.getLayer("states");
+
+ if (states != null) {
+ states.setFilter(eq(get("name"), literal("Texas")));
+ states.setFillOpacityTransition(new TransitionOptions(2500, 0));
+ states.setFillColorTransition(new TransitionOptions(2500, 0));
+ states.setProperties(
+ fillColor(Color.RED),
+ fillOpacity(0.25f)
+ );
+ } else {
+ Toast.makeText(RuntimeStyleActivity.this, "No states layer in this style", Toast.LENGTH_SHORT).show();
+ }
+ }, 2000);
+ }
+
+ private void styleLineFilterLayer() {
+ mapboxMap.setStyleUrl("asset://line_filter_style.json");
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(40, -97), 5));
+
+ Handler handler = new Handler(getMainLooper());
+ handler.postDelayed(() -> {
+ if (mapboxMap == null) {
+ return;
+ }
+
+ Timber.d("Styling filtered line layer");
+
+ LineLayer counties = (LineLayer) mapboxMap.getLayer("counties");
+
+ if (counties != null) {
+ counties.setFilter(eq(get("NAME10"), "Washington"));
+
+ counties.setProperties(
+ lineColor(Color.RED),
+ lineOpacity(0.75f),
+ lineWidth(5f)
+ );
+ } else {
+ Toast.makeText(RuntimeStyleActivity.this, "No counties layer in this style", Toast.LENGTH_SHORT).show();
+ }
+ }, 2000);
+ }
+
+ private void styleNumericFillLayer() {
+ mapboxMap.setStyleUrl("asset://numeric_filter_style.json");
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(40, -97), 5));
+
+ Handler handler = new Handler(getMainLooper());
+ handler.postDelayed(() -> {
+ if (mapboxMap == null) {
+ return;
+ }
+
+ Timber.d("Styling numeric fill layer");
+
+ FillLayer regions = (FillLayer) mapboxMap.getLayer("regions");
+
+ if (regions != null) {
+ regions.setFilter(all(
+ gte(toNumber(get("HRRNUM")), literal(200)),
+ lt(toNumber(get("HRRNUM")), literal(300)))
+ );
+
+ regions.setProperties(
+ fillColor(Color.BLUE),
+ fillOpacity(0.5f)
+ );
+ } else {
+ Toast.makeText(RuntimeStyleActivity.this, "No regions layer in this style", Toast.LENGTH_SHORT).show();
+ }
+ }, 2000);
+ }
+
+ private static class DefaultCallback implements MapboxMap.CancelableCallback {
+
+ @Override
+ public void onCancel() {
+ // noop
+ }
+
+ @Override
+ public void onFinish() {
+ // noop
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTestActivity.java
new file mode 100644
index 0000000000..ce3770d961
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTestActivity.java
@@ -0,0 +1,74 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test activity used for instrumentation test execution.
+ */
+public class RuntimeStyleTestActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_runtime_style);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> RuntimeStyleTestActivity.this.mapboxMap = mapboxMap);
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTimingTestActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTimingTestActivity.java
new file mode 100644
index 0000000000..dc4825c2db
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/RuntimeStyleTimingTestActivity.java
@@ -0,0 +1,96 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Color;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.CircleLayer;
+import com.mapbox.mapboxsdk.style.sources.VectorSource;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.circleRadius;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+/**
+ * Test activity for unit test execution
+ */
+public class RuntimeStyleTimingTestActivity extends AppCompatActivity {
+
+ public MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_runtime_style);
+
+ // Initialize map as normal
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(mapboxMap -> {
+ RuntimeStyleTimingTestActivity.this.mapboxMap = mapboxMap;
+ VectorSource museums = new VectorSource("museums_source", "mapbox://mapbox.2opop9hr");
+ mapboxMap.addSource(museums);
+
+ CircleLayer museumsLayer = new CircleLayer("museums", "museums_source");
+ museumsLayer.setSourceLayer("museum-cusco");
+ museumsLayer.setProperties(
+ visibility(VISIBLE),
+ circleRadius(8f),
+ circleColor(Color.argb(1, 55, 148, 179))
+ );
+
+ mapboxMap.addLayer(museumsLayer);
+ });
+ }
+
+ public MapboxMap getMapboxMap() {
+ return mapboxMap;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/StyleFileActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/StyleFileActivity.java
new file mode 100644
index 0000000000..4067f4ebd5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/StyleFileActivity.java
@@ -0,0 +1,177 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.content.Context;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.content.ContextCompat;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.Toast;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+
+import timber.log.Timber;
+
+/**
+ * Test activity showcasing how to use a file:// resource for the style.json and how to use MapboxMap#setStyleJson.
+ */
+public class StyleFileActivity extends AppCompatActivity {
+
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_style_file);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+
+ FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab_file);
+ fab.setColorFilter(ContextCompat.getColor(StyleFileActivity.this, R.color.primary));
+ fab.setOnClickListener(view -> new CreateStyleFileTask(view.getContext(), mapboxMap).execute());
+
+ FloatingActionButton fabStyleJson = (FloatingActionButton) findViewById(R.id.fab_style_json);
+ fabStyleJson.setColorFilter(ContextCompat.getColor(StyleFileActivity.this, R.color.primary));
+ fabStyleJson.setOnClickListener(view -> new LoadStyleFileTask(view.getContext(), mapboxMap).execute());
+ });
+ }
+
+ /**
+ * Task to read a style file from the raw folder
+ */
+ private static class LoadStyleFileTask extends AsyncTask<Void, Void, String> {
+ private WeakReference<Context> context;
+ private WeakReference<MapboxMap> mapboxMap;
+
+ LoadStyleFileTask(Context context, MapboxMap mapboxMap) {
+ this.context = new WeakReference<>(context);
+ this.mapboxMap = new WeakReference<>(mapboxMap);
+ }
+
+ @Override
+ protected String doInBackground(Void... voids) {
+ String styleJson = "";
+ try {
+ styleJson = ResourceUtils.readRawResource(context.get(), R.raw.sat_style);
+ } catch (Exception exception) {
+ Timber.e(exception, "Can't load local file style");
+ }
+ return styleJson;
+ }
+
+ @Override
+ protected void onPostExecute(String json) {
+ super.onPostExecute(json);
+ Timber.d("Read json, %s", json);
+ MapboxMap mapboxMap = this.mapboxMap.get();
+ if (mapboxMap != null) {
+ mapboxMap.setStyleJson(json);
+ }
+ }
+ }
+
+ /**
+ * Task to write a style file to local disk and load it in the map view
+ */
+ private static class CreateStyleFileTask extends AsyncTask<Void, Integer, Long> {
+ private File cacheStyleFile;
+ private WeakReference<Context> context;
+ private WeakReference<MapboxMap> mapboxMap;
+
+ CreateStyleFileTask(Context context, MapboxMap mapboxMap) {
+ this.context = new WeakReference<>(context);
+ this.mapboxMap = new WeakReference<>(mapboxMap);
+ }
+
+ @Override
+ protected Long doInBackground(Void... params) {
+ try {
+ cacheStyleFile = File.createTempFile("my-", ".style.json");
+ cacheStyleFile.createNewFile();
+ Timber.i("Writing style file to: %s", cacheStyleFile.getAbsolutePath());
+ Context context = this.context.get();
+ if (context != null) {
+ writeToFile(cacheStyleFile, ResourceUtils.readRawResource(context, R.raw.local_style));
+ }
+ } catch (Exception exception) {
+ Toast.makeText(context.get(), "Could not create style file in cache dir", Toast.LENGTH_SHORT).show();
+ }
+ return 1L;
+ }
+
+ protected void onPostExecute(Long result) {
+ // Actual file:// usage
+ MapboxMap mapboxMap = this.mapboxMap.get();
+ if (mapboxMap != null) {
+ mapboxMap.setStyleUrl("file://" + cacheStyleFile.getAbsolutePath());
+ }
+ }
+
+ private void writeToFile(File file, String contents) throws IOException {
+ BufferedWriter writer = null;
+ try {
+ writer = new BufferedWriter(new FileWriter(file));
+ writer.write(contents);
+ } finally {
+ if (writer != null) {
+ writer.close();
+ }
+ }
+ }
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolGeneratorActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolGeneratorActivity.java
new file mode 100644
index 0000000000..a1fe353945
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolGeneratorActivity.java
@@ -0,0 +1,352 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.PointF;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.expressions.Expression;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.style.sources.Source;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.List;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.concat;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.division;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.downcase;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.eq;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.match;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.number;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.pi;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.product;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.rgba;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.step;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.string;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.upcase;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.layers.Property.ICON_ANCHOR_BOTTOM;
+import static com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconOffset;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAnchor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;
+
+/**
+ * Test activity showcasing using a symbol generator that generates Bitmaps from Android SDK Views.
+ */
+public class SymbolGeneratorActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private static final String SOURCE_ID = "com.mapbox.mapboxsdk.style.layers.symbol.source.id";
+ private static final String LAYER_ID = "com.mapbox.mapboxsdk.style.layers.symbol.layer.id";
+ private static final String FEATURE_ID = "brk_name";
+ private static final String FEATURE_RANK = "scalerank";
+ private static final String FEATURE_NAME = "name_sort";
+ private static final String FEATURE_TYPE = "type";
+ private static final String FEATURE_REGION = "continent";
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_symbol_generator);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(final MapboxMap map) {
+ mapboxMap = map;
+ addSymbolClickListener();
+ new LoadDataTask(this).execute();
+ }
+
+ private void addSymbolClickListener() {
+ mapboxMap.setOnMapClickListener(point -> {
+ PointF screenPoint = mapboxMap.getProjection().toScreenLocation(point);
+ List<Feature> features = mapboxMap.queryRenderedFeatures(screenPoint, LAYER_ID);
+ if (!features.isEmpty()) {
+ Feature feature = features.get(0);
+ Timber.v("Feature was clicked with data: %s", feature.toJson());
+ Toast.makeText(
+ SymbolGeneratorActivity.this,
+ "hello from: " + feature.getStringProperty(FEATURE_NAME),
+ Toast.LENGTH_LONG).show();
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_generator_symbol, menu);
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == R.id.menu_action_icon_overlap) {
+ SymbolLayer layer = mapboxMap.getLayerAs(LAYER_ID);
+ layer.setProperties(iconAllowOverlap(!layer.getIconAllowOverlap().getValue()));
+ return true;
+ } else if (item.getItemId() == R.id.menu_action_filter) {
+ SymbolLayer layer = mapboxMap.getLayerAs(LAYER_ID);
+ layer.setFilter(eq(get(FEATURE_RANK), literal(1)));
+ Timber.e("Filter that was set: %s", layer.getFilter());
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ /**
+ * Utility class to generate Bitmaps for Symbol.
+ * <p>
+ * Bitmaps can be added to the map with {@link com.mapbox.mapboxsdk.maps.MapboxMap#addImage(String, Bitmap)}
+ * </p>
+ */
+ private static class SymbolGenerator {
+
+ /**
+ * Generate a Bitmap from an Android SDK View.
+ *
+ * @param view the View to be drawn to a Bitmap
+ * @return the generated bitmap
+ */
+ public static Bitmap generate(@NonNull View view) {
+ int measureSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+ view.measure(measureSpec, measureSpec);
+
+ int measuredWidth = view.getMeasuredWidth();
+ int measuredHeight = view.getMeasuredHeight();
+
+ view.layout(0, 0, measuredWidth, measuredHeight);
+ Bitmap bitmap = Bitmap.createBitmap(measuredWidth, measuredHeight, Bitmap.Config.ARGB_8888);
+ bitmap.eraseColor(Color.TRANSPARENT);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ return bitmap;
+ }
+ }
+
+ private static class LoadDataTask extends AsyncTask<Void, Void, FeatureCollection> {
+
+ private WeakReference<SymbolGeneratorActivity> activity;
+
+ LoadDataTask(SymbolGeneratorActivity activity) {
+ this.activity = new WeakReference<>(activity);
+ }
+
+ @Override
+ protected FeatureCollection doInBackground(Void... params) {
+ Context context = activity.get();
+ if (context != null) {
+ try {
+ // read local geojson from raw folder
+ String tinyCountriesJson = ResourceUtils.readRawResource(context, R.raw.tiny_countries);
+ return FeatureCollection.fromJson(tinyCountriesJson);
+
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(FeatureCollection featureCollection) {
+ super.onPostExecute(featureCollection);
+ SymbolGeneratorActivity activity = this.activity.get();
+ if (featureCollection == null || activity == null) {
+ return;
+ }
+
+ activity.onDataLoaded(featureCollection);
+ }
+ }
+
+ public void onDataLoaded(@NonNull FeatureCollection featureCollection) {
+ // create expressions
+ Expression iconImageExpression = string(get(literal(FEATURE_ID)));
+ Expression iconSizeExpression = division(number(get(literal(FEATURE_RANK))), literal(2.0f));
+ Expression textSizeExpression = product(get(literal(FEATURE_RANK)), pi());
+ Expression textFieldExpression = concat(upcase(literal("a ")), upcase(string(get(literal(FEATURE_TYPE)))),
+ downcase(literal(" IN ")), string(get(literal(FEATURE_REGION)))
+ );
+ Expression textColorExpression = match(get(literal(FEATURE_RANK)),
+ literal(1), rgba(255, 0, 0, 1.0f),
+ literal(2), rgba(0, 0, 255.0f, 1.0f),
+ rgba(0.0f, 255.0f, 0.0f, 1.0f)
+ );
+
+ rgba(
+ division(literal(255), get(FEATURE_RANK)),
+ literal(0.0f),
+ literal(0.0f),
+ literal(1.0f)
+ );
+
+ // create symbol layer
+ SymbolLayer symbolLayer = new SymbolLayer(LAYER_ID, SOURCE_ID)
+ .withProperties(
+ // icon configuration
+ iconImage(iconImageExpression),
+ iconAllowOverlap(false),
+ iconSize(iconSizeExpression),
+ iconAnchor(ICON_ANCHOR_BOTTOM),
+ iconOffset(step(zoom(), literal(new float[] {0f, 0f}),
+ stop(1, new Float[] {0f, 0f}),
+ stop(10, new Float[] {0f, -35f})
+ )),
+
+ // text field configuration
+ textField(textFieldExpression),
+ textSize(textSizeExpression),
+ textAnchor(TEXT_ANCHOR_TOP),
+ textColor(textColorExpression)
+ );
+
+ // add a geojson source to the map
+ Source source = new GeoJsonSource(SOURCE_ID, featureCollection);
+ mapboxMap.addSource(source);
+
+ // add symbol layer
+ mapboxMap.addLayer(symbolLayer);
+
+ // get expressions
+ Expression iconImageExpressionResult = symbolLayer.getIconImage().getExpression();
+ Expression iconSizeExpressionResult = symbolLayer.getIconSize().getExpression();
+ Expression textSizeExpressionResult = symbolLayer.getTextSize().getExpression();
+ Expression textFieldExpressionResult = symbolLayer.getTextField().getExpression();
+ Expression textColorExpressionResult = symbolLayer.getTextColor().getExpression();
+
+ // log expressions
+ Timber.e(iconImageExpressionResult.toString());
+ Timber.e(iconSizeExpressionResult.toString());
+ Timber.e(textSizeExpressionResult.toString());
+ Timber.e(textFieldExpressionResult.toString());
+ Timber.e(textColorExpressionResult.toString());
+
+ // reset expressions
+ symbolLayer.setProperties(
+ iconImage(iconImageExpressionResult),
+ iconSize(iconSizeExpressionResult),
+ textSize(textSizeExpressionResult),
+ textField(textFieldExpressionResult),
+ textColor(textColorExpressionResult)
+ );
+
+ new GenerateSymbolTask(mapboxMap, this).execute(featureCollection);
+ }
+
+ private static class GenerateSymbolTask extends AsyncTask<FeatureCollection, Void, HashMap<String, Bitmap>> {
+
+ private MapboxMap mapboxMap;
+ private WeakReference<Context> context;
+
+ GenerateSymbolTask(MapboxMap mapboxMap, Context context) {
+ this.mapboxMap = mapboxMap;
+ this.context = new WeakReference<>(context);
+ }
+
+ @SuppressWarnings("WrongThread")
+ @Override
+ protected HashMap<String, Bitmap> doInBackground(FeatureCollection... params) {
+ HashMap<String, Bitmap> imagesMap = new HashMap<>();
+ Context context = this.context.get();
+ List<Feature> features = params[0].features();
+ if (context != null && features != null) {
+ for (Feature feature : features) {
+ String countryName = feature.getStringProperty(FEATURE_ID);
+ TextView textView = new TextView(context);
+ textView.setBackgroundColor(context.getResources().getColor(R.color.blueAccent));
+ textView.setPadding(10, 5, 10, 5);
+ textView.setTextColor(Color.WHITE);
+ textView.setText(countryName);
+ imagesMap.put(countryName, SymbolGenerator.generate(textView));
+ }
+ }
+ return imagesMap;
+ }
+
+ @Override
+ protected void onPostExecute(HashMap<String, Bitmap> bitmapHashMap) {
+ super.onPostExecute(bitmapHashMap);
+ mapboxMap.addImages(bitmapHashMap);
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolLayerActivity.java
new file mode 100644
index 0000000000..d3cddeefb3
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/SymbolLayerActivity.java
@@ -0,0 +1,214 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.graphics.PointF;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.utils.BitmapUtils;
+
+
+import java.util.Arrays;
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.any;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.has;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.lte;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.not;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAnchor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textAnchor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textColor;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textField;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textFont;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.textSize;
+
+/**
+ * Test activity showcasing runtime manipulation of symbol layers.
+ */
+public class SymbolLayerActivity extends AppCompatActivity implements MapboxMap.OnMapClickListener {
+
+ public static final String MARKER_SOURCE = "marker-source";
+ public static final String MARKER_LAYER = "marker-layer";
+ private MapboxMap mapboxMap;
+ private MapView mapView;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_symbollayer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+
+ // Add a sdf image for the makers
+ Drawable icLayersDrawable = getResources().getDrawable(R.drawable.ic_layers);
+ Bitmap icLayersBitmap = BitmapUtils.getBitmapFromDrawable(icLayersDrawable);
+ mapboxMap.addImage(
+ "my-layers-image",
+ icLayersBitmap,
+ true
+ );
+
+ // Add a source
+ FeatureCollection markers = FeatureCollection.fromFeatures(new Feature[] {
+ Feature.fromGeometry(Point.fromLngLat(4.91638, 52.35673), featureProperties("Marker 1")),
+ Feature.fromGeometry(Point.fromLngLat(4.91638, 52.34673), featureProperties("Marker 2"))
+ });
+ mapboxMap.addSource(new GeoJsonSource(MARKER_SOURCE, markers));
+
+ // Add the symbol-layer
+ mapboxMap.addLayer(
+ new SymbolLayer(MARKER_LAYER, MARKER_SOURCE)
+ .withProperties(
+ iconImage("my-layers-image"),
+ iconAllowOverlap(true),
+ iconAnchor(Property.ICON_ANCHOR_BOTTOM),
+ textField(get("title")),
+ iconColor(Color.RED),
+ textColor(Color.RED),
+ textAnchor(Property.TEXT_ANCHOR_TOP),
+ textSize(10f)
+ ).withFilter((any(not(has("price")), lte(get("price"), literal(25)))))
+ );
+
+ // Show
+ mapboxMap.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(52.35273, 4.91638), 14));
+
+ // Set a click-listener so we can manipulate the map
+ mapboxMap.setOnMapClickListener(SymbolLayerActivity.this);
+ });
+ }
+
+ @Override
+ public void onMapClick(@NonNull LatLng point) {
+ // Query which features are clicked
+ PointF screenLoc = mapboxMap.getProjection().toScreenLocation(point);
+ List<Feature> features = mapboxMap.queryRenderedFeatures(screenLoc, MARKER_LAYER);
+
+ SymbolLayer layer = mapboxMap.getLayerAs(MARKER_LAYER);
+ if (features.size() == 0) {
+ // Reset
+ layer.setProperties(iconSize(1f));
+ } else {
+ layer.setProperties(iconSize(3f));
+ }
+ }
+
+ private void toggleTextSize() {
+ SymbolLayer layer = mapboxMap.getLayerAs(MARKER_LAYER);
+ layer.setProperties(layer.getTextSize().getValue() > 10 ? textSize(10f) : textSize(20f));
+ }
+
+ private void toggleTextField() {
+ SymbolLayer layer = mapboxMap.getLayerAs(MARKER_LAYER);
+ layer.setProperties("{title}".equals(layer.getTextField().getValue()) ? textField("āA") : textField("{title}"));
+ }
+
+ private void toggleTextFont() {
+ SymbolLayer layer = mapboxMap.getLayerAs(MARKER_LAYER);
+
+ String[] fonts = layer.getTextFont().getValue();
+ if (fonts == null || fonts.length == 0 || Arrays.asList(fonts).contains("Arial Unicode MS Regular")) {
+ layer.setProperties(textFont(new String[] {"DIN Offc Pro Bold", "Arial Unicode MS Bold"}));
+ } else {
+ layer.setProperties(textFont(new String[] {"DIN Offc Pro Medium", "Arial Unicode MS Regular"}));
+ }
+ }
+
+ private JsonObject featureProperties(String title) {
+ JsonObject object = new JsonObject();
+ object.add("title", new JsonPrimitive(title));
+ return object;
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_symbol_layer, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.action_toggle_text_size:
+ toggleTextSize();
+ return true;
+ case R.id.action_toggle_text_field:
+ toggleTextField();
+ return true;
+ case R.id.action_toggle_text_font:
+ toggleTextFont();
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/ZoomFunctionSymbolLayerActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/ZoomFunctionSymbolLayerActivity.java
new file mode 100644
index 0000000000..b2552789cd
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/style/ZoomFunctionSymbolLayerActivity.java
@@ -0,0 +1,191 @@
+package com.mapbox.mapboxsdk.maps.activity.style;
+
+import android.graphics.PointF;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import com.google.gson.JsonObject;
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
+import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
+import com.mapbox.mapboxsdk.testapp.R;
+import timber.log.Timber;
+
+import java.util.List;
+
+import static com.mapbox.mapboxsdk.style.expressions.Expression.get;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.literal;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.step;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.stop;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.switchCase;
+import static com.mapbox.mapboxsdk.style.expressions.Expression.zoom;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconSize;
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+/**
+ * Test activity showcasing changing the icon with a zoom function and adding selection state to a SymbolLayer.
+ */
+public class ZoomFunctionSymbolLayerActivity extends AppCompatActivity {
+
+ private static final String LAYER_ID = "symbolLayer";
+ private static final String SOURCE_ID = "poiSource";
+ private static final String BUS_MAKI_ICON_ID = "bus-11";
+ private static final String CAFE_MAKI_ICON_ID = "cafe-11";
+ private static final String KEY_PROPERTY_SELECTED = "selected";
+ private static final float ZOOM_STOP_MAX_VALUE = 12.0f;
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private GeoJsonSource source;
+ private SymbolLayer layer;
+
+ private boolean isInitialPosition = true;
+ private boolean isSelected = false;
+ private boolean isShowingSymbolLayer = true;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_zoom_symbol_layer);
+
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+ updateSource();
+ addLayer();
+ addMapClickListener();
+ });
+ }
+
+ private void updateSource() {
+ FeatureCollection featureCollection = createFeatureCollection();
+ if (source != null) {
+ source.setGeoJson(featureCollection);
+ } else {
+ source = new GeoJsonSource(SOURCE_ID, featureCollection);
+ mapboxMap.addSource(source);
+ }
+ }
+
+ private void toggleSymbolLayerVisibility() {
+ layer.setProperties(
+ visibility(isShowingSymbolLayer ? Property.NONE : Property.VISIBLE)
+ );
+ isShowingSymbolLayer = !isShowingSymbolLayer;
+ }
+
+ private FeatureCollection createFeatureCollection() {
+ Point point = isInitialPosition
+ ? Point.fromLngLat(-74.01618140, 40.701745)
+ : Point.fromLngLat(-73.988097, 40.749864);
+
+ JsonObject properties = new JsonObject();
+ properties.addProperty(KEY_PROPERTY_SELECTED, isSelected);
+ Feature feature = Feature.fromGeometry(point, properties);
+ return FeatureCollection.fromFeatures(new Feature[] {feature});
+ }
+
+ private void addLayer() {
+ layer = new SymbolLayer(LAYER_ID, SOURCE_ID);
+ layer.setProperties(
+ iconImage(
+ step(zoom(), literal(BUS_MAKI_ICON_ID),
+ stop(ZOOM_STOP_MAX_VALUE, CAFE_MAKI_ICON_ID)
+ )
+ ),
+ iconSize(
+ switchCase(
+ get(KEY_PROPERTY_SELECTED), literal(3.0f),
+ literal(1.0f)
+ )
+ ),
+ iconAllowOverlap(true)
+ );
+ mapboxMap.addLayer(layer);
+ }
+
+ private void addMapClickListener() {
+ mapboxMap.setOnMapClickListener(point -> {
+ PointF screenPoint = mapboxMap.getProjection().toScreenLocation(point);
+ List<Feature> featureList = mapboxMap.queryRenderedFeatures(screenPoint, LAYER_ID);
+ if (!featureList.isEmpty()) {
+ Feature feature = featureList.get(0);
+ boolean selectedNow = feature.getBooleanProperty(KEY_PROPERTY_SELECTED);
+ isSelected = !selectedNow;
+ updateSource();
+ } else {
+ Timber.e("No features found");
+ }
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.menu_symbols, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mapboxMap != null) {
+ if (item.getItemId() == R.id.menu_action_change_location) {
+ isInitialPosition = !isInitialPosition;
+ updateSource();
+ } else if (item.getItemId() == R.id.menu_action_toggle_source) {
+ toggleSymbolLayerVisibility();
+ }
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewAnimationActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewAnimationActivity.java
new file mode 100644
index 0000000000..0f2c3e7c94
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewAnimationActivity.java
@@ -0,0 +1,145 @@
+package com.mapbox.mapboxsdk.maps.activity.textureview;
+
+import android.animation.ObjectAnimator;
+import android.os.Bundle;
+import android.os.Handler;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.Locale;
+
+/**
+ * Test animating a {@link android.view.TextureView} backed map.
+ */
+public class TextureViewAnimationActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private Handler handler;
+ private Runnable delayed;
+
+ private static LatLng[] PLACES = {
+ new LatLng(37.7749, -122.4194), // SF
+ new LatLng(38.9072, -77.0369), // DC
+ new LatLng(52.3702, 4.8952), // AMS
+ new LatLng(60.1699, 24.9384), // HEL
+ new LatLng(-13.1639, -74.2236), // AYA
+ new LatLng(52.5200, 13.4050), // BER
+ new LatLng(12.9716, 77.5946), // BAN
+ new LatLng(31.2304, 121.4737) // SHA
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_textureview_animate);
+ handler = new Handler(getMainLooper());
+ setupToolbar();
+ setupMapView(savedInstanceState);
+ }
+
+ private void setupToolbar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+ }
+
+ private void setupMapView(Bundle savedInstanceState) {
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.getMapAsync(mapboxMap -> {
+ TextureViewAnimationActivity.this.mapboxMap = mapboxMap;
+
+ setFpsView(mapboxMap);
+
+ // Animate the map view
+ ObjectAnimator animation = ObjectAnimator.ofFloat(mapView, "rotationY", 0.0f, 360f);
+ animation.setDuration(3600);
+ animation.setRepeatCount(ObjectAnimator.INFINITE);
+ animation.start();
+
+ // Start an animation on the map as well
+ flyTo(mapboxMap, 0, 14);
+ });
+ }
+
+ private void flyTo(final MapboxMap mapboxMap, final int place, final double zoom) {
+ mapboxMap.animateCamera(
+ CameraUpdateFactory.newLatLngZoom(PLACES[place], zoom),
+ 10000,
+ new MapboxMap.CancelableCallback() {
+ @Override
+ public void onCancel() {
+ delayed = () -> {
+ delayed = null;
+ flyTo(mapboxMap, place, zoom);
+ };
+ handler.postDelayed(delayed, 2000);
+ }
+
+ @Override
+ public void onFinish() {
+ flyTo(mapboxMap, place == (PLACES.length - 1) ? 0 : place + 1, zoom);
+ }
+ });
+ }
+
+ private void setFpsView(MapboxMap mapboxMap) {
+ final TextView fpsView = (TextView) findViewById(R.id.fpsView);
+ mapboxMap.setOnFpsChangedListener(fps -> fpsView.setText(String.format(Locale.US, "FPS: %4.2f", fps)));
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ if (handler != null && delayed != null) {
+ handler.removeCallbacks(delayed);
+ }
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewDebugModeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewDebugModeActivity.java
new file mode 100644
index 0000000000..c202ed866c
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewDebugModeActivity.java
@@ -0,0 +1,265 @@
+package com.mapbox.mapboxsdk.maps.activity.textureview;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarDrawerToggle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.BaseAdapter;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.mapbox.mapboxsdk.constants.Style;
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
+import com.mapbox.mapboxsdk.style.layers.Layer;
+import com.mapbox.mapboxsdk.style.layers.Property;
+import com.mapbox.mapboxsdk.testapp.R;
+
+import java.util.List;
+import java.util.Locale;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
+
+/**
+ * Test activity showcasing the different debug modes and allows to cycle between the default map styles.
+ */
+public class TextureViewDebugModeActivity extends AppCompatActivity implements OnMapReadyCallback {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+ private ActionBarDrawerToggle actionBarDrawerToggle;
+ private int currentStyleIndex = 0;
+
+ private static final String[] STYLES = new String[] {
+ Style.MAPBOX_STREETS,
+ Style.OUTDOORS,
+ Style.LIGHT,
+ Style.DARK,
+ Style.SATELLITE,
+ Style.SATELLITE_STREETS,
+ Style.TRAFFIC_DAY,
+ Style.TRAFFIC_NIGHT
+ };
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_textureview_debug_mode);
+ setupToolbar();
+ setupMapView(savedInstanceState);
+ setupDebugChangeView();
+ setupStyleChangeView();
+ }
+
+ private void setupToolbar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+
+ DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ actionBarDrawerToggle = new ActionBarDrawerToggle(this,
+ drawerLayout,
+ R.string.navigation_drawer_open,
+ R.string.navigation_drawer_close
+ );
+ actionBarDrawerToggle.setDrawerIndicatorEnabled(true);
+ actionBarDrawerToggle.syncState();
+ }
+ }
+
+ private void setupMapView(Bundle savedInstanceState) {
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.addOnMapChangedListener(change -> {
+ if (change == MapView.DID_FINISH_LOADING_STYLE && mapboxMap != null) {
+ Timber.v("New style loaded with JSON: %s", mapboxMap.getStyleJson());
+ setupNavigationView(mapboxMap.getLayers());
+ }
+ });
+
+ mapView.setTag(true);
+ mapView.setStyleUrl(STYLES[currentStyleIndex]);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(this);
+ }
+
+ @Override
+ public void onMapReady(MapboxMap map) {
+ mapboxMap = map;
+ mapboxMap.getUiSettings().setZoomControlsEnabled(true);
+
+ setupNavigationView(mapboxMap.getLayers());
+ setupZoomView();
+ setFpsView();
+ }
+
+ private void setFpsView() {
+ final TextView fpsView = (TextView) findViewById(R.id.fpsView);
+ mapboxMap.setOnFpsChangedListener(fps -> fpsView.setText(String.format(Locale.US,"FPS: %4.2f", fps)));
+ }
+
+ private void setupNavigationView(List<Layer> layerList) {
+ final LayerListAdapter adapter = new LayerListAdapter(this, layerList);
+ ListView listView = (ListView) findViewById(R.id.listView);
+ listView.setAdapter(adapter);
+ listView.setOnItemClickListener((parent, view, position, id) -> {
+ Layer clickedLayer = adapter.getItem(position);
+ toggleLayerVisibility(clickedLayer);
+ closeNavigationView();
+ });
+ }
+
+ private void toggleLayerVisibility(Layer layer) {
+ boolean isVisible = layer.getVisibility().getValue().equals(Property.VISIBLE);
+ layer.setProperties(
+ visibility(
+ isVisible ? Property.NONE : Property.VISIBLE
+ )
+ );
+ }
+
+ private void closeNavigationView() {
+ DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);
+ drawerLayout.closeDrawers();
+ }
+
+ private void setupZoomView() {
+ final TextView textView = (TextView) findViewById(R.id.textZoom);
+ mapboxMap.setOnCameraChangeListener(position ->
+ textView.setText(String.format(getString(R.string.debug_zoom), position.zoom)));
+ }
+
+ private void setupDebugChangeView() {
+ FloatingActionButton fabDebug = (FloatingActionButton) findViewById(R.id.fabDebug);
+ fabDebug.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ Timber.d("Debug FAB: isDebug Active? %s", mapboxMap.isDebugActive());
+ mapboxMap.cycleDebugOptions();
+ }
+ });
+ }
+
+ private void setupStyleChangeView() {
+ FloatingActionButton fabStyles = (FloatingActionButton) findViewById(R.id.fabStyles);
+ fabStyles.setOnClickListener(view -> {
+ if (mapboxMap != null) {
+ currentStyleIndex++;
+ if (currentStyleIndex == STYLES.length) {
+ currentStyleIndex = 0;
+ }
+ mapboxMap.setStyleUrl(STYLES[currentStyleIndex], style -> Timber.d("Style loaded %s", style));
+ }
+ });
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ return actionBarDrawerToggle.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+ private static class LayerListAdapter extends BaseAdapter {
+
+ private LayoutInflater layoutInflater;
+ private List<Layer> layers;
+
+ LayerListAdapter(Context context, List<Layer> layers) {
+ this.layoutInflater = LayoutInflater.from(context);
+ this.layers = layers;
+ }
+
+ @Override
+ public int getCount() {
+ return layers.size();
+ }
+
+ @Override
+ public Layer getItem(int position) {
+ return layers.get(position);
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return position;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Layer layer = layers.get(position);
+ View view = convertView;
+ if (view == null) {
+ view = layoutInflater.inflate(android.R.layout.simple_list_item_2, parent, false);
+ ViewHolder holder = new ViewHolder(
+ (TextView) view.findViewById(android.R.id.text1),
+ (TextView) view.findViewById(android.R.id.text2)
+ );
+ view.setTag(holder);
+ }
+ ViewHolder holder = (ViewHolder) view.getTag();
+ holder.text.setText(layer.getClass().getSimpleName());
+ holder.subText.setText(layer.getId());
+ return view;
+ }
+
+ private static class ViewHolder {
+ final TextView text;
+ final TextView subText;
+
+ ViewHolder(TextView text, TextView subText) {
+ this.text = text;
+ this.subText = subText;
+ }
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewResizeActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewResizeActivity.java
new file mode 100644
index 0000000000..bbb0d32714
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewResizeActivity.java
@@ -0,0 +1,98 @@
+package com.mapbox.mapboxsdk.maps.activity.textureview;
+
+import android.os.Bundle;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+
+/**
+ * Test resizing a {@link android.view.TextureView} backed map on the fly.
+ */
+public class TextureViewResizeActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_textureview_resize);
+ setupToolbar();
+ setupMapView(savedInstanceState);
+ setupFab();
+ }
+
+ private void setupToolbar() {
+ ActionBar actionBar = getSupportActionBar();
+ if (actionBar != null) {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ getSupportActionBar().setHomeButtonEnabled(true);
+ }
+ }
+
+ private void setupMapView(Bundle savedInstanceState) {
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.getMapAsync(mapboxMap -> TextureViewResizeActivity.this.mapboxMap = mapboxMap);
+ }
+
+ private void setupFab() {
+ FloatingActionButton fabDebug = (FloatingActionButton) findViewById(R.id.fabResize);
+ fabDebug.setOnClickListener(view -> {
+ if (mapView != null) {
+ View parent = findViewById(R.id.coordinator_layout);
+ int width = parent.getWidth() == mapView.getWidth() ? parent.getWidth() / 2 : parent.getWidth();
+ int height = parent.getHeight() == mapView.getHeight() ? parent.getHeight() / 2 : parent.getHeight();
+ mapView.setLayoutParams(new CoordinatorLayout.LayoutParams(width, height));
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewTransparentBackgroundActivity.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewTransparentBackgroundActivity.java
new file mode 100644
index 0000000000..8322abfefc
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/activity/textureview/TextureViewTransparentBackgroundActivity.java
@@ -0,0 +1,94 @@
+package com.mapbox.mapboxsdk.maps.activity.textureview;
+
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.widget.ImageView;
+
+import com.mapbox.mapboxsdk.maps.MapView;
+import com.mapbox.mapboxsdk.maps.MapboxMap;
+import com.mapbox.mapboxsdk.testapp.R;
+import com.mapbox.mapboxsdk.maps.utils.ResourceUtils;
+
+import java.io.IOException;
+
+import timber.log.Timber;
+
+/**
+ * Example showcasing how to create a TextureView with a transparent background.
+ */
+public class TextureViewTransparentBackgroundActivity extends AppCompatActivity {
+
+ private MapView mapView;
+ private MapboxMap mapboxMap;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_textureview_transparent);
+ setupBackground();
+ setupMapView(savedInstanceState);
+ }
+
+ private void setupBackground() {
+ ImageView imageView = (ImageView) findViewById(R.id.imageView);
+ imageView.setImageResource(R.drawable.water);
+ imageView.setScaleType(ImageView.ScaleType.FIT_XY);
+ }
+
+ private void setupMapView(Bundle savedInstanceState) {
+ mapView = (MapView) findViewById(R.id.mapView);
+ mapView.onCreate(savedInstanceState);
+ mapView.getMapAsync(map -> {
+ mapboxMap = map;
+
+ try {
+ map.setStyleJson(ResourceUtils.readRawResource(getApplicationContext(), R.raw.no_bg_style));
+ } catch (IOException exception) {
+ Timber.e(exception);
+ }
+ });
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ mapView.onStart();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ mapView.onResume();
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ mapView.onPause();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ mapView.onStop();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mapView.onSaveInstanceState(outState);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mapView.onDestroy();
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ mapView.onLowMemory();
+ }
+
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/FontCache.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/FontCache.java
new file mode 100644
index 0000000000..9174d86a44
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/FontCache.java
@@ -0,0 +1,26 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.content.Context;
+import android.graphics.Typeface;
+
+import java.util.Hashtable;
+
+import timber.log.Timber;
+
+public class FontCache {
+
+ private static Hashtable<String, Typeface> fontCache = new Hashtable<>();
+
+ public static Typeface get(String name, Context context) {
+ Typeface tf = fontCache.get(name);
+ if (tf == null) {
+ try {
+ tf = Typeface.createFromAsset(context.getAssets(), name);
+ fontCache.put(name, tf);
+ } catch (Exception exception) {
+ Timber.e("Font not found");
+ }
+ }
+ return tf;
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/GeoParseUtil.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/GeoParseUtil.java
new file mode 100644
index 0000000000..fd225d0563
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/GeoParseUtil.java
@@ -0,0 +1,51 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.content.Context;
+import android.text.TextUtils;
+
+import com.mapbox.geojson.Feature;
+import com.mapbox.geojson.FeatureCollection;
+import com.mapbox.geojson.Point;
+import com.mapbox.mapboxsdk.geometry.LatLng;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+public class GeoParseUtil {
+
+ public static String loadStringFromAssets(final Context context, final String fileName) throws IOException {
+ if (TextUtils.isEmpty(fileName)) {
+ throw new NullPointerException("No GeoJSON File Name passed in.");
+ }
+ InputStream is = context.getAssets().open(fileName);
+ BufferedReader rd = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF-8")));
+ return readAll(rd);
+ }
+
+ public static List<LatLng> parseGeoJsonCoordinates(String geojsonStr) {
+ List<LatLng> latLngs = new ArrayList<>();
+ FeatureCollection featureCollection = FeatureCollection.fromJson(geojsonStr);
+ for (Feature feature : featureCollection.features()) {
+ if (feature.geometry() instanceof Point) {
+ Point point = (Point) feature.geometry();
+ latLngs.add(new LatLng(point.latitude(), point.longitude()));
+ }
+ }
+ return latLngs;
+ }
+
+ private static String readAll(Reader rd) throws IOException {
+ StringBuilder sb = new StringBuilder();
+ int cp;
+ while ((cp = rd.read()) != -1) {
+ sb.append((char) cp);
+ }
+ return sb.toString();
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/IconUtils.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/IconUtils.java
new file mode 100644
index 0000000000..ee8d197ed8
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/IconUtils.java
@@ -0,0 +1,31 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.ColorInt;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.v4.content.res.ResourcesCompat;
+import android.support.v4.graphics.drawable.DrawableCompat;
+
+import com.mapbox.mapboxsdk.annotations.Icon;
+import com.mapbox.mapboxsdk.annotations.IconFactory;
+
+public class IconUtils {
+
+ /**
+ * Demonstrates converting any Drawable to an Icon, for use as a marker icon.
+ */
+ public static Icon drawableToIcon(@NonNull Context context, @DrawableRes int id, @ColorInt int colorRes) {
+ Drawable vectorDrawable = ResourcesCompat.getDrawable(context.getResources(), id, context.getTheme());
+ Bitmap bitmap = Bitmap.createBitmap(vectorDrawable.getIntrinsicWidth(),
+ vectorDrawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ vectorDrawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ DrawableCompat.setTint(vectorDrawable, colorRes);
+ vectorDrawable.draw(canvas);
+ return IconFactory.getInstance(context).fromBitmap(bitmap);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ItemClickSupport.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ItemClickSupport.java
new file mode 100644
index 0000000000..ad04c98f78
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ItemClickSupport.java
@@ -0,0 +1,95 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+
+import com.mapbox.mapboxsdk.testapp.R;
+
+public class ItemClickSupport {
+ private final RecyclerView recyclerView;
+ private OnItemClickListener onItemClickListener;
+ private OnItemLongClickListener onItemLongClickListener;
+ private View.OnClickListener onClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ if (onItemClickListener != null) {
+ RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
+ onItemClickListener.onItemClicked(recyclerView, holder.getAdapterPosition(), view);
+ }
+ }
+ };
+ private View.OnLongClickListener onLongClickListener = new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View view) {
+ if (onItemLongClickListener != null) {
+ RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
+ return onItemLongClickListener.onItemLongClicked(recyclerView, holder.getAdapterPosition(), view);
+ }
+ return false;
+ }
+ };
+ private RecyclerView.OnChildAttachStateChangeListener attachListener
+ = new RecyclerView.OnChildAttachStateChangeListener() {
+ @Override
+ public void onChildViewAttachedToWindow(View view) {
+ if (onItemClickListener != null) {
+ view.setOnClickListener(onClickListener);
+ }
+ if (onItemLongClickListener != null) {
+ view.setOnLongClickListener(onLongClickListener);
+ }
+ }
+
+ @Override
+ public void onChildViewDetachedFromWindow(View view) {
+
+ }
+ };
+
+ private ItemClickSupport(RecyclerView recyclerView) {
+ this.recyclerView = recyclerView;
+ this.recyclerView.setTag(R.id.item_click_support, this);
+ this.recyclerView.addOnChildAttachStateChangeListener(attachListener);
+ }
+
+ public static ItemClickSupport addTo(RecyclerView view) {
+ ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
+ if (support == null) {
+ support = new ItemClickSupport(view);
+ }
+ return support;
+ }
+
+ public static ItemClickSupport removeFrom(RecyclerView view) {
+ ItemClickSupport support = (ItemClickSupport) view.getTag(R.id.item_click_support);
+ if (support != null) {
+ support.detach(view);
+ }
+ return support;
+ }
+
+ public ItemClickSupport setOnItemClickListener(OnItemClickListener listener) {
+ onItemClickListener = listener;
+ return this;
+ }
+
+ public ItemClickSupport setOnItemLongClickListener(OnItemLongClickListener listener) {
+ onItemLongClickListener = listener;
+ return this;
+ }
+
+ private void detach(RecyclerView view) {
+ view.removeOnChildAttachStateChangeListener(attachListener);
+ view.setTag(R.id.item_click_support, null);
+ }
+
+ public interface OnItemClickListener {
+
+ void onItemClicked(RecyclerView recyclerView, int position, View view);
+ }
+
+ public interface OnItemLongClickListener {
+
+ boolean onItemLongClicked(RecyclerView recyclerView, int position, View view);
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/OfflineUtils.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/OfflineUtils.java
new file mode 100644
index 0000000000..1caebdb0c8
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/OfflineUtils.java
@@ -0,0 +1,36 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.support.annotation.NonNull;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+import timber.log.Timber;
+
+import static com.mapbox.mapboxsdk.maps.activity.offline.OfflineActivity.JSON_CHARSET;
+import static com.mapbox.mapboxsdk.maps.activity.offline.OfflineActivity.JSON_FIELD_REGION_NAME;
+
+public class OfflineUtils {
+
+ public static String convertRegionName(@NonNull byte[] metadata) {
+ try {
+ String json = new String(metadata, JSON_CHARSET);
+ JsonObject jsonObject = new Gson().fromJson(json, JsonObject.class);
+ return jsonObject.get(JSON_FIELD_REGION_NAME).getAsString();
+ } catch (Exception exception) {
+ return null;
+ }
+ }
+
+ public static byte[] convertRegionName(String regionName) {
+ try {
+ JsonObject jsonObject = new JsonObject();
+ jsonObject.addProperty(JSON_FIELD_REGION_NAME, regionName);
+ return jsonObject.toString().getBytes(JSON_CHARSET);
+ } catch (Exception exception) {
+ Timber.e(exception, "Failed to encode metadata: ");
+ }
+ return null;
+ }
+
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ResourceUtils.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ResourceUtils.java
new file mode 100644
index 0000000000..18ab4c4b3d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ResourceUtils.java
@@ -0,0 +1,39 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.content.Context;
+import android.support.annotation.RawRes;
+import android.util.TypedValue;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.io.Writer;
+
+public class ResourceUtils {
+
+ public static String readRawResource(Context context, @RawRes int rawResource) throws IOException {
+ String json = "";
+ if (context != null) {
+ Writer writer = new StringWriter();
+ char[] buffer = new char[1024];
+ try (InputStream is = context.getResources().openRawResource(rawResource)) {
+ Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
+ int numRead;
+ while ((numRead = reader.read(buffer)) != -1) {
+ writer.write(buffer, 0, numRead);
+ }
+ }
+ json = writer.toString();
+ }
+ return json;
+ }
+
+ public static float convertDpToPx(Context context, float dp) {
+ return TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics());
+ }
+}
+
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TimingLogger.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TimingLogger.java
new file mode 100644
index 0000000000..5c6bb7e1b5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TimingLogger.java
@@ -0,0 +1,160 @@
+package com.mapbox.mapboxsdk.maps.utils;
+/*
+ * Copyright (C) 2007 The Android Open Source Project
+ *
+ * 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.
+ */
+
+import android.os.SystemClock;
+
+import java.util.ArrayList;
+
+import timber.log.Timber;
+
+/**
+ * A utility class to help log timings splits throughout a method call.
+ * Typical usage is:
+ * <p>
+ * <pre>
+ * TimingLogger timings = new TimingLogger(TAG, "methodA");
+ * // ... do some work A ...
+ * timings.addSplit("work A");
+ * // ... do some work B ...
+ * timings.addSplit("work B");
+ * // ... do some work C ...
+ * timings.addSplit("work C");
+ * timings.dumpToLog();
+ * </pre>
+ * <p>
+ * <p>The dumpToLog call would add the following to the log:</p>
+ * <p>
+ * <pre>
+ * D/TAG ( 3459): methodA: begin
+ * D/TAG ( 3459): methodA: 9 ms, work A
+ * D/TAG ( 3459): methodA: 1 ms, work B
+ * D/TAG ( 3459): methodA: 6 ms, work C
+ * D/TAG ( 3459): methodA: end, 16 ms
+ * </pre>
+ */
+public class TimingLogger {
+ /**
+ * The Log tag to use for checking Log.isLoggable and for
+ * logging the timings.
+ */
+ private String tag;
+ /**
+ * A label to be included in every log.
+ */
+ private String label;
+ /**
+ * Used to track whether Log.isLoggable was enabled at reset time.
+ */
+ private boolean disabled;
+ /**
+ * Stores the time of each split.
+ */
+ private ArrayList<Long> splits;
+ /**
+ * Stores the labels for each split.
+ */
+ private ArrayList<String> splitLabels;
+
+ /**
+ * Create and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ *
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public TimingLogger(String tag, String label) {
+ reset(tag, label);
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the specific tag. If the Log.isLoggable is not enabled to at
+ * least the Log.VERBOSE level for that tag at creation time then
+ * the addSplit and dumpToLog call will do nothing.
+ *
+ * @param tag the log tag to use while logging the timings
+ * @param label a string to be displayed with each log
+ */
+ public void reset(String tag, String label) {
+ this.tag = tag;
+ this.label = label;
+ reset();
+ }
+
+ /**
+ * Clear and initialize a TimingLogger object that will log using
+ * the tag and label that was specified previously, either via
+ * the constructor or a call to reset(tag, label). If the
+ * Log.isLoggable is not enabled to at least the Log.VERBOSE
+ * level for that tag at creation time then the addSplit and
+ * dumpToLog call will do nothing.
+ */
+ public void reset() {
+ disabled = false; // !Log.isLoggable(tag, Log.VERBOSE);
+ if (disabled) {
+ return;
+ }
+ if (splits == null) {
+ splits = new ArrayList<Long>();
+ splitLabels = new ArrayList<String>();
+ } else {
+ splits.clear();
+ splitLabels.clear();
+ }
+ addSplit(null);
+ }
+
+ /**
+ * Add a split for the current time, labeled with splitLabel. If
+ * Log.isLoggable was not enabled to at least the Log.VERBOSE for
+ * the specified tag at construction or reset() time then this
+ * call does nothing.
+ *
+ * @param splitLabel a label to associate with this split.
+ */
+ public void addSplit(String splitLabel) {
+ if (disabled) {
+ return;
+ }
+ long now = SystemClock.elapsedRealtime();
+ splits.add(now);
+ splitLabels.add(splitLabel);
+ }
+
+ /**
+ * Dumps the timings to the log using Timber.d(). If Log.isLoggable was
+ * not enabled to at least the Log.VERBOSE for the specified tag at
+ * construction or reset() time then this call does nothing.
+ */
+ public void dumpToLog() {
+ if (disabled) {
+ return;
+ }
+ Timber.d("%s: begin", label);
+ final long first = splits.get(0);
+ long now = first;
+ for (int i = 1; i < splits.size(); i++) {
+ now = splits.get(i);
+ final String splitLabel = splitLabels.get(i);
+ final long prev = splits.get(i - 1);
+ Timber.d("%s: %s ms, %s", label, (now - prev), splitLabel);
+ }
+ Timber.d("%s: end, %s ms", label, (now - first));
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TokenUtils.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TokenUtils.java
new file mode 100644
index 0000000000..1c699ae5de
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/TokenUtils.java
@@ -0,0 +1,37 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+
+import android.content.Context;
+import android.support.annotation.NonNull;
+
+import com.mapbox.mapboxsdk.Mapbox;
+
+public class TokenUtils {
+
+ /**
+ * <p>
+ * Returns the Mapbox access token set in the app resources.
+ * </p>
+ * It will first search for a token in the Mapbox object. If not found it
+ * will then attempt to load the access token from the
+ * {@code res/values/dev.xml} development file.
+ *
+ * @param context The {@link Context} of the {@link android.app.Activity} or {@link android.app.Fragment}.
+ * @return The Mapbox access token or null if not found.
+ */
+ public static String getMapboxAccessToken(@NonNull Context context) {
+ try {
+ // Read out AndroidManifest
+ String token = Mapbox.getAccessToken();
+ if (token == null || token.isEmpty()) {
+ throw new IllegalArgumentException();
+ }
+ return token;
+ } catch (Exception exception) {
+ // Use fallback on string resource, used for development
+ int tokenResId = context.getResources()
+ .getIdentifier("mapbox_access_token", "string", context.getPackageName());
+ return tokenResId != 0 ? context.getString(tokenResId) : null;
+ }
+ }
+}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ViewToBitmapUtil.java b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ViewToBitmapUtil.java
new file mode 100644
index 0000000000..962a862706
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/maps/utils/ViewToBitmapUtil.java
@@ -0,0 +1,22 @@
+package com.mapbox.mapboxsdk.maps.utils;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.support.annotation.NonNull;
+import android.view.View;
+
+/**
+ * Converts a View to a Bitmap so we can use an Android SDK View as a Symbol.
+ */
+public class ViewToBitmapUtil {
+
+ public static Bitmap convertToBitmap(@NonNull View view) {
+ view.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
+ Bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ return bitmap;
+ }
+}