Java - 在paintComponent中使用合成实现圆角面板

15

根据下面的原始问题,我现在正在为以下问题提供赏金:

基于 AlphaComposite 的圆角解决方案。

  • 请使用 JPanel 进行演示。
  • 圆角必须完全透明。
  • 必须能够支持 JPG 绘制,但仍具有圆角
  • 不能使用 setClip(或任何剪辑)
  • 必须具有良好的性能

希望有人能够迅速解决这个问题。看起来很容易。

如果有人有充分解释的理由表明这是不可能实现的,并且其他人也同意,我也会授予赏金。

这里是我所想的一个示例图像(但使用了AlphaCompositeenter image description here


原始问题

我一直在尝试找出一种使用合成方法做出圆角的方法,与How to make a rounded corner image in Javahttp://weblogs.java.net/blog/campbell/archive/2006/07/java_2d_tricker.html非常相似。

然而,我的尝试没有中间的 BufferedImage 不起作用 - 圆角目标合成似乎不影响源。 我尝试了不同的方法,但什么也没做出来。 应该得到一个圆角红色矩形,而不是一个方形。

所以,我真正有两个问题:

1)是否有一种方法使这个工作?

2)中间图像是否会生成更好的性能?

SSCCE:

测试面板TPanel

import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;

import javax.swing.JLabel;

public class TPanel extends JLabel {
int w = 300;
int h = 200;

public TPanel() {
    setOpaque(false);
    setPreferredSize(new Dimension(w, h));
        setMaximumSize(new Dimension(w, h));
        setMinimumSize(new Dimension(w, h));
}

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2d = (Graphics2D) g.create();

    // Yellow is the clipped area.
    g2d.setColor(Color.yellow);
    g2d.fillRoundRect(0, 0, w, h, 20, 20);
    g2d.setComposite(AlphaComposite.Src);

    // Red simulates the image.
    g2d.setColor(Color.red);
    g2d.setComposite(AlphaComposite.SrcAtop);

    g2d.fillRect(0, 0, w, h);
    }
}

和它的沙箱

import java.awt.Dimension;
import java.awt.FlowLayout;

import javax.swing.JFrame;

public class Sandbox {
public static void main(String[] args) {
    JFrame f = new JFrame();
        f.setMinimumSize(new Dimension(800, 600));
        f.setLocationRelativeTo(null);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setLayout(new FlowLayout());

        TPanel pnl = new TPanel();
        f.getContentPane().add(pnl);

        f.setVisible(true);
    }
}

请阅读@mre关于JButton和Graphics的问题。 - mKorbel
你是指的https://dev59.com/S17Va4cB1Zd3GeqPGAsq吗?那个问题涉及到填充到图形中的纯色,而不是图像...我正在寻找一种使用复合形状来设置蒙版,然后在其上绘制图像的方法。 - Ben
2个回答

7

关于您的性能问题,Java 2D Trickery文章中包含了一个链接,链接到了Chet Haase对中间图像使用方法的非常好的解释。

我认为O'Reilly的Java基础类速查表中的以下摘录可能对您有所帮助,以便理解AlphaComposite行为以及为什么中间图像可能是必要的技术。

AlphaComposite混合规则 SRC_OVER混合规则将可能半透明的源颜色覆盖在目标颜色上。这通常是我们执行图形操作时想要发生的情况。但是,AlphaComposite对象实际上还允许按照其他七个规则组合颜色。
在详细考虑混合规则之前,有一个重要的点需要理解。在屏幕上显示的颜色从不具有alpha通道。如果您可以看到颜色,则它是不透明的颜色。精确的颜色值可能基于透明度计算而选择,但是一旦选择了该颜色,颜色就驻留在某个视频卡的内存中,并且没有与其关联的alpha值。换句话说,在屏幕上绘制时,目标像素始终具有1.0的alpha值。
然而,在绘制到离屏图像时情况就不同了。当我们在本章稍后考虑Java 2D BufferedImage类时,您会看到可以在创建离屏图像时指定所需的颜色表示。默认情况下,BufferedImage对象将图像表示为RGB颜色数组,但是您还可以创建一个ARGB颜色数组的图像。这样的图像具有与其关联的alpha值,当您绘制到图像中时,alpha值将保留与您绘制的像素相关联。
在屏幕绘制和离屏绘制之间的区别很重要,因为一些混合规则是基于目标像素的alpha值而不是源像素的alpha值进行混合的。在屏幕绘制时,目标像素始终是不透明的(具有1.0的alpha值),但是在离屏绘制时,这不一定是情况。因此,当您绘制具有alpha通道的离屏图像时,某些混合规则才会有用。
稍微概括一下,我们可以说,在屏幕上绘制时,您通常使用默认的SRC_OVER混合规则,使用不透明颜色,并改变AlphaComposite对象使用的alpha值。然而,在使用具有alpha通道的离屏图像时,您可以利用其他混合规则。在这种情况下,您通常使用半透明颜色和半透明图像以及一个alpha值为1.0的AlphaComposite对象。

屏幕上显示的颜色从未具有阿尔法通道。太棒了。不确定为什么在过去两周中没有其他人解决这个问题。鉴于您处理了性能问题和问题的来源(更何况是从“可靠和官方来源”),因此授予奖励。干杯。 - Ben
顺便提一下,在《千万富翁客户端》杂志上读到了一篇文章,这让我对这个问题产生了兴趣:chuckle: 特别是合成方面的东西-他对合成非常兴奋。 - Ben

4
我已经研究了这个问题,但无法看到如何在单个系统类调用中完成此操作。
Graphics2D是一个抽象实例,实现为SunGraphics2D。源代码可在例如docjar上找到,因此我们可能只需“做同样的事情,但不同”的方式复制一些代码。但是,绘制图像的方法取决于一些不可用的“管道”类。虽然您可以使用类加载来处理,但可能会遇到某些本地优化类,无法操纵以实现理论上最佳的方法;您获得的只是将图像绘制为正方形。
但是,我们可以采用一种方法,在该方法中,我们自己的非本机(即:较慢)代码尽可能少地运行,并且不依赖于图像大小,而是依赖于圆角矩形中(相对)较低的面积。此外,不会在内存中复制图像,从而消耗大量内存。但是,如果您有很多内存,那么显然,在创建实例后,缓存的图像更快。
备选方案1:
import java.awt.Composite;
import java.awt.CompositeContext;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;

import javax.swing.JLabel;

public class TPanel2 extends JLabel implements Composite, CompositeContext {
private int w = 300;
private int h = 200;

private int cornerRadius = 20;
private int[] roundRect; // first quadrant
private BufferedImage image;
private int[][] first = new int[cornerRadius][];
private int[][] second = new int[cornerRadius][];
private int[][] third = new int[cornerRadius][];
private int[][] forth = new int[cornerRadius][];

public TPanel2() {
    setOpaque(false);
    setPreferredSize(new Dimension(w, h));
    setMaximumSize(new Dimension(w, h));
    setMinimumSize(new Dimension(w, h));

    // calculate round rect     
    roundRect = new int[cornerRadius];
    for(int i = 0; i < roundRect.length; i++) {
        roundRect[i] = (int)(Math.cos(Math.asin(1 - ((double)i)/20))*20); // x for y
    }

    image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); // all black
}

@Override
public void paintComponent(Graphics g) {
    // discussion:
    // We have to work with the passed Graphics object.

    if(g instanceof Graphics2D) {

        Graphics2D g2d = (Graphics2D) g;

        // draw the whole image and save the corners
        g2d.setComposite(this);
        g2d.drawImage(image, 0, 0, null);
    } else {
        super.paintComponent(g);
    }
}

@Override
public CompositeContext createContext(ColorModel srcColorModel,
        ColorModel dstColorModel, RenderingHints hints) {
    return this;
}

@Override
public void dispose() {

}

@Override
public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
    // lets assume image pixels >> round rect pixels
    // lets also assume bulk operations are optimized

    // copy current pixels
    for(int i = 0; i < cornerRadius; i++) {
        // quadrants

        // from top to buttom
        // first
        first[i] = (int[]) dstOut.getDataElements(src.getWidth() - (cornerRadius - roundRect[i]), i, cornerRadius - roundRect[i], 1, first[i]);

        // second
        second[i] = (int[]) dstOut.getDataElements(0, i, cornerRadius - roundRect[i], 1, second[i]);

        // from buttom to top
        // third
        third[i] = (int[]) dstOut.getDataElements(0, src.getHeight() - i - 1, cornerRadius - roundRect[i], 1, third[i]);

        // forth
        forth[i] = (int[]) dstOut.getDataElements(src.getWidth() - cornerRadius + roundRect[i], src.getHeight() - i - 1, cornerRadius - roundRect[i], 1, forth[i]);
    }

    // overwrite entire image as a square
    dstOut.setRect(src);

    // copy previous pixels back in corners
    for(int i = 0; i < cornerRadius; i++) {
        // first
        dstOut.setDataElements(src.getWidth() - cornerRadius + roundRect[i], i, first[i].length, 1, second[i]);

        // second
        dstOut.setDataElements(0, i, second[i].length, 1, second[i]);

        // third
        dstOut.setDataElements(0, src.getHeight() - i - 1, third[i].length, 1, third[i]);

        // forth
        dstOut.setDataElements(src.getWidth() - cornerRadius + roundRect[i], src.getHeight() - i - 1, forth[i].length, 1, forth[i]);
    }
}

}

替代方案2:

import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.CompositeContext;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import javax.swing.JLabel;

public class TPanel extends JLabel implements Composite, CompositeContext {
private int w = 300;
private int h = 200;

private int cornerRadius = 20;
private int[] roundRect; // first quadrant
private BufferedImage image;

private boolean initialized = false;
private int[][] first = new int[cornerRadius][];
private int[][] second = new int[cornerRadius][];
private int[][] third = new int[cornerRadius][];
private int[][] forth = new int[cornerRadius][];

public TPanel() {
    setOpaque(false);
    setPreferredSize(new Dimension(w, h));
    setMaximumSize(new Dimension(w, h));
    setMinimumSize(new Dimension(w, h));

    // calculate round rect     
    roundRect = new int[cornerRadius];
    for(int i = 0; i < roundRect.length; i++) {
        roundRect[i] = (int)(Math.cos(Math.asin(1 - ((double)i)/20))*20); // x for y
    }

    image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB); // all black
}

@Override
public void paintComponent(Graphics g) {
    if(g instanceof Graphics2D) {

        Graphics2D g2d = (Graphics2D) g;

        // draw 1 + 2 rectangles and copy pixels from image. could also be 1 rectangle + 4 edges
        g2d.setComposite(AlphaComposite.Src);

        g2d.drawImage(image, cornerRadius, 0, image.getWidth() - cornerRadius - cornerRadius, image.getHeight(), null);
        g2d.drawImage(image, 0, cornerRadius, cornerRadius, image.getHeight() - cornerRadius - cornerRadius, null);
        g2d.drawImage(image, image.getWidth() - cornerRadius, cornerRadius, image.getWidth(), image.getHeight() - cornerRadius, image.getWidth() - cornerRadius, cornerRadius, image.getWidth(), image.getHeight() - cornerRadius, null);

        // draw the corners using our own logic
        g2d.setComposite(this);

        g2d.drawImage(image, 0, 0, null);

    } else {
        super.paintComponent(g);
    }
}

@Override
public CompositeContext createContext(ColorModel srcColorModel,
        ColorModel dstColorModel, RenderingHints hints) {
    return this;
}

@Override
public void dispose() {

}

@Override
public void compose(Raster src, Raster dstIn, WritableRaster dstOut) {
    // assume only corners need painting

    if(!initialized) {
        // copy image pixels
        for(int i = 0; i < cornerRadius; i++) {
            // quadrants

            // from top to buttom
            // first
            first[i] = (int[]) src.getDataElements(src.getWidth() - cornerRadius, i, roundRect[i], 1, first[i]);

            // second
            second[i] = (int[]) src.getDataElements(cornerRadius - roundRect[i], i, roundRect[i], 1, second[i]);

            // from buttom to top
            // third
            third[i] = (int[]) src.getDataElements(cornerRadius - roundRect[i], src.getHeight() - i - 1, roundRect[i], 1, third[i]);

            // forth
            forth[i] = (int[]) src.getDataElements(src.getWidth() - cornerRadius, src.getHeight() - i - 1, roundRect[i], 1, forth[i]);
        }
        initialized = true;
    }       

    // copy image pixels into corners
    for(int i = 0; i < cornerRadius; i++) {
        // first
        dstOut.setDataElements(src.getWidth() - cornerRadius, i, first[i].length, 1, second[i]);

        // second
        dstOut.setDataElements(cornerRadius - roundRect[i], i, second[i].length, 1, second[i]);

        // third
        dstOut.setDataElements(cornerRadius - roundRect[i], src.getHeight() - i - 1, third[i].length, 1, third[i]);

        // forth
        dstOut.setDataElements(src.getWidth() - cornerRadius, src.getHeight() - i - 1, forth[i].length, 1, forth[i]);
    }
}

}

希望这可以帮到你,虽然这只是一个次优解决方案,但这就是生活(当一些图形大师来证明我错了时(??)...);-)

谢谢Thomas,+1感谢你的帮助,但我得跟随我的直觉,认为@Mark McLaren是“最正确”的。 - Ben
请原谅我没有直接使用AlphaComposite.SrcATop,我想你更感兴趣的是一个等效的工作解决方案。上述方法似乎满足了圆角矩形的5个标准,而且你可以轻松地添加抗锯齿/渐变边缘。这就是实用主义的体现。 - ThomasRS

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接