JavaFX17 学习手册(十)
十九、理解动画
在本章中,您将学习:
-
JavaFX 里有什么动画
-
关于 JavaFX 中用于在 JavaFX 中执行动画的类
-
如何执行时间轴动画以及如何在时间轴动画上设置提示点
-
如何控制动画,如播放、倒退、暂停和停止
-
如何使用过渡来执行动画
-
关于不同类型的插值器及其在动画中的作用
本章的例子在com.jdojo.animation包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.animation to javafx.graphics, javafx.base;
...
什么是动画?
在现实世界中,动画暗示了某种运动,这是通过快速连续显示图像而生成的。例如,当你看电影时,你正在观看图像,这些图像变化如此之快,以至于你产生了一种运动错觉。
在 JavaFX 中,动画被定义为随时间改变节点的属性。如果改变的属性决定了节点的位置,JavaFX 中的动画将产生电影中的运动错觉。不是所有的动画都必须包含运动;例如,随时间改变形状的fill属性是 JavaFX 中不涉及运动的动画。
为了理解动画是如何执行的,理解一些关键概念是很重要的:
-
时间表
-
关键帧
-
关键字值
-
内插器
动画是在一段时间内完成的。一个时间线表示在给定时刻动画期间与关键帧相关联的时间进程。一个关键帧代表在时间轴上特定时刻被动画化的节点的状态。关键帧有相关的键值。键值表示节点的属性值以及要使用的插值器。
假设你想在十秒钟内从左向右水平移动场景中的一个圆。图 19-1 显示了部分位置的圆。粗水平线表示时间线。实线圆圈表示时间轴上特定时刻的关键帧。与关键帧相关联的键值显示在顶行。例如,第五秒的关键帧的圆形的translateX属性值为 500,在图中显示为 tx=500。
图 19-1
使用时间轴沿水平线制作圆形动画
开发人员提供时间表、关键帧和关键值。在这个例子中,有五个关键帧。如果 JavaFX 在五个相应的时刻只显示五个关键帧,动画看起来会不稳定。为了提供平滑的动画,JavaFX 需要在时间轴上的任意时刻插入圆的位置。也就是说,JavaFX 需要在两个连续提供的关键帧之间创建中间关键帧。JavaFX 在一个插值器的帮助下完成这项工作。默认情况下,它使用一个线性插值器,随着时间线性改变动画的属性。也就是说,如果时间线上的时间超过了 x%,则属性值将是初始目标值和最终目标值之间的 x%。JavaFX 使用插值器创建带虚线轮廓的圆。
了解动画课程
JavaFX 中提供动画的类在javafx.animation包中,除了Duration类在javafx.util包中。图 19-2 显示了大多数动画相关类的类图。
图 19-2
动画中使用的核心类的类图
抽象的Animation类表示一个Animation。它包含所有类型的动画使用的通用属性和方法。
JavaFX 支持两种类型的动画:
-
时间轴动画
-
过渡
在时间轴动画中,创建时间轴并向其添加关键帧。JavaFX 使用插值器创建中间关键帧。Timeline类的一个实例代表一个时间轴动画。这种类型的动画需要更多一点的代码,但它给你更多的控制。
通常执行几种类型的动画(沿路径移动节点,随时间改变节点的不透明度等。).这些类型的动画被称为过渡。它们使用内部时间表来执行。Transition类的一个实例代表一个过渡动画。Transition类的几个子类支持特定类型的转换。例如,FadeTransition类通过随时间改变节点的不透明度来实现渐隐效果动画。您创建一个Transition类的实例(通常是它的一个子类的实例),并为要动画的属性指定初始值和最终值以及动画的持续时间。JavaFX 负责创建时间轴和执行动画。这种类型的动画更容易使用。
有时,您可能希望按顺序或同时执行多个过渡。SequentialTransition和ParallelTransition类分别允许您顺序地和同时地执行一组转换。
了解实用程序类
在讨论 JavaFX 动画的细节之前,我将讨论几个用于实现动画的实用程序类。下面几节将讨论这些类。
了解持续时间类
Duration类在javafx.util包中。它以毫秒、秒、分钟和小时表示持续时间。它是一个不可变的类。一个Duration代表一个动画的每个周期的时间量。一个Duration可以代表一个正的或负的持续时间。
您可以通过三种方式创建一个Duration对象:
-
使用构造器
-
使用工厂方法
-
使用
String格式的持续时间中的valueOf()方法
构造器花费的时间以毫秒为单位:
Duration tenMillis = new Duration(10);
工厂方法为不同的时间单位创建Duration对象。分别是millis()、seconds()、minutes()、hours():
Duration tenMillis = Duration.millis(10);
Duration tenSeconds = Duration.seconds(10);
Duration tenMinutes = Duration.minutes(10);
Duration tenHours = Duration.hours(10);
静态方法valueOf()接受一个包含持续时间的String参数,并返回一个Duration对象。参数的格式为“numberms|s|m|h”,其中number为时间量,ms、s、m、h分别表示milliseconds、seconds、minutes、hours。
Duration tenMillis = Duration.valueOf("10.0ms");
Duration tenMillisNeg = Duration.valueOf("-10.0ms");
您还可以分别使用Duration类的UNKNOWN和INDEFINITE常量来表示一段未知的时间和一段不确定的时间。您可以使用isIndefinite()和isUnknown()方法来检查持续时间是否表示不确定或未知的时间量。该类声明了另外两个常量,ONE和ZERO,分别代表一毫秒和零(无时间)的持续时间。
Duration类提供了几种操作持续时间的方法(将一个持续时间添加到另一个持续时间,将一个持续时间除以一个数,比较两个持续时间,等等。).清单 [19-1 展示了如何使用Duration类。
// DurationTest.java
package com.jdojo.animation;
import javafx.util.Duration;
public class DurationTest {
public static void main(String[] args) {
Duration d1 = Duration.seconds(30.0);
Duration d2 = Duration.minutes(1.5);
Duration d3 = Duration.valueOf("35.25ms");
System.out.println("d1 = " + d1);
System.out.println("d2 = " + d2);
System.out.println("d3 = " + d3);
System.out.println("d1.toMillis() = " + d1.toMillis());
System.out.println("d1.toSeconds() = " + d1.toSeconds());
System.out.println("d1.toMinutes() = " + d1.toMinutes());
System.out.println("d1.toHours() = " + d1.toHours());
System.out.println("Negation of d1 = " + d1.negate());
System.out.println("d1 + d2 = " + d1.add(d2));
System.out.println("d1 / 2.0 = " + d1.divide(2.0));
Duration inf = Duration.millis(1.0/0.0);
Duration unknown = Duration.millis(0.0/0.0);
System.out.println("inf.isIndefinite() = " +
inf.isIndefinite());
System.out.println("unknown.isUnknown() = " +
unknown.isUnknown());
}
}
d1 = 30000.0 ms
d2 = 90000.0 ms
d3 = 35.25 ms
d1.toMillis() = 30000.0
d1.toSeconds() = 30.0
d1.toMinutes() = 0.5
d1.toHours() = 0.008333333333333333
Negation of d1 = -30000.0 ms
d1 + d2 = 120000.0 ms
d1 / 2.0 = 15000.0 ms
inf.isIndefinite() = true
unknown.isUnknown() = true
Listing 19-1Using the Duration Class
了解 KeyValue 类
KeyValue类的一个实例表示一个键值,该键值是在动画过程中针对特定间隔插入的。它概括了三件事:
-
一个目标
-
目标的结束值
-
插值器
目标是一个WritableValue,它将所有 JavaFX 属性限定为一个目标。结束值是时间间隔结束时的目标值。插值器用于计算中间关键帧。
关键帧包含一个或多个关键值,它定义时间轴上的特定点。图 19-3 显示了时间线上的一个间隔。间隔由两个时刻定义:时刻 1 和时刻 2 。两个瞬间都有一个相关的关键帧;每个关键帧包含一个键值。动画可以在时间轴上向前或向后播放。当一个间隔开始时,目标的结束值取自该间隔的结束关键帧的关键值,其插值器用于计算中间关键帧。假设,在图中,动画正向播放,第一个瞬间发生在第二个瞬间之前。从时刻 1 到时刻 2,键值 2 的插值器将用于计算该间隔的关键帧。如果动画是反向进行的,键值 1 的插值器将用于计算从时刻 2 到时刻 1 的中间关键帧。
图 19-3
时间轴上两个瞬间的关键帧
KeyValue类是不可变的。它提供了两个构造器:
-
KeyValue(WritableValue<T> target, T endValue) -
KeyValue(WritableValue<T> target, T endValue, Interpolator interpolator)
Interpolator.LINEAR用作默认插值器,它随时间线性插值动画属性。稍后我将讨论不同类型的插值器。
下面的代码片段创建了一个Text对象和两个KeyValue对象。translateX地产是目标。0 和 100 是目标的最终值。使用默认插值器:
Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0);
下面的代码片段类似于前面显示的代码。它使用Interpolator.EASE_BOTH插值器,在开始和接近结束时减慢动画:
Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0,
Interpolator.EASE_BOTH);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0,
Interpolator.EASE_BOTH);
了解关键帧类
关键帧定义了时间轴上指定点的节点的目标状态。目标状态由与关键帧相关联的关键值来定义。
一个关键帧包含四件事:
-
时间轴上的瞬间
-
一组
KeyValue -
一个名字
-
一个
ActionEvent处理者
时间轴上与关键帧相关联的瞬间由一个Duration定义,它是时间轴上关键帧的偏移量。
KeyValues组定义了关键帧目标的结束值。
一个关键帧可以有一个可选的名称,该名称可以用作一个提示点,以便在动画过程中跳转到它所定义的时刻。Animation类的getCuePoints()方法返回Timeline上提示点的Map。
可选地,您可以将一个ActionEvent处理程序附加到一个KeyFrame上。在动画过程中,当关键帧到达时,就会调用ActionEvent处理程序。
KeyFrame类的一个实例代表一个关键帧。该类提供了几个构造器:
-
KeyFrame(Duration time, EventHandler<ActionEvent> onFinished, KeyValue... values) -
KeyFrame(Duration time, KeyValue... values) -
KeyFrame(Duration time, String name, EventHandler<ActionEvent> onFinished, Collection<KeyValue> values) -
KeyFrame(Duration time, String name, EventHandler<ActionEvent> onFinished, KeyValue... values) -
KeyFrame(Duration time, String name, KeyValue... values)
下面的代码片段创建了两个KeyFrame实例,分别在时间轴上的 0 秒和 3 秒指定了一个Text节点的translateX属性:
Text msg = new Text("JavaFX animation is cool!");
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), 0.0);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), 100.0);
KeyFrame initFrame = new KeyFrame(Duration.ZERO, initKeyValue);
KeyFrame endFrame = new KeyFrame(Duration.seconds(3), endKeyValue);
了解时间轴动画
时间轴动画用于制作节点的任何属性的动画。Timeline类的一个实例代表一个时间轴动画。使用时间轴动画包括以下步骤:
-
构建关键帧。
-
创建一个带有关键帧的
Timeline对象。 -
设置动画属性。
-
使用
play()方法运行动画。
您可以在创建Timeline时或之后添加关键帧。Timeline实例将所有关键帧保存在一个ObservableList<KeyFrame>对象中。getKeyFrames()方法返回列表。您可以随时修改关键帧列表。如果时间轴动画已经在运行,您需要停止并重新启动它,以获得修改后的关键帧列表。
Timeline类包含几个构造器:
-
Timeline() -
Timeline(double targetFramerate) -
Timeline(double targetFramerate, KeyFrame... keyFrames) -
Timeline(KeyFrame... keyFrames)
无参数构造器创建一个没有关键帧的Timeline,动画以最佳速度运行。其他构造器允许您指定动画的目标帧速率(即每秒的帧数)和关键帧。
注意,关键帧添加到Timeline的顺序并不重要。Timeline将根据它们的时间偏移对它们进行排序。
清单 19-2 中的程序启动了一个时间轴动画,该动画从右向左水平滚动文本,直到永远。图 19-4 显示了动画的截图。
图 19-4
使用时间轴动画滚动文本
// ScrollingText.java
// ...find in the book's download area.
Listing 19-2Scrolling Text Using a Timeline Animation
执行动画的逻辑在start()方法中。该方法首先创建一个Text对象,一个带有Text对象的Pane,并为舞台设置一个场景。在展示舞台之后,它设置一个动画。
它获取场景和Text对象的宽度:
double sceneWidth = scene.getWidth();
double msgWidth = msg.getLayoutBounds().getWidth();
创建了两个关键帧:一个用于时间= 0 秒,另一个用于时间= 3 秒。动画使用Text对象的translateX属性来改变其水平位置,使其滚动。在 0 秒时,Text被定位在场景宽度,所以它是不可见的。在三秒钟时,它被放置在场景的左侧,距离等于它的长度,因此它也是不可见的:
KeyValue initKeyValue = new KeyValue(msg.translateXProperty(), sceneWidth);
KeyFrame initFrame = new KeyFrame(Duration.ZERO, initKeyValue);
KeyValue endKeyValue = new KeyValue(msg.translateXProperty(), -1.0 * msgWidth);
KeyFrame endFrame = new KeyFrame(Duration.seconds(3), endKeyValue);
用两个关键帧创建一个Timeline对象:
Timeline timeline = new Timeline(initFrame, endFrame);
默认情况下,动画将只运行一次。也就是说,Text会从右向左滚动一次,动画就会停止。可以设置动画的循环次数,即动画需要运行的次数。通过将循环计数设置为Timeline.INDEFINITE,您可以永远运行动画:
timeline.setCycleCount(Timeline.INDEFINITE);
最后,通过调用play()方法启动动画:
timeline.play();
我们的例子有一个缺陷。当场景的宽度改变时,文本的滚动不会更新其初始水平位置。只要场景宽度发生变化,就可以通过更新初始关键帧来解决这个问题。将以下语句添加到列出 19-2 的start()方法中。它为场景宽度添加了一个ChangeListener,用于更新关键帧并重新启动动画:
scene.widthProperty().addListener( (prop, oldValue , newValue) -> {
KeyValue kv = new KeyValue(msg.translateXProperty(),
scene.getWidth());
KeyFrame kf = new KeyFrame(Duration.ZERO, kv);
timeline.stop();
timeline.getKeyFrames().clear();
timeline.getKeyFrames().addAll(kf, endFrame);
timeline.play();
});
创建一个只有一个关键帧的Timeline动画是可能的。关键帧被视为最后一个关键帧。Timeline使用正在制作动画的WritableValue的当前值合成一个初始关键帧(时间= 0 秒)。为了查看效果,让我们替换该语句
Timeline timeline = new Timeline(initFrame, endFrame);
在清单 19-2 中有如下内容:
Timeline timeline = new Timeline(endFrame);
Timeline将用Text对象的translateX属性的当前值 0.0 创建一个初始关键帧。这一次,Text的滚动方式有所不同。开始滚动时,将Text设置为 0.0,并向左滚动,因此它超出了场景。
控制动画
Animation类包含的属性和方法可以用来以各种方式控制动画。以下部分将解释这些属性和方法,以及如何使用它们来控制动画。
播放动画
Animation类包含四种播放动画的方法:
-
play() -
playFrom(Duration time) -
playFrom(String cuePoint) -
playFromStart()
play()方法从当前位置播放动画。如果动画从未开始或停止,它将从头开始播放。如果动画暂停,它将从暂停的位置播放。在调用play()方法之前,可以使用jumpTo(Duration time)和jumpTo(String cuePoint)方法将动画的当前位置设置为特定的持续时间或提示点。调用play()方法是异步的。动画可能不会立即开始。在动画运行时调用play()方法没有任何效果。
playFrom()方法从指定的持续时间或指定的提示点播放动画。调用这个方法相当于使用jumpTo()方法设置当前位置,然后调用play()方法。
playFromStart()方法从头开始播放动画(持续时间= 0)。
延迟动画的开始
您可以使用delay属性指定动画开始的延迟时间。该值在Duration中指定。默认情况下,它为零毫秒:
Timeline timeline = ...
// Delay the start of the animation by 2 seconds
timeline.setDelay(Duration.seconds(2));
// Play the animation
timeline.play();
停止动画
使用stop()方法停止正在运行的动画。如果动画没有运行,则该方法无效。当调用方法时,动画可能不会立即停止,因为方法是异步执行的。方法将当前位置重置为开始位置。即在stop()之后调用play()将从头开始播放动画:
Timeline timeline = ...
...
timeline.play();
...
timeline.stop();
暂停动画
使用pause()方法暂停动画。当动画未运行时调用此方法没有任何效果。此方法异步执行。当动画暂停时调用play()方法,从当前位置播放动画。如果你想从头开始播放动画,调用playFromStart()方法。
了解动画的状态
动画可以是以下三种状态之一:
-
运转
-
暂停
-
停止
这三种状态由Animation.Status枚举的RUNNING、STOPPED和PAUSED常量表示。您不能直接更改动画的状态。它是通过调用Animation类的方法之一来改变的。该类包含一个只读的status属性,可用于随时了解动画的状态:
Timeline timeline = ...
...
Animation.Status status = timeline.getStatus();
switch(status) {
case RUNNING:
System.out.println("Running");
break;
case STOPPED:
System.out.println("Stopped");
break;
case PAUSED:
System.out.println("Paused");
break;
}
循环播放动画
一个动画可以循环多次,甚至无限循环。cycleCount属性指定动画中的循环数,默认为 1。如果你想无限循环的运行动画,指定Animation.INDEFINITE为cycleCount。cycleCount必须设置为大于零的值。如果在动画运行过程中cycleCount发生变化,动画必须停止并重启以获得新值:
Timeline timeline1 = ...
Timeline1.setCycleCount(Timeline.INDEFINITE); // Run the animation forever
Timeline timeline2 = ...
Timeline2.setCycleCount(2); // Run the animation for two cycles
自动反转动画
默认情况下,动画仅向前运行。例如,我们的滚动文本动画在一个周期内从右向左滚动文本。在下一个循环中,再次从右向左滚动。
使用autoReverse属性,您可以定义动画是否在交替循环中反向执行。默认情况下,它被设置为 false。将其设定为 true 以反转动画的方向:
Timeline timeline = ...
timeline.setAutoReverse(true); // Reverse direction on alternating cycles
如果您更改了autoReverse,您需要停止并重启动画以使新值生效。
附加已完成的操作
当动画结束时,您可以执行一个ActionEvent处理程序。在动画运行时停止动画或终止应用程序将不会执行处理程序。您可以在Animation类的onFinished属性中指定处理程序。下面的代码片段将onFinished属性设置为在标准输出中打印消息的ActionEvent处理程序:
Timeline timeline = ...
timeline.setOnFinished(e -> System.out.print("Animation finished."));
请注意,具有Animation.INDEFINITE循环计数的动画将不会完成,并且将这样的动作附加到动画将永远不会执行。
了解动画的持续时间
动画包含两种类型的持续时间:
-
播放动画一个循环的持续时间
-
播放动画所有循环的持续时间
这些持续时间不是直接设置的。它们是使用动画的其他属性(循环计数、关键帧等)设置的。).
使用关键帧设置一个周期的持续时间。当动画以 1.0 的速率播放时,具有最大持续时间的关键帧决定了一个周期的持续时间。Animation类的只读cycleDuration属性报告一个周期的持续时间。
动画的总持续时间由只读的totalDuration属性报告。等于cycleCount * cycleDuration。如果cycleCount被设置为Animation.INDEFINITE,则totalDuration被报告为Duration.INDEFINITE。
请注意,动画的实际持续时间取决于由rate属性表示的播放速率。因为播放速率可以在动画运行时改变,所以没有简单的方法来计算动画的实际持续时间。
调整动画的速度
Animation类的rate属性指定动画的方向和速度。其值的符号表示方向。数值的大小表示速度。正值表示向前方向的间隙。负值表示反向运动。值 1.0 被认为是正常播放速率,值 2.0 是正常速率的两倍,值 0.50 是正常速率的一半,依此类推。0.0 的rate停止播放。
可以反转正在运行的动画的rate。在这种情况下,动画从当前位置反向播放已经过去的时间。请注意,您不能使用负的rate来启动动画。带有否定rate的动画将不会启动。只有在动画播放一段时间后,您才能将rate改为负值。
Timeline timeline = ...
// Play the animation at double the normal rate
Timeline.setRate(2.0);
...
timeline.play();
...
// Invert the rate of the play
timeline.setRate(-1.0 * timeline.getRate());
只读currentRate属性表示动画播放的当前速率(方向和速度)。rate和currentRate属性的值可能不相等。rate属性表示动画运行时的预期播放速率,而currentRate表示动画的播放速率。当动画停止或暂停时,currentRate值为 0.0。如果动画自动反转方向,currentRate将在反转过程中报告不同的方向;例如,如果rate为 1.0,则currentRate报告正向播放周期为 1.0,反向播放周期为–1.0。
理解提示点
您可以在时间轴上设置提示点。提示点被命名为时间轴上的瞬间。动画可以使用jumpTo(String cuePoint)方法跳转到提示点。一个动画保持一个提示点的ObservableMap<String,Duration>。地图中的关键字是提示点的名称,值是时间轴上相应的持续时间。使用getCuePoints()方法获得提示点地图的参考。
有两种方法可以向时间轴添加提示点:
-
为您添加到时间线的
KeyFrame命名,该时间线在提示点地图中添加提示点 -
将名称-持续时间对添加到由
Animation类的getCuePoints()方法返回的映射中提示每个动画都有两个预定义的提示点:“开始”和“结束”它们设置在动画的开始和结束处。这两个提示点不会出现在由
getCuePoints()方法返回的地图中。
下面的代码片段创建了一个名为“midway”的KeyFrame当它被添加到时间线时,一个名为“中途”的提示点将被自动添加到时间线。你可以使用jumpTo("midway")跳转到这个KeyFrame。
// Create a KeyFrame with name “midway”
KeyValue midKeyValue = ...
KeyFrame midFrame = new KeyFrame(Duration.seconds(5), "midway", midKeyValue);
下面的代码片段将两个提示点直接添加到时间轴的提示点映射中:
Timeline timeline = ...
timeline.getCuePoints().put("3 seconds", Duration.seconds(3));
timeline.getCuePoints().put("7 seconds", Duration.seconds(7));
清单 19-3 中的程序展示了如何在时间线上添加和使用提示点。它添加了一个带有“中途”名称的KeyFrame,该名称自动成为提示点。它将两个提示点“3 秒”和“7 秒”直接添加到提示点地图中。可用提示点列表显示在屏幕左侧的ListView中。一个Text物体以十秒的周期滚动。程序显示如图 19-5 所示的窗口。从列表中选择一个提示点,动画将从该点开始播放。
图 19-5
带有提示点列表的滚动文本
// CuePointTest.java
// ...find in the book's download area.
Listing 19-3Using Cue Points in Animation
理解转变
在前面的部分中,您看到了使用时间轴的动画,其中包括在时间轴上设置关键帧。在所有情况下使用时间轴动画并不容易。考虑在圆形路径中移动节点。创建关键帧和设置时间线以在圆形路径上移动节点并不容易。JavaFX 包含许多类(称为 transitions ),允许您使用预定义的属性来制作节点动画。
所有的转换类都继承自Transition类,而后者又继承自Animation类。Animation类中的所有方法和属性也可用于创建过渡。过渡类负责创建关键帧和设置时间轴。您需要指定节点、动画持续时间以及插值的结束值。特殊的过渡类可用于组合多个动画,这些动画可以按顺序或并行运行。
Transition类包含一个interpolator属性,该属性指定动画期间要使用的插值器。默认情况下,它使用Interpolator.EASE_BOTH,缓慢启动动画,加速,并在接近结束时减速。
了解渐变过渡
FadeTransition类的实例通过在指定的持续时间内逐渐增加或减少节点的opacity来表示节点的淡入或淡出效果。类别会定义下列属性来指定动画:
-
duration -
node -
fromValue -
toValue -
byValue
duration属性指定动画一个周期的duration。
node属性指定了其opacity属性被改变的节点。
fromValue属性指定不透明度的初始值。如果未指定,则使用节点的当前opacity。
toValue属性指定opacity的结束值。对于动画的一个周期,节点的opacity在初始值和toValue之间更新。
byValue属性允许您使用公式以不同的方式指定opacity的结束值
opacity_end_value = opacity_initial_value + byValue
byValue允许您通过增加或减少初始值一个偏移量来设置opacity的结束值。如果同时指定了toValue和byValue,则使用toValue。
假设您想要在动画中将节点的初始和结束不透明度设置在 1.0 和 0.5 之间。您可以通过将fromValue和toValue设置为 1.0 和 0.50 或者将fromValue和byValue设置为 1.0 和–0.50 来实现。
节点的有效opacity值介于 0.0 和 1.0 之间。可以将FadeTransition属性设置为超出范围。该转换负责将实际值箝位在该范围内。
以下代码片段通过在两秒钟内将opacity从 1.0 更改为 0.20,为Rectangle设置淡出动画:
Rectangle rect = new Rectangle(200, 50, Color.RED);
FadeTransition fadeInOut = new FadeTransition(Duration.seconds(2), rect);
fadeInOut.setFromValue(1.0);
fadeInOut.setToValue(.20);
fadeInOut.play();
清单 19-4 中的程序为Rectangle创建了一个无限循环中的淡出和淡入效果。
// FadeTest.java
package com.jdojo.animation;
import javafx.animation.FadeTransition;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import javafx.util.Duration;
public class FadeTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
Rectangle rect = new Rectangle(200, 50, Color.RED);
HBox root = new HBox(rect);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("Fade-in and Fade-out");
stage.show();
// Set up a fade-in and fade-out animation for the
// rectangle
FadeTransition fadeInOut =
new FadeTransition(Duration.seconds(2), rect);
fadeInOut.setFromValue(1.0);
fadeInOut.setToValue(.20);
fadeInOut.setCycleCount(FadeTransition.INDEFINITE);
fadeInOut.setAutoReverse(true);
fadeInOut.play();
}
}
Listing 19-4Creating a Fading Effect Using the FadeTransition Class
了解填充过渡
FillTransition类的实例通过在指定的范围和持续时间之间逐渐过渡形状的fill属性来表示形状的填充过渡。类别会定义下列属性来指定动画:
-
duration -
shape -
fromValue -
toValue
duration属性指定动画一个周期的duration。
shape属性指定了其fill属性被改变的Shape。
fromValue属性指定初始的fill颜色。如果未指定,则使用形状的当前fill。
toValue属性指定fill的结束值。
在动画的一个循环中,形状的fill在初始值和toValue之间更新。Shape类中的fill属性被定义为一个Paint。然而,fromValue和toValue属于Color类型。也就是说,填充过渡适用于两个Color,而不是两个Paint
以下代码片段通过在两秒钟内将fill从蓝紫色更改为天蓝色,为Rectangle设置填充过渡:
FillTransition fillTransition = new FillTransition(Duration.seconds(2), rect);
fillTransition.setFromValue(Color.BLUEVIOLET);
fillTransition.setToValue(Color.AZURE);
fillTransition.play();
清单 19-5 中的程序创建一个填充过渡,在两秒钟内将Rectangle的填充颜色从蓝紫色变为天蓝色,这是一个无限循环。
// FillTest.java
// ...find in the book's download area.
Listing 19-5Creating a Fill Transition Using the FillTransition Class
了解笔画过渡
StrokeTransition类的实例通过在指定的范围和持续时间之间逐渐转换形状的stroke属性来表示形状的笔画转换。描边过渡的工作方式与填充过渡相同,只是它插入形状的stroke属性,而不是fill属性。StrokeTransition类包含与FillTransition类相同的属性。有关更多详细信息,请参考“了解填充过渡”一节。下面的代码片段开始在无限循环中制作Rectangle的stroke动画。stroke在两秒钟的周期内从红色变为蓝色:
Rectangle rect = new Rectangle(200, 50, Color.WHITE);
StrokeTransition strokeTransition =
new StrokeTransition(Duration.seconds(2), rect);
strokeTransition.setFromValue(Color.RED);
strokeTransition.setToValue(Color.BLUE);
strokeTransition.setCycleCount(StrokeTransition.INDEFINITE);
strokeTransition.setAutoReverse(true);
strokeTransition.play();
了解翻译转换
TranslateTransition类的一个实例通过在指定的持续时间内逐渐改变节点的translateX、translateY和translateZ属性来表示节点的平移过渡。类别会定义下列属性来指定动画:
-
duration -
node -
fromX -
fromY -
fromZ -
toX -
toY -
toZ -
byX -
byY -
byZ
duration属性指定动画一个周期的duration。
node属性指定了其translateX、translateY和translateZ属性被改变的节点。
节点的初始位置由(fromX、fromY、fromZ)值定义。如果未指定,则使用节点的当前值(translateX、translateY、translateZ)作为初始位置。
(toX、toY、toZ)值指定结束位置。
(byX、byY、byZ)值允许您使用以下公式指定结束位置:
translateX_end_value = translateX_initial_value + byX
translateY_end_value = translateY_initial_value + byY
translateZ_end_value = translateZ_initial_value + byZ
如果同时指定了(toX、toY、toZ)和(byX、byY、byZ)值,则使用前者。
清单 19-6 中的程序通过在场景的宽度上滚动来为Text对象创建一个无限循环的平移过渡。清单 19-2 中的程序使用一个Timeline对象创建了相同的动画,只是有一点不同。他们使用不同的插值器。默认情况下,基于时间轴的动画使用Interpolator.LINEAR插值器,而基于过渡的动画使用Interpolator.EASE_BOTH插值器。当您运行清单 19-6 中的程序时,文本开始缓慢滚动,而在清单 19-2 中,文本一直以均匀的速度滚动。
// TranslateTest.java
// ...find in the book's download area.
Listing 19-6Creating a Translate Transition Using the TranslateTransition Class
了解旋转过渡
RotateTransition类的实例通过在指定的持续时间内逐渐改变其rotate属性来表示节点的旋转过渡。沿着指定的轴围绕节点的中心执行旋转。类别会定义下列属性来指定动画:
-
duration -
node -
axis -
fromAngle -
toAngle -
byAngle
duration属性指定动画一个周期的duration。
node属性指定了其rotate属性被改变的节点。
axis属性指定旋转轴。如果未指定,则使用node的rotationAxis属性的值,默认为Rotate.Z_AXIS。可能的值有Rotate.X_AXIS、Rotate.Y_AXIS和Rotate.Z_AXIS。
旋转的初始角度由fromAngle属性指定。如果未指定,节点的rotate属性的值将用作初始角度。
toAngle指定结束旋转角度。
byAngle允许您使用以下公式指定末端旋转角度:
rotation_end_value = rotation_initial_value + byAngle
如果同时指定了toAngle和byAngle值,则使用前者。所有角度都以度为单位。零度对应于 3 点钟位置。角度的正值是顺时针测量的。
清单 19-7 中的程序为Rectangle创建一个无限循环的旋转过渡。它交替顺时针和逆时针旋转Rectangle。
// RotateTest.java
// ...find in the book's download area.
Listing 19-7Creating a Rotate Transition Using the RotateTransition Class
了解规模变化
ScaleTransition类的实例通过在指定的持续时间内逐渐改变节点的scaleX、scaleY和scaleZ属性来表示节点的缩放过渡。类别会定义下列属性来指定动画:
-
duration -
node -
fromX -
fromY -
fromZ -
toX -
toY -
toZ -
byX -
byY -
byZ
duration属性指定动画一个周期的duration。
node属性指定了其scaleX、scaleY和scaleZ属性被改变的节点。
节点的初始比例由(fromX、fromY、fromZ)值定义。如果未指定,则使用节点的当前(scaleX、scaleY、scaleZ)值作为初始刻度。
(toX、toY、toZ)值指定结束刻度。
(byX、byY、byZ)值允许您使用以下公式指定结束刻度:
scaleX_end_value = scaleX_initial_value + byX
scaleY_end_value = scaleY_initial_value + byY
scaleZ_end_value = scaleZ_initial_value + byZ
如果同时指定了(toX、toY、toZ)和(byX、byY、byZ)值,则使用前者。
清单 19-8 中的程序通过在两秒钟内将Rectangle的宽度和高度在原始值的 100%和 20%之间改变,为其创建一个无限循环中的缩放过渡。
// ScaleTest.java
// ...find in the book's download area.
Listing 19-8Creating a Scale Transition Using the ScaleTransition Class
理解路径转换
PathTransition类的一个实例通过逐渐改变节点的translateX和translateY属性来表示节点的路径转换,从而在指定的持续时间内沿着路径移动节点。路径由一个Shape的轮廓定义。类别会定义下列属性来指定动画:
-
duration -
node -
path -
orientation
duration属性指定动画一个周期的duration。
node属性指定了其rotate属性被改变的节点。
属性定义了节点移动的路径。是一辆Shape。您可以使用Arc、Circle、Rectangle、Ellipse、Path、SVGPath等作为路径。
移动的节点可以保持相同的垂直位置,或者可以旋转它以保持它在路径上的任何点都垂直于路径的切线。属性指定了节点在路径上的垂直位置。它的值是PathTransition.OrientationType枚举的常量(NONE和ORTHOGONAL_TO_TANGENT)之一。默认为NONE,保持同样的直立姿势。ORTHOGONAL_TO_TANGENT值保持节点在任意点垂直于路径的切线。图 19-6 显示了使用PathTransition沿Circle移动的Rectangle的位置。注意使用ORTHOGONAL_TO_TANGENT方向时Rectangle沿路径旋转的方式。
图 19-6
使用 PathTransition 类的 orientation 属性的效果
您可以使用PathTransition类的属性或在构造器中指定路径转换的持续时间、路径和节点。该类包含以下构造器:
-
PathTransition() -
PathTransition(Duration duration, Shape path) -
PathTransition(Duration duration, Shape path, Node node)
清单 19-9 中的程序为Rectangle创建了一个无限循环中的路径转换。它沿着由Circle轮廓定义的圆形路径移动Rectangle。
// PathTest.java
// ...find in the book's download area.
Listing 19-9Creating a Path Transition Using the PathTransition Class
了解暂停转换
PauseTransition类的一个实例代表一个暂停转换。它导致指定持续时间的延迟。它的用途并不明显。它不能单独使用。通常,它用在连续转场中,在两个转场之间插入暂停。它定义了一个duration属性来指定延迟的持续时间。
当一个转换完成后,如果您想在指定的持续时间后执行一个ActionEvent处理程序,暂停转换也是有用的。你可以通过设置它的onFinished属性来实现,这个属性是在Animation类中定义的。
// Create a pause transition of 400 milliseconds that is the default duration
PauseTransition pt1 = new PauseTransition();
// Change the duration to 10 seconds
pt1.setDuration(Duration.seconds(10));
// Create a pause transition of 5 seconds
PauseTransition pt2 = new PauseTransition(Duration.seconds(5));
如果您更改正在运行的暂停过渡的持续时间,您需要停止并重新启动过渡以获得新的持续时间。当我讨论顺序转换时,你会有一个例子。
了解顺序转换
SequentialTransition类的一个实例代表一个顺序转换。它按顺序执行一系列动画。动画列表可包含基于时间线的动画、基于过渡的动画或两者。
SequentialTransition类包含一个node属性,如果动画没有指定节点,该属性将用作列表中动画的节点。如果所有动画都指定一个节点,则不使用该属性。
一个SequentialTransition在一个ObservableList<Animation>中维护动画。getChildren()方法返回列表的引用。
下面的代码片段创建了一个渐变过渡、一个暂停过渡和一个路径过渡。三个过渡被添加到顺序过渡中。播放连续过渡时,它将按顺序播放渐变过渡、暂停过渡和路径过渡:
FadeTransition fadeTransition = ...
PauseTransition pauseTransition = ...
PathTransition pathTransition = ...
SequentialTransition st = new SequentialTransition();
st.getChildren().addAll(fadeTransition, pauseTransition, pathTransition);
st.play();
Tip
SequentialTransition类包含让你指定动画和节点列表的构造器。
清单 19-10 中的程序创建一个缩放过渡、填充过渡、暂停过渡和路径过渡,它们被添加到顺序过渡中。顺序转换在无限循环中运行。当程序运行时
-
它将矩形放大两倍,然后缩小到原始大小。
-
它将矩形的填充颜色从红色改为蓝色,然后再变回红色。
-
它暂停 200 毫秒,然后在标准输出上打印一条消息。
-
它沿着圆的轮廓移动矩形。
-
前述动画序列被无限重复。
// SequentialTest.java
// ...find in the book's download area.
Listing 19-10Creating a Sequential Transition Using the SequentialTransition Class
理解并行转换
ParallelTransition类的一个实例代表一个并行转换。它同时执行一系列动画。动画列表可包含基于时间线的动画、基于过渡的动画或两者。
ParallelTransition类包含一个node属性,如果动画没有指定节点,该属性将用作列表中动画的节点。如果所有动画都指定一个节点,则不使用该属性。
一个ParallelTransition在一个ObservableList<Animation>中维护动画。getChildren()方法返回列表的引用。
下面的代码片段创建了一个渐变过渡和一个路径过渡。这些过渡被添加到平行过渡中。播放连续过渡时,它将应用淡入淡出效果,同时移动节点:
FadeTransition fadeTransition = ...
PathTransition pathTransition = ...
ParallelTransition pt = new ParallelTransition();
pt.getChildren().addAll(fadeTransition, pathTransition);
pt.play();
Tip
ParallelTransition类包含让你指定动画和节点列表的构造器。
清单 19-11 中的程序创建了一个渐变过渡和一个旋转过渡。它将它们添加到并行转换中。当程序运行时,矩形旋转,同时淡入/淡出。
// ParallelTest.java
// ...find in the book's download area.
Listing 19-11Creating a Parallel Transition Using the ParallelTransition Class
理解插值器
插值器是抽象Interpolator类的一个实例。插值器在动画中起着重要的作用。它的工作是在动画过程中计算中间关键帧的关键值。实现定制插值器很容易。你需要子类化Interpolator类并覆盖它的curve()方法。向curve()方法传递当前间隔经过的时间。时间在 0.0 和 1.0 之间标准化。间隔的开始和结束值分别为 0.0 和 1.0。当间隔时间过去一半时,传递给该方法的值将是 0.50。该方法的返回值指示动画属性的变化部分。
以下插值器称为线性插值器,其curve()方法返回传入的参数值:
Interpolator linearInterpolator = new Interpolator() {
@Override
protected double curve(double timeFraction) {
return timeFraction;
}
};
线性插值器要求动画属性的变化百分比与时间间隔的时间进程相同。
一旦有了自定义插值器,就可以用它来为基于时间轴的动画中的关键帧构造关键值。对于基于过渡的动画,您可以将其用作过渡类的interpolator属性。
动画 API 调用Interpolator的interpolate()方法。如果动画属性是Number的实例,则返回
startValue + (endValue - startValue) * curve(timeFraction)
否则,如果动画属性是Interpolatable的实例,它将插值工作委托给Interpolatable的interpolate()方法。否则,插值器默认为离散插值器,当时间分数为 1.0 时返回 1.0,否则返回 0.0。
JavaFX 提供了一些动画中常用的标准插值器。它们在Interpolator类中作为常量或其静态方法可用:
-
线性插值器
-
离散插值器
-
简易插补器
-
缓出插值器
-
Ease-both 插值器
-
样条插值器
-
切线插值器
了解线性插值器
Interpolator.LINEAR常量代表线性插值器。它随时间线性插值节点的动画属性值。时间间隔内属性的百分比变化与经过时间的百分比相同。
了解分立插值器
Interpolator.DISCRETE常量代表离散插值器。离散插值器从一个关键帧跳到下一个关键帧,不提供中间关键帧。当时间分数为 1.0 时,插值器的curve()方法返回 1.0,否则返回 0.0。也就是说,动画属性值在间隔的整个持续时间内保持其初始值。它会在间隔结束时跳到结束值。清单 19-12 中的程序对所有关键帧使用离散插值器。当你运行这个程序时,它将文本从一个关键帧转移到另一个关键帧。将此示例与使用线性插值器的滚动文本示例进行比较。滚动文本示例平滑地移动了文本,而这个示例在移动中产生了抖动。
// HoppingText.java
// ...find in the book's download area.
Listing 19-12Using a Discrete Interpolator to Animate Hopping Text
了解渐强插值器
Interpolator.EASE_IN常量代表渐强插值器。它在时间间隔的前 20%缓慢启动动画,然后加速。
了解渐出插值器
Interpolator.EASE_OUT常量代表一个渐出插值器。它在 80%的时间间隔内以恒定的速度播放动画,之后速度变慢。
了解 Ease-Both 插值器
Interpolator.EASE_BOTH常量代表一个简单的内插器。在时间间隔的前 20%和后 20%播放动画较慢,否则保持恒定速度。
了解样条插值器
Interpolator.SPLINE(double x1,double y1, double x2, double y2)静态方法返回一个样条插值器。它使用三次样条曲线形状来计算间隔中任意点的动画速度。参数(x1,y1)和(x2,y2)用(0,0)和(1,1)作为隐式锚点来定义三次样条形状的控制点。参数值介于 0.0 和 1.0 之间。
三次样条形状上给定点的斜率定义了该点的加速度。接近水平线的斜率表示减速,而接近垂直线的斜率表示加速。例如,使用(0,0,1,1)作为SPLINE方法的参数创建一个具有恒定速度的插值器,而参数(0.5,0,0.5,1.0)将创建一个在前半段加速而在后半段减速的插值器。更多详情请参考 www.w3.org/TR/SMIL/smil-animation.html#animationNS-OverviewSpline 。
了解切线插值器
Interpolator.TANGENT静态方法返回一个切线插值器,它定义了关键帧前后动画的行为。所有其他插值器在两个关键帧之间插值数据。如果为关键帧指定切线插值器,它将用于插值关键帧前后的数据。动画曲线是根据关键帧之前指定持续时间的切线(称为入切线)和关键帧之后指定持续时间的切线(称为出切线)来定义的。该插值器仅用于基于时间轴的动画,因为它影响两个间隔。
TANGENT静态方法被重载:
-
Interpolator TANGENT(Duration t1, double v1, Duration t2, double v2) -
Interpolator TANGENT(Duration t, double v)
在第一个版本中,参数t1和t2分别是关键帧前后的持续时间。参数v1和v2是正切值和正切值。也就是说,v1是持续时间t1的正切值,v2是持续时间t2的正切值。第二个版本为两对指定了相同的值。
摘要
在 JavaFX 中,动画被定义为随时间改变节点的属性。如果改变的属性决定了节点的位置,那么 JavaFX 中的动画会产生一种运动的错觉。不是所有的动画都必须包含运动;例如,随时间改变一个Shape的fill属性是 JavaFX 中一个不涉及运动的动画。
动画是在一段时间内完成的。一个时间线表示在给定时刻动画期间与关键帧相关联的时间进程。一个关键帧代表在时间轴上特定时刻被动画化的节点的状态。关键帧有相关的键值。键值表示节点的属性值以及要使用的插值器。
时间轴动画用于制作节点的任何属性的动画。Timeline类的一个实例代表一个时间轴动画。使用时间轴动画包括以下步骤:构造关键帧,创建带有关键帧的Timeline对象,设置动画属性,并使用play()方法运行动画。您可以在创建Timeline时或之后添加关键帧。Timeline实例将所有关键帧保存在一个ObservableList<KeyFrame>对象中。getKeyFrames()方法返回列表。您可以随时修改关键帧列表。如果时间轴动画已经在运行,您需要停止并重新启动它,以获得修改后的关键帧列表。
Animation类包含几个属性和方法来控制动画,比如播放、倒退、暂停和停止。
您可以在时间轴上设置提示点。提示点被命名为时间轴上的瞬间。动画可以使用jumpTo(String cuePoint)方法跳转到提示点。
在所有情况下使用时间轴动画并不容易。JavaFX 包含许多类(称为 transitions ),允许您使用预定义的属性来制作节点动画。所有的转换类都继承自Transition类,而后者又继承自Animation类。过渡类负责创建关键帧和设置时间轴。您需要指定节点、动画持续时间以及插值的结束值。特殊的过渡类可用于组合多个动画,这些动画可以按顺序或并行运行。Transition类包含一个interpolator属性,该属性指定在动画过程中使用的插值器。默认情况下,它使用Interpolator.EASE_BOTH,缓慢启动动画,加速,并在接近结束时减速。
插值器是抽象Interpolator类的一个实例。它的工作是在动画过程中计算中间关键帧的关键值。JavaFX 提供了几个内置插值器,如线性、离散、渐进和渐出。您还可以轻松实现自定义插值器。你需要子类化Interpolator类并覆盖它的curve()方法。向curve()方法传递当前间隔经过的时间。时间在 0.0 和 1.0 之间标准化。该方法的返回值指示动画属性的变化部分。
下一章将讨论如何在 JavaFX 应用程序中整合不同类型的图表。
二十、理解图表
在本章中,您将学习:
-
什么是图表
-
JavaFX 中的图表 API 是什么
-
如何使用图表 API 创建不同类型的图表
-
如何用 CSS 设计图表样式
本章的例子在com.jdojo.chart包中。为了让它们工作,您必须在module-info.java文件中添加相应的一行:
...
opens com.jdojo.chart to javafx.graphics, javafx.base;
...
什么是图表?
图表是数据的图形表示。图表为可视化分析大量数据提供了一种更简单的方法。通常,它们用于监控和报告目的。存在不同类型的图表。它们表示数据的方式不同。并非所有类型的图表都适合分析所有类型的数据。例如,折线图适合于了解数据的比较趋势,而条形图适合于比较不同类别的数据。
JavaFX 支持图表,通过编写几行代码就可以将图表集成到 Java 应用程序中。它包含一个全面的、可扩展的图表 API,为几种类型的图表提供内置支持。
了解图表 API
图表 API 由javafx.scene.chart包中许多预定义的类组成。图 20-1 显示了代表不同类型图表的类的类图。
图 20-1
JavaFX 中表示图表的类的类图
抽象Chart是所有图表的基类。它继承了Node类。图表可以添加到场景图中。它们也可以像其他节点一样使用 CSS 样式。我将在讨论特定图表类型的章节中讨论图表样式。Chart类包含所有类型图表通用的属性和方法。
JavaFX 将图表分为两类:
-
没有轴的图表
-
具有 x 轴和 y 轴的图表
PieChart类属于第一类。它没有轴,是用来画饼图的。
XYChart类属于第二类。它是所有有两个轴的图表的抽象基类。它的子类,例如,LineChart,BarChart等。,表示特定类型的图表。
JavaFX 中的每个图表都有三个部分:
-
一个标题
-
传奇
-
内容(或数据)
不同类型的图表对数据的定义不同。Chart类包含以下所有类型图表共有的属性:
-
title -
titleSide -
legend -
legendSide -
legendVisible -
animated
属性指定了图表的标题。属性指定了标题的位置。默认情况下,标题位于图表内容的上方。它的值是Side枚举的常量之一:TOP(默认)、RIGHT、BOTTOM和LEFT。
通常,图表使用不同类型的符号来表示不同类别的数据。图例列出了符号及其说明。legend属性是一个Node,它指定了图表的图例。默认情况下,图例位于图表内容的下方。legendSide属性指定了图例的位置,它是Side枚举的常量之一:TOP、RIGHT、BOTTOM(默认)和LEFT。legendVisible属性指定图例是否可见。默认情况下,它是可见的。
animated属性指定图表内容的变化是否以某种类型的动画显示。默认情况下,这是真的。
用 CSS 设计图表样式
您可以设置所有类型图表的样式。Chart类定义了所有类型图表共有的属性。图表的默认 CSS 样式类名是图表。您可以为 CSS 中的所有图表指定legendSide、legendVisible和titleSide属性,如下所示:
.chart {
-fx-legend-side: top;
-fx-legend-visible: true;
-fx-title-side: bottom;
}
每个图表定义两个子结构:
-
chart-title -
chart-content
chart-title是一个Label,chart-content是一个Pane。以下样式将所有图表的背景色设置为黄色,标题字体设置为 Arial 16px 粗体:
.chart-content {
-fx-background-color: yellow;
}
.chart-title {
-fx-font-family: "Arial";
-fx-font-size: 16px;
-fx-font-weight: bold;
}
图例的默认样式类名称是图表图例。以下样式将图例背景色设置为浅灰色:
.chart-legend {
-fx-background-color: lightgray;
}
每个图例都有两个子结构:
-
chart-legend-item -
chart-legend-item-symbol
chart-legend-item是一个Label,它代表图例中的文本。chart-legend-item-symbol是一个Node,代表标签旁边的符号,默认为圆形。以下样式将图例中标签的字体大小设置为 10px,并将图例符号设置为箭头:
.chart-legend-item {
-fx-font-size: 16px;
}
.chart-legend-item-symbol {
-fx-shape: "M0 -3.5 v7 l 4 -3.5z";
}
Note
本章中的许多例子使用外部资源,如 CSS 文件。您需要确保ResourceUtil类指向resources目录(随书提供的源代码包的一部分)。从 www.apress.com/source-code 下载源码。
图表示例中使用的数据
我将很快讨论不同类型的图表。图表将使用表 20-1 中的数据,其中有世界上一些国家的实际和估计人口。数据摘自联合国在 www.un.org 发表的报告。人口数值已经四舍五入。
表 20-1
世界上一些国家目前和估计的人口(百万)
| |One thousand nine hundred and fifty
|
Two thousand
|
Two thousand and fifty
|
Two thousand one hundred
|
Two thousand one hundred and fifty
|
Two thousand two hundred
|
Two thousand two hundred and fifty
|
Two thousand three hundred
| | --- | --- | --- | --- | --- | --- | --- | --- | --- | | 中国 | Five hundred and fifty-five | One thousand two hundred and seventy-five | One thousand three hundred and ninety-five | One thousand one hundred and eighty-two | One thousand one hundred and forty-nine | One thousand two hundred and one | One thousand two hundred and forty-seven | One thousand two hundred and eighty-five | | 印度 | Three hundred and fifty-eight | One thousand and seventeen | One thousand five hundred and thirty-one | One thousand four hundred and fifty-eight | One thousand three hundred and eight | One thousand three hundred and four | One thousand three hundred and forty-two | One thousand three hundred and seventy-two | | 巴西 | Fifty-four | One hundred and seventy-two | Two hundred and thirty-three | Two hundred and twelve | Two hundred and two | Two hundred and eight | Two hundred and sixteen | Two hundred and twenty-three | | 英国 | Fifty | Fifty-nine | Sixty-six | Sixty-four | Sixty-six | sixty-nine | Seventy-one | Seventy-three | | 美国 | One hundred and fifty-eight | Two hundred and eighty-five | Four hundred and nine | Four hundred and thirty-seven | Four hundred and fifty-three | Four hundred and seventy | Four hundred and eighty-three | Four hundred and ninety-three |
了解饼图
饼图由一个圆组成,该圆被分成不同圆心角的扇形。通常,馅饼是圆形的。扇区也被称为饼图块或饼图片。圆圈中的每个扇形代表某种数量。扇形面积的圆心角与它所代表的量成正比。图 20-2 显示了五个国家在 2000 年的人口饼图。
图 20-2
显示 2000 年五个国家人口的饼图
PieChart类的一个实例代表一个饼图。该类包含两个构造器:
-
PieChart() -
PieChart(ObservableList<PieChart.Data> data)
无参数构造器创建一个没有内容的饼图。您可以稍后使用其data属性添加内容。第二个构造器创建一个以指定数据为内容的饼图。
// Create an empty pie chart
PieChart chart = new PieChart();
饼图中的一个扇区被指定为PieChart.Data类的一个实例。一个切片有一个名称(或标签)和一个饼图值,分别由PieChart.Data类的name和pieValue属性表示。以下语句为饼图创建一个切片。切片名称为“China”,饼图值为 1275:
PieChart.Data chinaSlice = new PieChart.Data("China", 1275);
饼图的内容(所有切片)在ObservableList<PieChart.Data>中指定。下面的代码片段创建了一个ObservableList<PieChart.Data>,并向其中添加了三个饼图切片:
ObservableList<PieChart.Data> chartData = FXCollections.observableArrayList();
chartData.add(new PieChart.Data("China", 1275));
chartData.add(new PieChart.Data("India", 1017));
chartData.add(new PieChart.Data("Brazil", 172));
现在,您可以使用第二个构造器通过指定图表内容来创建饼图:
// Create a pie chart with content
PieChart charts = new PieChart(chartData);
您将使用 2050 年不同国家的人口作为我们所有饼图的数据。清单 20-1 包含一个实用程序类。它的getChartData()方法返回一个PieChart.Data的ObservableList作为饼图的数据。您将在本节的示例中使用这个类。
// PieChartUtil.java
package com.jdojo.chart;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.PieChart;
public class PieChartUtil {
public static ObservableList<PieChart.Data> getChartData() {
ObservableList<PieChart.Data> data =
FXCollections. observableArrayList();
data.add(new PieChart.Data("China", 1275));
data.add(new PieChart.Data("India", 1017));
data.add(new PieChart.Data("Brazil", 172));
data.add(new PieChart.Data("UK", 59));
data.add(new PieChart.Data("USA", 285));
return data;
}
}
Listing 20-1A Utility Class to Generate Data for Pie Charts
PieChart类包含几个属性:
-
data -
startAngle -
clockwise -
labelsVisible -
labelLineLength
data属性指定了ObservableList<PieChart.Data>.中图表的内容
startAngle属性指定第一个饼图扇区开始的角度,以度为单位。默认情况下,它是零度,对应于三点钟的位置。逆时针测量为正startAngle。例如,90 度的startAngle将从 12 点钟位置开始。
clockwise属性指定切片是否从startAngle开始顺时针放置。默认情况下,这是真的。
labelsVisible属性指定切片的标签是否可见。切片的标签显示在切片附近,并放置在切片外部。使用PieChart.Data类的 name 属性指定切片的标签。在图 20-2 中,“中国”、“印度”、“巴西”等。是切片的标签。
标签和切片通过直线连接。属性指定了这些线的长度。其默认值为 20.0 像素。
清单 20-2 中的程序使用饼图显示五个国家在 2000 年的人口。该程序创建一个空的饼图并设置其标题。图例位于左侧。之后,它为图表设置数据。数据是在getChartData()方法中生成的,该方法返回一个ObservableList<PieChart.Data>,其中包含作为饼图扇区标签的国家名称和作为饼图值的人口。程序显示如图 20-2 所示的窗口。
// PieChartTest.java
package com.jdojo.chart;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.geometry.Side;
import javafx.scene.Scene;
import javafx.scene.chart.PieChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class PieChartTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
PieChart chart = new PieChart();
chart.setTitle("Population in 2000");
// Place the legend on the left side
chart.setLegendSide(Side.LEFT);
// Set the data for the chart
ObservableList<PieChart.Data> chartData =
PieChartUtil.getChartData();
chart.setData(chartData);
StackPane root = new StackPane(chart);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("A Pie Chart");
stage.show();
}
}
Listing 20-2Using the PieChart Class to Create a Pie Chart
自定义饼图扇区
每个饼图切片数据由一个Node表示。使用PieChart.Data类的getNode()方法可以获得对Node的引用。将切片添加到饼图时,会创建Node。因此,在将切片添加到图表中之后,必须对代表切片的PieChart.Data调用getNode()方法。否则返回null。清单 20-3 中的程序定制了一个饼图的所有饼图扇区,为它们添加了一个工具提示。工具提示显示切片名称、饼图值和百分比饼图值。addSliceTooltip()方法包含访问切片Nodes和添加工具提示的逻辑。您可以自定义饼图切片来制作动画,让用户使用鼠标将它们从饼图中拖出,等等。见图 20-3 。
图 20-3
显示工具提示及其饼图值和占总饼图百分比的饼图扇区
// PieSliceTest.java
// ...find in the book's download area.
Listing 20-3Adding Tool Tips to Pie Slices
用 CSS 样式化饼图
除了在PieChart类中定义的data属性之外,所有属性都可以使用 CSS 进行样式化,如下所示:
.chart {
-fx-clockwise: false;
-fx-pie-label-visible: true;
-fx-label-line-length: 10;
-fx-start-angle: 90;
}
添加到饼图中的每个饼图扇区都添加了四个样式类:
-
chart-pie -
data<i> -
default-color<j> -
negative
样式类名data<i>中的<i>是切片索引。第一片有阶级data0,第二片data1,第三片data2等等。
样式类名default-color<j>中的<j>是该系列的颜色索引。在饼图中,您可以将每个切片视为一个系列。默认 CSS (Modena.css)定义了八种系列颜色。如果您的饼图扇区超过八个扇区,扇区颜色将会重复。当我在下一节讨论双轴图表时,图表中系列的概念会更加明显。
只有当切片的数据为负时,才会添加负样式类。
如果您希望将样式应用于所有饼图扇区,请为图表饼图样式类名称定义样式。以下样式将为所有饼图切片设置一个带有 2px 背景插入的白色边框。当您设置 2px 插入时,它将在两个切片之间显示更宽的间隙:
.chart-pie {
-fx-border-color: white;
-fx-background-insets: 2;
}
您可以使用以下样式定义饼图扇区的颜色。它只为五个切片定义颜色。超过第六个的切片将使用默认颜色:
.chart-pie.default-color0 {-fx-pie-color: red;}
.chart-pie.default-color1 {-fx-pie-color: green;}
.chart-pie.default-color2 {-fx-pie-color: blue;}
.chart-pie.default-color3 {-fx-pie-color: yellow;}
.chart-pie.default-color4 {-fx-pie-color: tan;}
使用超过八种系列颜色
一个图表中很可能有八个以上的系列(饼图中的切片),并且您不希望这些系列的颜色重复。我们将针对饼图讨论这种技术。但是,它也可以用于双轴图表。
假设您想使用一个显示十个国家人口的饼图。如果您使用此饼图的代码,第九个和第十个扇区的颜色将分别与第一个和第二个扇区的颜色相同。首先,您需要定义第九个和第十个切片的颜色,如清单 20-4 所示。
/* additional_series_colors.css */
.chart-pie.default-color8 {
-fx-pie-color: gold;
}
.chart-pie.default-color9 {
-fx-pie-color: khaki;
}
Listing 20-4Additional Series Colors
饼图扇区和图例符号将被分配样式类名称,如default-color0、default-color2 … default-color7。您需要识别与索引大于 7 的数据项相关联的切片和图例符号的节点,并用新的名称替换它们的default-color<j>样式类名称。例如,对于第九个和第十个切片,样式类名称是default-color0和default-color1,因为颜色系列号被指定为(dataIndex % 8))。你将用default-color9和default-color10来代替它们。
清单 20-5 中的程序显示了如何改变切片和图例符号的颜色。它向饼图添加十个扇区。setSeriesColorStyles()方法替换第九个和第十个切片的切片节点的样式类名称及其相关的图例符号。图 20-4 为饼状图。注意“德国”和“印度尼西亚”的颜色是 CSS 中设置的金色和卡其色。注释start()方法中的最后一条语句,它是对setSeriesColorStyles()的调用,您会发现“德国”和“印度尼西亚”的颜色与“中国”和“印度”的颜色相同
图 20-4
使用超过八种切片颜色的饼图
// PieChartExtraColor.java
// ...find in the book's download area.
Listing 20-5A Pie Chart Using Color Series Up to Index 10
为饼图扇区使用背景图像
您也可以在饼图切片中使用背景图像。以下样式定义了第一个饼图扇区的背景图像:
.chart-pie.data0 {
-fx-background-image: url("china_flag.jpg");
}
清单 20-6 包含一个名为 pie_slice.css 的 CSS 文件的内容。它定义了指定用于饼图扇区的背景图像、图例符号的首选大小以及连接饼图扇区及其标签的线的长度的样式。
// pie_slice.css
/* Set a background image for pie slices */
.chart-pie.data0 {-fx-background-image: url("china_flag.jpg");}
.chart-pie.data1 {-fx-background-image: url("india_flag.jpg");}
.chart-pie.data2 {-fx-background-image: url("brazil_flag.jpg");}
.chart-pie.data3 {-fx-background-image: url("uk_flag.jpg");}
.chart-pie.data4 {-fx-background-image: url("usa_flag.jpg");}
/* Set the preferred size for legend symbols */
.chart-legend-item-symbol {
-fx-pref-width: 100;
-fx-pref-height: 30;
}
.chart {
-fx-label-line-length: 10;
}
Listing 20-6 A CSS for Customizing Pie Slices
清单 20-7 中的程序创建了一个饼图。它使用的数据与您在前面的示例中使用的数据相同。不同的是,它设置了一个在 pie_slice.css 文件中定义的 CSS:
// Set a CSS for the scene
scene.getStylesheets().addAll("resources/css/pie_slice.css");
产生的窗口如图 20-5 所示。请注意,切片和图例符号显示了国家的国旗。请记住,您已经将图表数据的索引和 CSS 文件中的索引进行了匹配,以匹配国家及其国旗,这一点很重要。
图 20-5
使用背景图像作为其切片的饼图
Tip
还可以设置饼图中连接饼图扇区及其标签、饼图扇区标签和图例符号的线条形状的样式。
// PieChartCustomSlice.java
// ...find in the book's download area.
Listing 20-7Using Pie Slices with a Background Image
了解 xy 图表
抽象类XYChart<X,Y>的具体子类的实例定义了一个双轴图表。通用类型参数X和Y分别是沿 x 轴和 y 轴绘制的值的数据类型。
在 xy 图表中表示坐标轴
抽象类Axis<T>的具体子类的一个实例定义了XYChart中的一个轴。图 20-6 显示了代表轴的类的类图。
图 20-6
在 xy 图表中表示轴的类的类图
抽象的Axis<T>类是所有表示轴的类的基类。通用参数T是沿轴绘制的值的类型,例如String、Number等。坐标轴显示刻度和刻度标签。Axis<T>类包含定制刻度和刻度标签的属性。一个轴可以有一个标签,在label属性中指定。
具体的子类CategoryAxis和NumberAxis分别用于绘制沿轴的String和Number数据值。它们包含特定于数据值的属性。例如,NumberAxis继承了ValueAxis<T>的lowerBound和upperBound属性,它们指定了绘制在轴上的数据的下限和上限。默认情况下,坐标轴上的数据范围是根据数据自动确定的。您可以通过将Axis<T>类中的autoRanging属性设置为 false 来关闭这个特性。以下代码片段创建了CategoryAxis和NumberAxis的实例,并设置了它们的标签:
CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");
Tip
使用CategoryAxis沿轴绘制String值,使用NumberAxis沿轴绘制数值。
向 xy 图表添加数据
XYChart中的数据代表由 x 轴和 y 轴定义的 2D 平面中的点。使用 x 和 y 坐标来指定 2D 平面中的点,这两个坐标分别是沿 x 轴和 y 轴的值。XYChart中的数据被指定为命名序列的ObservableList。一个序列由多个数据项组成,这些数据项是 2D 平面上的点。点的呈现方式取决于图表类型。例如,散点图显示一个点的符号,而条形图显示一个点的条形。
嵌套的静态XYChart.Data<X,Y>类的一个实例代表一个系列中的一个数据项。类别会定义下列属性:
-
XValue -
YValue -
extraValue -
node
XValue和YValue分别是数据项沿 x 轴和 y 轴的值。它们的数据类型需要与图表的 x 轴和 y 轴的数据类型相匹配。extraValue是一个Object,可以用来存储数据项的任何附加信息。其用途取决于图表类型。如果图表不使用该值,您可以将它用于任何其他目的:例如,存储数据项的工具提示值。node为图表中的数据项指定要呈现的节点。默认情况下,图表将根据图表类型创建合适的节点。
假设一个XYChart的两个轴都绘制数值。以下代码片段为图表创建了一些数据项。数据项是 1950 年、2000 年和 2050 年的中国人口:
XYChart.Data<Number, Number> data1 = new XYChart.Data<>(1950, 555);
XYChart.Data<Number, Number> data2 = new XYChart.Data<>(2000, 1275);
XYChart.Data<Number, Number> data3 = new XYChart.Data<>(2050, 1395);
嵌套静态XYChart.Series<X,Y>类的一个实例代表一系列数据项。类别会定义下列属性:
-
name -
data -
chart -
node
name是系列的名称。data是XYChart.Data<X,Y>的一个ObservableList。chart是对系列所属图表的只读引用。node是该系列要显示的Node。默认节点是根据图表类型自动创建的。以下代码片段创建一个系列,设置其名称,并向其中添加数据项:
XYChart.Series<Number, Number> seriesChina = new XYChart.Series<>();
seriesChina.setName("China");
seriesChina.getData().addAll(data1, data2, data3);
XYChart类的data属性代表图表的数据。它是一架XYChart.Series级的ObservableList。假设数据系列seriesIndia和seriesUSA存在,以下代码片段为XYChart图表创建并添加数据:
XYChart<Number, Number> chart = ...
chart.getData().addAll(seriesChina, seriesIndia, seriesUSA);
系列数据项的显示方式取决于特定的图表类型。每种图表类型都有区分不同系列的方法。
您将多次重用代表某些国家某些年份人口的同一系列数据项。清单 20-8 有一个实用程序类的代码。该类由两个静态方法组成,它们生成并返回XYChart数据。getCountrySeries()方法返回系列列表,该列表沿 x 轴绘制年份,沿 y 轴绘制相应的人口。getYearSeries()方法返回一个系列列表,该列表沿 x 轴绘制国家,沿 y 轴绘制相应的人口。在后面的章节中,您将调用这些方法来为我们的XYCharts获取数据。
// XYChartDataUtil.java
// ...abbreviated, find the full listing in the book's download area.
package com.jdojo.chart;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.chart.XYChart;
@SuppressWarnings("unchecked")
public class XYChartDataUtil {
public static
ObservableList<XYChart.Series<Number, Number>> getCountrySeries() {
XYChart.Series<Number, Number> seriesChina =
new XYChart.Series<>();
seriesChina.setName("China");
seriesChina.getData().addAll(
new XYChart.Data<>(1950, 555),
new XYChart.Data<>(2000, 1275),
...
);
...
ObservableList<XYChart.Series<Number, Number>> data =
FXCollections.<XYChart.Series<Number, Number>>observableArrayList();
data.addAll(seriesChina, seriesIndia, seriesUSA);
return data;
}
...
}
Listing 20-8A Utility Class to Generate Data Used in XYCharts
了解条形图
条形图将数据项呈现为水平或垂直的矩形条。条形的长度与数据项的值成比例。
BarChart类的一个实例代表一个条形图。在条形图中,一个轴必须是CategoryAxis,另一个轴必须是ValueAxis / NumberAxis。根据CategoryAxis是 x 轴还是 y 轴,垂直或水平绘制条形。
BarChart包含两个属性来控制一个类别中两个条形之间的距离和两个类别之间的距离:
-
barGap -
categoryGap
默认值是barGap的 4 个像素和categoryGap的 10 个像素。
BarChart类包含三个构造器,通过指定轴、数据和两个类别之间的间距来创建条形图:
-
BarChart(Axis<X> xAxis, Axis<Y> yAxis) -
BarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data) -
BarChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data, double categoryGap)
请注意,在创建条形图时,必须至少指定轴。以下代码片段创建了两个轴和一个包含这些轴的条形图:
CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");
// Create a bar chart
BarChart<String, Number> chart = new BarChart<>(xAxis, yAxis);
当类别轴作为 x 轴添加时,图表中的条形将垂直显示。您可以使用其setData()方法用数据填充图表:
// Set the data for the chart
chart.setData(XYChartDataUtil.getYearSeries());
清单 20-9 中的程序显示了如何创建和填充如图 20-7 所示的垂直条形图。
图 20-7
垂直条形图
// VerticalBarChart.java
// ...find in the book's download area.
Listing 20-9Creating a Vertical Bar Chart
清单 20-10 中的程序显示了如何创建和填充如图 20-8 所示的水平条形图。程序需要在XYChart.Series<Number,String>的ObservableList中向图表提供数据。XYChartDataUtil类中的getYearSeries()方法返回XYChart.Series<String,Number>。程序中的getChartData()方法根据需要将系列数据从<String,Number>转换为<Number,String>格式,以创建水平条形图。
图 20-8
水平条形图
// HorizontalBarChart.java
// ...find in the book's download area.
Listing 20-10Creating a Horizontal Bar Chart
Tip
条形图中的每个条形都用一个节点表示。通过向表示数据项的节点添加事件处理程序,用户可以与条形图中的条形进行交互。请参考饼图上的部分,查看为饼图扇区添加工具提示的示例。
用 CSS 设计条形图的样式
默认情况下,BarChart被赋予样式类名称:图表和条形图。
以下样式将所有条形图的barGap和categoryGap属性的默认值设置为 0px 和 20px。同一类别中的条形将彼此相邻放置:
.bar-chart {
-fx-bar-gap: 0;
-fx-category-gap: 20;
}
您可以为每个系列或系列中的每个数据项自定义条形图的外观。BarChart中的每个数据项由一个节点表示。该节点获得五个默认的样式类名称:
-
chart-bar -
series<i> -
data<j> -
default-color<k> -
negative
在series<i>中,<i>是数列指标。例如,第一个系列的样式类名称为series0,第二个系列的样式类名称为series1,依此类推。
在data<j>中,<j>是数据项在一个序列中的索引。例如,每个系列中的第一个数据项的样式类名称为data0,第二个为data1,依此类推。
在default-color<k>中,<k>是系列颜色索引。例如,第一个系列中的每个数据项将获得一个样式类名称default-color0,第二个系列中的每个数据项将获得一个样式类名称default-color1,依此类推。默认的 CS 仅定义八种系列颜色。<k>的值等于(i%8,其中i是序列索引。也就是说,如果条形图中有八个以上的系列,系列颜色将会重复。请参考饼图部分,了解如何对指数大于 8 的系列使用独特的颜色。逻辑将类似于饼图中使用的逻辑,不同之处在于,这一次,您将在一个系列中查找bar-legend-symbol,而不是pie-legend-symbol。
如果数据值为负,则添加negative类。
条形图中的每个图例项都有以下样式类名称:
-
chart-bar -
series<i> -
bar-legend-symbol -
default-color<j>
在series<i>中,<i>是数列指标。在default-color<j>中,<j>是系列的颜色指数。如果系列数超过八个,图例颜色将重复,就像条形颜色一样。
以下样式定义了序列索引为 0、8、16、24 等的所有数据项的条形颜色。,为蓝色:
.chart-bar.default-color0 {
-fx-bar-fill: blue;
}
了解堆积条形图
堆积条形图是条形图的变体。在堆积条形图中,类别中的条形是堆积的。除了条形的位置之外,它的工作方式与条形图相同。
StackedBarChart类的一个实例表示一个堆积条形图。这些条可以水平或垂直放置。如果 x 轴是一个CategoryAxis,则条形垂直放置。否则,它们会水平放置。和BarChart一样,其中一个轴必须是CategoryAxis,另一个必须是ValueAxis / NumberAxis。
StackedBarChart类包含一个categoryGap属性,该属性定义相邻类别中条形之间的间距。默认间隙为 10px。与BarChart类不同,StackedBarChart类不包含barGap属性,因为一个类别中的条总是堆叠的。
StackedBarChart类的构造器类似于BarChart类的构造器。它们允许您指定坐标轴、图表数据和类别间距。
为BarChart和StackedBarChart创建CategoryAxis有一个显著的区别。BarChart从数据中读取类别值,而您必须显式地将所有类别值添加到StackedBarChart的CategoryAxis中:
CategoryAxis xAxis = new CategoryAxis();
xAxis.setLabel("Country");
// Must set the categories in a StackedBarChart explicitly. Otherwise,
// the chart will not show bars.
xAxis.getCategories().addAll("China," "India," "Brazil," "UK," "USA");
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");
StackedBarChart<String, Number> chart = new StackedBarChart<>(xAxis, yAxis);
清单 20-11 中的程序展示了如何创建一个垂直堆积条形图。图表如图 20-9 所示。要创建水平堆积条形图,使用CategoryAxis作为 y 轴。
图 20-9
垂直堆积条形图
// VerticalStackedBarChart.java
// ...find in the book's download area.
Listing 20-11Creating a Vertical Stacked Bar Chart
用 CSS 样式化 StackedBarChart
默认情况下,StackedBarChart被赋予样式类名称:图表和堆积条形图。
以下样式将所有堆积条形图的categoryGap属性的默认值设置为 20px。类别中的条形将彼此相邻放置:
.stacked-bar-chart {
-fx-category-gap: 20;
}
在堆积条形图中,分配给表示条形图和图例项的节点的样式类名称与条形图的名称相同。更多细节请参考章节"用 CSS 样式化条形图"。
了解散点图
条形图将数据项呈现为符号。一个序列中的所有数据项都使用相同的符号。数据项符号的位置由数据项沿 x 轴和 y 轴的值决定。
ScatterChart类的一个实例表示一个散点图。x 轴和 y 轴可以使用任何类型的Axis。该类不定义任何附加属性。它包含允许您通过指定轴和数据来创建散点图的构造器:
-
ScatterChart(Axis<X> xAxis, Axis<Y> yAxis) -
ScatterChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)
回想一下,默认情况下,Axis的autoRanging被设置为 true。如果您在散点图中使用数值,请确保将autoRanging设置为 false。适当设置数值范围以在图表中获得均匀分布的点是很重要的。否则,这些点可能密集地分布在一个小区域内,很难看清图表。
清单 20-12 中的程序展示了如何创建和填充如图 20-10 所示的散点图。两个轴都是数字轴。x 轴是定制的。自动排列被设置为 false 设定合理的下限和上限。刻度单位设置为 50。如果您不自定义这些属性,ScatterChart将自动确定它们,并且图表数据将难以读取:
NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("Year");
xAxis.setAutoRanging(false);
xAxis.setLowerBound(1900);
xAxis.setUpperBound(2300);
xAxis.setTickUnit(50);
图 20-10
散点图
// ScatterChartTest.java
package com.jdojo.chart;
import javafx.application.Application;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.ScatterChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
public class ScatterChartTest extends Application {
public static void main(String[] args) {
Application.launch(args);
}
@Override
public void start(Stage stage) {
NumberAxis xAxis = new NumberAxis();
xAxis.setLabel("Year");
// Customize the x-axis, so points are scattered uniformly
xAxis.setAutoRanging(false);
xAxis.setLowerBound(1900);
xAxis.setUpperBound(2300);
xAxis.setTickUnit(50);
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population (in millions)");
ScatterChart<Number,Number> chart =
new ScatterChart<>(xAxis, yAxis);
chart.setTitle("Population by Year and Country");
// Set the data for the chart
ObservableList<XYChart.Series<Number,Number>> chartData =
XYChartDataUtil.getCountrySeries();
chart.setData(chartData);
StackPane root = new StackPane(chart);
Scene scene = new Scene(root);
stage.setScene(scene);
stage.setTitle("A Scatter Chart");
stage.show();
}
}
Listing 20-12Creating a Scatter Chart
Tip
您可以使用数据项的node属性来指定ScatterChart中的符号。
用 CSS 设计散点图的样式
除了图表之外,ScatterChart没有被赋予任何额外的样式类名称。
您可以自定义每个系列或系列中每个数据项的符号外观。ScatterChart中的每个数据项由一个节点表示。该节点获得五个默认的样式类名称:
-
chart-symbol -
series<i> -
data<j> -
default-color<k> -
negative
关于这些样式类名称中的<i>、<j>和<k>的含义,请参考“用 CSS 样式化条形图”一节。
散点图中的每个图例项都有以下样式类名称:
-
chart-symbol -
series<i> -
data<j> -
default-color<k>
以下样式将第一个系列中的数据项显示为填充蓝色的三角形。注意,仅定义了八个颜色系列。之后,颜色会按照饼图中详细讨论的内容重复出现。
.chart-symbol.default-color0 {
-fx-background-color: blue;
-fx-shape: "M5, 0L10, 5L0, 5z";
}
了解折线图
折线图通过用线段连接数据项来显示一系列数据项。可选地,数据点本身可以由符号表示。您可以将折线图视为散点图,其中的符号由直线段连接成一系列。通常,折线图用于查看一段时间内或一个类别中的数据变化趋势。
LineChart类的一个实例代表一个折线图。该类包含一个createSymbols属性,默认设置为 true。它控制是否为数据点创建符号。将其设置为 false 将只显示连接系列中数据点的直线。
LineChart类包含两个通过指定轴和数据来创建折线图的构造器:
-
LineChart(Axis<X> xAxis, Axis<Y> yAxis) -
LineChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)
清单 20-13 中的程序显示了如何创建和填充如图 20-11 所示的折线图。该程序与使用散点图的程序相同,只是它使用了LineChart类。图表将圆圈显示为数据项的符号。创建折线图后,可以使用以下语句删除符号:
图 20-11
折线图
// LineChartTest.java
// ...find in the book's download area.
Listing 20-13Creating a Line Chart
// Do not create the symbols for the data items
chart.setCreateSymbols(false);
用 CSS 设计折线图的样式
除了图表之外,LineChart没有被赋予任何额外的样式类名称。下面的样式指定LineChart不应该创建符号:
.chart {
-fx-create-symbols: false;
}
LineChart创建一个Path节点来显示连接一个系列的所有数据点的线条。为系列的线条分配以下样式类名称:
-
chart-series-line -
series<i> -
default-color<j>
这里,<i>是系列索引,<j>是系列的颜色索引。
如果createSymbols属性设置为 true,则为每个数据点创建一个符号。每个符号节点都被指定了以下样式类名称:
-
chart-line-symbol -
series<i> -
data<j> -
default-color<k>
这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。
每个系列都分配有一个图例项,该图例项获得以下样式类名称:
-
chart-line-symbol -
series<i> -
default-color<j>
以下样式将系列颜色索引 0 的线条笔触设置为蓝色。该系列的符号也以蓝色显示:
.chart-series-line.default-color0 {
-fx-stroke: blue;
}
.chart-line-symbol.default-color0 {
-fx-background-color: blue, white;
}
了解 BubbleChart
气泡图与散点图非常相似,只是它能够表示一个数据点的三个值。气泡用于表示一个系列中的一个数据项。您可以设置气泡的半径来表示数据点的第三个值。
BubbleChart类的一个实例代表一个气泡图。该类不定义任何新属性。气泡图使用XYChart.Data类的extraValue属性来获取气泡的半径。气泡是一个椭圆,其半径根据轴使用的比例进行缩放。如果 x 轴和 y 轴的比例几乎相等,气泡看起来更像一个圆(或在一个方向上拉伸得更少)。
Tip
默认情况下设置气泡半径,并使用轴的比例因子进行缩放。如果轴的比例因子非常小,您可能看不到气泡。要查看气泡,请将数据项中的extraValue设置为较高的值,或者沿轴使用较高的比例因子。
BubbleChart类定义了两个构造器:
-
BubbleChart(Axis<X> xAxis, Axis<Y> yAxis) -
BubbleChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)
清单 20-14 中的程序展示了如何创建如图 20-12 所示的气泡图。图表数据被传递给setBubbleRadius()方法,该方法将所有数据点的extraValue显式设置为 20px。如果你想用气泡的半径来表示另一个维度的数据,你可以相应地设置extraValue。
图 20-12
气泡图
// BubbleChartTest.java
// ...find in the book's download area.
Listing 20-14Creating a Bubble Chart
用 CSS 样式化 BubbleChart
除了图表之外,BubbleChart没有被赋予任何额外的样式类名称。
您可以为每个系列或系列中的每个数据项自定义气泡的外观。BubbleChart中的每个数据项由一个节点表示。该节点获得四个默认的样式类名称:
-
chart-bubble -
series<i> -
data<j> -
default-color<k>
这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。
每个系列都分配有一个图例项,图例项具有以下样式类名称:
-
chart-bubble -
series<i> -
bubble-legend-symbol -
default-color<k>
这里,<i>和<k>的含义与前面描述的相同。
以下样式将系列颜色索引 0 的填充颜色设置为蓝色。第一个系列中数据项的气泡和图例符号将以蓝色显示。对于系列索引 8、16、24 等,颜色会重复出现。
.chart-bubble.default-color0 {
-fx-bubble-fill: blue;
}
了解面积图
面积图是折线图的变体。它绘制连接系列中所有数据项的线条,此外,还填充线条和 x 轴之间的绘制区域。不同的颜色用于绘制不同系列的区域。
AreaChart的一个实例代表一个面积图。像LineChart类一样,该类包含一个createSymbols属性来控制是否在数据点绘制符号。默认情况下,它被设置为 true。该类包含两个构造器:
-
AreaChart(Axis<X> xAxis, Axis<Y> yAxis) -
AreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)
清单 20-15 中的程序显示了如何创建如图 20-13 所示的面积图。程序中没有什么新东西,除了您已经使用了AreaChart类来创建图表。请注意,一个系列的区域覆盖了前一个系列的区域。
图 20-13
面积图
// AreaChartTest.java
// ...find in the book's download area.
Listing 20-15Creating an Area Chart
用 CSS 样式化面积图
除了图表之外,AreaChart没有被赋予任何额外的样式类名称。下面的样式指定AreaChart不应该创建表示数据点的符号:
.chart {
-fx-create-symbols: false;
}
一个AreaChart中的每个序列由一个包含两个Path节点的Group表示。一个Path代表连接系列中所有数据点的线段,另一个Path代表系列覆盖的区域。代表系列线段的Path节点被赋予以下样式类名称:
-
chart-series-area-line -
series<i> -
default-color<j>
这里,<i>是系列索引,<j>是系列的颜色索引。
代表系列区域的Path节点被赋予以下样式类名称:
-
chart-series-area-fill -
series<i> -
default-color<j>
这里,<i>是系列索引,<j>是系列的颜色索引。
如果createSymbols属性设置为 true,则为每个数据点创建一个符号。每个符号节点都被指定了以下样式类名称:
-
chart-area-symbol -
series<i> -
data<j> -
default-color<k>
这里,<i>是系列索引,<j>是系列内的数据项索引,<k>是系列的颜色索引。
每个系列都分配有一个图例项,该图例项获得以下样式类名称:
-
chart-area-symbol -
series<i> -
area-legend-symbol -
default-color<j>
这里,<i>是系列索引,<j>是系列的颜色索引。
以下样式将系列颜色索引 0 的区域填充颜色设置为不透明度为 20%的蓝色。确保为区域填充设置透明颜色,因为区域在AreaChart中重叠:
.chart-series-area-fill.default-color0 {
-fx-fill: rgba(0, 0, 255, 0.20);
}
以下样式将蓝色设置为系列颜色索引 0 的符号、线段和图例符号的颜色:
/* Data point symbols color */
.chart-area-symbol.default-color0\. {
-fx-background-color: blue, white;
}
/* Series line segment color */
.chart-series-area-line.default-color0 {
-fx-stroke: blue;
}
/* Series legend symbol color */
.area-legend-symbol.default-color0 {
-fx-background-color: blue, white;
}
了解堆叠面积图
堆积面积图是面积图的变体。它通过为每个系列绘制一个区域来绘制数据项。与面积图不同,系列的面积不会重叠;它们堆叠在一起。
StackedAreaChart的一个实例代表一个堆积面积图。像AreaChart类一样,该类包含一个createSymbols属性。该类包含两个构造器:
-
StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis) -
StackedAreaChart(Axis<X> xAxis, Axis<Y> yAxis, ObservableList<XYChart.Series<X,Y>> data)
清单 20-16 中的程序展示了如何创建如图 20-14 所示的堆积面积图。该程序与创建一个AreaChart的程序相同,除了您已经使用了StackedAreaChart类来创建图表。
图 20-14
堆积面积图
// StackedAreaChartTest.java
// ...find in the book's download area.
Listing 20-16Creating a Stacked Area Chart
用 CSS 样式化 StackedAreaChart
设计一个StackedAreaChart和设计一个AreaChart是一样的。更多细节请参考“用 CSS 样式化区域图”一节。
自定义 XYChart 外观
您已经看到了如何应用特定于图表的 CSS 样式来自定义图表的外观。在本节中,您将看到更多定制XYChart图和轴的方法。XYChart类包含几个布尔属性来改变图表绘图的外观:
-
alternativeColumnFillVisible -
alternativeRowFillVisible -
horizontalGridLinesVisible -
verticalGridLinesVisible -
horizontalZeroLineVisible -
verticalZeroLineVisible
图表区分为由列和行组成的网格。穿过 y 轴上的主要刻度绘制水平线,组成行。穿过构成列的 x 轴上的主要记号绘制垂直线。
设置交替行/列填充
alternativeColumnFillVisible和alternativeRowFillVisible控制是否填充网格中的交替列和行。默认情况下,alternativeColumnFillVisible设置为假,alternativeRowFillVisible设置为真。
在撰写本文时,设置alternativeColumnFillVisible和alternativeRowFillVisible属性在 JavaFX 8 中没有任何效果,Java FX 8 默认使用 Modena CSS。有两种解决方案。您可以使用以下语句将 Caspian CSS 用于您的应用程序:
Application.setUserAgentStylesheet(Application.STYLESHEET_CASPIAN);
另一个解决方案是在您的应用程序 CSS 中包含以下样式:
.chart-alternative-column-fill {
-fx-fill: #eeeeee;
-fx-stroke: transparent;
-fx-stroke-width: 0;
}
.chart-alternative-row-fill {
-fx-fill: #eeeeee;
-fx-stroke: transparent;
-fx-stroke-width: 0;
}
这些样式取自里海 CSS。这些样式将 Modena CSS 中的fill和stroke属性设置为null。
显示零线轴
图表的坐标轴不能包含零线。是否包含零线取决于轴所代表的下限和上限。horizontalZeroLineVisible和verticalZeroLineVisible控制零线是否可见。默认情况下,它们是可见的。请注意,轴的零线仅在轴有正负数据要绘制时才可见。如果 y 轴上有负值和正值,则会出现一个额外的水平轴,指示 y 轴上的零值。同样的规则也适用于 x 轴上的值。如果使用轴的下限和上限显式设置轴的范围,则零线的可见性取决于零是否落在该范围内。
显示网格线
horizontalGridLinesVisible和verticalGridLinesVisible指定水平和垂直网格线是否可见。默认情况下,两者都设置为 true。
格式化数字刻度标签
有时,您可能想要格式化显示在数轴上的值。出于不同的原因,您希望格式化数值轴的标签:
-
您想要为记号标签添加前缀或后缀。例如,您可能希望将数字 100 显示为$100 或 100 万。
-
您可能正在提供图表刻度数据,以获得轴的适当刻度值。例如,对于实际值 100,您可能向图表提供 10。在这种情况下,您希望显示标签的实际值 100。
ValueAxis类包含一个tickLabelFormatter属性,它是一个StringConverter,用于格式化刻度标签。默认情况下,数字轴的刻度标签使用默认格式化程序进行格式化。默认格式化程序是静态内部类NumberAxis.DefaultFormatter的一个实例。
在我们的XYChart示例中,您已经将 y 轴的标签设置为“人口(百万)”,以表示该轴上的刻度值以百万为单位。您可以使用标签格式化程序将“M”附加到刻度值,以表示相同的含义。以下代码片段将完成这一任务:
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population");
// Use a formatter for tick labels on y-axis to append
// M (for millions) to the population value
yAxis.setTickLabelFormatter(new StringConverter<Number>() {
@Override
public String toString(Number value) {
// Append M to the value
return Math.round(value.doubleValue()) + "M";
}
@Override
public Number fromString(String value) {
// Strip M from the value
value = value.replaceAll("M", "");
return Double.parseDouble(value);
}
});
NumberAxis.DefaultFormatter更适合为记号标签添加前缀或后缀。该格式化程序与轴的autoRanging属性保持同步。您可以向构造器传递前缀和后缀。下面的代码片段与前面的代码片段实现了相同的功能:
NumberAxis yAxis = new NumberAxis();
yAxis.setLabel("Population");
yAxis.setTickLabelFormatter(new NumberAxis.DefaultFormatter(yAxis, null, "M"));
您可以定制一个Axis的几个视觉方面。更多细节请参考Axis类及其子类的 API 文档。
清单 20-17 中的程序显示了如何定制折线图。图表如图 20-15 所示。它格式化 y 轴上的刻度标签,将“M”追加到标签值。它隐藏网格线并显示替代的列填充。
图 20-15
带有格式化刻度标签和自定义绘图的折线图
// CustomizingCharts.java
// ...find in the book's download area.
Listing 20-17Formatting Tick Labels and Customizing Chart Plot
摘要
图表是数据的图形表示。图表为可视化分析大量数据提供了一种更简单的方法。通常,它们用于报告目的。存在不同类型的图表。它们表示数据的方式不同。并非所有类型的图表都适合分析所有类型的数据。例如,折线图适合于了解数据的比较趋势,而条形图适合于比较不同类别的数据。
JavaFX 支持图表,通过编写几行代码就可以将图表集成到 Java 应用程序中。它包含一个全面的、可扩展的图表 API,为几种类型的图表提供内置支持。图表 API 由javafx.scene.chart包中许多预定义的类组成。这些级别中有几个是Chart、XYChart、PieChart、BarChart和LineChart。
抽象Chart是所有图表的基类。它继承了Node类。图表可以添加到场景图中。它们也可以像其他节点一样使用 CSS 样式。JavaFX 中的每个图表都有三个部分:标题、图例和数据。不同类型的图表对数据的定义不同。Chart类包含处理标题和图例的属性。
图表可以是动画的。Chart类中的animated属性指定图表内容的变化是否以某种类型的动画显示。默认情况下,是true。
饼图由一个圆组成,该圆被分成不同圆心角的扇形。通常,馅饼是圆形的。扇区也被称为饼图或饼图。圆圈中的每个扇形代表某种数量。扇形面积的圆心角与它所代表的量成正比。PieChart类的一个实例代表一个饼图。
条形图将数据项呈现为水平或垂直的矩形条。条形的长度与数据项的值成比例。BarChart类的一个实例代表一个条形图。
堆积条形图是条形图的变体。在堆积条形图中,类别中的条形是堆积的。除了条形的位置之外,它的工作方式与条形图相同。StackedBarChart类的一个实例表示一个堆积条形图。
散点图将数据项呈现为符号。一个序列中的所有数据项都使用相同的符号。数据项符号的位置由数据项沿 x 轴和 y 轴的值决定。ScatterChart类的一个实例表示一个散点图。
折线图通过用线段连接数据项来显示一系列数据项。可选地,数据点本身可以由符号表示。您可以将折线图视为散点图,其中的符号由直线段连接成一系列。通常,折线图用于查看一段时间内或一个类别中的数据变化趋势。LineChart类的一个实例代表一个折线图。
气泡图与散点图非常相似,只是它能够表示一个数据点的三个值。气泡用于表示一个系列中的一个数据项。您可以设置气泡的半径来表示数据点的第三个值。BubbleChart类的一个实例代表一个气泡图。
面积图是折线图的变体。它绘制连接系列中所有数据项的线条,此外,还填充线条和 x 轴之间的绘制区域。不同的颜色用于绘制不同系列的区域。AreaChart的一个实例代表一个面积图。
堆积面积图是面积图的变体。它通过为每个系列绘制一个区域来绘制数据项。与面积图不同,系列的面积不会重叠;它们堆叠在一起。StackedAreaChart的一个实例代表一个堆积面积图。
除了使用 CSS 自定义图表的外观之外,Chart API 还提供了一些属性和方法来自定义图表的外观,例如添加替代的行/列填充、显示零线轴、显示网格线以及格式化数字刻度标签。
下一章将讨论如何使用 Image API 在 JavaFX 中处理图像。