如何在不包括贝塞尔曲线控制点的情况下计算 Android 中路径的边界?

How to calculate the bounds of a Path in Android without including the control points of Bezier curves?

Android 有一个路径 class 和一个方法 computeBounds()

https://developer.android.com/reference/android/graphics/Path#computeBounds(android.graphics.RectF,%20boolean)

但是在贝塞尔曲线路径的情况下,边界包括曲线的控制点。

这并不理想,因为我只想计算贝塞尔曲线创建的实际形状的边界。

似乎 Skia(Android 使用的底层库)有一个 computeTightBounds 方法可以满足我的要求,但 Android 似乎没有公开它。

https://api.skia.org/classSkPath.html#a597c8fcc5e4750542e2688b057a14e9e

如何在不包括控制点的情况下计算边界?

没有任何直接的方法。 我的解决方案是在绘制过程中根据贝塞尔曲线的终点更新一个RectF。

final RectF mRect = new RectF(999999,999999,-1,-1);

public void updateBounds(final float endPointX, final float endPointY) {
    if (mRect.left == 999999) mRect.set(endPointX, endPointY, endPointX, endPointY);
    else mRect.union(endPointX, endPointY);
}

但是,此解决方案不考虑 StrokeWidth 计数,而只考虑 virtual/invisible 路径(“宽度”等于零)。因此,如果您的需求是“计算有效绘制路径边界”,则需要subtract/sum一半“Path.getStrokewidth() " 在边界计算期间。

没有办法直接获取

要获得一组高精度的边界,您需要自己计算,方法是遍历路径段并计算每个贝塞尔曲线、直线、圆弧等的实际边界。不幸的是 Android不提供在 Path 创建后单步执行元素的方法。所以如果你需要一个高精度的结果,希望你在做之前知道路径的组成。

不过,有一种方法可以使用 PathMeasure 获得具有可配置精度的结果。 尝试以下操作。 此代码尚未经过测试。

void computeExactBounds(RectF bounds, Path path, int accuracy)
{
   bool first = true;

   // Make sure accuracy defaults to something reasonable
   if (accuracy < 2)
      accuracy = 100;

   PathMeasure pm = new PathMeasure(path, false);
   // Step through all the subpaths of this path
   while (nextContour())
   {
      float  pathLen = pm.getLength();

      // To compute the bounds, we calculate the position of 'accuracy'
      // number of positions along the path.  The higher that accuracy is,
      // the more accurate the result, but the longer it will take.
      // But note that, in some extreme cases, it is possible that the
      // real path bounds may be slightly larger than the bounds that
      // this method returns.
   
      float  pos = new float[];
      // Initialise bounds to the path start coordinates
      pm.getPosTan(0, pos, null);
      if (first) {
         bounds.set(pos[0], pos[1], pos[0], pos[1]);
         first = false;
      }
      else
         bounds.union(pos[0], pos[1]);

      // Step through 'accuracy' positions along the path,
      // making sure bounds includes all those positions
      for (int i=1; i <= accuracy; i++) {
         pm.getPosTan(pathLen * i / accuracy, pos, null);
         bounds.union(pos[0], pos[1]);
      }
   }
}

您可以通过使用二进制搜索自适应拆分来改进此代码 子路径分成更小的段(而不是沿着路径以小的固定增量陡峭)。这就是 path.approximate() 做。见下文。


如果您使用 API 26 或更高版本,您可以使用 path.approximate() 计算位置来加快此代码的速度。

这段代码也没有经过测试:)

void computeExactBounds(RectF bounds, Path path, float acceptableError)
{
   // A typical value of acceptableError might be 0.5. But if, say,
   // your path uses extremely small or large coordinate values, you
   // might need to adjust this.  Choose a value that works out as
   // equivalent to around half a pixel at the current scale;

   float[]  pos = path.approximate(acceptableError);
   for (int i = 0; i < pos.length; i += 3)
   {
      if (i == 0)
         bounds.set(pos[i+1], pos[i+2], pos[i+1], pos[i+2]);
      else
         bounds.union(pos[i+1], pos[i+2]);
   }
}