Android 地图 API v2 中的彩色折线

2023-11-30

我想在 android 地图 api 版本 2 中绘制折线。我希望它有多种颜色,最好有渐变。但在我看来,折线只允许有单色。 我怎样才能做到这一点?我已经有了 api-v1 覆盖层来绘制我喜欢的内容,所以大概我可以重用一些代码

public class RouteOverlayGoogle extends Overlay {
    public void draw(Canvas canvas, MapView mapView, boolean shadow) {
       //(...) draws line with color representing speed
    }

我知道自从有人问这个问题以来已经很长时间了但仍然没有渐变折线(截至撰写本文时,~2015 年 5 月)并且绘制多条折线实际上并不能解决问题(锯齿状边缘,在处理数百个点时有相当大的滞后,只是在视觉上不太吸引人)。

当我必须实现渐变折线时,我最终所做的是实现TileOverlay这会将折线渲染到画布上,然后将其光栅化(有关我为此编写的特定代码,请参阅此要点https://gist.github.com/Dagothig/5f9cf0a4a7a42901a7b2).

该实现不会尝试进行任何类型的视口剔除,因为我最终不需要它来达到我想要的性能(我不确定数字,但每个图块不到一秒,并且多个图块将被同时渲染)。

正确渲染渐变折线可能非常棘手,但是因为您要处理不同的视口(位置和大小):除此之外,我还遇到了一些高缩放级别(20+)下浮动精度限制的问题。最后,我没有使用画布中的缩放和平移功能,因为我会遇到奇怪的损坏问题。

如果您使用与我所拥有的类似的数据结构(纬度、经度和时间戳),则需要注意的其他事情是您需要多个段才能正确渲染渐变(我最终一次使用 3 个点)。

为了后代的缘故,我还将把代码的要点保留在这里: (预测是使用完成的https://github.com/googlemaps/android-maps-utils如果你想知道在哪里com.google.maps.android.projection.SphericalMercatorProjection来自)

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;

import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;

import java.io.ByteArrayOutputStream;
import java.util.List;


/**
 * Tile overlay used to display a colored polyline as a replacement for the non-existence of gradient
 * polylines for google maps
 */
public class ColoredPolylineTileOverlay<T extends ColoredPolylineTileOverlay.PointHolder> implements TileProvider {

    public static final double LOW_SPEED_CLAMP_KMpH = 0;
    public static final double LOW_SPEED_CLAMP_MpS = 0;
    // TODO: calculate speed as highest speed of pointsCollection
    public static final double HIGH_SPEED_CLAMP_KMpH = 50;
    public static final double HIGH_SPEED_CLAMP_MpS = HIGH_SPEED_CLAMP_KMpH * 1000 / (60 * 60);
    public static final int BASE_TILE_SIZE = 256;

    public static int[] getSpeedColors(Context context) {
        return new int[] {
                context.getResources().getColor(R.color.polyline_low_speed),
                context.getResources().getColor(R.color.polyline_med_speed),
                context.getResources().getColor(R.color.polyline_high_speed)
        };
    }

    public static float getSpeedProportion(double metersPerSecond) {
        return (float)(Math.max(Math.min(metersPerSecond, HIGH_SPEED_CLAMP_MpS), LOW_SPEED_CLAMP_MpS) / HIGH_SPEED_CLAMP_MpS);
    }

    public static int interpolateColor(int[] colors, float proportion) {
        int rTotal = 0, gTotal = 0, bTotal = 0;
        // We correct the ratio to colors.length - 1 so that
        // for i == colors.length - 1 and p == 1, then the final ratio is 1 (see below)
        float p = proportion * (colors.length - 1);

        for (int i = 0; i < colors.length; i++) {
            // The ratio mostly resides on the 1 - Math.abs(p - i) calculation :
            // Since for p == i, then the ratio is 1 and for p == i + 1 or p == i -1, then the ratio is 0
            // This calculation works BECAUSE p lies within [0, length - 1] and i lies within [0, length - 1] as well
            float iRatio = Math.max(1 - Math.abs(p - i), 0.0f);
            rTotal += (int)(Color.red(colors[i]) * iRatio);
            gTotal += (int)(Color.green(colors[i]) * iRatio);
            bTotal += (int)(Color.blue(colors[i]) * iRatio);
        }

        return Color.rgb(rTotal, gTotal, bTotal);
    }

    protected final Context context;
    protected final PointCollection<T> pointsCollection;
    protected final int[] speedColors;
    protected final float density;
    protected final int tileDimension;
    protected final SphericalMercatorProjection projection;

    // Caching calculation-related stuff
    protected LatLng[] trailLatLngs;
    protected Point[] projectedPts;
    protected Point[] projectedPtMids;
    protected double[] speeds;

    public ColoredPolylineTileOverlay(Context context, PointCollection pointsCollection) {
        super();

        this.context = context;
        this.pointsCollection = pointsCollection;
        speedColors = getSpeedColors(context);
        density = context.getResources().getDisplayMetrics().density;
        tileDimension = (int)(BASE_TILE_SIZE * density);
        projection = new SphericalMercatorProjection(BASE_TILE_SIZE);
        calculatePointsAndSpeeds();
    }
    public void calculatePointsAndSpeeds() {
        trailLatLngs = new LatLng[pointsCollection.getPoints().size()];
        projectedPts = new Point[pointsCollection.getPoints().size()];
        projectedPtMids = new Point[Math.max(pointsCollection.getPoints().size() - 1, 0)];
        speeds = new double[Math.max(pointsCollection.getPoints().size() - 1, 0)];

        List<T> points = pointsCollection.getPoints();
        for (int i = 0; i < points.size(); i++) {
            T point = points.get(i);
            LatLng latLng = point.getLatLng();
            trailLatLngs[i] = latLng;
            projectedPts[i] = projection.toPoint(latLng);

            // Mids
            if (i > 0) {
                LatLng previousLatLng = points.get(i - 1).getLatLng();
                LatLng latLngMid = SphericalUtil.interpolate(previousLatLng, latLng, 0.5);
                projectedPtMids[i - 1] = projection.toPoint(latLngMid);

                T previousPoint = points.get(i - 1);
                double speed = SphericalUtil.computeDistanceBetween(latLng, previousLatLng) / ((point.getTime() - previousPoint.getTime()) / 1000.0);
                speeds[i - 1] = speed;
            }
        }
    }

    @Override
    public Tile getTile(int x, int y, int zoom) {
        // Because getTile can be called asynchronously by multiple threads, none of the info we keep in the class will be modified
        // (getTile is essentially side-effect-less) :
        // Instead, we create the bitmap, the canvas and the paints specifically for the call to getTile

        Bitmap bitmap = Bitmap.createBitmap(tileDimension, tileDimension, Bitmap.Config.ARGB_8888);

        // Normally, instead of the later calls for drawing being offset, we would offset them using scale() and translate() right here
        // However, there seems to be funky issues related to float imprecisions that happen at large scales when using this method, so instead
        // The points are offset properly when drawing
        Canvas canvas = new Canvas(bitmap);

        Matrix shaderMat = new Matrix();
        Paint gradientPaint = new Paint();
        gradientPaint.setStyle(Paint.Style.STROKE);
        gradientPaint.setStrokeWidth(3f * density);
        gradientPaint.setStrokeCap(Paint.Cap.BUTT);
        gradientPaint.setStrokeJoin(Paint.Join.ROUND);
        gradientPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
        gradientPaint.setShader(new LinearGradient(0, 0, 1, 0, speedColors, null, Shader.TileMode.CLAMP));
        gradientPaint.getShader().setLocalMatrix(shaderMat);

        Paint colorPaint = new Paint();
        colorPaint.setStyle(Paint.Style.STROKE);
        colorPaint.setStrokeWidth(3f * density);
        colorPaint.setStrokeCap(Paint.Cap.BUTT);
        colorPaint.setStrokeJoin(Paint.Join.ROUND);
        colorPaint.setFlags(Paint.ANTI_ALIAS_FLAG);

        // See https://developers.google.com/maps/documentation/android/views#zoom for handy info regarding what zoom is
        float scale = (float)(Math.pow(2, zoom) * density);

        renderTrail(canvas, shaderMat, gradientPaint, colorPaint, scale, x, y);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
        return new Tile(tileDimension, tileDimension, baos.toByteArray());
    }

    public void renderTrail(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, float scale, int x, int y) {
        List<T> points = pointsCollection.getPoints();
        double speed1, speed2;
        MutPoint pt1 = new MutPoint(), pt2 = new MutPoint(), pt3 = new MutPoint(), pt1mid2 = new MutPoint(), pt2mid3 = new MutPoint();

        // Guard statement: if the trail is only 1 point, just render the point by itself as a speed of 0
        if (points.size() == 1) {
            pt1.set(projectedPts[0], scale, x, y, tileDimension);
            speed1 = 0;
            float speedProp = getSpeedProportion(speed1);

            colorPaint.setStyle(Paint.Style.FILL);
            colorPaint.setColor(interpolateColor(speedColors, speedProp));
            canvas.drawCircle((float) pt1.x, (float) pt1.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
            colorPaint.setStyle(Paint.Style.STROKE);

            return;
        }

        // Guard statement: if the trail is exactly 2 points long, just render a line from A to B at d(A, B) / t speed
        if (points.size() == 2) {
            pt1.set(projectedPts[0], scale, x, y, tileDimension);
            pt2.set(projectedPts[1], scale, x, y, tileDimension);
            speed1 = speeds[0];
            float speedProp = getSpeedProportion(speed1);

            drawLine(canvas, colorPaint, pt1, pt2, speedProp);

            return;
        }

        // Because we want to be displaying speeds as color ratios, we need multiple points to do it properly:
        // Since we use calculate the speed using the distance and the time, we need at least 2 points to calculate the distance;
        // this means we know the speed for a segment, not a point.
        // Furthermore, since we want to be easing the color changes between every segment, we have to use 3 points to do the easing;
        // every line is split into two, and we ease over the corners
        // This also means the first and last corners need to be extended to include the first and last points respectively
        // Finally (you can see about that in getTile()) we need to offset the point projections based on the scale and x, y because
        // weird display behaviour occurs
        for (int i = 2; i < points.size(); i++) {
            pt1.set(projectedPts[i - 2], scale, x, y, tileDimension);
            pt2.set(projectedPts[i - 1], scale, x, y, tileDimension);
            pt3.set(projectedPts[i], scale, x, y, tileDimension);

            // Because we want to split the lines in two to ease over the corners, we need the middle points
            pt1mid2.set(projectedPtMids[i - 2], scale, x, y, tileDimension);
            pt2mid3.set(projectedPtMids[i - 1], scale, x, y, tileDimension);

            // The speed is calculated in meters per second (same format as the speed clamps); because getTime() is in millis, we need to correct for that
            speed1 = speeds[i - 2];
            speed2 = speeds[i - 1];
            float speed1Prop = getSpeedProportion(speed1);
            float speed1to2Prop = getSpeedProportion((speed1 + speed2) / 2);
            float speed2Prop = getSpeedProportion(speed2);

            // Circle for the corner (removes the weird empty corners that occur otherwise)
            colorPaint.setStyle(Paint.Style.FILL);
            colorPaint.setColor(interpolateColor(speedColors, speed1to2Prop));
            canvas.drawCircle((float)pt2.x, (float)pt2.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
            colorPaint.setStyle(Paint.Style.STROKE);

            // Corner
            // Note that since for the very first point and the very last point we don't split it in two, we used them instead.
            drawLine(canvas, shaderMat, gradientPaint, colorPaint, i - 2 == 0 ? pt1 : pt1mid2, pt2, speed1Prop, speed1to2Prop);
            drawLine(canvas, shaderMat, gradientPaint, colorPaint, pt2, i == points.size() - 1 ? pt3 : pt2mid3, speed1to2Prop, speed2Prop);
        }
    }

    /**
     * Note: it is assumed the shader is 0, 0, 1, 0 (horizontal) so that it lines up with the rotation
     * (rotations are usually setup so that the angle 0 points right)
     */
    public void drawLine(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio1, float ratio2) {
        // Degenerate case: both ratios are the same; we just handle it using the colorPaint (handling it using the shader is just messy and ineffective)
        if (ratio1 == ratio2) {
            drawLine(canvas, colorPaint, pt1, pt2, ratio1);
            return;
        }
        shaderMat.reset();

        // PS: don't ask me why this specfic orders for calls works but other orders will mess up
        // Since every call is pre, this is essentially ordered as (or my understanding is that it is):
        // ratio translate -> ratio scale -> scale to pt length -> translate to pt start -> rotate
        // (my initial intuition was to use only post calls and to order as above, but it resulted in odd corruptions)

        // Setup based on points:
        // We translate the shader so that it is based on the first point, rotated towards the second and since the length of the
        // gradient is 1, then scaling to the length of the distance between the points makes it exactly as long as needed
        shaderMat.preRotate((float) Math.toDegrees(Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x)), (float)pt1.x, (float)pt1.y);
        shaderMat.preTranslate((float)pt1.x, (float)pt1.y);
        float scale = (float)Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
        shaderMat.preScale(scale, scale);

        // Setup based on ratio
        // By basing the shader to the first ratio, we ensure that the start of the gradient corresponds to it
        // The inverse scaling of the shader means that it takes the full length of the call to go to the second ratio
        // For instance; if d(ratio1, ratio2) is 0.5, then the shader needs to be twice as long so that an entire call (1)
        // Results in only half of the gradient being used
        shaderMat.preScale(1f / (ratio2 - ratio1), 1f / (ratio2 - ratio1));
        shaderMat.preTranslate(-ratio1, 0);

        gradientPaint.getShader().setLocalMatrix(shaderMat);

        canvas.drawLine(
                (float)pt1.x,
                (float)pt1.y,
                (float)pt2.x,
                (float)pt2.y,
                gradientPaint
        );
    }
    public void drawLine(Canvas canvas, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio) {
        colorPaint.setColor(interpolateColor(speedColors, ratio));
        canvas.drawLine(
                (float)pt1.x,
                (float)pt1.y,
                (float)pt2.x,
                (float)pt2.y,
                colorPaint
        );
    }

    public interface PointCollection<T extends PointHolder> {
        List<T> getPoints();
    }
    public interface PointHolder {
        LatLng getLatLng();
        long getTime();
    }
    public static class MutPoint {
        public double x, y;

        public MutPoint set(Point point, float scale, int x, int y, int tileDimension) {
            this.x = point.x * scale - x * tileDimension;
            this.y = point.y * scale - y * tileDimension;
            return this;
        }
    }
}

请注意,此实现假设了两个相对较大的事情:

  1. 折线已经完成
  2. 只有一条折线。

我认为处理(1)不会很困难。但是,如果您打算以这种方式绘制多条折线,则可能需要考虑一些方法来增强性能(保留折线的边界框,以便能够轻松丢弃那些不适合视口的折线)。

关于使用 a 还要记住一件事TileOverlay它是在动作完成之后渲染的,而不是在动作期间渲染的;因此,您可能需要在其下方使用实际的单色折线来备份覆盖层,以使其具有一定的连续性。

PS:这是我第一次尝试回答问题,所以如果有什么我应该修复或做不同的事情,请告诉我。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android 地图 API v2 中的彩色折线 的相关文章

  • 如何获取之前的碎片?

    为了在我的应用程序中重用某些片段 我需要知道哪个片段是返回堆栈上的第二个片段 为了做到这一点 我正在使用getFragmentManager getFragments 显示以下错误 但有效 FragmentManager getFragme
  • Android webview 滚动不起作用

    我正在尝试在网络视图中向下滚动到页面底部 我正在使用谷歌在其教程中提供的网络视图示例 我正在使用这行代码来尝试滚动 但它不起作用 mWebView pageDown true 关于如何使其以编程方式滚动有什么建议吗 谢谢 public cl
  • 使用 google Directions API 的地图视图绘制方向 - 解码折线

    我正在尝试使用 Google 方向 API 在我的地图视图上显示方向 但我在从 JSON 响应获取数据时遇到困难 我可以获得 级别 和 点 字符串 但无法弄清楚如何将它们解码为地图上的点 任何帮助将非常感激 我有一个类可以为您解码它们 添加
  • 如何强制 Eclipse 将 xml 布局和样式显示为文本?

    我最近升级到带有 ADT 20 0 3 的 Eclipse 4 2 Juno 如果我查看旧项目中的布局或样式 Eclipse 只会向我显示其适当的基于控件的编辑器 我想编辑语法突出显示的 xml 文本 我没有找到将插件的编辑器切换到此模式的
  • Android 如何更改 OnTouchListener 上的按钮背景

    你好 我在 xml 中有一个按钮 我正在使用OnTouchListener在我的活动中获得button按下并释放 但问题是 当我按下按钮时背景颜色没有改变 当我延长可能的活动时OnClickListener背景正在改变 任何人都可以告诉我的
  • 使用 ADB 命令获取 IMEI 号码 Android 12

    对于 11 之前的 Android 版本 我使用以下命令从我的设备获取 IMEI 号码 adb shell service call iphonesubinfo 4 cut c 52 66 tr d space or adb shell s
  • 带操作按钮的颤动本地通知

    我在我的 flutter 项目中尝试了 flutter 本地通知插件 它在简单通知上工作正常 但我需要带有操作按钮的通知功能 请帮助我或建议我实现此功能 不幸的是 flutter local notifications 插件尚不支持操作按钮
  • Android在排序列表时忽略大小写

    我有一个名为路径的列表 我目前正在使用以下代码对字符串进行排序 java util Collections sort path 这工作正常 它对我的 列表进行排序 但是它以不同的方式处理第一个字母的情况 即它用大写字母对列表进行排序 然后用
  • 出现错误错误:res/menu/mainMenu.xml:文件名无效:必须仅包含[a-z0-9_。]

    我是安卓新手 刚刚开始使用 我在 res 文件夹中创建了一个文件 menu mainMenu xml 但我得到了错误 Error res menu mainMenu xml invalid file name must contain on
  • Android onChange 事件未在 android 5 (Lollipop) 上的 chrome 历史记录的 contentObserver 中触发

    我注意到我的 chrome 历史记录和书签的 contentObservers 在 android lolipop 上不再触发 该代码在旧版本的 android 上完美运行 无论 chrome 版本如何 但在 Lollipop 上它不再运行
  • Android 应用程序中的 Eszett (ß)

    我的 res layout activity 文件中的德语 字符在我的应用程序中自动转换为 ss 即使我将语言和键盘设置为德语 它仍然不会显示 Android 中可以显示 吗 edit
  • 如何检查 Android 中连接的 wifi 网络是否处于活动状态

    如何自动检查android中连接的WiFi网络上的互联网是否处于活动状态 我可以检查 wifi 是否已启用或 wifi 网络是否已连接 但我不确定如何检查互联网是否已连接 这可能吗 private boolean connectionAva
  • 画透明圆,外面填充

    我有一个地图视图 我想在其上画一个圆圈以聚焦于给定区域 但我希望圆圈倒转 也就是说 圆的内部不是被填充 而是透明的 其他所有部分都被填充 请参阅这张图片了解我的意思 http i imgur com zxIMZ png 上半部分显示了我可以
  • Android 相机未保存在特定文件夹 [MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA]

    当我在 Intent 中使用 MediaStore INTENT ACTION STILL IMAGE CAMERA 时遇到问题 相机正常启动 但它不会将文件保存在我的特定文件夹 photo 中 但是当我使用 MediaStore ACTI
  • 如何修改 Skobbler 注释而不重新添加它

    我必须修改 SKAnnotation 的图像 注释生成器代码 private SKAnnotation getAnnotationFromView int id int minZoomLvl View view SKAnnotation a
  • Android:RecyclerView 不显示片段中的列表项

    有人可以帮我尝试让我的 RecyclerView 出现吗 如果我不在片段中实现它 就会出现这种情况 然而 当我尝试将其实现到片段中时 CarFront 中的其他 XML 代码与 RecyclerView 分开显示 我的日志中收到此错误 E
  • javafx android 中的文本字段和组合框问题

    我在简单的 javafx android 应用程序中遇到问题 问题是我使用 gradle javafxmobile plugin 在 netbeans ide 中构建了非常简单的应用程序 其中包含一些文本字段和组合框 我在 android
  • 如何在基本活动中使用 ViewBinding 的抽象?

    我正在创建一个基类 以便子级的所有绑定都将设置在基类中 我已经做到了这一点 abstract class BaseActivity2 b AppCompatActivity private var viewBinding B null pr
  • Android应用程序可以像旧的普通java小程序一样嵌入到网页中吗?

    我对 android 平台一无所知 也无法在互联网上找到这个基本问题的答案 更新 好的 我本身无法嵌入 Android 应用程序 但是我可以在 Android Webbrowser 中嵌入 Java 的东西吗 不可以 您无法将 Androi
  • Android 中带有组的列表视图

    我有一个列表视图 每行都有一些日期和文本 我可以像 iPhone 中那样将这个 listView 分组 组之间有标题吗 在 android 中是否可能 请帮忙 即 我需要在 Listview 行之间有标题栏 以便如果我使用日期对其进行分组

随机推荐