Coverage Report - javafx.animation.Timeline
 
Classes in this File Line Coverage Branch Coverage Complexity
Timeline
95%
164/172
60%
232/384
0
Timeline$1
100%
2/2
N/A
0
Timeline$1TimingTargetAdapter$anon1
81%
17/21
100%
4/4
0
Timeline$2
100%
2/2
N/A
0
Timeline$Intf
N/A
N/A
0
Timeline$KFPair
100%
3/3
50%
2/4
0
Timeline$KFPair$Intf
N/A
N/A
0
Timeline$KFPairList
83%
10/12
50%
15/30
0
Timeline$KFPairList$Intf
N/A
N/A
0
Timeline$SubTimeline
100%
3/3
50%
2/4
0
Timeline$SubTimeline$Intf
N/A
N/A
0
 
 1  
 /*
 2  
  * Copyright 2008 Sun Microsystems, Inc.  All Rights Reserved.
 3  
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 4  
  *
 5  
  * This code is free software; you can redistribute it and/or modify it
 6  
  * under the terms of the GNU General Public License version 2 only, as
 7  
  * published by the Free Software Foundation.  Sun designates this
 8  
  * particular file as subject to the "Classpath" exception as provided
 9  
  * by Sun in the LICENSE file that accompanied this code.
 10  
  *
 11  
  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  
  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  
  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  
  * version 2 for more details (a copy is included in the LICENSE file that
 15  
  * accompanied this code).
 16  
  *
 17  
  * You should have received a copy of the GNU General Public License version
 18  
  * 2 along with this work; if not, write to the Free Software Foundation,
 19  
  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  
  *
 21  
  * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 22  
  * CA 95054 USA or visit www.sun.com if you need additional information or
 23  
  * have any questions.
 24  
  */
 25  
 
 26  
 package javafx.animation;
 27  
 
 28  
 import com.sun.javafx.runtime.Pointer;
 29  
 import com.sun.scenario.animation.Clip;
 30  
 import com.sun.scenario.animation.Interpolators;
 31  
 import com.sun.scenario.animation.TimingTarget;
 32  
 import com.sun.scenario.animation.TimingTargetAdapter;
 33  
 import javafx.lang.Duration;
 34  
 import javafx.lang.Sequences;
 35  
 import java.lang.Object;
 36  
 import java.lang.System;
 37  
 import java.util.ArrayList;
 38  
 import java.lang.System;
 39  
 
 40  
 /**
 41  
  * Represents an animation, defined by one or more {@code KeyFrame}s.
 42  
  */
 43  201
 public class Timeline {
 44  
 
 45  
     /**
 46  
      * Used to specify an animation that repeats indefinitely (until
 47  
      * the {@code stop()} method is called).
 48  
      */
 49  4
     public static attribute INDEFINITE = -1;
 50  
 
 51  
     /**
 52  
      * Defines the number of cycles in this animation.
 53  
      * The {@code repeatCount} may be {@code INDEFINITE}
 54  
      * for animations that repeat indefinitely, but must otherwise be >= 0.
 55  
      * The default value is 1.
 56  
      */
 57  59
     public attribute repeatCount: Number = 1.0;
 58  
 
 59  
     /**
 60  
      * Defines whether this animation reverses direction on alternating
 61  
      * cycles.
 62  
      * If {@code true}, the animation will proceed forward on
 63  
      * the first cycle, then reverses on the second cycle, and so on.
 64  
      * The default value is {@code false}, indicating that the
 65  
      * animation will loop such that each cycle proceeds
 66  
      * forward from the initial {@code KeyFrame}.
 67  
      */
 68  61
     public attribute autoReverse: Boolean = false;
 69  
 
 70  
     /**
 71  
      * Defines whether this animation reverses direction in place
 72  
      * each time {@code start()} is called.  
 73  
      * If {@code true}, the animation will initially proceed forward,
 74  
      * then restarts in place except heading in opposite direction.
 75  
      * The default value is {@code false}, indicating that the
 76  
      * animation will restart from the initial {@code KeyFrame}
 77  
      * each time {@code start()} is called.
 78  
      */
 79  13
     public attribute toggle: Boolean = false on replace {
 80  8
         isReverse = true;
 81  
     };
 82  
 
 83  
     /**
 84  
      * Defines the sequence of {@code KeyFrame}s in this animation.
 85  
      * If a {@code KeyFrame} is not provided for the {@code time==0s}
 86  
      * instant, one will be synthesized using the target values
 87  
      * that are current at the time {@code start()} is called.
 88  
      */
 89  10
     public attribute keyFrames: KeyFrame[] on replace {
 90  5
         invalidate();
 91  
     };
 92  
 
 93  
     /**
 94  
      * Read-only attribute that indicates whether the animation is
 95  
      * currently running.
 96  
      * <p>
 97  
      * This value is initially {@code false}.
 98  
      * It will become {@code true} after {@code start()} has been called,
 99  
      * and then becomes {@code false} again after the animation ends
 100  
      * naturally, or after an explicit call to {@code stop()}.
 101  
      * <p>
 102  
      * Note that {@code running} will remain {@code true} even when
 103  
      * {@code paused==true}.
 104  
      */
 105  44
     public /*controlled*/ attribute running: Boolean = false;
 106  
 
 107  
     /**
 108  
      * Read-only attribute that indicates whether the animation is
 109  
      * currently paused.  
 110  
      * <p>
 111  
      * This value is initially {@code false}.
 112  
      * It will become {@code true} after {@code pause()} has been called
 113  
      * on a running animation, and then becomes {@code false} again after
 114  
      * an explicit call to {@code resume()} or {@code stop()}.
 115  
      * <p>
 116  
      * Note that {@code running} will remain {@code true} even when
 117  
      * {@code paused==true}.
 118  
      */
 119  38
     public /*controlled*/ attribute paused: Boolean = false;
 120  
 
 121  
     // if false, indicates that the internal (optimized) data structure
 122  
     // needs to be rebuilt
 123  39
     private attribute valid = false;
 124  
     function invalidate() {
 125  5
         valid = false;
 126  
     }
 127  
 
 128  
     // duration is inferred from time of last key frame and durations
 129  
     // of any sub-timelines in rebuildTargets()
 130  218
     private attribute duration: Number = -1;
 131  
 
 132  
     function getTotalDur():Number {
 133  
         if (not valid) {
 134  19
             rebuildTargets();
 135  
         }
 136  
         if (duration < 0 or repeatCount < 0) {
 137  19
             return -1;
 138  
         }
 139  19
         return duration * repeatCount;
 140  
     }
 141  
 
 142  
     /**
 143  
      * Starts (or restarts) the animation.
 144  
      * <p>
 145  
      * If {@code toggle==false} and the animation is currently running,
 146  
      * the animation will be restarted from its initial position.
 147  
      * <p>
 148  
      * If {@code toggle==true} and the animation is currently running,
 149  
      * the animation will immediately change direction in place and
 150  
      * continue on in that new direction.  When the animation finishes
 151  
      * in one direction, calling {@code start()} again will restart the
 152  
      * animation in the opposite direction.
 153  
      */
 154  
     public function start() {
 155  
         if (toggle) {
 156  
             // change direction in place
 157  
             if (clip == null) {
 158  4
                 buildClip();
 159  
             }
 160  4
             isReverse = not isReverse;
 161  4
             offsetValid = false;
 162  4
             frameIndex = keyFrames.size() - frameIndex;
 163  
             if (not clip.isRunning()) {
 164  4
             clip.start();
 165  
             }
 166  18
         } else {
 167  
             // stop current clip and restart from beginning
 168  5
             buildClip();
 169  5
             clip.start();
 170  
         }
 171  
     }
 172  
 
 173  
     /**
 174  
      * Stops the animation.  If the animation is not currently running,
 175  
      * this method has no effect.
 176  
      */
 177  
     public function stop() {
 178  0
         clip.stop();
 179  
     }
 180  
 
 181  
     /**
 182  
      * Pauses the animation.  If the animation is not currently running,
 183  
      * this method has no effect.
 184  
      */
 185  
     public function pause() {
 186  0
         clip.pause();
 187  
     }
 188  
 
 189  
     /**
 190  
      * Resumes the animation from a paused state.  If the animation is
 191  
      * not currently running or not currently paused, this method has
 192  
      * no effect.
 193  
      */
 194  
     public function resume() {
 195  0
         clip.resume();
 196  
     }
 197  
 
 198  5
     private function buildClip() {
 199  
         if (clip <> null and clip.isRunning()) {
 200  5
             clip.stop();
 201  
         }
 202  5
         clip = Clip.create(Clip.INDEFINITE, adapter);
 203  5
         clip.setInterpolator(Interpolators.getLinearInstance());
 204  
     }
 205  
 
 206  100
     private attribute clip: Clip;
 207  189
     private attribute sortedFrames: KeyFrame[];
 208  194
     private attribute targets: ArrayList = new ArrayList();
 209  64
     private attribute subtimelines: ArrayList = new ArrayList();
 210  25
     private attribute adapter: TimingTarget = createAdapter();
 211  
 
 212  104
     private attribute cycleIndex: Integer = 0;
 213  260
     private attribute frameIndex: Integer = 0;
 214  
 
 215  66
     private attribute isReverse: Boolean = true;
 216  41
     private attribute offsetT: Number = 0;
 217  48
     private attribute lastElapsed: Number = 0;
 218  45
     private attribute offsetValid: Boolean = false;
 219  
 
 220  
     //
 221  
     // Need to revalidate everything (call rebuildTargets() again) if
 222  
     // any of the following change after construction:
 223  
     //   - Timeline.keyFrames (insert, delete, or replace)
 224  
     //   - KeyFrame.time (any)
 225  
     //   - KeyValue.target (any)
 226  
     //
 227  
     // The following should be safe to change at any time:
 228  
     //   - Timeline.repeatCount
 229  
     //   - Timeline.autoReverse
 230  
     //   - Timeline.toggle
 231  
     //   - KeyValue.value
 232  
     //   - KeyValue.interpolate
 233  
     //
 234  5
     private function rebuildTargets():Void {
 235  5
         targets.clear();
 236  5
         subtimelines.clear();
 237  5
         duration = 0;
 238  
         if (sizeof keyFrames == 0) {
 239  5
             return;
 240  
         }
 241  
 
 242  5
         sortedFrames = Sequences.sort(keyFrames) as KeyFrame[];
 243  
 
 244  10
         var zeroFrame:KeyFrame;
 245  5
         if (sortedFrames[0].time == 0s) {
 246  3
             zeroFrame = sortedFrames[0];
 247  
         } else {
 248  7
             zeroFrame = KeyFrame { time: 0s };
 249  
         }
 250  
 
 251  5
         for (keyFrame in keyFrames) {
 252  
             if (duration >= 0) {
 253  30
                 duration = java.lang.Math.max(duration, keyFrame.time.millis);
 254  
             }
 255  
 
 256  
             if (keyFrame.timelines <> null) {
 257  30
                 for (timeline in keyFrame.timelines) {
 258  2
                     var subDur = timeline.getTotalDur();
 259  
                     if (duration >= 0 and subDur >= 0) {
 260  1
                         duration = java.lang.Math.max(duration, keyFrame.time.millis + subDur);
 261  
                     } else {
 262  1
                         duration = -1;
 263  
                     }
 264  3
                     var sub = SubTimeline {
 265  1
                         startTime: keyFrame.time
 266  1
                         timeline: timeline
 267  
                     }
 268  1
                     subtimelines.add(sub);
 269  
                 }
 270  
             }
 271  
 
 272  30
             for (keyValue in keyFrame.values) {
 273  
                 // TODO: targets should really be Map<Pointer,List<KFPair>>
 274  54
                 var pairlist: KFPairList;
 275  27
                 for (i in [0..<targets.size()]) {
 276  24
                     var pl = targets.get(i) as KFPairList;
 277  24
                     if (pl.target == keyValue.target) {
 278  
                         // already have a KFPairList for this target
 279  24
                         pairlist = pl;
 280  24
                         break;
 281  
                     }
 282  
                 }
 283  27
                 if (pairlist == null) {
 284  6
                     pairlist = KFPairList { 
 285  3
                         target: keyValue.target 
 286  
                     }
 287  3
                     if (keyFrame.time <> 0s) {
 288  
                         // get current value and attach it to zero frame
 289  3
                         var kv = KeyValue {
 290  1
                             target: keyValue.target;
 291  1
                             value: keyValue.target.get();
 292  
                         }
 293  3
                         var kfp = KFPair {
 294  1
                             value: kv
 295  1
                             frame: zeroFrame
 296  
                         }
 297  1
                         pairlist.add(kfp);
 298  
                     }
 299  3
                     targets.add(pairlist);
 300  
                 }
 301  81
                 var kfpair = KFPair {
 302  27
                     frame: keyFrame
 303  27
                     value: keyValue
 304  
                 }
 305  27
                 pairlist.add(kfpair);
 306  
             }
 307  
         }
 308  
 
 309  5
         valid = true;
 310  
     }
 311  
 
 312  15
     function process(totalElapsed:Number):Void {
 313  
         // 1. calculate totalDur
 314  
         // 2. modify totalElapsed depending on direction
 315  
         // 3. clamp totalElapsed and set needsStop if necessary
 316  
         // 4. calculate curT and cycle based on totalElapsed
 317  
         // 5. decide whether to increment or decrement cycle/frame index, depending on direction
 318  
         // 6. visit key frames
 319  
         // 7. do interpolation between active key frames
 320  
         // 8. visit subtimelines
 321  
         // 9. stop clip if needsStop
 322  
 
 323  30
         var needsStop = false;
 324  30
         var totalDur = getTotalDur();
 325  
 
 326  15
         if (totalDur >= 0) {
 327  15
             if (toggle) {
 328  8
                 if (not offsetValid) {
 329  
                     if (isReverse) {
 330  2
                         offsetT = totalElapsed + lastElapsed;
 331  
                     } else {
 332  6
                         offsetT = totalElapsed - lastElapsed;
 333  
                     }
 334  4
                     offsetValid = true;
 335  
                 }
 336  
 
 337  
                 // adjust totalElapsed to account for direction (the
 338  
                 // incoming totalElapsed value will continue to increase
 339  
                 // monotonically regardless of how many times the direction
 340  
                 // has been reversed, so here we just massage it back into
 341  
                 // the range [0,totalDur] so that other calculations below
 342  
                 // will work as usual)
 343  
                 if (isReverse) {
 344  4
                     totalElapsed = offsetT - totalElapsed;
 345  
                 } else {
 346  12
                     totalElapsed = totalElapsed - offsetT;
 347  
                 }
 348  
             }
 349  
 
 350  
             // process one last pulse to ensure targets reach their end values
 351  
             if (toggle and isReverse) {
 352  4
                 if (totalElapsed <= 0) {
 353  2
                     totalElapsed = 0;
 354  2
                     needsStop = true;
 355  
                 }
 356  
             } else {
 357  26
                 if (totalElapsed >= totalDur) {
 358  8
                     totalElapsed = totalDur;
 359  8
                     needsStop = true;
 360  
                 }
 361  
             }
 362  
 
 363  
             // capture last adjusted totalElapsed value (used in toggle case)
 364  15
             lastElapsed = totalElapsed;
 365  
         }
 366  
 
 367  30
         var curT:Number;
 368  30
         var cycle:Integer;
 369  30
         var backward = false;
 370  
         if (duration < 0) {
 371  
             // indefinite duration (e.g. will occur when a sub-timeline
 372  
             // has indefinite repeatCount); always stay on zero cycle
 373  0
             curT = totalElapsed;
 374  0
             cycle = 0;
 375  15
         } else {
 376  15
             curT = totalElapsed % duration;
 377  15
             cycle = totalElapsed / duration as Integer;
 378  15
             if (curT == 0 and totalElapsed <> 0) {
 379  
                 // we're at the end, or exactly on a cycle boundary;
 380  
                 // treat this as the "1.0" case of the previous cycle
 381  
                 // instead of the "0.0" case of the current cycle
 382  
                 // TODO: there's probably a better way to deal with this...
 383  10
                 curT = duration;
 384  10
                 cycle -= 1;
 385  
             }
 386  
             if (autoReverse) {
 387  15
                 if (cycle % 2 == 1) {
 388  0
                     curT = duration - curT;
 389  0
                     backward = true;
 390  
                 }
 391  
             }
 392  
         }
 393  
 
 394  
         // look through each KeyFrame and see if we need to visit its
 395  
         // key values and its action function
 396  
         if (toggle and isReverse) {
 397  4
             backward = not backward;
 398  8
             while (cycleIndex > cycle) {
 399  
                 // we're on a new cycle; visit any key frames that we may
 400  
                 // have missed along the way
 401  4
                 visitCycle(cycleIndex, cycleIndex > cycle+1);
 402  4
                 cycleIndex--;
 403  
             }
 404  
         } else {
 405  34
             while (cycleIndex < cycle) {
 406  
                 // we're on a new cycle; visit any key frames that we may
 407  
                 // have missed along the way
 408  8
                 visitCycle(cycleIndex, cycleIndex < cycle-1);
 409  8
                 cycleIndex++;
 410  
             }
 411  
         }
 412  15
         visitFrames(curT, backward, false);
 413  
 
 414  
         // now handle the active interval for each target
 415  15
         for (i in [0..<targets.size()]) {
 416  26
             var pairlist = targets.get(i) as KFPairList;
 417  26
             var kfpair1 = pairlist.get(0);
 418  26
             var leftT = kfpair1.frame.time.millis;
 419  
 
 420  
             if (curT < leftT) {
 421  
                 // haven't yet reached the first key frame
 422  
                 // for this target
 423  13
                 continue;
 424  
             }
 425  
 
 426  26
             var v1:KeyValue;
 427  26
             var v2:KeyValue;
 428  26
             var segT = 0.0;
 429  
 
 430  13
             for (j in [1..<pairlist.size()]) {
 431  
                 // find keyframes on either side of the curT value
 432  90
                 var kfpair2 = pairlist.get(j);
 433  90
                 var rightT = kfpair2.frame.time.millis;
 434  45
                 if (curT <= rightT) {
 435  13
                     v1 = kfpair1.value;
 436  13
                     v2 = kfpair2.value;
 437  13
                     segT = (curT - leftT) / (rightT - leftT);
 438  13
                     break;
 439  
                 }
 440  
 
 441  32
                 kfpair1 = kfpair2;
 442  32
                 leftT = kfpair1.frame.time.millis;
 443  
             }
 444  
 
 445  
             if (v1 <> null and v2 <> null) {
 446  13
                 pairlist.target.set(v2.interpolate.interpolate(v1.value, v2.value, segT));
 447  
             } 
 448  
         }
 449  
 
 450  
         // look through all sub-timelines and recursively call process()
 451  
         // on any active SubTimeline objects
 452  15
         for (i in [0..<subtimelines.size()]) {
 453  1
             var sub = subtimelines.get(i) as SubTimeline;
 454  1
             if (curT >= sub.startTime.millis) {
 455  1
                 var subDur = sub.timeline.getTotalDur();
 456  
                 if (subDur < 0 or curT <= sub.startTime.millis + subDur) {
 457  1
                     sub.timeline.process(curT - sub.startTime.millis);
 458  
                 }
 459  
             }
 460  
         }
 461  
 
 462  
         if (needsStop and clip <> null) {
 463  15
             clip.stop();
 464  
         }
 465  
     }
 466  
 
 467  
     private function visitCycle(cycle:Integer, catchingUp:Boolean) {
 468  24
         var cycleBackward = false;
 469  
         if (autoReverse) {
 470  
             if (cycle % 2 == 1) {
 471  12
                 cycleBackward = true;
 472  
             }
 473  
         }
 474  
         if (toggle and isReverse) {
 475  12
             cycleBackward = not cycleBackward;
 476  
         }
 477  24
         var cycleT = if (cycleBackward) 0 else duration;
 478  12
         visitFrames(cycleT, cycleBackward, catchingUp);
 479  
         // avoid repeated visits to terminals in autoReverse case
 480  12
         frameIndex = if (autoReverse) 1 else 0;
 481  
     }
 482  
 
 483  
     private function visitFrames(curT:Number, backward:Boolean, catchingUp:Boolean) {
 484  
         if (backward) {
 485  18
             var i1 = sortedFrames.size()-1-frameIndex;
 486  18
             var i2 = 0;
 487  9
             for (fi in [i1..i2 step -1]) {
 488  27
                 var kf = sortedFrames[fi];
 489  
                 if (curT <= kf.time.millis) {
 490  
                     if (not (catchingUp and kf.canSkip)) {
 491  25
                         kf.visit();
 492  
                     }
 493  25
                     frameIndex++;
 494  
                 } else {
 495  52
                     break;
 496  
                 }
 497  
             }
 498  54
         } else {
 499  36
             var i1 = frameIndex;
 500  36
             var i2 = sortedFrames.size()-1;
 501  18
             for (fi in [i1..i2]) {
 502  70
                 var kf = sortedFrames[fi];
 503  
                 if (curT >= kf.time.millis) {
 504  
                     if (not (catchingUp and kf.canSkip)) {
 505  67
                         kf.visit();
 506  
                     }
 507  67
                     frameIndex++;
 508  
                 } else {
 509  137
                     break;
 510  
                 }
 511  
             }
 512  
         }
 513  
     }
 514  
 
 515  0
     private function createAdapter():TimingTarget {
 516  127
         TimingTargetAdapter {
 517  32
             public function begin() : Void {
 518  9
                 running = true;
 519  9
                 paused = false;
 520  
 
 521  
                 if (toggle and isReverse) {
 522  2
                     cycleIndex = (repeatCount-1) as Integer;
 523  2
                     lastElapsed = getTotalDur();
 524  9
                 } else {
 525  7
                     cycleIndex = 0;
 526  7
                     lastElapsed = 0;
 527  
                 }
 528  9
                 frameIndex = 0;
 529  9
                 offsetT = 0;
 530  9
                 offsetValid = false;
 531  
             }
 532  
             
 533  14
             public function timingEvent(fraction, totalElapsed) : Void {
 534  14
                 process(totalElapsed as Number);
 535  
             }
 536  
 
 537  0
             public function pause() : Void {
 538  0
                 paused = true;
 539  
             }
 540  
 
 541  0
             public function resume() : Void {
 542  0
                 paused = false;
 543  
             }
 544  
 
 545  22
             public function end() : Void {
 546  9
                 running = false;
 547  9
                 paused = false;
 548  
             }
 549  
         }
 550  
     }
 551  
 }
 552  
 
 553  112
 class KFPair {
 554  1396
     attribute frame:KeyFrame;
 555  110
     attribute value:KeyValue;
 556  
 }
 557  
 
 558  352
 class KFPairList {
 559  59
     attribute target:Pointer;
 560  742
     private attribute pairs:ArrayList = new ArrayList();
 561  
 
 562  
     function size(): Integer {
 563  13
         return pairs.size();
 564  
     }
 565  
 
 566  28
     function add(pair:KFPair): Void {
 567  
         // keep list sorted chronologically
 568  28
         for (i in [0..<pairs.size()]) {
 569  238
             var listval = get(i);
 570  238
             if (pair.frame.time < listval.frame.time) {
 571  0
                 pairs.add(i, pair);
 572  0
                 return;
 573  
             }
 574  
         }
 575  28
         pairs.add(pair);
 576  
     }
 577  
 
 578  
     function get(i:Integer): KFPair {
 579  296
         return pairs.get(i) as KFPair;
 580  
     }
 581  
 }
 582  
 
 583  4
 class SubTimeline {
 584  6
     attribute startTime:Duration;
 585  7
     attribute timeline:Timeline;
 586  
 }