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

标签 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]);
            }
        }

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

最佳答案

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

/**
 * 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);
    }
  }

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

我还没有实际测试过这段代码,只是手写的。所以希望它能按预期工作。我最终会抽出时间来测试它,很难找到时间来写我已经完成的事情。如果您有任何问题,请随时询问他们。抱歉这么久才回答!

编辑:一个小小的事后想法 - 传递给 generateRenderabePopups(...)StringDrawInformation List 的顺序是按优先级顺序排列的。每个列表元素都会与所有当前已验证的元素进行比较。第一个未检查的元素始终验证成功,因为没有比较。第二个未检查的元素将根据第一个进行检查,因为第一个已验证。第三个未检查的元素最多可以检查 2 个其他元素,第四个最多可以检查 3 个。基本上,位置 i 的元素可能会检查 i-1 个其他元素。因此,如果重要的话,请将更重要的文本放在列表的前面,将最不重要的文本放在列表的后面。

关于java - 如何检查两个drawString()字符串是否相交?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35169375/

相关文章:

java - 360k文档后的ElasticSearch docker “HTTP/1.1 429 Too Many Requests”

java - JTable 自动滚动到底部

java - 主动渲染时按键监听器不工作

java - 声明 List l = new ArrayList() 和 ArrayList al = new ArrayList()

java - 如何解决JBoss Remote错误?

java - 当滚动 Pane 包裹文本 Pane 时,如何防止 JScrollPane 箭头键处理移动插入符

java - 向数据库返回一个 int 值

java - 用Java画一个漂亮的圆圈

java - JPanel 确切的绘图尺寸/限制?

java - 如何在共享首选项中分配唯一的字符串值