Android:如何使旋转的OSM地图填满整个屏幕

10

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

目前我正在实现的其中一个功能是根据手机通过GPS确定的方向使地图旋转。我已经成功实现了旋转,但由于我的代码旋转了整个画布--这可能是一种相当天真的方法--因此现在屏幕上有空白角落,没有加载新瓦片来填补这些像素不再被旋转后的瓦片所覆盖的区域。

经过一番搜索,我找到了一些关于如何解决这个问题的建议,但迄今为止没有运气。

Mr. Romain Guy的文章中,他提到:

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

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

这是我在XML文件中做的事情,就像该线程中所建议的那样吗?

还是说我可以在处理地图旋转的子类化RelativeLayout类中覆盖onMeasure()函数?

欢迎提出意见和提示。

更新:

为了寻找一个可行的解决方案,我试图改变画布的大小。我的想法是,使用比实际屏幕尺寸更大的画布尺寸,可以将空白角落完全移出屏幕。但不幸的是,似乎没有真正的canvas.size()选项;我所找到的最好的选项是canvas.scale()。
使用canvas.scale(),我能够将画布在水平和垂直方向上放大两倍。然而,这意味着图像被有效地放大了,导致地图瓦片的像素化无法接受。
有人知道canvas的大小在哪里声明,改变画布的大小是否可能解决我的问题吗?
1个回答

4

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

dispatchDraw(...)函数本质上拦截对onDraw(...)函数的调用,在该函数的输入上执行一些特定的操作,然后释放它以进行处理。在我们的情况下,我们将希望在实际onDraw(...)函数处理之前旋转mapView画布。这就是为什么我们需要重写此函数的原因。

具体来说,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画布的任何旋转都会导致与该Activity相关的所有其他内容也一起旋转(这是由于我的具体实现而产生的副作用);也就是说,在被旋转后,触摸事件坐标仍然相对于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>

我想强调这个解决方案并不是最好的解决方案,但对于我的概念验证应用程序来说,它运行良好!


如果画布旋转角度不是180的倍数,你能否相对地分派触摸事件呢? 例如:50、90、270、300度? - Mani
1
是的,任何任意的旋转角度都可以。我能够从手机的磁力计中获取读数,并使用这些读数来旋转画布,以便地图视图的顶部始终显示用户正前方的地标。希望这样说得清楚! - Tim Severeijns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接