Chris Oliver's Weblog
- All
- F3
- JavaFX
- Programming
- Research
Simple yet elegant vector user interfaces in JavaFX 1.0
It's very easy to create simple yet elegant custom vector user interface elements in JavaFX 1.0 by means of simple compositions of basic shapes. The above example consists entirely of compositions of simple triangles and (rounded) rectangles, together with some text.
The outer shell is a round rectangle from which two other round rectangles have been "subtracted", one for the control area, and one for the track of the slider. Behind this shape is a semi-transparent round rectangle of the same size. Due to the background color of the scene in the screenshot, you can't really tell, but the result is that you can partially "see through" these areas.
The "play", "back", and "forward", buttons are composed of a single triangle or two "added" together. The "pause" button consists of two rectangles "added" together. Finally, the thumb on the slider is simply a rectangle that's been rotated.
In JavaFX 1.0, you can declaratively compose vector shapes by means of the ShapeSubtract node. Although it's my personal opinion that this API element is poorly named and its member variables (a and b) overly obscure, nevertheless it's good enough to get the job done for now.
The a instance variable of ShapeSubtract takes a list of shapes which will be added together. Its b instance variable takes a list of shapes which will then be subtracted from that. ShapeSubtract is itself a shape and may be used in a larger composition.
Using JavaFX script, it's then very easy to factor such into reusable custom scene graph elements, and to make them interactive and/or animated.
Below is the full source code for the example.
/*
* Main.fx
*
*/
package moviecontrol;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.text.*;
import javafx.scene.*;
import javafx.scene.shape.*;
import javafx.scene.transform.*;
import javafx.scene.paint.*;
import javafx.scene.paint.Color.*;
import javafx.scene.input.*;
import java.lang.Math;
import java.lang.System;
import javafx.animation.*;
def defaultFillColor = Color.color(.8, .8, .8, 1);
def selectedFillColor = WHITE;
class MovieButton extends CustomNode {
// interface
public var action: function():Void;
public var icon: Shape;
public var selectedIcon: Shape;
public var selected: Boolean;
// implementation
var mouseOver: Boolean = bind hover;
var mousePress: Boolean = false;
var fillColor = bind if (mouseOver and mousePress) selectedFillColor else defaultFillColor;
var path = bind
if (selected and selectedIcon != null)
ShapeSubtract { fill: bind fillColor, a: selectedIcon }
else
ShapeSubtract { fill: bind fillColor, a: icon };
override protected function create():Node {
Group {
// center it
translateX: bind -path.boundsInLocal.width / 2;
translateY: bind -path.boundsInLocal.height / 2;
// mouse behavior
onMouseReleased: function(e) {
if (mouseOver) {
if (action != null) action();
selected = not selected;
}
mousePress = false;
}
onMousePressed: function(e) {
mousePress = true;
}
// make an internal scene consisting of the icon shape
// and an invisiable rectangle bounding it (so mouse
// events anywhere within its bounding box are
// accepted
content:
[Rectangle {
height: bind path.boundsInLocal.height;
width: bind path.boundsInLocal.width;
opacity: 0;
fill: Color.BLACK;
},
Group {
content: bind path;
}];
}
}
}
class MovieControl extends CustomNode {
public var back: function():Void;
public var fwd: function():Void;
public var paused: Boolean;
public var loaded: Duration;
public var setPosition: function(pos:Duration):Void;
public var duration: Duration = 0s on replace {
updateAlpha();
}
public var position: Duration = 0s on replace {
updateAlpha();
}
function updateAlpha():Void {
if (duration != null and position != null and duration != 0s) {
positionAlpha = position.toMillis() / duration.toMillis();
}
}
var positionAlpha: Number;
override protected function create():Node {
Group {
translateX: -150;
translateY: -32;
var bg:Rectangle;
content:
[bg = Rectangle { // semi-transparent background
height: 64;
width: 300;
arcHeight: 20;
arcWidth: 20;
fill: Color.color(0, 0, 0, 0.2);
},
ShapeSubtract { // subtract the control area and slider track from the main body
fill: defaultFillColor;
a: Rectangle {
height: 64;
width: 300;
arcHeight: 20;
arcWidth: 20;
}
b:
[Rectangle {
x: 1;
y: 1;
arcHeight: 20;
arcWidth: 20;
width: 298;
height: 48;
},
Rectangle {
x: 50;
y: 50;
height: 13;
width: 200;
arcHeight: 13;
arcWidth: 13;
}]
},
Group { // place the text for the elapsed time and duration
translateY: 52;
var font = Font {size: 11};
content:
[Text {
x: 10;
y: 0;
textOrigin: TextOrigin.TOP
font: font;
fill: BLACK;
content: bind "{%tM position}:{%tS position}";
},
Text {
x: 254;
y: 0;
textOrigin: TextOrigin.TOP
font: font;
fill: BLACK;
content: bind if (duration == null or position == null) "" else "-{%tM duration.sub(position)}:{%tS duration.sub(position)}";
}]
},
Group { // handle the slider thumb
var thumbX: Number = bind positionAlpha * 190;
translateX: bind 51 + thumbX;
translateY: 52.5;
var thumb: Rectangle;
var startX = 0.0;
onMousePressed: function(e) {
startX = thumbX;
}
onMouseDragged: function(e) {
var x = startX + e.dragX;
x = Math.max(Math.min(x, 190), 0);
positionAlpha = x / 190;
if (setPosition != null) { setPosition(position); };
}
content: thumb = Rectangle {
var c = 8.0;
transforms: Transform.rotate(45, c/2, c/2);
height: c;
width: c;
var thumbMousePress = false;
onMousePressed: function(e) {
thumbMousePress = true;
}
onMouseReleased: function(e) {
thumbMousePress = false;
}
fill: bind if (thumbMousePress) selectedFillColor else defaultFillColor;
}
},
Group { // construct the various buttons
translateX: 100;
translateY: 24;
// functions for basic shape elements that
// are composed below
var u = 16.0;
var bar = function() {
Rectangle {
height: u;
width: u/3
}
};
var leftArrow = function() {
Polygon {
points: [0, u/2, u, 0, u, u];
}
};
var rightArrow = function() {
Polygon {
points: [0, 0, u, u/2, 0, u];
}
}
var backIcon = function() {
ShapeSubtract {
a:
[leftArrow(),
ShapeSubtract {
translateX: u;
a: leftArrow()
}]
}
};
var fwdIcon = function() {
ShapeSubtract {
a: [rightArrow(),
ShapeSubtract {
translateX: u;
a: rightArrow()
}];
}
};
var playIcon = function() {
ShapeSubtract {
transforms:
[Transform.scale(1.5, 1.5)];
a: rightArrow();
}
};
var pauseIcon = function() {
ShapeSubtract {
transforms:
[Transform.scale(1.5, 1.5)];
a:
[bar(), ShapeSubtract { translateX: u/2; a: bar()}];
}
};
content:
Group {
var buttons =
[MovieButton {
icon: backIcon();
action: bind back;
},
MovieButton {
icon: pauseIcon();
selected: bind paused with inverse;
selectedIcon: playIcon()
},
MovieButton {
icon: fwdIcon();
action: bind fwd;
}];
content: for (i in buttons)
Group {
translateX: indexof i * 42;
content: i;
}
}
}]
}
}
}
/**
* @author coliver
*/
// As a test simulate playing movies with a timeline
var duration = 5m;
function reset():Void {
simulator.stop();
paused = true;
}
var simulator = Timeline {
keyFrames:
KeyFrame {
time: duration
}
repeatCount: Timeline.INDEFINITE;
};
var paused = true on replace {
if (paused) { simulator.pause() } else { simulator.play() }
}
Stage{
title: "Movie Control"
width: 500
height: 400
scene: Scene{
fill: BLACK;
content: MovieControl {
translateX: 250
translateY: 180
setPosition: function(pos:Duration) {
simulator.time = pos;
}
fwd: reset
back: reset
paused: bind paused with inverse;
duration: bind duration;
position: bind simulator.time with inverse;
}
}
}
Posted at 10:42AM Jan 08, 2009 by Christopher Oliver in JavaFX | Comments[7]
That's really really interesting. The result is very neat and has a "pro" look. Adding or substracting shapes really seems to be very simple.
I'm not a Flash expert, but creating custom components with behavior (and really being able to reuse them) is much more complex in XUL (XBL is not so simple to use and understand). I'm not speaking of Javascript where it is really a nightmare ;-)
I guess that, in JavaFX, merging declarative programming and logic is "natural". I'm sure the ability to bind everything is one of the keys to this simplicity.
Posted by Hervé on January 08, 2009 at 02:02 PM PST #
You've got to get rid of that awful public/private stuff out of the language.
And case sensitivity if you can.
Posted by Cas on January 18, 2009 at 08:33 AM PST #
You've got to get rid of that awful public/private stuff out of the language.
And case sensitivity.
Posted by Cas on January 18, 2009 at 01:40 PM PST #
http://www.Sohbetizm.Net
thank you very much.. very good index.
Posted by çet on May 18, 2009 at 04:15 AM PDT #
http://www.smsmatbaa.com
Posted by matbaa on June 22, 2009 at 10:01 AM PDT #
Thanks for sharing.
Posted by laptop batteries on November 05, 2009 at 01:30 AM PST #
thank you for sharing thin information!
Posted by china mobile phone on November 20, 2009 at 12:27 AM PST #