Skip to main content

Animating layouts part III - beneath the hood

Posted by kirillcool on September 21, 2006 at 12:10 AM PDT

This is the third part in a series about automatically animated layouts in Swing applications.

  • The first part introduced the TransitionLayout. This part showed animated versions of BorderLayout and FlowLayout.
  • The second part showed the TransitionLayout applied to a (at least partially) real-world image viewer that allows live thumbnail resizing.

This part describes the current implementation. Important note: - this may not be the best solution. It may not work in many cases. Most probably, there's better and more elegant solution which is no more intrusive than the current one. I'd be more than happy to hear about alternatives and discuss them either in the comments section or privately.

So, how does it work? The idea is simple - wrap the original layout manager and delegate all the layout / size computations requests to it. When are the animations made - after the call to the original doLayout. The wrapper layout manager keeps track of bounds of all container subcomponents before and after the call to the doLayout method. In addition, it keeps track of the visibility status between successive calls to the doLayout method. After the original layout manager is done setting the subcomponents bounds, there are four different cases that trigger different transition / animation routes:

  • A subcomponent wasn't visible before and isn't visible now. This is a no-brainer - do nothing.
  • A subcomponent wasn't visible before and is visible now. This means that it needs to "fade in" - a request for fade-in animation is dispatched on the fade tracker. On every fade cycle, the translucency of the component is interpolated (alpha goes from 0.0 to 1.0) and the component is repainted. See below on how the translucency is set and respected.
  • A subcomponent was visible before and is not visible now. This means that it needs to "fade out". The sequence is almost the same as before, but first the component is set back to visible status (otherwise it wouldn't paint itself). After the fade completes, the visibility status is set back to hidden.
  • A subcomponent was visible before and is visible now. First we check if the old bounds and new bounds differ. If not - nothing is done. If the differ, a request for transition / fade animation is dispatched. On each fade cycle, the component bounds is adjusted to account for the movement. In addition, the component translucency is interpolated with reverse parabolic to make it partially transparent during the middle section of the movement. This allows overlapping components to be visible at the same time, producing a visually pleasing effect. Needless to say that on each fade cycle the subcomponent is repainted.

There are two distinct animations being done - movement and fade. The movement is LAF-agnostic and works under Metal or any other core / third-party LAF. The fade part is trickier, since it involves changing the opacity of the Graphics that is used to paint the component (and its children) during the animation cycle (see below for the alternatives that have been considered). The current implementation requires custom support from either LAF or the component itself. In the first case, the LAF delegates need to be augmented as described below - no changes need to be done to the application code. In the second case the application code needs to be changed for proper fade animations.

When the fade animation begins, the entire subcomponent hierarchy is made non-opaque. At each fade cycle, a TransitionLayout.ALPHA client property is set on every component in that hierarchy. The value is a Float object that contains the current alpha for the painting. When the fade animation ends, this property is recursively cleared and the opacity is restored (recursively) to the previous values. All LAF delegates are augmented to introduce the following code in the update method (inherited from the ComponentUI):

  public void __update(Graphics g, JComponent c) {

    super.update(g, c);

  }



  public void update(Graphics g, JComponent c) {

    Graphics2D graphics = (Graphics2Dg;

    Composite old = graphics.getComposite();

    Object alpha = c.getClientProperty(TransitionLayout.ALPHA);

    if (alpha instanceof Float) {

      graphics.setComposite(AlphaComposite.getInstance(

          AlphaComposite.SRC_OVER, ((Floatalpha).floatValue()));

    }

    this.__update(graphics, c);

    graphics.setComposite(old);

  }

If a specific LAF delegate already has update implemented, this method is renamed and called from the new implementation. Otherwise, a forwarding implementation is synthesized (like in the code above). The augmentation process is done using the ASM bytecode manipulation framework in much the same way as described here.

Here are alternatives that have been considered:

  • Using a custom RepaintManager that sets the relevant composite on the getOffscreenBuffer and getVolatileOffscreenBuffer. The first disadvantage is that it would require setting a custom repaint manager in addition to setting a custom layout manager. In addition, this might interfere with the existing custom repaint managers already installed in a client application. Furthermore, this may not be forward-compatible is the underlying implementation of the painting pipeline is changed.
  • Using a glass pane to make the transitions and fades. The first disadvantage is that a client application may already have a glass pane installed. In addition, the entire container would have to be repainted by the glass pane in order to "hide" the final state of visible elements. Furthermore, looking forward to setting the TransitionLayout on multiple containers in the same screen may make the glass pane-based implementation more complex.

As already mentioned earlier, the current implementation may be far from best or most elegant. The two possible alternatives outlined above may prove to be better and more cross-LAF friendly. There might as well be another route that i don't see. You are more than welcome to suggest alternatives.

And now to the shortcomings of the current implementation (and possible stumbling blocks for alternative implementations):

  • The fading only works on "widgetized" LAF delegates. The process of "widgetizing" a LAF is fully-automated using supplied Ant tasks and has been successfully tested on six other third-party LAFs.
  • The application code should be careful in calling revalidate or repaint on the subcomponents before calling doLayout on the container. This will cause flickers since the subcomponent will be momentarily shown in its new location (revalidate eventually causes component repaint).
  • The application code should be careful in overriding various paint methods that may operate on a Graphics that ignores the fade translucency. As much as it's not recommended to provide custom paint logic, sometimes it can't be done in a different way. One option would be to change the application code, querying the TransitionManager.ALPHA client property on the component and setting the relevant AlphaComposite.
  • No fades are done on components that have been removed. At this stage, the only option (without glass pane) would be to temporarily add them back on fade start and remove them on fade end. However, this poses quite significant risk to the application code integrity and the robustness of the custom layout implementations. So - the components that have been removed just disappear.

What now? If you're using Substance, take the latest drop of 3.1dev that contains the TransitionLayout. If not, head right here to take the latest drop of 1.1dev of laf-widget. In order to set the new layout, just call:

    this.putClientProperty(LafWidget.ANIMATION_KIND, AnimationKind.DEBUG);

    TransitionLayoutManager.getInstance().track(this, true);

The first line is optional - it just sets the animation rate to be extra slow, so you'll be able to verify that the transitions and fades are done properly. The boolean parameter in the second line indicates whether the fade animations should be initiated. If you're running under Substance, set it to true. Otherwise you can set it to false to prevent unnecessary CPU use (that will do nothing) or leave it to true.

Try it on your applications and let me know if you find any problems (i'm most certain you will).

Related Topics >>