1 /* 2 * Copyright (c) 2008, 2014, Oracle and/or its affiliates. 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. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle 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 Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.scenario.animation.shared; 27 28 import java.util.Arrays; 29 import java.util.Collection; 30 import java.util.Comparator; 31 import javafx.animation.Animation.Status; 32 import javafx.animation.KeyFrame; 33 import javafx.animation.Timeline; 34 import javafx.event.ActionEvent; 35 import javafx.event.EventHandler; 36 import javafx.util.Duration; 37 import com.sun.javafx.animation.TickCalculation; 38 39 /** 40 * An instance of ClipCore handles the core part of a clip. 41 * 42 * The functionality to react on a pulse-signal from the timer is implemented in 43 * two classes: ClipEnvelope and ClipCore. ClipEnvelope is responsible for the 44 * "loop-part" (keeping track of the number of cycles, handling the direction of 45 * the clip etc.). ClipCore takes care of the inner part (interpolating the 46 * values, triggering the action-functions, ...) 47 * 48 * Both classes have an abstract public definition and can only be created using 49 * the factory method create(). The intend is to provide a general 50 * implementation plus eventually some fast-track implementations for common use 51 * cases. 52 */ 53 54 // @@OPT 55 // - Use known information (kf) in visitKeyFrame to set values? 56 57 public class TimelineClipCore { 58 59 private static final int UNDEFINED_KEYFRAME = -1; 60 61 /** 62 * Note: this comparator imposes orderings that are inconsistent with 63 * equals. 64 */ 65 private static final Comparator<KeyFrame> KEY_FRAME_COMPARATOR = (kf1, kf2) -> kf1.getTime().compareTo(kf2.getTime()); 66 67 // The owner of this ClipCore 68 Timeline timeline; 69 70 // The sorted list of keyframes 71 private KeyFrame[] keyFrames = new KeyFrame[0]; 72 private long[] keyFrameTicks = new long[0]; 73 74 private ClipInterpolator clipInterpolator; 75 76 public TimelineClipCore(Timeline timeline) { 77 this.timeline = timeline; 78 this.clipInterpolator = ClipInterpolator.create(keyFrames, keyFrameTicks); 79 } 80 81 /** 82 * Changes the keyframes. 83 */ 84 public Duration setKeyFrames(Collection<KeyFrame> keyFrames) { 85 final int n = keyFrames.size(); 86 final KeyFrame[] sortedKeyFrames = new KeyFrame[n]; 87 keyFrames.toArray(sortedKeyFrames); 88 Arrays.sort(sortedKeyFrames, KEY_FRAME_COMPARATOR); 89 90 this.keyFrames = sortedKeyFrames; 91 keyFrameTicks = new long[n]; 92 for (int i = 0; i < n; ++i) { 93 keyFrameTicks[i] = TickCalculation.fromDuration(this.keyFrames[i].getTime()); 94 } 95 clipInterpolator = clipInterpolator.setKeyFrames(sortedKeyFrames, keyFrameTicks); 96 return (n == 0) ? Duration.ZERO 97 : sortedKeyFrames[n-1].getTime(); 98 } 99 100 public void notifyCurrentRateChanged() { 101 // special case: if clip is toggled while stopped, we want to revisit 102 // all key frames 103 if (timeline.getStatus() != Status.RUNNING) { 104 clearLastKeyFrame(); 105 } 106 } 107 108 /** 109 * This method is called if while visiting a keyframe of a timeline the time 110 * or rate are changed, or if the timeline is stopped. In these cases 111 * visiting the keyframes must be aborted. 112 */ 113 public void abort() { 114 aborted = true; 115 } 116 117 private boolean aborted = false; 118 119 // The index of the keyframe that was visited last 120 private int lastKF = UNDEFINED_KEYFRAME; 121 122 // The position where clip currently stands 123 private long curTicks = 0; 124 125 private void clearLastKeyFrame() { 126 lastKF = UNDEFINED_KEYFRAME; 127 } 128 129 public void jumpTo(long ticks, boolean forceJump) { 130 lastKF = UNDEFINED_KEYFRAME; 131 curTicks = ticks; 132 if (timeline.getStatus() != Status.STOPPED || forceJump) { 133 if (forceJump) { 134 clipInterpolator.validate(false); 135 } 136 clipInterpolator.interpolate(ticks); 137 } 138 } 139 140 public void start(boolean forceSync) { 141 clearLastKeyFrame(); 142 clipInterpolator.validate(forceSync); 143 if (curTicks > 0) { 144 clipInterpolator.interpolate(curTicks); 145 } 146 } 147 148 /** 149 * Called to visit all keyframes within a specified time-interval. 150 */ 151 public void playTo(long ticks) { 152 aborted = false; 153 final boolean forward = curTicks <= ticks; 154 155 if (forward) { 156 final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? 0 157 : (keyFrameTicks[lastKF] <= curTicks) ? lastKF + 1 158 : lastKF; 159 final int toKF = keyFrames.length; 160 for (int fi = fromKF; fi < toKF; fi++) { 161 final long kfTicks = keyFrameTicks[fi]; 162 if (kfTicks > ticks) { 163 lastKF = fi - 1; 164 break; 165 } 166 if (kfTicks >= curTicks) { 167 visitKeyFrame(fi, kfTicks); 168 if (aborted) { 169 break; 170 } 171 } 172 } 173 } else { 174 final int fromKF = (lastKF == UNDEFINED_KEYFRAME) ? keyFrames.length - 1 175 : (keyFrameTicks[lastKF] >= curTicks) ? lastKF - 1 176 : lastKF; 177 for (int fi = fromKF; fi >= 0; fi--) { 178 final long kfTicks = keyFrameTicks[fi]; 179 if (kfTicks < ticks) { 180 lastKF = fi + 1; 181 break; 182 } 183 if (kfTicks <= curTicks) { 184 visitKeyFrame(fi, kfTicks); 185 if (aborted) { 186 break; 187 } 188 } 189 } 190 } 191 if (!aborted 192 && ((lastKF == UNDEFINED_KEYFRAME) 193 || keyFrameTicks[lastKF] != ticks || (keyFrames[lastKF] 194 .getOnFinished() == null))) { 195 setTime(ticks); 196 clipInterpolator.interpolate(ticks); 197 } 198 } 199 200 private void setTime(long ticks) { 201 curTicks = ticks; 202 AnimationAccessor.getDefault().setCurrentTicks(timeline, ticks); 203 } 204 205 /** 206 * Visit a single keyframe. 207 * 208 * @param kfIndex 209 * the index of the keyframe in the keyframe-array 210 * @param kfTicks 211 * the time of that keyframe 212 */ 213 private void visitKeyFrame(int kfIndex, long kfTicks) { 214 if (kfIndex != lastKF) { // suppress double visiting on toggle for 215 // autoReverse 216 lastKF = kfIndex; 217 218 final KeyFrame kf = keyFrames[kfIndex]; 219 final EventHandler<ActionEvent> onFinished = kf.getOnFinished(); 220 221 if (onFinished != null) { 222 // visit the action of this keyframe 223 setTime(kfTicks); 224 clipInterpolator.interpolate(kfTicks); 225 try { 226 onFinished.handle(new ActionEvent(kf, null)); 227 } catch (Throwable ex) { 228 ex.printStackTrace(); 229 } 230 } 231 } 232 } 233 }