Отключение центра MapFragment для анимации, перемещающей как целевой лат /lng, так и уровень масштабирования

У меня есть пользовательский интерфейс с MapFragment с прозрачным надписью сверху. Карта занимает весь экран, тогда как представление - это только левая треть экрана. В результате, по умолчанию "центр" карты отключен. Когда кто-то нажимает на маркер, я хочу центрировать этот маркер в полностью видимой области MapFragment (не в центре самого MapFragment).

Так как это сложно описать словами, позвольте мне использовать несколько снимков. Предположим, что так выглядит мой пользовательский интерфейс:

Default Map

Когда пользователь нажимает маркер, я хочу как центрировать его, так и увеличивать масштаб, чтобы увидеть его ближе. Без каких-либо корректировок вы получите следующее:

Map with incorrectly centered marker

Я хочу, чтобы маркер был центрирован, но в пространстве справа, например:

Map with correctly centered marker

Очень легко достичь этого подвига, если вы не меняете уровень масштабирования с помощью проекции карты:

// 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));

Однако, если вы одновременно меняете уровень масштабирования, приведенные выше вычисления не работают (поскольку смещения lat/lng изменяются с разными уровнями масштабирования).

Позвольте мне запустить список попыток:

  • Быстрое изменение уровня масштабирования, выполнение вычислений, возврат к исходной позиции камеры, а затем анимация. К сожалению, внезапное изменение камеры (даже если это только на долю секунды), к сожалению, очень очевидно, и я бы хотел избежать мерцания.

  • Наложение двух MapFragments поверх друг друга, если вы выполните вычисления, пока отображается другое. Я обнаружил, что MapFragments на самом деле не построены, чтобы быть многослойными друг над другом (на этом пути есть неизбежные ошибки).

  • Изменение местоположения экрана x/y на разницу в квадрате уровня масштабирования. Теоретически это должно работать, но оно всегда отключается совсем немного (~.1 широта/долгота, которого достаточно, чтобы уйти).

Есть ли способ вычисления offsetTarget даже при изменении уровня масштабирования?

Ответ 1

Самая последняя версия библиотеки игровых сервисов добавляет функцию setPadding() к GoogleMap. Это дополнение проецирует все движения камеры в центр офсетной карты. Этот документ описывает, как ведет себя поведение клавиатуры.

Заданный здесь вопрос теперь можно просто решить, добавив левое дополнение, эквивалентное ширине левого макета.

mMap.setPadding(leftPx, topPx, rightPx, bottomPx);

Ответ 2

Я нашел решение, которое действительно подходит для проблемы. Решение состоит в том, чтобы вычислить центр смещения в требуемом масштабировании от исходного смещения, а затем анимировать карту. Для этого сначала переместите камеру карты на желаемый масштаб, вычислите смещение для этого уровня масштабирования, а затем восстановите исходное масштабирование. После вычисления нового центра мы можем сделать анимацию с помощью CameraUpdateFactory.newLatLngZoom.

Надеюсь, это поможет!

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));

}

Ответ 3

Изменить: приведенный ниже код предназначен для устаревших карт v1. В этом SO-ответе говорится, как выполнять аналогичные действия в v2: Как получить диапазон широты/долготы в Google Map V2 для Android

Ключевые методы, которые вам нужны, должны работать в long/lat и процентах вместо пикселей. Прямо сейчас, mapview будет увеличивать масштаб вокруг центра карты, а затем переходить к "повторному" контакту в открытой области. Вы можете получить ширину после увеличения в градусах, коэффициент в некоторых смещениях экрана, а затем повторно разместить карту. Ниже приведен пример скриншота приведенного ниже кода в действии после нажатия кнопки "+": перед enter image description here после enter image description here Основной код:

@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));

            }

Весь код:

    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;
    }

}

и базовый макет:

<?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>

Если это не полностью ответит на ваш вопрос, сообщите мне, потому что это был забавный эксперимент.

Ответ 4

Объект проектирования API не так уж и полезен, я столкнулся с той же проблемой и откатился.

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);
    }
}

Используя этот код, вы можете преобразовать карту вокруг новой цели (т.е. не центра). Я решил использовать систему опорных точек, где (0,5, 0,5) является значением по умолчанию и представляет собой середину экрана. (0,0) верхний левый и (1, 1) слева внизу.

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);
}

В принципе, вы используете проекцию, чтобы получить координаты XY в мире, а затем сметете эту точку и верните ее обратно в LatLng. Затем вы можете передать это на свою карту. PointD - простой тип, который содержит x, y как удваивает, а также делает поворот.

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;
    }
}

Обратите внимание, что если вы одновременно обновляете опору и местоположение карты, важно, чтобы вы использовали новый подшипник в getOffsetLocation.

Ответ 5

Самый простой способ - связать анимацию камеры. Сначала вы обычно увеличиваете изображение до изображения # 2, используя animateCamera с 3 параметрами, а в CancellableCallback.onFinish вы выполняете расчеты проецирования и оживляете, используя свой код, чтобы изменить центр.

Я верю (не тестировал), это будет выглядеть очень хорошо, если вы установите некоторые хорошие значения продолжительности анимации для обеих анимаций. Возможно, вторая анимация должна быть намного быстрее, чем первая. Кроме того, если ваш прозрачный, наложенный вид не отображается, когда маркер не выбран, хорошим UX будет анимировать его слева слева во время второй анимации.

Трудным способом было бы реализовать собственную Spherical Mercator Projection, как тот, который использует API Карт API v2. Если вам нужно только долготное смещение, это не должно быть так сложно. Если ваши пользователи могут изменить подшипник, и вы не хотите, чтобы reset он равнялся 0 при масштабировании, вам тоже нужно сдвиг широты, и это, вероятно, будет немного сложнее реализовать.

Я также думаю, что было бы хорошо иметь функциональность Maps API v2, где вы можете получить проекцию для любого CameraPosition и не только текущую, чтобы вы могли отправить запрос функции здесь: http://code.google.com/p/gmaps-api-issues/issues/list?can=2&q=apitype=Android2. Вы можете легко использовать его в своем коде для эффекта, который вы хотите.

Ответ 6

Для установки маркера мы используем Overlays в Android, In overlay class= = > на методе рисования поместите эту вещь

boundCenter (маркер);

а в конструкторе также упоминается следующее:

    public xxxxxxOverlay(Drawable marker, ImageView dragImage, MapView map) {
            super(boundCenter(marker)); 
.
.
.
.
.
    }

Ответ 7

У меня почти такая же проблема, как и вы (только мое смещение вертикально), и я пробовал решение, которое не работает должным образом, но может помочь вам (нам) найти лучшее решение.

Я сделал, чтобы переместить камеру карты, чтобы получить проекцию на цель, и примените там смещение.

Я поместил код в класс Utils и выглядел так:

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);
}

Затем вы можете использовать CameraBuilder, возвращенный этими методами для анимации, вместо исходной (не смещенной) цели.

Он работает (более или менее), но имеет две большие проблемы:

  • Карта иногда мерцает (это самая большая проблема, потому что она делает непригодным взлома)
  • Это взлом, который может работать сегодня и может не работать завтра (я знаю, что тот же самый алгоритм работает на iOS, например).

Итак, я не пошел на это, но мне подумалось об альтернативе: если у вас была идентичная карта, но скрытая, вы могли бы использовать эту карту для промежуточных вычислений, а затем использовать конечный результат на видимой карте.

Я думаю, что эта альтернатива будет работать, но это ужасно, так как вам нужно будет использовать две идентичные карты с накладными расходами, которые прилагаются к ней, для вас и для устройства.

Как я уже сказал, у меня нет окончательного решения для этого, но, возможно, это немного помогает.

Надеюсь, это поможет!

Ответ 8

Я разрешил проблему, вычислив вектор расстояния, используя значения LatLng двух неподвижных точек на карте, затем создав еще один LatLng, используя позицию маркера, этот вектор и заданное значение отношения. В этом есть много объектов, но это не зависит от текущего уровня масштабирования и вращения карты и может просто отлично работать для вас!

// 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);

Ответ 9

Я имел дело с одной и той же задачей и закончил аналогичное решение, как ваш последний вариант:

Изменение местоположения экрана x/y на разницу в квадрате уровня масштабирования. Теоретически это должно работать, но оно всегда выключается совсем немного (~.1 широта/долгота, которого достаточно, чтобы уйти).

Но вместо деления точки смещения на 2<<zoomDifference я вычисляю смещение как различия между значениями долготы и широты. Они double, поэтому он дает мне достаточно хорошую точность. Поэтому попробуйте конвертировать ваши смещения в lat/lng, а не ваше местоположение в пиксели, и просто выполните один и тот же расчет.


Edited

Наконец, мне удалось решить это по-другому (наиболее подходящий, как я считаю). Map API v2 имеет параметры дополнений. Просто настройте его, чтобы камера не взяла всю скрытую часть MapView. Затем камера будет отражать центр заполненной области после animateCamera()