Commit 2820e72a authored by Evan W. Patton's avatar Evan W. Patton Committed by Jeffrey Schiller

Implement custom zoom controls

The Android SDK provided zoom controls used by osmdroid are not scaled
by ScaledFrameLayout, and so do not work correctly when Sizing is
Fixed. Further, the ZoomButtonsController is deprecated starting with
SDK 26 and Google recommends that developers provide custom views for
zoom controls.

This change implements zoom controls for the Map component similar to
those shown in the designer.

Change-Id: If5d15ea1387161f42a9bd6b59b210ea23baa85a0
parent 7ec248ca
...@@ -5,18 +5,12 @@ ...@@ -5,18 +5,12 @@
package com.google.appinventor.components.runtime.util; package com.google.appinventor.components.runtime.util;
import java.io.File; import android.content.Context;
import java.io.IOException; import android.graphics.Canvas;
import java.io.InputStream; import android.graphics.Paint;
import java.util.ArrayList; import android.graphics.Picture;
import java.util.Collections; import android.graphics.drawable.BitmapDrawable;
import java.util.HashMap; import android.graphics.drawable.Drawable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import android.graphics.drawable.PictureDrawable; import android.graphics.drawable.PictureDrawable;
import android.location.Location; import android.location.Location;
import android.os.Bundle; import android.os.Bundle;
...@@ -24,8 +18,30 @@ import android.os.Handler; ...@@ -24,8 +18,30 @@ import android.os.Handler;
import android.os.Message; import android.os.Message;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
import android.widget.RelativeLayout;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import com.google.appinventor.components.common.ComponentConstants; import com.google.appinventor.components.common.ComponentConstants;
import com.google.appinventor.components.runtime.Form;
import com.google.appinventor.components.runtime.LocationSensor; import com.google.appinventor.components.runtime.LocationSensor;
import com.google.appinventor.components.runtime.util.MapFactory.HasFill;
import com.google.appinventor.components.runtime.util.MapFactory.HasStroke;
import com.google.appinventor.components.runtime.util.MapFactory.MapCircle;
import com.google.appinventor.components.runtime.util.MapFactory.MapController;
import com.google.appinventor.components.runtime.util.MapFactory.MapEventListener;
import com.google.appinventor.components.runtime.util.MapFactory.MapFeature;
import com.google.appinventor.components.runtime.util.MapFactory.MapLineString;
import com.google.appinventor.components.runtime.util.MapFactory.MapMarker;
import com.google.appinventor.components.runtime.util.MapFactory.MapPolygon;
import com.google.appinventor.components.runtime.util.MapFactory.MapRectangle;
import com.google.appinventor.components.runtime.util.MapFactory.MapType;
import com.google.appinventor.components.runtime.view.ZoomControlView;
import org.osmdroid.api.IGeoPoint; import org.osmdroid.api.IGeoPoint;
import org.osmdroid.config.Configuration; import org.osmdroid.config.Configuration;
import org.osmdroid.events.MapListener; import org.osmdroid.events.MapListener;
...@@ -52,32 +68,17 @@ import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer; ...@@ -52,32 +68,17 @@ import org.osmdroid.views.overlay.mylocation.IMyLocationConsumer;
import org.osmdroid.views.overlay.mylocation.IMyLocationProvider; import org.osmdroid.views.overlay.mylocation.IMyLocationProvider;
import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay; import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay;
import com.caverock.androidsvg.SVG; import java.io.File;
import com.caverock.androidsvg.SVGParseException; import java.io.IOException;
import com.google.appinventor.components.runtime.Form; import java.io.InputStream;
import com.google.appinventor.components.runtime.util.MapFactory.HasFill; import java.util.ArrayList;
import com.google.appinventor.components.runtime.util.MapFactory.HasStroke; import java.util.Collections;
import com.google.appinventor.components.runtime.util.MapFactory.MapCircle; import java.util.HashMap;
import com.google.appinventor.components.runtime.util.MapFactory.MapController; import java.util.HashSet;
import com.google.appinventor.components.runtime.util.MapFactory.MapEventListener; import java.util.Iterator;
import com.google.appinventor.components.runtime.util.MapFactory.MapFeature; import java.util.List;
import com.google.appinventor.components.runtime.util.MapFactory.MapMarker; import java.util.Map;
import com.google.appinventor.components.runtime.util.MapFactory.MapPolygon; import java.util.Set;
import com.google.appinventor.components.runtime.util.MapFactory.MapRectangle;
import com.google.appinventor.components.runtime.util.MapFactory.MapLineString;
import com.google.appinventor.components.runtime.util.MapFactory.MapType;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Picture;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Animation;
class NativeOpenStreetMapController implements MapController, MapListener { class NativeOpenStreetMapController implements MapController, MapListener {
/* copied from SVG */ /* copied from SVG */
...@@ -91,6 +92,7 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -91,6 +92,7 @@ class NativeOpenStreetMapController implements MapController, MapListener {
private static final String TAG = NativeOpenStreetMapController.class.getSimpleName(); private static final String TAG = NativeOpenStreetMapController.class.getSimpleName();
private boolean caches; private boolean caches;
private final Form form; private final Form form;
private RelativeLayout containerView;
private MapView view; private MapView view;
private MapType tileType; private MapType tileType;
private boolean zoomEnabled; private boolean zoomEnabled;
...@@ -104,6 +106,7 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -104,6 +106,7 @@ class NativeOpenStreetMapController implements MapController, MapListener {
private TouchOverlay touch = null; private TouchOverlay touch = null;
private OverlayInfoWindow defaultInfoWindow = null; private OverlayInfoWindow defaultInfoWindow = null;
private boolean ready = false; private boolean ready = false;
private ZoomControlView zoomControls = null;
private static class AppInventorLocationSensorAdapter implements IMyLocationProvider, private static class AppInventorLocationSensorAdapter implements IMyLocationProvider,
LocationSensor.LocationSensorListener { LocationSensor.LocationSensorListener {
...@@ -286,12 +289,19 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -286,12 +289,19 @@ class NativeOpenStreetMapController implements MapController, MapListener {
} }
} }
}); });
zoomControls = new ZoomControlView(view);
userLocation = new MyLocationNewOverlay(locationProvider, view); userLocation = new MyLocationNewOverlay(locationProvider, view);
containerView = new RelativeLayout(form);
containerView.setClipChildren(true);
containerView.addView(view, new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
containerView.addView(zoomControls);
zoomControls.setVisibility(View.GONE); // not shown by default
} }
@Override @Override
public View getView() { public View getView() {
return view; return containerView;
} }
@Override @Override
...@@ -312,6 +322,7 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -312,6 +322,7 @@ class NativeOpenStreetMapController implements MapController, MapListener {
@Override @Override
public void setZoom(int zoom) { public void setZoom(int zoom) {
view.getController().setZoom((double) zoom); view.getController().setZoom((double) zoom);
zoomControls.updateButtons();
} }
@Override @Override
...@@ -386,8 +397,11 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -386,8 +397,11 @@ class NativeOpenStreetMapController implements MapController, MapListener {
@Override @Override
public void setZoomControlEnabled(boolean enabled) { public void setZoomControlEnabled(boolean enabled) {
view.setBuiltInZoomControls(enabled); if (zoomControlEnabled != enabled) {
zoomControls.setVisibility(enabled ? View.VISIBLE : View.GONE);
zoomControlEnabled = enabled; zoomControlEnabled = enabled;
containerView.invalidate();
}
} }
@Override @Override
...@@ -1160,6 +1174,7 @@ class NativeOpenStreetMapController implements MapController, MapListener { ...@@ -1160,6 +1174,7 @@ class NativeOpenStreetMapController implements MapController, MapListener {
@Override @Override
public boolean onZoom(ZoomEvent event) { public boolean onZoom(ZoomEvent event) {
zoomControls.updateButtons();
for (MapEventListener listener : eventListeners) { for (MapEventListener listener : eventListeners) {
listener.onZoom(); listener.onZoom();
} }
......
// -*- mode: java; c-basic-offset: 2; -*-
// Copyright 2018 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime.view;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import com.google.appinventor.components.runtime.util.ViewUtil;
import org.osmdroid.views.MapView;
public class ZoomControlView extends LinearLayout {
private final MapView parent;
private final Button zoomIn;
private final Button zoomOut;
private float density;
public ZoomControlView(MapView parent) {
super(parent.getContext());
density = parent.getContext().getResources().getDisplayMetrics().density;
this.parent = parent;
this.setOrientation(LinearLayout.VERTICAL);
zoomIn = new Button(parent.getContext());
zoomOut = new Button(parent.getContext());
initButton(zoomIn, "+");
initButton(zoomOut, "−");
zoomIn.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
ZoomControlView.this.parent.getController().zoomIn();
}
});
zoomOut.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View view) {
ZoomControlView.this.parent.getController().zoomOut();
}
});
ViewUtil.setBackgroundDrawable(zoomIn, getZoomInDrawable(density));
ViewUtil.setBackgroundDrawable(zoomOut, getZoomOutDrawable(density));
int[][] states = new int[][] {
new int[] {-android.R.attr.state_enabled },
new int[] { android.R.attr.state_enabled }
};
int[] colors = new int[] {
Color.LTGRAY,
Color.BLACK
};
zoomIn.setTextColor(new ColorStateList(states, colors));
zoomOut.setTextColor(new ColorStateList(states, colors));
addView(zoomIn);
addView(zoomOut);
this.setPadding((int)(12 * density), (int)(12 * density), 0, 0);
updateButtons();
}
/**
* Update the state of the zoom buttons based on the current map tile layer and its zoom level.
*/
@SuppressWarnings("WeakerAccess")
public final void updateButtons() {
zoomIn.setEnabled(parent.canZoomIn());
zoomOut.setEnabled(parent.canZoomOut());
}
private void initButton(Button button, String text) {
button.setText(text);
button.setTextSize(22);
button.setPadding(0, 0, 0, 0);
button.setWidth((int)(30 * density));
button.setHeight((int)(30 * density));
button.setSingleLine();
button.setGravity(Gravity.CENTER);
}
private static Drawable getZoomInDrawable(float density) {
final int R = (int)(4 * density);
ShapeDrawable drawable = new ShapeDrawable(new RoundRectShape(new float[] { R, R, R, R, 0, 0, 0, 0 }, null, null));
drawable.getPaint().setColor(Color.WHITE);
return drawable;
}
private static Drawable getZoomOutDrawable(float density) {
final int R = (int)(4 * density);
ShapeDrawable drawable = new ShapeDrawable(new RoundRectShape(new float[] { 0, 0, 0, 0, R, R, R, R }, null, null));
drawable.getPaint().setColor(Color.WHITE);
return drawable;
}
}
// -*- mode: java; c-basic-offset: 2; -*- // -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. // Copyright © 2017-2018 Massachusetts Institute of Technology, All rights reserved.
// Released under the Apache License, Version 2.0 // Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime; package com.google.appinventor.components.runtime;
import com.google.appinventor.components.runtime.shadows.ShadowEventDispatcher; import com.google.appinventor.components.runtime.shadows.ShadowEventDispatcher;
import com.google.appinventor.components.runtime.shadows.org.osmdroid.views.ShadowMapView;
import com.google.appinventor.components.runtime.util.ErrorMessages; import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.MapFactory.MapFeature; import com.google.appinventor.components.runtime.util.MapFactory.MapFeature;
import com.google.appinventor.components.runtime.util.MapFactory.MapMarker;
import com.google.appinventor.components.runtime.util.YailList; import com.google.appinventor.components.runtime.util.YailList;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowView;
import java.util.Collections; import java.util.Collections;
...@@ -178,7 +177,7 @@ public class FeatureCollectionTest extends MapTestBase { ...@@ -178,7 +177,7 @@ public class FeatureCollectionTest extends MapTestBase {
Marker marker = new Marker(getMap()); Marker marker = new Marker(getMap());
collection.removeFeature(marker); collection.removeFeature(marker);
assertEquals(0, collection.Features().size()); assertEquals(0, collection.Features().size());
ShadowMapView view = Shadow.extract(getMap().getView()); ShadowView view = Shadow.extract(getMap().getView());
view.clearWasInvalidated(); view.clearWasInvalidated();
collection.Features(YailList.makeList(Collections.singletonList(marker))); collection.Features(YailList.makeList(Collections.singletonList(marker)));
assertTrue(view.wasInvalidated()); assertTrue(view.wasInvalidated());
...@@ -203,7 +202,7 @@ public class FeatureCollectionTest extends MapTestBase { ...@@ -203,7 +202,7 @@ public class FeatureCollectionTest extends MapTestBase {
} }
private void testFeatureListSetter(MapFeature feature) { private void testFeatureListSetter(MapFeature feature) {
ShadowMapView view = Shadow.extract(getMap().getView()); ShadowView view = Shadow.extract(getMap().getView());
view.clearWasInvalidated(); view.clearWasInvalidated();
collection.Features(YailList.makeList(Collections.singletonList(feature))); collection.Features(YailList.makeList(Collections.singletonList(feature)));
assertTrue(view.wasInvalidated()); assertTrue(view.wasInvalidated());
......
// -*- mode: java; c-basic-offset: 2; -*- // -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. // Copyright © 2017-2018 Massachusetts Institute of Technology, All rights reserved.
// Released under the Apache License, Version 2.0 // Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
package com.google.appinventor.components.runtime; package com.google.appinventor.components.runtime;
import com.google.appinventor.components.runtime.shadows.ShadowAsynchUtil; import com.google.appinventor.components.runtime.shadows.ShadowAsynchUtil;
import com.google.appinventor.components.runtime.shadows.org.osmdroid.views.ShadowMapView;
import com.google.appinventor.components.runtime.util.ErrorMessages; import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.GeometryUtil; import com.google.appinventor.components.runtime.util.GeometryUtil;
import com.google.appinventor.components.runtime.util.YailList; import com.google.appinventor.components.runtime.util.YailList;
...@@ -15,6 +14,7 @@ import org.junit.Before; ...@@ -15,6 +14,7 @@ import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowView;
import java.io.IOException; import java.io.IOException;
import java.util.Collections; import java.util.Collections;
...@@ -306,9 +306,9 @@ public class MapTest extends MapTestBase { ...@@ -306,9 +306,9 @@ public class MapTest extends MapTestBase {
public void testFeatureListRemoval() { public void testFeatureListRemoval() {
Marker marker1 = new Marker(map); Marker marker1 = new Marker(map);
Marker marker2 = new Marker(map); Marker marker2 = new Marker(map);
((ShadowMapView) Shadow.extract(map.getView())).clearWasInvalidated(); ((ShadowView) Shadow.extract(map.getView())).clearWasInvalidated();
map.Features(YailList.makeList(Collections.singletonList(marker1))); map.Features(YailList.makeList(Collections.singletonList(marker1)));
assertTrue(((ShadowMapView) Shadow.extract(map.getView())).wasInvalidated()); assertTrue(((ShadowView) Shadow.extract(map.getView())).wasInvalidated());
assertEquals(1, map.Features().size()); assertEquals(1, map.Features().size());
assertTrue(map.Features().contains(marker1)); assertTrue(map.Features().contains(marker1));
assertFalse(map.Features().contains(marker2)); assertFalse(map.Features().contains(marker2));
......
// -*- mode: java; c-basic-offset: 2; -*- // -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. // Copyright © 2017-2018 Massachusetts Institute of Technology, All rights reserved.
// Released under the Apache License, Version 2.0 // Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
...@@ -139,7 +139,7 @@ public class MapTestBase extends RobolectricTestBase { ...@@ -139,7 +139,7 @@ public class MapTestBase extends RobolectricTestBase {
public void setUp() { public void setUp() {
super.setUp(); super.setUp();
map = new Map(getForm()); map = new Map(getForm());
map.getView().measure(ComponentConstants.MAP_PREFERRED_WIDTH, ComponentConstants.MAP_PREFERRED_HEIGHT); map.getView().requestLayout();
map.getView().layout(0, 0, ComponentConstants.MAP_PREFERRED_WIDTH, ComponentConstants.MAP_PREFERRED_HEIGHT); map.getView().layout(0, 0, ComponentConstants.MAP_PREFERRED_WIDTH, ComponentConstants.MAP_PREFERRED_HEIGHT);
} }
} }
// -*- mode: java; c-basic-offset: 2; -*- // -*- mode: java; c-basic-offset: 2; -*-
// Copyright © 2017 Massachusetts Institute of Technology, All rights reserved. // Copyright © 2017-2018 Massachusetts Institute of Technology, All rights reserved.
// Released under the Apache License, Version 2.0 // Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0 // http://www.apache.org/licenses/LICENSE-2.0
...@@ -7,13 +7,13 @@ package com.google.appinventor.components.runtime; ...@@ -7,13 +7,13 @@ package com.google.appinventor.components.runtime;
import android.graphics.Color; import android.graphics.Color;
import com.google.appinventor.components.runtime.shadows.ShadowEventDispatcher; import com.google.appinventor.components.runtime.shadows.ShadowEventDispatcher;
import com.google.appinventor.components.runtime.shadows.org.osmdroid.views.ShadowMapView;
import com.google.appinventor.components.runtime.util.ErrorMessages; import com.google.appinventor.components.runtime.util.ErrorMessages;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.locationtech.jts.geom.Geometry; import org.locationtech.jts.geom.Geometry;
import org.osmdroid.util.GeoPoint; import org.osmdroid.util.GeoPoint;
import org.robolectric.shadow.api.Shadow; import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowView;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertFalse;
...@@ -271,23 +271,23 @@ public class MarkerTest extends MapTestBase { ...@@ -271,23 +271,23 @@ public class MarkerTest extends MapTestBase {
@Test @Test
public void testVisibleNoInvalidate() { public void testVisibleNoInvalidate() {
ShadowMapView mapView = Shadow.extract(getMap().getView()); ShadowView mapView = Shadow.extract(getMap().getView());
Marker marker = new Marker(getMap()); Marker marker = new Marker(getMap());
int invalidateCalls = mapView.invalidateCalls; mapView.clearWasInvalidated();
marker.Visible(true); marker.Visible(true);
assertTrue(getMap().getController().isFeatureVisible(marker)); assertTrue(getMap().getController().isFeatureVisible(marker));
assertEquals(invalidateCalls, mapView.invalidateCalls); assertFalse(mapView.wasInvalidated()); // Marker is visible by default
} }
@Test @Test
public void testVisibleInvalidate() { public void testVisibleInvalidate() {
ShadowMapView mapView = Shadow.extract(getMap().getView()); ShadowView mapView = Shadow.extract(getMap().getView());
Marker marker = new Marker(getMap()); Marker marker = new Marker(getMap());
int invalidateCalls = mapView.invalidateCalls; mapView.clearWasInvalidated();
marker.Visible(true); marker.Visible(true);
marker.Visible(false); marker.Visible(false);
assertFalse(getMap().getController().isFeatureVisible(marker)); assertFalse(getMap().getController().isFeatureVisible(marker));
assertEquals(invalidateCalls + 1, mapView.invalidateCalls); assertTrue(mapView.wasInvalidated());
} }
@Test @Test
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment