 |
Animating layouts part III - beneath the hood
Posted by kirillcool on September 21, 2006 at 12:10 AM | Comments (12)
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 = (Graphics2D) g;
Composite old = graphics.getComposite();
Object alpha = c.getClientProperty(TransitionLayout.ALPHA);
if (alpha instanceof Float) {
graphics.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, ((Float) alpha).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).
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
I didn't know that you are using ASM in Substance and Laf-Widget projects. Would you mind to write some info about those projects and their ASM usage that I can publish at ASM's users page?
Posted by: euxx on September 21, 2006 at 12:54 AM
-
Since you change the Composite of the Graphics2D it is important that the users of the LaF/animations know they might have to take that into account. The same happens in SwingX with the JXPanel, when its opacity is set to < 100% (a RepaintManager is used.) If you write a custom component that does use an AlphaComposite, you might have glitches on screen because you would then discard the transparency set by the animation layer. When using SrcOver myself, I used to take that into account in my code by checking the current composite; when it's an AlphaComposite.SrcOver, I would mix the the alpha of the current composite with the alpha of my own composite (alpha1 * alpha2.) This should work with the other AlphaComposites but I am not sure about the result. To work around this problem, Aerith' animated transition just use a glass pane (the glass pane does hide everything that is involved in the transition) and work on a "screenshot" of the application. We used only cross-fade but we had other implementations working (movement for instance.)
Posted by: gfx on September 21, 2006 at 05:37 AM
-
Hello Kirill
That's right, there seem to be no good way to implement components translucency in Swing
I don't like custom RepaintManagers either (but SwingLabs guys use it extensively)
I hope we will add a better API to the next release
Thanks
alexp
Posted by: alexfromsun on September 21, 2006 at 07:22 AM
-
Sorry guys, java.net screwed up my message because I put a "lower than" inside. So, I was saying that if you are not aware of how the LaF/animation layers to its job, you might get intro trouble if you write custom component that do set Composites on the Graphics2D. What I did in several occasions was to check the current Composite and if it was an AlphaComposite, I would mix its alpha value with the alpha value of the composite I wanted to set.
Posted by: gfx on September 21, 2006 at 09:15 AM
-
euxx,
The linked document describes what is done on the bytecode level to change the behaviour of all (40+) UI delegates. This could have been done manually, of course, but then it would be a mess to maintain in Substance and a major pain to adopt in other LAFs. I considered a few alternatives such as dynamic proxies (unfortunately the relevant methods aren't defined in interfaces) and AOP (not as clean as bytecode stuff) and eventually decided to try ASM (since i already toyed with BCEL before). The ASMifierClassVisitor was a definite winner for me (don't know if BCEL has something like this), so after a few days of tweaking it, it now works not only on compiled Substance classes, but also on Looks, Squareness, Liquid, Infonode etc.
Posted by: kirillcool on September 21, 2006 at 09:16 AM
-
Romain,
You are correct - if a component provides custom painting logic, then it would have to be changed to account for the already set SrcOver composite. That's one of the shortcomings of the current implementations. By the way, what did you do when the current composite type was different from the one you wanted to set?
Alex,
I hoped you would come up with a better way as the insider :)
Posted by: kirillcool on September 21, 2006 at 09:19 AM
-
Kirill,
Fortunately, I never had to deal with this problem. That said, since all the AlphaComposite allow you to set an extra opacity that is applied onto the source, I think it could work with pretty much any of them. It's even easier now you can call derive(float) on an AlphaComposite instance.
To avoid this issue, we used a glass pane in the transition framework used in Aerith. It has other shortcomings though.
Posted by: gfx on September 21, 2006 at 10:57 AM
-
Romain - indeed true. Unfortunately i haven't been able to come up with a satisfactory cross-LAF translucency solution as well. Perhaps one of the readers will help...
Posted by: kirillcool on September 21, 2006 at 11:02 AM
-
Well, it's not just cross-laf, but also for custom components. The only solution I can think of to get rid of this issue right now is to paint pictures on top of the frame (saving the glass pane and restoring it is not hard) but the performance might be... herm... creepy :)
Posted by: gfx on September 21, 2006 at 01:36 PM
-
Not only that - i haven't tried this on multiple containers, but it's my goal to make this work in this case. Using glass pane will be harder in this case, since they (the containers) will all have to share (and paint) on the same glass pane. In addition, the components should remain responsive to the mouse events while they are animated (you should be able to click a moving button and have its action listeners invoked), and with glass pane it's problematic.
Posted by: kirillcool on September 21, 2006 at 01:45 PM
-
Well, I think (but I might be wrong) our solution worked with multiple containers but I'm not sure components should still respond to mouse events. It could be problematic too, to let the user click stuff in some particular cases (besides the fact that clicking something in motion is hard.)
Posted by: gfx on September 21, 2006 at 02:08 PM
-
I meant to say that having the same glass pane manage multiple containers is more complex than having glass pane managing one container, especially if you're planning on defining dirty (repaint) regions to make it faster. About the mouse responsiveness - in the original examples (with three buttons) when the button passes under the cursor, it starts the rollover animation process which is consistent with the rollovers in the regular state. In the second example when the images are moving to their new locations, the use may decide to click one of them to open it in a separate frame. When the animation is relatively slow, the application may decide to honor that click. I just don't want to impose even more restrictions on the application code just because of the transitions and animations.
Posted by: kirillcool on September 21, 2006 at 02:16 PM
|