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

标签 android layout openstreetmap

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

我目前正在实现的功能之一是根据手机(通过 GPS)确定的方位旋转 map 。我能够毫不费力地实现实际的旋转,但是由于我的代码旋转了整个 Canvas ——这可能是一种相当幼稚的方法——我现在在屏幕上有空白的角落,没有加载新的图 block 来弥补这一事实旋转的图 block 不再填充这些像素。

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

Mr. Romain Guy's posts 之一中,他提到了以下内容:

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

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

这是我在 XML 文件中所做的事情吗,如 this thread 中所建议的那样?

或者,我能否以某种方式重写处理 map 旋转的子类 RelativeLayout 类中的 onMeasure() 函数?

非常欢迎提出建议和提示。

更新:

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

使用 canvas.scale(),我能够在水平和垂直维度上将 Canvas 的比例增加 2 倍。然而,这意味着图像实际上被放大了,导致 map 图 block 出现 Not Acceptable 像素化。

有谁知道在哪里声明 Canvas 的大小,如果改变 Canvas 的大小实际上可以解决我的问题?

最佳答案

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

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

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

我的代码如下;请注意,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 Canvas 的任何旋转最终都会旋转,而不仅仅是 map 的图形表示,还有与该 Activity 相关的所有其他内容(这是我的特定实现的副作用);也就是说,触摸事件坐标在旋转后仍然相对于 mapView Canvas ,而不是相对于实际手机。例如,如果我们想象 Canvas 旋转了 180 度,那么如果用户试图向左平移 map ,它就会移动到向右的 map ,因为一切都是颠倒的!

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

/**
 * @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()

最后,要使 map Activity 的相应 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>

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

关于Android:获取旋转的 OSM 贴图以填充整个屏幕,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/7237276/

相关文章:

java - 使同一包中的另一个类只能访问变量

符合 "Metrics and Grids"建议的 Android 布局示例

java - 忽略布局边框

java - 如何在android中的osm map 中添加更多标记

android - osmbonuspack 中道路距离和持续时间的单位

android - 为什么某些 Android 设备可能会使用更多的图形内存来运行相同的应用程序?

javascript - 如何根据请求从 JS 文件发送 JSON 响应?

android - getStringExtra 返回 NULL

javascript - HTML/CSS "Pop-Up"窗口和禁用背景

javascript - 在 OpenLayers 3 中设置图标颜色的问题