The Source for Java Technology Collaboration
User: Password:



Kirill Grouchnikov

Kirill Grouchnikov's Blog

Playing with Color

Posted by kirillcool on January 17, 2007 at 09:40 AM | Comments (12)

Seeing that this subject has popped up twice over the last two days (here and here), i decided to share a little trick to make a Color object mutable.

In general, the Color class doesn't allow changing the existing object. However, since it's not final, we can use the Delegate pattern and direct all methods to a single "master" color that is computed dynamically. The master color computation can depend, for example, on rollover state (illustrated below) which will allow creating rollover effects without triggering the property change events (on the "foregroundColor" property).

Here is how it's done.

public class RolloverForeground extends JFrame {

  public RolloverForeground() {
    this.setLayout(new FlowLayout());

    final JButton button = new JButton("sample");
    button.setForeground(new Color(0x000000) {
      private Color getDelegate() {
        return button.getModel().isRollover() ? Color.red : Color.black;
      }

      @Override
      public Color brighter() {
        return getDelegate().brighter();
      }

      @Override
      public synchronized PaintContext createContext(ColorModel cm,
          Rectangle r, Rectangle2D r2d, AffineTransform xform,
          RenderingHints hints) {
        return getDelegate().createContext(cm, r, r2d, xform, hints);
      }

      @Override
      public Color darker() {
        return getDelegate().darker();
      }

      @Override
      public boolean equals(Object obj) {
        return getDelegate().equals(obj);
      }

      @Override
      public int getAlpha() {
        return getDelegate().getAlpha();
      }

      @Override
      public int getBlue() {
        return getDelegate().getBlue();
      }

      @Override
      public float[] getColorComponents(ColorSpace cspace,
          float[] compArray) {
        return getDelegate().getColorComponents(cspace, compArray);
      }

      @Override
      public float[] getColorComponents(float[] compArray) {
        return getDelegate().getColorComponents(compArray);
      }

      @Override
      public ColorSpace getColorSpace() {
        return getDelegate().getColorSpace();
      }

      @Override
      public float[] getComponents(ColorSpace cspace, float[] compArray) {
        return getDelegate().getComponents(cspace, compArray);
      }

      @Override
      public float[] getComponents(float[] compArray) {
        return getDelegate().getComponents(compArray);
      }

      @Override
      public int getGreen() {
        return getDelegate().getGreen();
      }

      @Override
      public int getRed() {
        return getDelegate().getRed();
      }

      @Override
      public int getRGB() {
        return getDelegate().getRGB();
      }

      @Override
      public float[] getRGBColorComponents(float[] compArray) {
        return getDelegate().getRGBColorComponents(compArray);
      }

      @Override
      public float[] getRGBComponents(float[] compArray) {
        return getDelegate().getRGBComponents(compArray);
      }

      @Override
      public int getTransparency() {
        return getDelegate().getTransparency();
      }
    });

    this.add(button);
    button.getModel().addChangeListener(new ChangeListener() {
      public void stateChanged(ChangeEvent e) {
        button.repaint();
      }
    });

    this.setSize(200100);
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  }

  public static void main(String[] argsthrows Exception {
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        new RolloverForeground().setVisible(true);
      }
    });
  }
}

The most interesting part is the getDelegate method in our foreground color. It returns black or red depending on the rollover state of the model. All the Color methods use this color, delegating the real implementation to it. Screenshot of the application in default state:

default.png

Screenshot of the application in rollover state:

rollover.png

Note that this technique can be easily extended to smoothly animate the foreground color from black to red without triggering property change events along the way.


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Given the above example, wouldn't it be a lot simpler to do this?

    final JButton button = new JButton("sample") {
    {
    getModel().addChangeListener(new ChangeListener() {
    public void stateChanged(ChangeEvent e) {
    setForeground(getModel().isRollover() ? Color.red : Color.black);
    repaint();
    }
    });
    }
    };
    this.add(button);

    Posted by: goron on January 18, 2007 at 05:01 AM

  • Oh, I see, you said you wanted to avoid property change events.... my bad :-)

    Posted by: goron on January 18, 2007 at 05:01 AM

  • ... in which case, how about:

    final JButton button = new JButton("sample") {
    {
    getModel().addChangeListener(new ChangeListener() {
    public void stateChanged(ChangeEvent e) {
    repaint();
    }
    });
    }
    @Override
    public Color getForeground() {
    return getModel().isRollover() ? Color.red : Color.black;
    }
    };

    I'm not a huge fan of masses of boilerplate.

    Posted by: goron on January 18, 2007 at 05:06 AM

  • The last example is good only if you're the one creating the button. For UI delegates, this is not the case, so you can't use the code that you propose. UI delegates replace the current colors (those that do not implement UIResource) on the already existing controls. So, if you want to provide color animations without triggering property change events, the approach outlined in the article still works.

    Posted by: kirillcool on January 18, 2007 at 08:07 AM

  • Ah OK. I was confused by this:

    private Color getDelegate() {
    return button.getModel().isRollover() ? Color.red : Color.black;
    }

    Your specialised color had intimate knowledge of the button in which it was being used.

    So how do you go about setting this up for all JButtons? I understand of role of ComponentUI and how these are registered with the UIManager...
    Sorry if I'm being a bit dense :-)

    Posted by: goron on January 18, 2007 at 10:21 AM

  • The actual code in Substance is (naturally) more complicated than the example in this entry. It has a reference to the button, so it can query the button model and other relevant state.

    Posted by: kirillcool on January 18, 2007 at 10:36 AM

  • Note that this nifty trick breaks the contract that Color objects are immutable, which could lead to problems.

    For example, we only validate the color set to the graphics context once when it's set, and then select the pipeline accordingly. So, if the color changes afterwards (say, to a translucent from opaque), we may not notice it and the results would be bad. It may not be a problem in your case since you get a new Graphics object every time, but there may be other cases.


    I know that the spec doesn't say that Color is immutable explicitly (it probably should), but there's plenty of clues: for instance in comments like "Every color has an implicit alpha value of 1.0 or an explicit one provided in the constructor." which implies that the alpha is supplied in the constructor and that is the alpha for the color, not "the initial alpha".

    The fact that you're allowed to override get* methods is basically an API hole which is unfortunately too late to fix (the reason for those methods not being final has been addressed in jdk7 - it has to do with a special case - the SystemColor class).

    Thanks,
    Dmitri
    Java2D Team

    Posted by: trembovetski on January 19, 2007 at 03:49 PM

  • Dmitri,
    Thanks for the detailed response. Pretty much the only reason i had to resort to this hackery is because of the SwingUtilities2.drawStringUnderlineCharAt (sorry for beating the old horse, but this is indeed the case). Let's say i define a dark brown skin with bright orange rollovers. So, the button text would be white on regular buttons (that have dark brown background) and black on active / rolled over buttons (that have bright orange background). If you take a look at the BasicButtonUI.paintText, it's really very simple -sets the color and calls the method in SwingUtilities2.

    So, the options would be:

    To pull the desktop-AA code in the overriding implementation of paintText (of ButtonUI and other relevant UI delegates) and call BasicGraphicsUtils.drawStringUnderlineCharAt. This would pretty much involve copy-pasting all the logic from the base implementation, inheriting all unfound bugs and blocking off new functionality in subsequent JDK versions.
    Call setForegroundColor on relevant model changes (this gets even more complicated on rollover animations, when you would smoothly change the color from white to black), triggering unnecessary property change events and repaints.


    In my specific case, i don't change the opacity of the color, so hopefully this wouldn't be a problem (and, as you point out, the color changes do not happen on the same Graphics). And may i say that it was a very surprising discovery to find out that Color is not a final class. Your last comment is particularly interesting. Are you saying that in Dolphin you're planning to change the API in such a way that the code above will stop working?

    Thanks
    Kirill

    Posted by: kirillcool on January 19, 2007 at 04:05 PM

  • Are you saying that in Dolphin you're planning to change the API in such a way that the code above will stop working?

    No, unfortunately, we can't, even though we really want to =) We just found a better way to achieve the same effect - it should have been done this way originally. See bugs 6389283, 4559156 for (regrettably few) details.

    Thanks,
    Dmitri

    Posted by: trembovetski on January 19, 2007 at 04:27 PM

  • Dmitri - which effect is that? That the code in this example will not work under b06 of Dolphin? If so, i'll have to find some other workaround :)

    Posted by: kirillcool on January 19, 2007 at 04:44 PM

  • Under "the same effect" I meant that the SystemColor class doesn't need to override .get* methods to achieve what it needs to do.

    Your code won't break.

    Dmitri

    Posted by: trembovetski on January 21, 2007 at 03:51 PM

  • Thanks Dmitri. Most probably i will remove this hack in the next version (for some new features that i'm planning).

    Posted by: kirillcool on January 22, 2007 at 04:36 PM





Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds