问题
Using the following code, I am able to draw a dashed line:
public void foo(Graphics2D g2d, Shape shape)
{
Stroke stroke = BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10, new float[]{10}, 0);
g2d.setStroke(stroke);
g2d.draw(shape);
}
Once a shape is created, I want to be able to zoom on that shape (up to 20 000 time). The issue I have is that, when I zoom too much on the shape, the application start to lag and will eventually, if I continue to zoom, crash.
With a plain line, I have no issue.
Therefore, my question is the following: Is there a way to draw very big shape (e.g: A rectangle of 200 000 pixels by 300 000 pixels) with a dash line ?
Thank you.
Edit:
Here is an short example where I was able to reproduce my issue:
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.JFrame;
import javax.swing.JPanel;
class Scale
{
private static int _scale = 1;
public static int getScale()
{
return _scale;
}
public static void setScale(int scale)
{
_scale = scale;
}
}
class Surface extends JPanel implements ActionListener
{
private static Surface _surface;
boolean isBlue = false;
private Surface()
{
}
public static Surface getInstance()
{
if (_surface == null)
{
_surface = new Surface();
}
return _surface;
}
private void doDrawing(Graphics g)
{
Shape rectangle = new Rectangle(0, 0, 600 * Scale.getScale(), 400 * Scale.getScale());
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(Color.blue);
Stroke stroke = new BasicStroke(10, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 10.0f, new float[]{10.0f}, 0);
g2d.setStroke(stroke);
// Adding a clip don't seem to do the trick :(
g2d.clip(new Rectangle(0, 0, 100, 100));
long startTime = System.nanoTime();
g2d.draw(rectangle);
long elapseTime = System.nanoTime() - startTime;
// Printing the time it took each time I render my shape. As the size increase, the time increase. If the shape decrease, the time decrease as well.
System.out.println(elapseTime);
g2d.dispose();
}
@Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
doDrawing(g);
}
@Override
public void actionPerformed(ActionEvent e)
{
repaint();
}
}
public class MainFrame extends JFrame implements KeyListener
{
public MainFrame()
{
initUI();
setFocusable(true);
addKeyListener(this);
}
public static void main(String[] args)
{
EventQueue.invokeLater(new Runnable()
{
@Override
public void run()
{
MainFrame ex = new MainFrame();
ex.setVisible(true);
}
});
}
private void initUI()
{
final Surface surface = Surface.getInstance();
add(surface);
setTitle("My boggus apps");
setSize(600, 400);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
@Override
public void keyTyped(final KeyEvent e)
{
}
@Override
public void keyPressed(final KeyEvent e)
{
int key = e.getKeyCode();
if (key == KeyEvent.VK_UP)
{
Scale.setScale(Scale.getScale() * 2);
if (Scale.getScale() > 200000)
{
Scale.setScale(200000);
}
Surface.getInstance().repaint();
}
else if (key == KeyEvent.VK_DOWN)
{
Scale.setScale(Scale.getScale() / 2);
if (Scale.getScale() < 1)
{
Scale.setScale(1);
}
Surface.getInstance().repaint();
}
else
{
System.out.println(key);
}
}
@Override
public void keyReleased(final KeyEvent e)
{
}
}
回答1:
Bad rendering performance for long lines with dashed strokes is a very long known and still unresolved issue in AWT Graphics2D and also JavaFX 2:
- AWT: https://bugs.openjdk.java.net/browse/JDK-6620013
- JavaFX 2: https://bugs.openjdk.java.net/browse/JDK-8160600
It is still unresolved in Java 8u152 and 9.0.1. However, performance in 9.0.1 is slightly better (about 1.5s vs. 4.0s in one of my tests 9.0.1 vs. 8u152).
Background: Long dashed lines are resolved to each individual short dash they contain, even if none of these dashes even touches the clipping area.
Known but mostly unsatisfying workarounds:
- Use solid lines. (does not really solve the problem)
- Manually calculate the lines' intersection points with the clip rect. Draw only the short visible parts within the clip as new Line2Ds. Drop lines that do not even touch the clip. (complicated, restricted to easy cases like Rectangle2D or Line2D, not applicable to general shapes)
- Use solid lines with textured paints like e.g. checkerboards or grids. (easy, very angle dependent, no precise dashes, moiré effects)
回答2:
Here's a solution using area intersection or line/rectangle intersection depending on the nature of the path. This is not the ideal solution because it will not position dashes properly for paths much, much larger than the clipping region. However, it keeps those cases to a minimum.
private void drawLimited(Shape primitive, Graphics2D canvas) {
if (!(canvas.getStroke() instanceof BasicStroke) || ((BasicStroke) canvas.getStroke()).getDashArray() == null) {
// draw normally
canvas.draw(primitive);
return;
}
Rectangle r = canvas.getClipBounds();
// use a large padding to exclude all but the worst performance cases
int pad = Ints.max(canvas.getStroke() instanceof BasicStroke ? (int) Math.ceil(((BasicStroke) canvas.getStroke()).getLineWidth()) : 5,
r.width * 50, r.height * 50);
Rectangle paddedClip = new Rectangle(r.x - pad, r.y - pad,
r.width + 2 * pad, r.height + 2 * pad);
Shape toDraw = intersectPath(paddedClip, primitive);
if (toDraw != null) {
canvas.draw(toDraw);
}
}
private static @Nullable Shape intersectPath(Rectangle2D rectangle, Shape path) {
Rectangle2D r2 = path.getBounds2D();
if (r2.getWidth() == 0 && r2.getHeight() == 0) {
return null;
} else if (rectangle.contains(r2)) {
return path;
}
if (r2.getWidth() == 0 || r2.getHeight() == 0) {
// we have a flat shape, so area intersection doesn't work -- this is not precisely correct for multi-part paths, but close enough?
path = new Line2D.Double(r2.getMinX(), r2.getMinY(), r2.getMaxX(), r2.getMaxY());
}
if (path instanceof Line2D.Double) {
Line2D line = (Line2D) path;
return line.intersects(rectangle) ? intersect(line, rectangle) : null;
} else {
Area a = new Area(rectangle);
a.intersect(new Area(path));
return a;
}
}
private static @Nullable Line2D.Double intersect(Line2D.Double l, Rectangle2D r) {
if (r.contains(l.getP1()) && r.contains(l.getP2())) {
return l;
}
// parameterize line as x=x1+t*(x2-x1), y=y1+t*(y2-y1), so line is between 0 and 1
// then compute t values for lines bounding rectangles, and intersect the three intervals
// [0,1], [tx1,tx2], and [ty1,ty2]
double tx1 = l.x1 == l.x2 ? (between(l.x1, r.getMinX(), r.getMaxX()) ? 0 : -1) : (r.getMinX() - l.x1) / (l.x2 - l.x1);
double tx2 = l.x1 == l.x2 ? (between(l.x1, r.getMinX(), r.getMaxX()) ? 1 : -1) : (r.getMaxX() - l.x1) / (l.x2 - l.x1);
double ty1 = l.y1 == l.y2 ? (between(l.x1, r.getMinY(), r.getMaxY()) ? 0 : -1) : (r.getMinY() - l.y1) / (l.y2 - l.y1);
double ty2 = l.y1 == l.y2 ? (between(l.x1, r.getMinY(), r.getMaxY()) ? 1 : -1) : (r.getMaxY() - l.y1) / (l.y2 - l.y1);
double t0 = max(0, min(tx1, tx2), min(ty1, ty2));
double t1 = min(1, max(tx1, tx2), max(ty1, ty2));
return t0 > t1 ? null : new Line2D.Double(l.x1 + t0 * (l.x2 - l.x1), l.y1 + t0 * (l.y2 - l.y1),
l.x1 + t1 * (l.x2 - l.x1), l.y1 + t1 * (l.y2 - l.y1));
}
private static boolean between(double x, double t0, double t1) {
return x >= t0 ? x <= t1 : x >= t1;
}
来源:https://stackoverflow.com/questions/47102734/performances-issue-when-drawing-dashed-line-in-java