【问题标题】:Android: Getting a Rotated OSM Map to Fill the Entire ScreenAndroid:让旋转的 OSM 地图填满整个屏幕
【发布时间】:2017-05-30 00:04:59
【问题描述】:

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

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

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

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

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

听取他关于地图旋转的建议,我按照建议实施了 dispatchDraw() 和 dispatchTouchEvent(),但我在他提到我需要增加 MapView 大小的部分遇到问题?

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

或者,我能否以某种方式覆盖处理地图旋转的子类 RelativeLayout 类中的 onMeasure() 函数?

欢迎提出建议和提示。

更新:

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

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

有谁知道画布的大小是在哪里声明的,改变画布的大小是否真的可以解决我的问题?

【问题讨论】:

    标签: android layout openstreetmap


    【解决方案1】:

    我最终听从了 Romain Guy 的建议(与我在问题中发布的建议相同)。也就是说,我通过扩展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 画布的任何旋转最终不仅会旋转地图的图形表示,还会旋转与该 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?
    • 是的,任意旋转角度都可以。我能够从手机的磁力计中获取读数,然后使用这些读数来旋转画布,以便 mapView 的顶部始终显示直接在用户面前的地标。希望这是有道理的!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-05
    • 1970-01-01
    相关资源
    最近更新 更多