If you are observant, you will not fail to notice that this post also serves to emphasize some of the many reasons that I love, and love working with, Caspian.
Warning #1 This post is not an introduction to writing controls in JavaFX. I will not cover the basics of the Control - Skin - Behavior classes. A quick and simple tutorial on this topic can be found here.
Warning #2 All of the information in this post applies only to JavaFX 1.2.1. One of the dangers of using non-public classes is that they can change significantly from version to version (and they certainly will for JavaFX 1.3). If there is interest, I will update this information after JavaFX 1.3 is released.
Apology #1 I discovered the information I'm about to present here while writing my own controls. None of this should be considered official Caspian documentation since I do not work for Sun and I do not have access to their source code (but hopefully I will when they release the source someday). Everything here is accurate as far as I can tell, but I apologize if I have any of the details wrong. Hopefully someone from Sun will read this and let me know if I've made any mistakes.
Knock-knock Joke #1 (of 100) Knock, knock. Who's there? Yewben. Yewben who? Yewben warned, now on with the post!
Caspian 101 - Colors and Gradients
Caspian controls in JavaFX 1.2.1 all make use of a mixin class calledColorable
. This mixin provides the ubiquitous base
and accent
properties that can be used to style the controls. The base
property specifies the base color of the control while the accent
property specifies the accent color on those controls that support it. ListView, for example, uses the accent
property to specify the color with which it draws the selection highlight in the list. Usage of the Colorable mixin makes Caspian controls incredibly easy to style. Two other colors are generated from the base color: the over color, which is used when the mouse pointer is over the control, and the pressed color, used when a control is (you guessed it) pressed. Caspian also generates five
Paint
objects from the base color: the body paint, the text paint, the border paint, the highlight line paint, and the shadow highlight paint. The screenshot below shows an application that displays these various colors and paints. It has been annotated with lines that show where they are used on a Caspian button (click the image to see a larger version). If you launch the application, you can see that the button is a live control that has been scaled up to four times its normal size. If you hover the mouse cursor over the button or click on it, you will see the control's use of the over and pressed colors as well. Caspian is remarkably clever about generating nice looking controls from a single color.
The
Caspian
class in the com.sun.javafx.scene.control.caspian
package provides easy access to these color-generating capabilities via module level functions: Caspian.getOverColor( base: Color )
Caspian.getPressedColor( base: Color )
Caspian.getBodyPaint( base: Color )
Caspian.getTextPaint( base: Color )
Caspian.getBorderPaint( base: Color )
Caspian.getHighlightLinePaint( base: Color )
Caspian.getShadowHighlightPaint( base: Color )
public class MyControlSkin extends Skin, Colorable { def bodyPaint = bind Caspian.getBodyPaint( base ); def overPaint = bind Caspian.getBodyPaint( Caspian.getOverColor( base ) ); def pressedPaint = bind Caspian.getBodyPaint( Caspian.getPressedColor( base ) ); var mousePressed = false; def background = Rectangle { fill: bind { if (mousePressed) { pressedPaint } else if (background.hover) { overPaint } else { bodyPaint } } stroke: bind Caspian.getBorderPaint( base ) blocksMouse: true onMousePressed: function( me: MouseEvent ) { mousePressed = true; // ... may also need to notify the Behavior ... } onMouseReleased: function( me: MouseEvent ) { mousePressed = false; // ... may also need to notify the Behavior ... } } // ... I'm too modest to show more skin ... }With just a few lines of code you get nice Caspian gradients for your control plus your custom control will respond to the same
base
style property as any core JavaFX control. In this simple example I only changed the body paint in response to mouse events. The core JavaFX controls also tend to update their border and highlight colors in response to hover and pressed events. You can see this when you run the Caspian Colors application above and interact with the button. Caspian 102 - Animated State Transitions
Getting the colors and gradients right is only half the battle. Caspian also uses nice animated transitions when the control changes its state. For instance, when a button is disabled it doesn't just flip to a disabled state, it animates the transition with a smooth fade of the button's opacity. Similar animations occur when a control gets enabled, gains or loses focus, gets hovered over, or gets clicked. Caspian provides a smooth and fluid user experience.This is something I want to support in my own controls since I am a subscriber to the Chet Haas school of thought regarding the positive role that good, subtle animation cues can play in a user interface. Animating all of those state transitions could become a real chore, so it's a good thing that you can cheat and take advantage of the hard work already done by the team at Sun. Allow me to introduce you to the
States
, State
, and StateTransition
classes. All of these little beauties are found in the com.sun.javafx.animation.transition
package. The
State
class allows you to define each state of your control. The class has four properties of interest: id | The name of the state. |
defaultState | Should be set to true if this state is the one in which your control starts. |
active | Should be set to true when this state is active. Normally this will be bound to some property of the control. For example, when defining the "hover" state, you would bind the state's active property to the skin node's hover property. |
priority | An integer value that is used to establish precedence when multiple states are active at the same time. |
StateTransition
class derives from Transition
and allows you to define the animation that will occur when your control moves from one state to another. The properties of interest in this class are id
, fromState
, toState
, and animation
. The fromState
property is a string that allows you to specify the id
of the state you are transitioning from. Likewise for the toState
property. And finally, the
States
class keeps track of your control's states and manages the transitions between them. The following code shows an example of the states and transitions for a custom control. def FAST_TRANSITION = 250ms; def SLOW_TRANSITION = 500ms; public class MyControlSkin extends Skin, Colorable { override var color = base; // color is another property defined in Colorable def over = bind Caspian.getOverColor( base ); def pressed = bind Caspian.getPressedColor( base ); var mousePressed = false; def states = States { states: [ State { id: "disabled", active: bind control.disabled } State { id: "armed", active: bind mousePressed } State { id: "hover", active: bind control.hover } State { id: "enabled", active: bind not control.disabled, defaultState: true } ] transitions: [ StateTransition { id: "Enter-Enabled" toState: "enabled" animation: ParallelTransition { content: [ ColorTransition { colorable: this toValue: base duration: SLOW_TRANSITION } FadeTransition { node: bind node toValue: 1.0 duration: FAST_TRANSITION } ] } } StateTransition { id: "Enter-Hover" toState: "hover" animation: ColorTransition { colorable: this toValue: over duration: FAST_TRANSITION } } StateTransition { id: "Enter-Armed" toState: "armed" animation: ColorTransition { colorable: this toValue: pressed duration: FAST_TRANSITION } } StateTransition { id: "Enter-Disabled" toState: "disabled" animation: FadeTransition { node: bind node toValue: 0.33 duration: FAST_TRANSITION } } ] } // ... Move along, nothing more to see ... }True to the spirit of JavaFX's declarative syntax, all you have to do is declare your states and the transitions between them and the rest is handled for you! You can't ask for easier animations than that.
In addition to those shown above, many controls also have "focused", "focused+hover", and "focused+armed" states defined as well (along with their corresponding transitions). Note that I did not specify any priorities in the
State
declarations above. That is because the order in which the states are declared establishes a default priority. You only need to specify an explicit priority if you want to override this default. Also note that I didn't specify a fromState
in any of my state transitions. I was telling the States
class that it can use that transition when entering the target state from any other state. You only need to specify a fromState
if you want the transition to be used only when entering a state from one other particular state. This code makes use of another Caspian gem, the
ColorTransition
class which is found in the com.sun.javafx.scene.control.caspian
package. It will animate the Colorable
mixin's color
property from one value to another. This is yet another reason to make use of the Colorable
mixin in your code - easy color animations! The Etched Button Control
This control was created because I wanted a close button for the XPane control that looked like it was etched into the background of the title bar. I wanted the etched button to fit in as closely as possible with the other Caspian controls as well. The resulting control is shown in the image below.Clicking on the disable check box will disable both the etched button and the normal Caspian button. You can also compare their over and pressed animations to verify that they are a close match. This control shows that it is possible to match the look and feel of the Caspian controls very closely by simply using some of the classes that are provided with the standard JavaFX runtime.
P.S.
Knock, knock. Who's there? Yewer. Yewer who? Yewer afraid I was going to tell 100 knock-knock jokes weren't you!