Disabling MapFragment center for animations moving both target lat / lng and zoom level

I have a user interface with MapFragment with a transparent caption on top. The map occupies the entire screen, while the view is only the left third of the screen. As a result, by default the center of the card is disabled. When someone clicks on a marker, I want to center this marker in the fully visible area of ​​MapFragment (not in the center of MapFragment itself).

Since this is difficult to describe in words, let me use a few shots. Suppose my user interface looks like this:

Default map

When the user clicks the marker, I want to both center it and zoom in to see it closer. Without any adjustments, you will receive the following:

Map with incorrectly centered marker

I want the marker to be centered, but in the space on the right, for example:

Map with correctly centered marker

It is very easy to achieve this feat if you do not change the zoom level using the map projection:

// Offset the target latitude/longitude by preset x/y ints LatLng target = new LatLng(latitude, longitude); Projection projection = getMap().getProjection(); Point screenLocation = projection.toScreenLocation(target); screenLocation.x += offsetX; screenLocation.y += offsetY; LatLng offsetTarget = projection.fromScreenLocation(screenLocation); // Animate to the calculated lat/lng getMap().animateCamera(CameraUpdateFactory.newLatLng(offsetTarget)); 

However, if you change the zoom level at the same time, the above calculations do not work (since lat / lng offsets change with different zoom levels).

Let me run a list of attempts:

  • Quickly change the zoom level, perform calculations, return to the original position of the camera, and then the animation. Unfortunately, the sudden change in the camera (even if it is only for a split second), unfortunately, is very obvious, and I would like to avoid flickering.

  • Overlay two MapFragments on top of each other if you perform calculations while another is displayed. I found that MapFragments are not actually built to be layered on top of each other (there are inevitable errors along the way).

  • Change the location of the x / y screen to the difference in the squared zoom level. Theoretically, this should work, but it always turns off quite a bit (~ .1 latitude / longitude, which is enough to leave).

Is there a way to calculate offsetTarget even when changing the zoom level?

+42
android android-maps-v2
Mar 25 '13 at 22:23
source share
9 answers

The latest version of the game services library adds the setPadding() function to GoogleMap . This add-on projects all camera movements in the center of the offset card. This document describes how keyboard behavior behaves.

The question asked here can now be simply solved by adding a left pad equivalent to the width of the left layout.

 mMap.setPadding(leftPx, topPx, rightPx, bottomPx); 
+25
Dec 19 '13 at 1:36
source share

I found a solution that really suits the problem. The solution is to calculate the center of offset at the desired scale from the original offset, and then animate the map. To do this, first move the map camera to the desired scale, calculate the offset for this zoom level, and then restore the original zoom. After computing the new center, we can do the animation using CameraUpdateFactory.newLatLngZoom.

Hope this helps!

 private void animateLatLngZoom(LatLng latlng, int reqZoom, int offsetX, int offsetY) { // Save current zoom float originalZoom = mMap.getCameraPosition().zoom; // Move temporarily camera zoom mMap.moveCamera(CameraUpdateFactory.zoomTo(reqZoom)); Point pointInScreen = mMap.getProjection().toScreenLocation(latlng); Point newPoint = new Point(); newPoint.x = pointInScreen.x + offsetX; newPoint.y = pointInScreen.y + offsetY; LatLng newCenterLatLng = mMap.getProjection().fromScreenLocation(newPoint); // Restore original zoom mMap.moveCamera(CameraUpdateFactory.zoomTo(originalZoom)); // Animate a camera with new latlng center and required zoom. mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(newCenterLatLng, reqZoom)); } 
+12
Nov 19 '14 at 17:03
source share

Edit: The code below is for legacy v1 cards. This SO answer says how to perform similar actions in v2: How to get latitude / longitude range in Google Map V2 for Android

The key methods you need should work in long / lat and percent instead of pixels. Right now, mapview will zoom in around the center of the map, and then move to “re-contact” in the open area. You can get the width after the increase in degrees, the ratio at some screen offsets, and then reposition the map. Below is an example of a screenshot of the code below in action after clicking the "+" button: before enter image description here after enter image description here Main code:

 @Override public void onZoom(boolean zoomIn) { // TODO Auto-generated method stub controller.setZoom(zoomIn ? map.getZoomLevel()+1 : map.getZoomLevel()-1); int bias = (int) (map.getLatitudeSpan()* 1.0/3.0); // a fraction of your choosing map.getLongitudeSpan(); controller.animateTo(new GeoPoint(yourLocation.getLatitudeE6(),yourLocation.getLongitudeE6()-bias)); } 

All code:

  package com.example.testmapview; import com.google.android.maps.GeoPoint; import com.google.android.maps.ItemizedOverlay; import com.google.android.maps.MapActivity; import com.google.android.maps.MapController; import com.google.android.maps.MapView; import android.os.Bundle; import android.app.Activity; import android.graphics.drawable.Drawable; import android.view.Menu; import android.widget.ZoomButtonsController; import android.widget.ZoomButtonsController.OnZoomListener; public class MainActivity extends MapActivity { MapView map; GeoPoint yourLocation; MapController controller; ZoomButtonsController zoomButtons; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); map=(MapView)findViewById(R.id.mapview); map.setBuiltInZoomControls(true); controller = map.getController(); zoomButtons = map.getZoomButtonsController(); zoomButtons.setOnZoomListener(new OnZoomListener(){ @Override public void onVisibilityChanged(boolean arg0) { // TODO Auto-generated method stub } @Override public void onZoom(boolean zoomIn) { // TODO Auto-generated method stub controller.setZoom(zoomIn ? map.getZoomLevel()+1 : map.getZoomLevel()-1); int bias = (int) (map.getLatitudeSpan()* 1.0/3.0); // a fraction of your choosing map.getLongitudeSpan(); controller.animateTo(new GeoPoint(yourLocation.getLatitudeE6(),yourLocation.getLongitudeE6()-bias)); } }); //Dropping a pin, setting center Drawable marker=getResources().getDrawable(android.R.drawable.star_big_on); MyItemizedOverlay myItemizedOverlay = new MyItemizedOverlay(marker); map.getOverlays().add(myItemizedOverlay); yourLocation = new GeoPoint(0, 0); controller.setCenter(yourLocation); myItemizedOverlay.addItem(yourLocation, "myPoint1", "myPoint1"); controller.setZoom(5); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.activity_main, menu); return true; } @Override protected boolean isRouteDisplayed() { // TODO Auto-generated method stub return false; } } 

and basic layout:

 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.testmapview" android:versionCode="1" android:versionName="1.0" > <meta-data android:name="com.google.android.maps.v2.API_KEY" android:value="YOURKEYHERE:)"/> <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="16" /> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:theme="@style/AppTheme" > <uses-library android:name="com.google.android.maps"/> <activity android:name="com.example.testmapview.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest> 

If this does not fully answer your question, let me know because it was a fun experiment.

+11
Mar 29 '13 at 21:04
source share

The API design object is not so useful, I ran into the same problem and rolled back.

 public class SphericalMercatorProjection { public static PointD latLngToWorldXY(LatLng latLng, double zoomLevel) { final double worldWidthPixels = toWorldWidthPixels(zoomLevel); final double x = latLng.longitude / 360 + .5; final double siny = Math.sin(Math.toRadians(latLng.latitude)); final double y = 0.5 * Math.log((1 + siny) / (1 - siny)) / -(2 * Math.PI) + .5; return new PointD(x * worldWidthPixels, y * worldWidthPixels); } public static LatLng worldXYToLatLng(PointD point, double zoomLevel) { final double worldWidthPixels = toWorldWidthPixels(zoomLevel); final double x = point.x / worldWidthPixels - 0.5; final double lng = x * 360; double y = .5 - (point.y / worldWidthPixels); final double lat = 90 - Math.toDegrees(Math.atan(Math.exp(-y * 2 * Math.PI)) * 2); return new LatLng(lat, lng); } private static double toWorldWidthPixels(double zoomLevel) { return 256 * Math.pow(2, zoomLevel); } } 

Using this code, you can transform the map around a new target (i.e., not the center). I decided to use a reference point system, where (0,5, 0,5) is the default and represents the middle of the screen. (0,0) top left and (1, 1) bottom left.

 private LatLng getOffsetLocation(LatLng location, double zoom) { Point size = getSize(); PointD anchorOffset = new PointD(size.x * (0.5 - anchor.x), size.y * (0.5 - anchor.y)); PointD screenCenterWorldXY = SphericalMercatorProjection.latLngToWorldXY(location, zoom); PointD newScreenCenterWorldXY = new PointD(screenCenterWorldXY.x + anchorOffset.x, screenCenterWorldXY.y + anchorOffset.y); newScreenCenterWorldXY.rotate(screenCenterWorldXY, cameraPosition.bearing); return SphericalMercatorProjection.worldXYToLatLng(newScreenCenterWorldXY, zoom); } 

Basically, you use the projection to get the XY coordinates in the world, and then sweep this point and return it back to LatLng. Then you can transfer it to your card. PointD is a simple type that contains x, y as doubles, and also makes a turn.

 public class PointD { public double x; public double y; public PointD(double x, double y) { this.x = x; this.y = y; } public void rotate(PointD origin, float angleDeg) { double rotationRadians = Math.toRadians(angleDeg); this.x -= origin.x; this.y -= origin.y; double rotatedX = this.x * Math.cos(rotationRadians) - this.y * Math.sin(rotationRadians); double rotatedY = this.x * Math.sin(rotationRadians) + this.y * Math.cos(rotationRadians); this.x = rotatedX; this.y = rotatedY; this.x += origin.x; this.y += origin.y; } } 

Please note that if you are updating the map support and location at the same time, it is important that you use the new bearing in getOffsetLocation.

+3
May 20 '14 at 20:51
source share

The easiest way is to link the camera animation. First you usually enlarge the image to image # 2 using animateCamera with 3 parameters , and in CancellableCallback.onFinish you perform projection calculations and animate using your code to change the center.

I believe (not tested) that will look very good if you set some good animation durations for both animations. Perhaps the second animation should be much faster than the first. In addition, if your transparent, overlaid view is not displayed when the marker is not selected, a good UX will animate it from left to left during the second animation.

A difficult way would be to implement your own Spherical Mercator Projection, like the one that uses the Maps API v2. If you only need a longitudinal offset, it should not be so difficult. If your users can change the bearing and you do not want to reset it to 0 when scaling, you also need a latitude shift, and this will probably be a little more difficult to implement.

I also think that it would be nice to have the functionality of the Maps API v2, where you can get a projection for any CameraPosition and not only the current one, so you can send a function request here: http://code.google.com/p/gmaps-api -issues / issues / list? can = 2 & q = apitype = Android2 . You can easily use it in your code for the effect you want.

+2
Apr 03 '13 at 18:13
source share

To set the marker, we use Overlays in Android, In overlay class = => on the drawing method put this thing

boundCenter (marker);

and the constructor also mentions the following:

  public xxxxxxOverlay(Drawable marker, ImageView dragImage, MapView map) { super(boundCenter(marker)); . . . . . } 
0
Apr 04 '13 at 6:36 on
source share

I have almost the same problem as you (only my bias is vertical), and I tried a solution that does not work properly, but can help you (us) find the best solution.

I did to move the map camera to get the projection onto the target, and apply the offset there.

I put the code in the Utils class and looked like this:

 public static CameraBuilder moveUsingProjection(GoogleMap map, CameraBuilder target, int verticalMapOffset) { CameraPosition oldPosition = map.getCameraPosition(); map.moveCamera(target.build(map)); CameraBuilder result = CameraBuilder.builder() .target(moveUsingProjection(map.getProjection(), map.getCameraPosition().target, verticalMapOffset)) .zoom(map.getCameraPosition().zoom).tilt(map.getCameraPosition().tilt); map.moveCamera(CameraUpdateFactory.newCameraPosition(oldPosition)); return result; } public static LatLng moveUsingProjection(Projection projection, LatLng latLng, int verticalMapOffset) { Point screenPoint = projection.toScreenLocation(latLng); screenPoint.y -= verticalMapOffset; return projection.fromScreenLocation(screenPoint); } 

Then you can use CameraBuilder, returned by these methods for animation, instead of the original (not biased) target.

It works (more or less), but has two big problems:

  • The card sometimes flickers (this is the biggest problem because it makes unsuitable hacking)
  • This is a hack that may work today and may not work tomorrow (I know that the same algorithm works on iOS, for example).

So, I didn’t go for it, but I thought of an alternative: if you had an identical map, but hidden, you could use this map for intermediate calculations, and then use the final result on the visible map.

I think this alternative will work, but it's terrible, since you will need to use two identical cards with the overhead that comes with it, for you and for the device.

As I said, I don’t have a final solution for this, but maybe this helps a bit.

Hope this helps!

0
May 29 '13 at 19:29
source share

I solved the problem by calculating the distance vector using the LatLng values ​​of two fixed points on the map, then creating another LatLng using the marker position, this vector and the given ratio value. There are many objects in this, but it does not depend on the current level of scaling and rotation of the map and may just work just fine for you!

 // Just a helper function to obtain the device display size in px Point displaySize = UIUtil.getDisplaySize(getActivity().getWindowManager().getDefaultDisplay()); // The two fixed points: Bottom end of the map & Top end of the map, // both centered horizontally (because I want to offset the camera vertically... // if you want to offset it to the right, for instance, use the left & right ends) Point up = new Point(displaySize.x / 2, 0); Point down = new Point(displaySize.x / 2, displaySize.y); // Convert to LatLng using the GoogleMap object LatLng latlngUp = googleMap.getProjection().fromScreenLocation(up); LatLng latlngDown = googleMap.getProjection().fromScreenLocation(down); // Create a vector between the "screen point LatLng" objects double[] vector = new double[] {latlngUp.latitude - latlngDown.latitude, latlngUp.longitude - latlngDown.longitude}; // Calculate the offset position LatLng latlngMarker = marker.getPosition(); double offsetRatio = 1.0/2.5; LatLng latlngOffset = new LatLng(latlngMarker.latitude + (offsetRatio * vector[0]), latlngMarker.longitude + (offsetRatio * vector[1])); // Move the camera to the desired position CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLng(latlngOffset); googleMap.animateCamera(cameraUpdate); 
0
Sep 13 '13 at 10:59 on
source share

I dealt with the same task and ended up a similar solution as your last option:

Change the location of the x / y screen to the difference in the squared zoom level. Theoretically, this should work, but it always turns off quite a bit (~ .1 latitude / longitude, which is enough to leave).

But instead of dividing the offset point by 2<<zoomDifference I calculate the offset as the difference between longitude and latitude. They are double , so it gives me pretty good accuracy. So try converting your offsets to lat / lng, not your location into pixels, and just do the same calculation.




Edited

Finally, I managed to solve it differently (the most suitable, as I think). Map API v2 has add-on options . Just configure it so that the camera does not take the entire hidden part of MapView . Then the camera will reflect the center of the filled area after animateCamera()

0
Dec 05 '14 at 13:27
source share



All Articles