Android:获取旋转的 OSM 地图以填充整个屏幕

发布于 2024-12-02 04:07:17 字数 1229 浏览 3 评论 0原文

我正在开发一个应用程序,该应用程序使用 OpenStreetMap (OSM) API 在由静态图块组成的离线地图上显示各种兴趣点。

我目前正在实现的功能之一是让地图根据手机(通过 GPS)确定的方位进行旋转。我不需要太多的努力就能实现实际的旋转,但由于我的代码旋转整个画布——也许是一种相当幼稚的方法——我现在屏幕上有空白的角落,没有加载新的图块来补偿这一事实旋转的图块不再填充这些像素。

经过一番谷歌搜索后,我发现了一些关于如何解决这个问题的建议,但到目前为止还没有运气。

先生之一。 Romain Guy 的帖子,他提到了以下内容:

我过去曾这样做过,它需要创建一个自定义 在dispatchDraw()方法中旋转Canvas的ViewGroup。你 还需要增加MapView的大小(以便它绘制足够的 旋转时的像素。)您还需要旋转触摸事件 调度触摸事件()。或者,如果您使用 Android 3.0,您可以简单地调用 theMapView.rotate()

采纳他关于地图旋转的建议,我已经按照建议实现了dispatchDraw()和dispatchTouchEvent(),但是我在他提到我需要增加MapView的大小的部分遇到了麻烦?

这是我在 XML 文件中执行的操作吗,就像 这个线程

或者,我可以以某种方式重写处理地图旋转的子类RelativeLayout 类中的onMeasure() 函数吗?

非常欢迎建议和提示。

更新:

为了找到此问题的可接受的解决方案,我尝试更改画布的大小。我的想法是,如果画布尺寸大于实际屏幕尺寸,我也许能够将空白角完全移出屏幕。不幸的是,似乎没有实际的 canvas.size() 选项;我发现最好的是canvas.scale()。

使用 canvas.scale(),我能够将画布的水平和垂直尺寸的比例增加 2 倍。然而,这意味着图像被有效地放大,导致地图图块出现不可接受的像素化。

有谁知道画布的大小在哪里声明,以及更改画布的大小是否实际上可以解决我的问题?

I'm working on an application that uses the OpenStreetMap (OSM) API to display various points of interest on an offline map, composed of static tiles.

One of the features that I am currently implementing is having the map rotate in accordance with the bearing determined by the phone (via GPS). I was able to implement the actual rotation without too much effort, but since my code rotates the entire canvas -- perhaps a rather naive approach -- I now have blank corners on the screen where no new tiles are being loaded to compensate for the fact that the rotated tiles no longer fill these pixels.

After a bit of Googling, I found a few suggestions as to how I might be able to solve this issue, but so far no luck.

In one of Mr. Romain Guy's posts, he mentions the following:

I have done this in the past and it requires to create a custom
ViewGroup that rotates the Canvas in the dispatchDraw() method. You
also need to increase the size of the MapView (so that it draws enough
pixels when rotated.) You will also need to rotate the touch events in
dispatchTouchEvent(). Or if you use Android 3.0 you can simply call
theMapView.rotate()

Taking his advice on map rotation, I have implemented dispatchDraw() and dispatchTouchEvent() as suggested, but I'm having trouble with the part where he mentions that I need to increase the size of the MapView?

Is this something that I do in the XML file, like suggested in this thread?

Or, can I somehow override the onMeasure() function in my subclassed RelativeLayout class that handles my map rotation?

Suggestions and hints are most welcome.

UPDATE:

In an attempt to find an acceptable solution to this problem, I tried to change the size of the canvas. The thinking was that with a canvas size that is bigger than the actual screen size, I might be able to move the blank corners off the screen entirely. Unfortunately, there does not appear to be an actual canvas.size() option; the best I found was canvas.scale().

Using canvas.scale(), I was able to increase the scale of the canvas by a factor of 2 in both the horizontal as well as vertical dimensions. This means, however, that the image is effectively zoomed in, causing unacceptable pixelation to the map tiles.

Does anyone know where the size of the canvas gets declared, and if changing the size of the canvas might actually solve my problem?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

最单纯的乌龟 2024-12-09 04:07:17

我最终遵循了罗曼·盖伊的建议(与我在问题中发布的建议相同)。也就是说,我通过扩展 RelativeLayout 创建了自己的自定义 ViewGroup,然后增加了 mapView 的大小以覆盖整个屏幕。扩展 RelativeLayout ViewGroup 是必要的,这样我就可以覆盖 dispatchDraw(...)onMeasure(...) 以及作为 dispatchTouchEvent(...) 函数来为我的应用程序启用所需的地图旋转功能。

dispatchDraw(...) 函数本质上拦截对 onDraw(...) 函数的调用,对该函数的输入执行一些特定操作,然后释放它待处理。在我们的例子中,我们需要在 mapView 画布到达实际的 onDraw(...) 函数之前旋转它。这就是为什么我们需要重写这个函数。

具体来说,dispatchDraw(...) 函数将画布对象作为输入,该对象(在本例中)表示 OSM mapView 对象(如下面的 XML 文件中所定义) )。如果要对画布应用旋转,我们需要找到地图的中心,平移(即移动)地图,使地图的中心位于坐标系的原点上,然后将地图旋转坐标系的原点,然后,最后,我们希望将这个修改后的画布分派到渲染管道中的下一个阶段。

我的代码如下;请注意,Manager 是我自己创建的单例,除非您自己编写,否则它不会存在于您的实现中!

    /**
 * @param pCanvas
 * @return void
 * 
 * This function intercepts all dispatches to the onDraw function of the 
 * super, and it rotates the canvas in accordance with the user's wishes
 * using the phone bearing as determined either through the magnetometer
 * or GPS fixes.
 */
@Override
protected void dispatchDraw(final Canvas pCanvas) {
    final long startMs = System.currentTimeMillis();

    // If automatic map rotation has been enabled, get bearing from phone:
    if (Manager.getInstance().getMapRotationMode() != Constants.DISABLED) {
        mBearing = Manager.getInstance().getPhoneBearing();

        // Save the state of the transformation matrix:
        pCanvas.save(Canvas.MATRIX_SAVE_FLAG);

        // getWidth() and getHeight() return the size of the canvas as 
        // defined in the XML file, and not the size of the screen!
        int canvasOffsetX = -(getWidth() / 2) + (screenWidth / 2);
        int canvasOffsetY = -(getHeight() / 2) + (screenHeight / 2);

        // Set origin of canvas to center of map:
        pCanvas.translate(canvasOffsetX, canvasOffsetY); 

        // Rotate the canvas to the correct bearing:
        pCanvas.rotate(-mBearing, getWidth() / 2, getHeight() / 2);

        // Pass on the rotated canvas, and restore after that:
        super.dispatchDraw(pCanvas);

        // Balance out the call to save, and restore the matrix to 
        // saved state:
        pCanvas.restore();          
    } // end if

    else { // If map rotation has not been enabled:
        super.dispatchDraw(pCanvas);
    } // end else

    final long endMs = System.currentTimeMillis();
    if (LOG_ENABLED) {
        Log.i(TAG, "mapView Dispatch Time: " + (endMs - startMs) + "ms");
    } // end if
} // end dispatchDraw()

接下来,我们需要重写 dispatchTouchEvent(...),因为 OSM mapView 画布的任何旋转最终不仅会旋转地图的图形表示,还会旋转与该活动相关的所有其他内容(这是我的具体实现的副作用);也就是说,触摸事件坐标在旋转后仍然相对于 mapView 画布,而不是相对于实际手机。例如,如果我们想象画布旋转 180 度,那么如果用户尝试将地图平移到左侧,它会移动到右侧的地图,因为一切都是颠倒的!

在代码中,您可以按如下方式更正此问题:

/**
 * @param event
 * @return boolean
 * 
 * This function intercepts all interactions with the touch display (that is, 
 * all touchEvents), and for each finger on the screen, or pointer, the
 * function applies the necessary rotation to counter the rotation of the 
 * map. The coordinate of each pointer is also modified so that it returns
 * the correct location on the enlarged canvas. This was necessary to return
 * the correct coordinate for actions such as double-tap, and proper icon 
 * identification upon clicking an icon.
 */
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    // Get the number of pointers (i.e. fingers on screen) from the passed
    // in MotionEvent:
    float degrees = Manager.getInstance().getPhoneBearing();
    int numPointers = event.getPointerCount();
    int[] pointerIDs = new int[numPointers];
    PointerCoords[] pointerCoords = new PointerCoords[numPointers];

    // Extract all pointers from the touch event:
    for (int i = 0; i < numPointers; i++) {
        pointerIDs[i] = event.getPointerId(i);
        pointerCoords[i] = new PointerCoords();

        event.getPointerCoords(i, pointerCoords[i]);
    } // end for

    // Correct each pointer coordinate set for map rotation:
    for (int i = 0; i < numPointers; i++) {
        // x and y end up representing points on the canvas, although they
        // are derived from points on the screen:
        float x = pointerCoords[i].x;
        float y = pointerCoords[i].y;

        // Get the center of the MapView:
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;

        // Convert to radians
        float rad = (float) ((degrees * Math.PI) / 180f);
        float s = (float) Math.sin(rad);
        float c = (float) Math.cos(rad);

        // Translate point to origin:
        x -= centerX;
        y -= centerY;

        // Apply rotation
        float tmpX = x * c - y * s;
        float tmpY = x * s + y * c;
        x = tmpX;
        y = tmpY;           

        // Offset the coordinates to compensate for the fact that the
        // canvas is 1200 by 1200, the phone screen is smaller, and
        // they don't overlap nicely:
        x += (600 - (screenWidth / 2)) * c - (600 - (screenHeight / 2)) * s;
        y += (600 - (screenWidth / 2)) * s + (600 - (screenHeight / 2)) * c;

        // Translate point back:
        x += centerX;
        y += centerY;

        pointerCoords[i].x = x;
        pointerCoords[i].y = y;

        // Catlog:
        if (LOG_ENABLED) Log.i(TAG, "(" + x + ", " + y + ")");
    } // end for

    // Create new event to pass along the modified pointers.
    // Need API level 9 or higher to make this work!
    MotionEvent newEvent = MotionEvent.obtain(event.getDownTime(), event
            .getEventTime(), event.getAction(), event.getPointerCount(),
            pointerIDs, pointerCoords, event.getMetaState(), event
                    .getXPrecision(), event.getYPrecision(), event
                    .getDeviceId(), event.getEdgeFlags(),
            event.getSource(), event.getFlags());

    // Dispatch the newly modified touch event:
    return super.dispatchTouchEvent(newEvent);
} // end dispatchTouchEvent()

最后,获取地图活动的相应 XML 使其正常工作的技巧是利用 FrameLayout 作为我的所有其他 GUI 元素的父级。布局。这使我能够使 mapView 尺寸远大于 Nexus One 上显示屏的尺寸(480 x 800)。此解决方案还允许我在 FrameLayout 中嵌套 RelativeLayout,同时在使用 match_parent 和类似参数时仍然尊重设备的实际显示尺寸。

我的 XML 布局的相关部分如下所示:

<?xml version="1.0" encoding="utf-8"?>

<!--Note that the layout width and height is defined in px and not dip!-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/MapViewLayout">

<a.relevant.path.RotatingRelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="1200px"
    android:layout_height="1200px">

    <org.osmdroid.views.MapView
        android:id="@+id/mapview"
        android:layout_width="1200px" 
        android:layout_height="1200px"
        android:enabled="true"      
        android:clickable="true"/>          
</a.relevant.path.RotatingRelativeLayout>   

<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/leftSlideHandleButton" 
        android:layout_width="match_parent"
        android:layout_height="60dip" 
        android:layout_centerHorizontal="true"
        android:background="#D0000000">

        <Button 
            android:id="@+id/mapZoomOutButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_out_button"
            android:layout_alignParentLeft="true"
            android:onClick="zoomOutButton"/>

        <Button 
            android:id="@+id/mapZoomInButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_in_button"
            android:layout_alignParentRight="true"
            android:onClick="zoomInButton"/>

        <TextView 
            android:id="@+id/headerSpeedText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Speed: "
            android:textSize="12sp"
            android:paddingLeft="15dip"
            android:layout_toRightOf="@id/mapZoomOutButton"/>

        <TextView 
            android:id="@+id/headerSpeedReading"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="N/A"
            android:textSize="12sp"
            android:paddingLeft="27dip"
            android:layout_toRightOf="@id/headerSpeedText"/>

        <TextView 
            android:id="@+id/headerBearingText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Bearing: "
            android:paddingLeft="15dip"
            android:textSize="12sp"
            android:layout_toRightOf="@id/mapZoomOutButton"
            android:layout_below="@id/headerSpeedText"/>

        <!-- Et Cetera... -->

    </RelativeLayout>
</FrameLayout>

我想指出的是,该解决方案绝不是最好的解决方案,但它对于我的概念验证应用程序来说效果很好!

I ended up following Romain Guy's advice (the same advice that I posted in my question). That is, I created my own custom ViewGroup by extending the RelativeLayout, and I then increased the size of my mapView to provide coverage of the entire screen. Extending the RelativeLayout ViewGroup was necessary so that I could override the dispatchDraw(...), the onMeasure(...), as well as the dispatchTouchEvent(...) functions to enable the desired map rotation features for my application.

The dispatchDraw(...) function essentially intercepts calls to the onDraw(...) function, performs a few specific manipulations on the input to that function and then releases it to be processed. In our case, we'll want to rotate the mapView canvas before it makes its way to the actual onDraw(...) function. This is why we'll need to override this function.

Specifically, the dispatchDraw(...) function takes as input a canvas object, which (in this case) represents the OSM mapView object (as defined in the XML file below). If a rotation is to be applied to the canvas, we'll want to locate the center of the map, translate (i.e. move) the map so that the center of the map sits on the origin of the coordinate system, rotated the map about the origin of the coordinate system, and then, finally, we'll want to dispatch this modified canvas to the next stage in the rendering pipeline.

My code for this is below; note that Manager is my own singleton creation that won't exist in your implementation unless you write one yourself!

    /**
 * @param pCanvas
 * @return void
 * 
 * This function intercepts all dispatches to the onDraw function of the 
 * super, and it rotates the canvas in accordance with the user's wishes
 * using the phone bearing as determined either through the magnetometer
 * or GPS fixes.
 */
@Override
protected void dispatchDraw(final Canvas pCanvas) {
    final long startMs = System.currentTimeMillis();

    // If automatic map rotation has been enabled, get bearing from phone:
    if (Manager.getInstance().getMapRotationMode() != Constants.DISABLED) {
        mBearing = Manager.getInstance().getPhoneBearing();

        // Save the state of the transformation matrix:
        pCanvas.save(Canvas.MATRIX_SAVE_FLAG);

        // getWidth() and getHeight() return the size of the canvas as 
        // defined in the XML file, and not the size of the screen!
        int canvasOffsetX = -(getWidth() / 2) + (screenWidth / 2);
        int canvasOffsetY = -(getHeight() / 2) + (screenHeight / 2);

        // Set origin of canvas to center of map:
        pCanvas.translate(canvasOffsetX, canvasOffsetY); 

        // Rotate the canvas to the correct bearing:
        pCanvas.rotate(-mBearing, getWidth() / 2, getHeight() / 2);

        // Pass on the rotated canvas, and restore after that:
        super.dispatchDraw(pCanvas);

        // Balance out the call to save, and restore the matrix to 
        // saved state:
        pCanvas.restore();          
    } // end if

    else { // If map rotation has not been enabled:
        super.dispatchDraw(pCanvas);
    } // end else

    final long endMs = System.currentTimeMillis();
    if (LOG_ENABLED) {
        Log.i(TAG, "mapView Dispatch Time: " + (endMs - startMs) + "ms");
    } // end if
} // end dispatchDraw()

Next we'll need to override dispatchTouchEvent(...), because any rotation of the OSM mapView canvas ends up rotating not only the graphical representation of the map, but also everything else related to that Activity (this occurs as a side effect of my specific implementation); that is, touch event coordinates remain relative to the mapView canvas after being rotated and not relative to the actual phone. For example, if we imagine the canvas being rotated by 180 degrees, then if the user attempts to pan the map to the left, it will instead move to the map to the right, since everything is upside-down!

In code, you might correct for this problem as follows:

/**
 * @param event
 * @return boolean
 * 
 * This function intercepts all interactions with the touch display (that is, 
 * all touchEvents), and for each finger on the screen, or pointer, the
 * function applies the necessary rotation to counter the rotation of the 
 * map. The coordinate of each pointer is also modified so that it returns
 * the correct location on the enlarged canvas. This was necessary to return
 * the correct coordinate for actions such as double-tap, and proper icon 
 * identification upon clicking an icon.
 */
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
    // Get the number of pointers (i.e. fingers on screen) from the passed
    // in MotionEvent:
    float degrees = Manager.getInstance().getPhoneBearing();
    int numPointers = event.getPointerCount();
    int[] pointerIDs = new int[numPointers];
    PointerCoords[] pointerCoords = new PointerCoords[numPointers];

    // Extract all pointers from the touch event:
    for (int i = 0; i < numPointers; i++) {
        pointerIDs[i] = event.getPointerId(i);
        pointerCoords[i] = new PointerCoords();

        event.getPointerCoords(i, pointerCoords[i]);
    } // end for

    // Correct each pointer coordinate set for map rotation:
    for (int i = 0; i < numPointers; i++) {
        // x and y end up representing points on the canvas, although they
        // are derived from points on the screen:
        float x = pointerCoords[i].x;
        float y = pointerCoords[i].y;

        // Get the center of the MapView:
        int centerX = getWidth() / 2;
        int centerY = getHeight() / 2;

        // Convert to radians
        float rad = (float) ((degrees * Math.PI) / 180f);
        float s = (float) Math.sin(rad);
        float c = (float) Math.cos(rad);

        // Translate point to origin:
        x -= centerX;
        y -= centerY;

        // Apply rotation
        float tmpX = x * c - y * s;
        float tmpY = x * s + y * c;
        x = tmpX;
        y = tmpY;           

        // Offset the coordinates to compensate for the fact that the
        // canvas is 1200 by 1200, the phone screen is smaller, and
        // they don't overlap nicely:
        x += (600 - (screenWidth / 2)) * c - (600 - (screenHeight / 2)) * s;
        y += (600 - (screenWidth / 2)) * s + (600 - (screenHeight / 2)) * c;

        // Translate point back:
        x += centerX;
        y += centerY;

        pointerCoords[i].x = x;
        pointerCoords[i].y = y;

        // Catlog:
        if (LOG_ENABLED) Log.i(TAG, "(" + x + ", " + y + ")");
    } // end for

    // Create new event to pass along the modified pointers.
    // Need API level 9 or higher to make this work!
    MotionEvent newEvent = MotionEvent.obtain(event.getDownTime(), event
            .getEventTime(), event.getAction(), event.getPointerCount(),
            pointerIDs, pointerCoords, event.getMetaState(), event
                    .getXPrecision(), event.getYPrecision(), event
                    .getDeviceId(), event.getEdgeFlags(),
            event.getSource(), event.getFlags());

    // Dispatch the newly modified touch event:
    return super.dispatchTouchEvent(newEvent);
} // end dispatchTouchEvent()

Finally, the trick to getting the corresponding XML for the map activity to work properly is to utilize a FrameLayout as the parent to all other GUI elements in my layout. This allowed me to make the mapView dimensions considerably larger than the dimensions of the display on my Nexus One (480 by 800). This solution also allowed me to nest a RelativeLayout inside my FrameLayout while still respecting the device's actual display dimensions when using match_parent and similar parameters.

The relevant portion of my XML layout looks like this:

<?xml version="1.0" encoding="utf-8"?>

<!--Note that the layout width and height is defined in px and not dip!-->

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/MapViewLayout">

<a.relevant.path.RotatingRelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="1200px"
    android:layout_height="1200px">

    <org.osmdroid.views.MapView
        android:id="@+id/mapview"
        android:layout_width="1200px" 
        android:layout_height="1200px"
        android:enabled="true"      
        android:clickable="true"/>          
</a.relevant.path.RotatingRelativeLayout>   

<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/leftSlideHandleButton" 
        android:layout_width="match_parent"
        android:layout_height="60dip" 
        android:layout_centerHorizontal="true"
        android:background="#D0000000">

        <Button 
            android:id="@+id/mapZoomOutButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_out_button"
            android:layout_alignParentLeft="true"
            android:onClick="zoomOutButton"/>

        <Button 
            android:id="@+id/mapZoomInButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:background="@drawable/zoom_in_button"
            android:layout_alignParentRight="true"
            android:onClick="zoomInButton"/>

        <TextView 
            android:id="@+id/headerSpeedText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Speed: "
            android:textSize="12sp"
            android:paddingLeft="15dip"
            android:layout_toRightOf="@id/mapZoomOutButton"/>

        <TextView 
            android:id="@+id/headerSpeedReading"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="N/A"
            android:textSize="12sp"
            android:paddingLeft="27dip"
            android:layout_toRightOf="@id/headerSpeedText"/>

        <TextView 
            android:id="@+id/headerBearingText"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:textColor="#33B5E5"
            android:text="Bearing: "
            android:paddingLeft="15dip"
            android:textSize="12sp"
            android:layout_toRightOf="@id/mapZoomOutButton"
            android:layout_below="@id/headerSpeedText"/>

        <!-- Et Cetera... -->

    </RelativeLayout>
</FrameLayout>

I'd like to remark that this solution is by no means the best solution, but that it worked out fine for my proof-of-concept application!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文