【问题标题】:How can I tell if a closed path contains a given point?如何判断封闭路径是否包含给定点?
【发布时间】:2011-02-05 13:21:57
【问题描述】:

在 Android 中,我有一个 Path 对象,我碰巧知道它定义了一条闭合路径,我需要确定给定点是否包含在路径中。我所希望的是类似于

path.contains(int x, int y)

但这似乎不存在。

我正在寻找这个的具体原因是因为我在屏幕上有一组定义为路径的形状,我想弄清楚用户点击了哪个形状。如果有更好的方法来解决这个问题,例如使用不同的 UI 元素而不是自己“艰难地”做,我愿意接受建议。

如果必须,我愿意自己编写算法,但我猜这意味着不同的研究。

【问题讨论】:

    标签: android graphics


    【解决方案1】:

    这是我所做的,它似乎有效:

    RectF rectF = new RectF();
    path.computeBounds(rectF, true);
    region = new Region();
    region.setPath(path, new Region((int) rectF.left, (int) rectF.top, (int) rectF.right, (int) rectF.bottom));
    

    现在您可以使用region.contains(x,y) 方法。

    Point point = new Point();
    mapView.getProjection().toPixels(geoPoint, point);
    
    if (region.contains(point.x, point.y)) {
      // Within the path.
    }
    

    ** 2010 年 6 月 7 日更新** 如果 rectF 太大,region.setPath 方法将导致我的应用程序崩溃(没有警告消息)。这是我的解决方案:

    // Get the screen rect.  If this intersects with the path's rect
    // then lets display this zone.  The rectF will become the 
    // intersection of the two rects.  This will decrease the size therefor no more crashes.
    Rect drawableRect = new Rect();
    mapView.getDrawingRect(drawableRect);
    
    if (rectF.intersects(drawableRect.left, drawableRect.top, drawableRect.right, drawableRect.bottom)) {
       // ... Display Zone.
    }
    

    【讨论】:

    • 为什么我在使用上面的代码时将 region.getBounds() 设为 0,0,0,0?它不适用于我的项目。我在哪里犯错?
    • 不幸的是,如果路径不是常规的 Rect,如果它不在路径中,也会在路径中看到触摸点。一个简单的例子是三角形。矩形-三角形的边界是矩形!因此,例如,如果您点击斜边上方,则触摸点仍在界中!也许更好的解决方案是:stackoverflow.com/questions/7044838/…
    • 它的工作,但有时区域取点在封闭路径之外
    • 这是否真的将路径或只是路径的边界矩形作为内部/外部的标准?
    【解决方案2】:

    android.graphics.Path 类没有这样的方法。 Canvas 类确实有一个可以设置为路径的剪辑区域,没有办法针对一个点对其进行测试。您可以尝试 Canvas.quickReject,针对单点矩形(或 1x1 Rect)进行测试。不过,我不知道这是否真的会检查路径或仅检查封闭的矩形。

    Region 类显然只跟踪包含的矩形。

    您可以考虑将您的每个区域绘制成一个 8 位 alpha 层位图,其中每个 Path 填充其自己的“颜色”值(确保在您的 Paint 中关闭抗锯齿功能)。这会为每个填充了填充它的路径的索引的路径创建一种掩码。然后,您可以将像素值用作路径列表的索引。

    Bitmap lookup = Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
    //do this so that regions outside any path have a default
    //path index of 255
    lookup.eraseColor(0xFF000000);
    
    Canvas canvas = new Canvas(lookup);
    Paint paint = new Paint();
    
    //these are defaults, you only need them if reusing a Paint
    paint.setAntiAlias(false);
    paint.setStyle(Paint.Style.FILL);
    
    for(int i=0;i<paths.size();i++)
        {
        paint.setColor(i<<24); // use only alpha value for color 0xXX000000
        canvas.drawPath(paths.get(i), paint); 
        }
    

    然后查找点,

    int pathIndex = lookup.getPixel(x, y);
    pathIndex >>>= 24;
    

    如果有未填充的点,请务必检查 255(无路径)。

    【讨论】:

    • 啊,好吧,我喜欢。无论如何,额外的内存并没有做任何有用的事情!我遇到的一个问题是 ALPHA_8 除了使用 getPixel 返回 0 之外,不会给我任何东西。我不得不放弃并使用 ARGB_8888。我几乎没有找到关于 ALPHA_8 格式及其限制的文档,但它在这里肯定对我不起作用。谢谢布赖恩。
    • 锻炼记忆力!整个 Skia Android 2D 框架的文档不足。遗憾的是 4 倍内存要求,但至少 Android 屏幕很小。
    • @TomSeago 嗨,我也遇到了同样的问题,我也必须使用 ARGB_8888,如果使用 ALPHA_8 则不会返回!
    • 有关算法替代方案,请参阅:geeksforgeeks.org/…
    • @TomSeago 我知道这篇文章很古老,但我发现了问题。 Alpha 值不像普通颜色那样起作用 - 如果位图设置为 alpha=255,即开始时完全不透明度,那么绘图命令将不会改变任何内容。解决方案:在eraseColor 中使用0x00000000 并使用(255-i)&lt;&lt;24 代替i&lt;&lt;24 进行颜色定义和255-(lookup.getPixel(x, y)&gt;&gt;&gt;24 进行查找。
    【解决方案3】:

    WebKit 的 SkiaUtils 为 Randy Findley 的错误提供了 C++ 解决方法:

    bool SkPathContainsPoint(SkPath* originalPath, const FloatPoint& point, SkPath::FillType ft)
    {
      SkRegion rgn;
      SkRegion clip;
    
      SkPath::FillType originalFillType = originalPath->getFillType();
    
      const SkPath* path = originalPath;
      SkPath scaledPath;
      int scale = 1;
    
      SkRect bounds = originalPath->getBounds();
    
      // We can immediately return false if the point is outside the bounding rect
      if (!bounds.contains(SkFloatToScalar(point.x()), SkFloatToScalar(point.y())))
          return false;
    
      originalPath->setFillType(ft);
    
      // Skia has trouble with coordinates close to the max signed 16-bit values
      // If we have those, we need to scale. 
      //
      // TODO: remove this code once Skia is patched to work properly with large
      // values
      const SkScalar kMaxCoordinate = SkIntToScalar(1<<15);
      SkScalar biggestCoord = std::max(std::max(std::max(bounds.fRight, bounds.fBottom), -bounds.fLeft), -bounds.fTop);
    
      if (biggestCoord > kMaxCoordinate) {
          scale = SkScalarCeil(SkScalarDiv(biggestCoord, kMaxCoordinate));
    
          SkMatrix m;
          m.setScale(SkScalarInvert(SkIntToScalar(scale)), SkScalarInvert(SkIntToScalar(scale)));
          originalPath->transform(m, &scaledPath);
          path = &scaledPath;
      }
    
      int x = static_cast<int>(floorf(point.x() / scale));
      int y = static_cast<int>(floorf(point.y() / scale));
      clip.setRect(x, y, x + 1, y + 1);
    
      bool contains = rgn.setPath(*path, clip);
    
      originalPath->setFillType(originalFillType);
      return contains;
    }
    

    【讨论】:

      【解决方案4】:

      我知道我参加聚会有点晚了,但我会通过像确定一个点是否在多边形中一样思考来解决这个问题。

      http://en.wikipedia.org/wiki/Point_in_polygon

      当您查看贝塞尔样条曲线而不是线段时,数学计算会更慢,但从该点绘制射线仍然有效。

      【讨论】:

      • 派对还没结束,我的朋友,世界还在为此苦苦挣扎。你好,从 2015 年开始 :)
      【解决方案5】:

      为了完整起见,我想在这里做几点说明:

      从 API 19 开始,路径有一个 intersection operation。您可以在测试点周围创建一个非常小的方形路径,将其与路径相交,然后查看结果是否为空。

      您可以将路径转换为区域并执行contains() 操作。然而,区域在整数坐标中工作,我认为它们使用转换后的(像素)坐标,所以你必须使用它。我还怀疑转换过程是计算密集型的。

      Hans 发布的边缘交叉算法既好又快,但您必须非常小心某些极端情况,例如光线直接穿过顶点、与水平边缘相交或舍入错误时是个问题,一直都是。

      winding number 方法几乎是万无一失的,但涉及大量三角函数并且计算量很大。

      This paper by Dan Sunday 提供了一种混合算法,该算法与绕组数一样准确,但在计算上却与光线投射算法一样简单。它的优雅让我大吃一惊。

      请参阅https://stackoverflow.com/a/33974251/338479 了解我的代码,该代码将对由线段、圆弧和圆组成的路径进行路径点计算。

      【讨论】:

        猜你喜欢
        • 2013-07-08
        • 1970-01-01
        • 2014-03-15
        • 2012-05-17
        • 2010-09-13
        • 2019-08-17
        • 2019-06-02
        • 2021-02-07
        • 2014-11-02
        相关资源
        最近更新 更多