概述

Timeline 控件是 日期线事件线 的容器。它显示在 图形 控件上方,并提供多个用于滚动和缩放的方法,二者都可以带动画或不带动画执行。Timeline 还会跟踪当前时间(参见 TimeTracker)。

Timeline 控件截图

导航

Timeline 用于在时间中导航。它提供了跳转到当前时间或指定时间的方法。也可以请求它显示特定时间单位(“显示天”)或某个时间范围。

  • showNow(), showNow(boolean center) - 更改 Timeline 模型的开始时间,使当前时间(已存储在 TimelineModel 中)显示在 Dateline 的左边缘或正中间。
  • showTime(Instant time), showTime(Instant time, boolean center) - 更改 Timeline 模型的开始时间,使给定时间显示在 Dateline 的左边缘或正中间。
  • showRange(Instant start, Instant end), showRange(Instant start, Duration duration), showRange(TimeInterval range) - 更改 Timeline 模型的开始时间和“每像素毫秒数”值,使给定时间范围在 Dateline 中完全可见。
  • showTemporalUnit(TemporalUnit unit, double width) - 更改 Timeline 模型的开始时间和“每像素毫秒数”值,使给定时间单位用于 Dateline。Dateline 中每个单元格的宽度将等于给定宽度。

需要注意的是,Timeline 与 Dateline 协作时只能尽力满足这些请求,因为它们依赖于 Dateline 模型中是否存在可用的 Dateline 分辨率。

上述方法可以带动画或不带动画执行。该动画可通过两个属性控制:moveAnimatedmoveDuration。Timeline 上提供了这些属性相应的 getter 和 setter 方法。

缩放

Timeline 负责管理所有与缩放相关的内容。用户可以按 + / - 键按指定缩放因子提高或降低缩放级别,也可以按住 SHIFT 键并拖动鼠标,通过“套索”选择一个时间区间。结果会形成一个选中的时间区间,并存储在只读属性 selectedTimeInterval 中。Timeline 会监听该属性的变化,并自动尝试让选中的时间区间占满整个可用宽度,最终触发放大操作。

  • zoomIn(), zoomOut() - 让 Timeline 修改 Timeline 模型,使得到的可见时间范围等于当前时间范围乘以或除以当前缩放因子。
  • zoom(double factor, boolean zoomIn, Instant frozenTime) - 使用给定缩放因子执行缩放操作(放大或缩小)。Timeline 会尝试将给定的“冻结”时间保持在其当前位置。这种行为对于基于捏合手势的缩放非常有用,因为 UI 会向某个特定时间“缩入”。
  • setZoomLassoEnabled(boolean), boolean isZoomLassoEnabled(); - 控制缩放套索是否可用。

与移动操作一样,缩放操作也可以以动画或非动画方式执行。可以使用 zoomAnimatedzoomDuration 这两个属性进行控制。

另一个属性用于微调缩放行为,因为某些应用程序在缩放时希望保持开始时间、结束时间或中心时间不变。为此,应用程序可以设置 zoomMode 属性。该枚举的可能值为 KEEP_START_TIMEKEEP_END_TIMECENTER

滚动

Timeline 支持以两种不同速度向左和向右滚动。

  • scrollLeft(), scrollLeftFast() - 更改 Timeline 模型的开始时间属性,使 Dateline 最终从更早的时间开始。
  • scrollRight(), scrollRightFast() - 更改 Timeline 模型的开始时间属性,使 Dateline 最终从更晚的时间开始。

用户可以通过 +- 键调用这些方法。如果用户同时按下 SHIFT,滚动会以快速模式执行。

TimeTracker

Timeline 控件负责跟踪时间。这意味着它会更新底层 Timeline 模型的 now 属性。Timeline 实现了启动和停止时间跟踪的方法,但 now 的实际更新会委托给时间跟踪器类。

  • startTimeTracking(), stopTimeTracking() - 启动和停止时间跟踪。这些方法会调用 TimeTracker 类上的等效方法。
  • timeTrackerProperty(), setTimeTracker(TimeTracker tracker), TimeTracker getTimeTracker() - 时间跟踪器属性及其 getter 和 setter 方法。默认时间跟踪器(使用系统时间)可以替换为自定义实现。

可见时间区间

两个只读属性用于跟踪 Timeline 显示的最早时间和最晚时间。它们分别称为 visibleStartTimevisibleEndTime,可以使用 getVisibleStartTime()getVisibleEndTime()getVisibleDuration() 方法处理它们。

Timeline 模型

Timeline 控件使用类型为 TimelineModel 的模型。该模型为 Timeline 和 Dateline 提供正常工作所需的最重要参数。Timeline 模型可以针对不同时间单位进行类型化。FlexGanttFX 随附 ChronoUnitTimelineModelSimpleUnitTimelineModel

开始时间与每像素毫秒数

TimelineModel 最重要的两个属性是 startTimemillisPerPixel(MPP)。开始时间决定甘特图中的第一个可见时间,而 Timeline 的当前宽度结合 MPP 值决定最后一个可见时间,从而决定可见时间范围。增大 MPP 值会使 Timeline 显示更大的时间范围,减小该值则会得到更短的时间范围。Timeline 类中用于显示某个时间、滚动到某个时间、缩放到某个范围的方法,都是通过操作这两个变量来实现其目的。下表列出了与这些属性相关的方法:

  • ObjectProperty<Instant> startTimeProperty(); setStartTime(Instant time); Instant getStartTime(); - 存储、设置和获取当前开始时间,即甘特图中的第一个可见时间。最早可能的开始时间可以通过 horizonStartTime 属性限制。
  • DoubleProperty millisPerPixel(); setMillisPerPixel(double mpp); double getMillisPerPixel(); - 存储、设置和获取每像素毫秒数值(mpp)。mpp 的默认值为 24 * 60 * 60 * 1000 / 30,这会使一天的宽度为 30 像素。

当前时间/当前位置

甘特图通常需要标记“当前”时间。该时间可以是系统时间(java.time.Instant.now()),也可以是由应用程序控制的任意值。后者常见于运行某种仿真的软件,其中甘特图用于跟踪仿真时间。为支持这些用例,Timeline 模型定义了名为 now 的属性。

now 的值通常由可通过 Timeline 控制的时间跟踪器更新。

  • ObjectProperty<Instant> nowProperty(); void setNow(Instant now); Instant getNow(); - 存储、设置和获取当前时间。
  • ReadOnlyDoubleProperty nowLocation(); double getNowLocation(); - 存储并获取当前时间的位置。now 位置由模型根据开始时间和每像素毫秒数值计算。该属性是只读属性,因为 now 位置始终依赖于 now 时间的值。只能通过更改 now 本身来更改该位置。

时间与坐标计算

Timeline 模型的主要用途是在时间与位置之间相互转换。为此,模型提供了多个方法:

  • double calculateLocationForTime(Instant); - 返回给定时间对应的 x 坐标。
  • Instant calculateTimeForLocation(double); - 返回给定 x 坐标对应的时间。

范围

排程应用程序通常使用一个范围,该范围由最早时间和最晚时间定义。这些时间可能基于已加载的数据集(活动开始和结束时间的最小/最大计算),也可能基于规划范围(Q1、Q2、Q3、Q4)。设置 horizonStartTimehorizonEndTime 的值可确保用户无法滚动到该范围之外的时间。

最高与最低时间单位

并非所有应用程序都需要某个时间单位类型中的全部可用单位。例如,java.time.temporal.ChronoUnit 定义了从纳秒到千年的单位。highestTemporalUnitlowestTemporalUnit 属性使应用程序能够将单位范围限制为更合理的区间,例如从小时到月。

时间跟踪器

时间跟踪器可用于更新 TimelineModel 的 now 属性。在大多数情况下,“now”时间等同于系统时间,但在仿真软件中可能并非如此。要使用时间跟踪器,只需创建 TimeTracker 实例,并将其 timeProperty() 绑定到 Timeline 模型的 nowProperty()

示例

下面是默认时间跟踪器类的完整代码。

/**
 * Copyright (C) 2014 - 2016 Dirk Lemmermann Software & Consulting (dlsc.com)
 *
 * This file is part of FlexGanttFX.
 */
package com.flexganttfx.view.timeline;

import java.time.Instant;
import java.util.logging.Level;

import com.flexganttfx.core.LoggingDomain;
import com.flexganttfx.model.timeline.TimelineModel;

import javafx.application.Platform;
import javafx.beans.property.ReadOnlyObjectProperty;
import javafx.beans.property.ReadOnlyObjectWrapper;

/**
 * A time tracker can be used to update the property
 * {@link TimelineModel#nowProperty()}. In most cases the time "now" will be
 * equivalent to the system time but in simulations this might not be the case.
 * The time tracker can be used in combination with the {@link TimelineModel} by
 * binding the {@link TimelineModel#nowProperty()} to the
 * {@link TimeTracker#timeProperty()}.
 *
 * @since 1.0
 */
public class TimeTracker extends Thread {

   private boolean running = true;

   private long delay = 1000;

   private boolean stopped;

   /**
    * Constructs a new tracker.
    *
    * @since 1.0
    */
   public TimeTracker() {
      setName("Time Tracker");
      setDaemon(true);
   }

   private final ReadOnlyObjectWrapper<Instant> time = new ReadOnlyObjectWrapper<>(
         this, "time", Instant.now());

   public final ReadOnlyObjectProperty<Instant> timeProperty() {
      return time.getReadOnlyProperty();
   }

   public final Instant getTime() {
      return time.get();
   }

   /**
    * Returns the delay in milliseconds between updates of
    * {@link TimelineModel#nowProperty()}. The default is 1000 millis.
    *
    * @return the default delay between update calls
    * @since 1.0
    */
   public final long getDelay() {
      return delay;
   }

   /**
    * Sets the delay between updates of {@link TimelineModel#nowProperty()}.
    * The default is 1000 millis.
    *
    * @param millis
    *            the new delay
    * @throws IllegalArgumentException
    *             if the delay is zero or smaller
    * @since 1.0
    */
   public final void setDelay(long millis) {
      if (millis <= 0) {
         throw new IllegalArgumentException(
               "delay must be larger than zero but was" + millis); //$NON-NLS-1$
      }

      this.delay = millis;
   }

   /**
    * Starts the tracking of the time.
    *
    * @since 1.0
    */
   public final void startTracking() {
      if (stopped) {
         throw new IllegalStateException(
               "Time tracker has already been stopped and can not be started again.");
      } else {
         running = true;
         start();
      }
   }

   @Override
   public void run() {
      while (running) {
         Platform.runLater(() -> time.set(getNow()));
         try {
            Thread.sleep(delay);
         } catch (InterruptedException e) {
            LoggingDomain.CONFIG.log(Level.WARNING,
                  "problem in update thread", e); //$NON-NLS-1$
         }
      }
   }

   /**
    * Stops the tracking of the time.
    *
    * @since 1.0
    */
   public final void stopTracking() {
      stopped = true;
      running = false;
   }

   /**
    * Override to return the instant that will be set as "now" on the timeline
    * model. The default implementation uses {@link Instant#now()}.
    *
    * @see TimelineModel#setNow(Instant)
    *
    * @return the "now" instant
    */
   protected Instant getNow() {
      return Instant.now();
   }
}