如何检查两个drawString()字符串是否相交?

时间:2016-02-03 05:45:17

标签: java swing graphics awt java-2d

就像标题所说的那样,我需要先看看两个字符串位置是否相交,然后再用graphics2d绘制它们。这样我就没有相互之间的字符串,所以你无法阅读它们。

一些细节:

屏幕尺寸为1000x1000像素。我以10毫秒的固定间隔随机生成坐标位置和字体。然后(也是每10毫秒)我使用g2d.drawString()来画出“popup!”这个词。使用我之前存储的随机字体和随机位置,在paintComponent()方法的屏幕上。但是,由于我随机生成坐标,这意味着我偶尔会有消息重叠。如何通过不允许它生成重叠的坐标或不打印重叠的消息来确保不会发生这种情况?

代码:

Font[] popups = new Font[20];
int[][] popupsLoc = new int[20][2];
Random rn = new Random();
public void addPopup() { //is being called every 10 miliseconds by timer 
    boolean needPopup = false;
        int where = 0;
        for(int i = 0; i < popups.length; i++) {
            if(popups[i] == null) {
                needPopup = true;
                where = i;
                }
            }
        if(needPopup == true) {
            popups[where] = new Font("STENCIL", Font.BOLD, rn.nextInt(100) + 10);
            popupsLoc[where][0] = rn.nextInt(800);
            popupsLoc[where][1] = rn.nextInt(800);
        }
    }
} //in paintComponent() I iterate through the popups[] array and draw the element with the font

油漆代码:

public void paintComponent(Graphics g) {
        super.paintComponent(g);

        setBackground(Color.BLACK);
        Graphics2D g2d = (Graphics2D) g;


        for(int i = 0; i < popups.length; i++) {
            if(popups[i] != null) {
                g2d.setColor(popupColor);
                g2d.setFont(popups[i]);
                g2d.drawString("Popup!", popupsLoc[i][0], popupsLoc[i][1]);
            }
        }
}

示例 enter image description here

正如您所看到的,屏幕右下方的两条消息重叠在此处。我该如何防止这种情况?

编辑:我找到了一个非常简单的解决方案。

public void addPopup() {

            boolean needPopup = false;
            int where = 0;
            for (int i = 0; i < popups.length; i++) {

                if (popups[i] == null) {
                    needPopup = true;
                    where = i;
                }
            }
            if (needPopup == true) {
                boolean doesIntersect = false;
                popups[where] = new Font("STENCIL", Font.BOLD, rn.nextInt(100) + 10);
                popupsLoc[where][0] = rn.nextInt(800);
                popupsLoc[where][1] = rn.nextInt(800);

                FontMetrics metrics = getFontMetrics(popups[where]);
                int hgt = metrics.getHeight();
                int wdh = metrics.stringWidth("Popup!");
                popupsHitbox[where] = new Rectangle(popupsLoc[where][0], popupsLoc[where][1], wdh, hgt);
                //System.out.println(hgt);

                for (int i = where + 1; i < popups.length; i++) {
                    if (popupsHitbox[i] != null) {
                        if (popupsHitbox[where].intersects(popupsHitbox[i]))
                            doesIntersect = true;

                    }
                }
                if (doesIntersect == true) {
                    popups[where] = null;
                    popupsLoc[where][0] = 0;
                    popupsLoc[where][1] = 0;
                    popupsHitbox[where] = null;
                    addPopup();
                }
            }

    }

然后当我画画时:

for (int i = 0; i < popups.length; i++) {
            if (popups[i] != null) {
                g2d.setColor(popupColor);
                g2d.setFont(popups[i]);
                FontMetrics metrics = getFontMetrics(popups[i]);
                g2d.drawString("Popup!", popupsLoc[i][0], popupsLoc[i][1]+metrics.getHeight());
                //g2d.draw(popupsHitbox[i]);
            }
        }

解释是这样的:当我创建弹出字体/坐标位置时,我还使用坐标位置和FontMetrics创建一个矩形“hitbox”来获取消息的大小(以像素为单位),然后我将此矩形存储为阵列。之后,我有一个名为doesIntersect的布尔标志,它被初始化为false。我遍历所有的hitbox并检查当前的一个是否与其他任何一个相交。如果是这样,我将标志设置为true。然后,在检查之后,如果该标志为true,则将该数组中的该位置重置为null并重新调用addPopup()。 (这里可能会有一些递归)最后,当我画画时,我只是在坐标位置绘制字符串,(因为字符串从左下方绘制,因此y +高度)。可能不是很干净,但它确实有效。

1 个答案:

答案 0 :(得分:0)

我创建了一个静态实用程序类,用于为给定的ShapeString渲染表面生成准确的Graphics2D实例,这将有效地计算交叉点检测,而不会出现仅使用边界框的错误。

/**
 * Provides methods for generating accurate shapes describing the area a particular {@link String} will occupy when
 * drawn alongside methods which can calculate the intersection of those shapes efficiently and accurately.
 * 
 * @author Emily Mabrey (emabrey@users.noreply.github.com)
 */

public class TextShapeIntersectionCalculator {

  /**
   * An {@link AffineTransform} which returns the given {@link Area} unchanged.
   */
  private static final AffineTransform NEW_AREA_COPY = new AffineTransform();

  /**
   * Calculates the delta between two single coordinate values.
   * 
   * @param coordinateA
   *        The origination coordinate which we are calculating from
   * @param coordinateB
   *        The destination coordinate which the delta takes us to
   * @return A coordinate value delta which expresses the change from A to B
   */
  private static int getCoordinateDelta(final int coordinateA, final int coordinateB) {

    return coordinateB - coordinateA;
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param string
   *        An {@link AttributedString} containing the data describing which characters to draw alongside the
   *        {@link Attribute Attributes} describing how those characters should be drawn.
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final AttributedString string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string.getIterator(), fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param attributes
   *        A non-null {@link Map} object populated with {@link Attribute} objects which will be used to determine the
   *        glyphs and styles for rendering the character data
   * @param string
   *        A {@link String} containing the character data which is to be drawn
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final Map<? extends Attribute, ?> attributes,
    final String string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string, attributes, fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Calls {@link #getTextShape(TextLayout)} using a {@link TextLayout} generated from the provided objects and
   * returns the generated {@link Shape}.
   * 
   * @param graphicsContext
   *        A non-null {@link Graphics2D} object with the configuration of the desired drawing surface
   * @param outputFont
   *        A non-null {@link Font} object used to determine the glyphs and styles for rendering the character data
   * @param string
   *        A {@link String} containing the character data which is to be drawn
   * @return A {@link Shape} generated via {@link #getTextShape(TextLayout)}
   */
  public static Shape getTextShape(final Graphics2D graphicsContext, final Font outputFont, final String string) {

    final FontRenderContext fontContext = graphicsContext.getFontRenderContext();

    final TextLayout textLayout = new TextLayout(string, outputFont, fontContext);

    return getTextShape(textLayout);
  }

  /**
   * Determines the {@link Shape} which should be generated by rendering the given {@link TextLayout} object using the
   * internal {@link Graphics2D} rendering state alongside the internal {@link String} and {@link Font}. The returned
   * {@link Shape} is a potentially disjoint union of all the glyph shapes generated from the character data. Note that
   * the states of the mutable contents of the {@link TextLayout}, such as {@link Graphics2D}, will not be modified.
   * 
   * @param textLayout
   *        A {@link TextLayout} with an available {@link Graphics2D} object
   * @return A {@link Shape} which is likely a series of disjoint polygons
   */
  public static Shape getTextShape(final TextLayout textLayout) {

    final int firstSequenceEndpoint = 0, secondSequenceEndpoint = textLayout.getCharacterCount();
    final Shape generatedCollisionShape = textLayout.getBlackBoxBounds(firstSequenceEndpoint, secondSequenceEndpoint);

    return generatedCollisionShape;

  }

  /**
   * Converts the absolute coordinates of {@link Shape Shapes} a and b into relative coordinates and uses the converted
   * coordinates to call and return the result of {@link #checkForIntersection(Shape, Shape, int, int)}.
   * 
   * @param a
   *        A shape located with a user space location
   * @param aX
   *        The x coordinate of {@link Shape} a
   * @param aY
   *        The y coordinate of {@link Shape} a
   * @param b
   *        A shape located with a user space location
   * @param bX
   *        The x coordinate of {@link Shape} b
   * @param bY
   *        The x coordinate of {@link Shape} b
   * @return True if the two shapes at the given locations intersect, false if they do not intersect.
   */
  public static boolean checkForIntersection(final Shape a, final int aX, final int aY, final Shape b, final int bX,
    final int bY) {

    return checkForIntersection(a, b, getCoordinateDelta(aX, bX), getCoordinateDelta(aY, bY));
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked in a way which
   * fails quickly if there is no intersection and which succeeds using the least amount of calculation required to
   * determine there is an intersection. The location of {@link Shape} a is considered to be the origin and the position
   * of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  public static boolean checkForIntersection(final Shape a, final Shape b, int relativeDeltaX, int relativeDeltaY) {

    return isIntersectionUsingSimpleBounds(a, b, relativeDeltaX, relativeDeltaY)
      && isIntersectionUsingAdvancedBounds(a, b, relativeDeltaX, relativeDeltaY)
      && isIntersectionUsingExactAreas(a, b, relativeDeltaX, relativeDeltaY);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a fast but
   * extremely simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and
   * the position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided
   * coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingSimpleBounds(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Rectangle rectA = a.getBounds();
    final Rectangle rectB = b.getBounds();

    rectB.setLocation(rectA.getLocation());
    rectB.translate(relativeDeltaX, relativeDeltaY);

    return rectA.contains(rectB);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a slightly
   * simplified bounding box calculation. The location of {@link Shape} a is considered to be the origin and the
   * position of {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate
   * deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingAdvancedBounds(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Rectangle2D rectA = a.getBounds();
    final Rectangle2D rectB = b.getBounds();

    rectB.setRect(rectA.getX() + relativeDeltaX, rectA.getY() + relativeDeltaY, rectB.getWidth(), rectB.getHeight());

    return rectA.contains(rectB);
  }

  /**
   * Detects if two shapes with relative user space locations intersect. The intersection is checked using a slow but
   * perfectly accurate calculation. The location of {@link Shape} a is considered to be the origin and the position of
   * {@link Shape} b is defined relative to the position of {@link Shape} a using the provided coordinate deltas.
   * 
   * @param a
   *        The shape placed at what is considered the origin
   * @param b
   *        The shape placed in the position relative to a
   * @param relativeDeltaX
   *        The delta x coordinate of {@link Shape} b compared to the x coordination of {@link Shape} a (which is always
   *        0).
   * @param relativeDeltaY
   *        The delta y coordinate of {@link Shape} b compared to the y coordination of {@link Shape} a (which is always
   *        0).
   * @return True to indicate the provided {@link Shape Shapes} intersect when placed in the indicated locations.
   */
  private static boolean isIntersectionUsingExactAreas(final Shape a, final Shape b, int relativeDeltaX,
    int relativeDeltaY) {

    final Area aClone = new Area(a).createTransformedArea(NEW_AREA_COPY);
    final Area bClone = new Area(b).createTransformedArea(NEW_AREA_COPY);

    bClone.transform(AffineTransform.getTranslateInstance(relativeDeltaX, relativeDeltaY));
    aClone.intersect(bClone);

    return !aClone.isEmpty();
  }

}

使用此类,您应该能够在实际字符字形不在任何地方绘制String,即使您要绘制的地点位于另一个String的边界框内。

我重写了你给我使用我的新交叉检测的代码,但在重写它时我清理它并添加了一些新类来改进它。这两个类只是数据结构,在重写代码时需要它们:

class StringDrawInformation {

    public StringDrawInformation(final String s, final Font f, final Color c, final int x, final int y) {
      this.text = s;
      this.font = f;
      this.color = c;
      this.x = x;
      this.y = y;
    }

    public final String text;

    public final Font font;

    public final Color color;

    public int x, y;
  }

class DrawShape {

    public DrawShape(final Shape s, final StringDrawInformation drawInfo) {
      this.shape = s;
      this.drawInfo = drawInfo;
    }

    public final Shape shape;

    public StringDrawInformation drawInfo;
  }

使用我的三个新类我重写了你的代码:

  private static final Random random = new Random();

  public static final List<StringDrawInformation> generateRandomDrawInformation(int newCount) {

    ArrayList<StringDrawInformation> newInfos = new ArrayList<>();

    for (int i = 0; newCount > i; i++) {
      String s = "Popup!";
      Font f = new Font("STENCIL", Font.BOLD, random.nextInt(100) + 10);
      Color c = Color.WHITE;
      int x = random.nextInt(800);
      int y = random.nextInt(800);
      newInfos.add(new StringDrawInformation(s, f, c, x, y));
    }

    return newInfos;
  }

  public static List<DrawShape> generateRenderablePopups(final List<StringDrawInformation> in, Graphics2D g2d) {

    List<DrawShape> outShapes = new ArrayList<>();

    for (StringDrawInformation currentInfo : in) {
      Shape currentShape = TextShapeIntersectionCalculator.getTextShape(g2d, currentInfo.font, currentInfo.text);
      boolean placeIntoOut = true;

      for (DrawShape nextOutShape : outShapes) {
        if (TextShapeIntersectionCalculator.checkForIntersection(nextOutShape.shape, nextOutShape.drawInfo.x,
          nextOutShape.drawInfo.y, currentShape, currentInfo.x, currentInfo.y)) {
          // we found an intersection so we dont place into out and we stop verifying
          placeIntoOut = false;
          break;
        }
      }

      if (placeIntoOut) {
        outShapes.add(new DrawShape(currentShape, currentInfo));
      }
    }

    return outShapes;

  }

  private List<StringDrawInformation> popups = generateRandomDrawInformation(20);

  public void paintComponent(Graphics g) {

    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g;
    g2d.setBackground(Color.BLACK);

    for (DrawShape renderablePopup : generateRenderablePopups(popups, g2d)) {
      g2d.setColor(renderablePopup.drawInfo.color);
      g2d.setFont(renderablePopup.drawInfo.font);
      g2d.drawString(renderablePopup.drawInfo.text, renderablePopup.drawInfo.x, renderablePopup.drawInfo.y);
    }
  }

重写代码很容易修改,以使用更多形状,不同字体,不同颜色等,而不是非常难以修改。我将不同的数据包装成超类型,它封装了较小的数据类型,使它们更易于使用。我的改写并不完美,但希望这会有所帮助。

我还没有真正测试过这段代码,只是手工编写。所以希望它按预期工作。我最终会去测试它,很难找到时间来写我已经完成的事情。如果您有任何问题,请随时问他们。对不起,我花了这么长时间才得到答案!

编辑:一个小的事后想法 - 传递给StringDrawInformation的{​​{1}} List的顺序按优先顺序排列。将每个列表元素与所有当前验证的元素进行比较。第一个未经检查的元素总是成功验证,因为没有比较。第2个未经检查的元素将针对第1个进行检查,因为第1个已经过验证。可以针对最多2个其他元素检查第3个未经检查的元素,第4个到3个。基本上,可以针对i-1个其他元素检查位置i中的元素。因此,如果重要,请将更重要的文本放在列表的前面,将最不重要的文本放在列表的后面。