long (@NotNull Collection<@NotNull Animation> animations) { if (myDisposed.get()) { LOG.warn("Animator is already disposed"); return Long.MAX_VALUE; } var from = Integer.MAX_VALUE; var to = 0; for (Animation animation : animations) { from = Math.min(animation.getDelay(), from); to = Math.max(animation.getDelay() + animation.getDuration(), to); } final var delay = animations.isEmpty() ? 0 : from; final var duration = animations.isEmpty() ? 0 : to - from; final var taskId = myRunning.incrementAndGet(); if (!myIgnorePowerSaveMode && PowerSaveMode.isEnabled() || Registry.is("ui.no.bangs.and.whistles", false) || RemoteDesktopService.isRemoteSession() || duration == 0) { myCurrentAnimatorFuture = myService.schedule(() -> { if (taskId < myRunning.get()) { for (Animation animation : animations) { animation.fireEvent(Animation.Phase.CANCELLED); } return; } for (Animation animation : animations) { try { animation.fireEvent(Animation.Phase.SCHEDULED); animation.update(1.0); animation.fireEvent(Animation.Phase.UPDATED); animation.fireEvent(Animation.Phase.EXPIRED); } catch (Throwable t) { LOG.error(t); } } myRunning.compareAndSet(taskId, taskId + 1); }, delay, TimeUnit.MILLISECONDS); return taskId; } final var stat = new Statistic(myName, taskId); stat.start = System.nanoTime(); if (myPeriod < 16) { // do not enable this until it's really necessary JBAnimatorHelper.requestHighPrecisionTimer(this); } myCurrentAnimatorFuture = myService.schedule(new Runnable() { final Type type = myType; final int period = myPeriod; final boolean cycle = myCyclic; @Nullable FrameCounter frameCounter; @NotNull LinkedHashSet<Animation> scheduledAnimations = new LinkedHashSet<>(); //private final long animationStarted = System.nanoTime(); private long nextScheduleTime = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(delay); // ns private void prepareAnimations() { frameCounter = create(type, period, duration); scheduledAnimations = new LinkedHashSet<>(animations); for (Animation animation : scheduledAnimations) { animation.fireEvent(Animation.Phase.SCHEDULED); } } private void finalizeRunning() { JBAnimatorHelper.cancelHighPrecisionTimer(JBAnimator.this); stat.end = System.nanoTime(); myStatistic = stat; } @Override public void run() { stat.count.incrementAndGet(); // There's a penalty for run a task. // To decrease cumulative penalty difference between real start // and expected is subtracted from the next delay long wasLate = System.nanoTime() - nextScheduleTime; if (wasLate < 0) { LOG.warn("Negative animation late value"); wasLate = 0; } if (taskId < myRunning.get()) { finalizeRunning(); for (Animation animation : scheduledAnimations) { animation.fireEvent(Animation.Phase.CANCELLED); } return; } if (frameCounter == null) { prepareAnimations(); } long totalFrames = frameCounter.getTotalFrames(); long currentFrame = Math.min(frameCounter.getNextFrame(cycle), totalFrames); long currentDelay = frameCounter.getDelay(currentFrame); double timeline = (double) currentFrame / totalFrames; if (currentFrame >= totalFrames && cycle) { frameCounter = null; } final var expired = new LinkedList<Animation>(); for (Animation animation : scheduledAnimations) { double start = (double) (animation.getDelay() - delay) / duration; double end = start + (double) animation.getDuration() / duration; if (start <= timeline) try { double current = (timeline - start) / (end - start); animation.update(MathUtil.clamp(current, 0.0, 1.0)); animation.fireEvent(Animation.Phase.UPDATED); } catch (Throwable t) { LOG.error(t); } if (timeline > end) { expired.add(animation); } } expired.forEach(scheduledAnimations::remove); boolean isProceed = currentFrame < totalFrames || cycle; if (isProceed) { long nextDelay = Math.max(TimeUnit.MILLISECONDS.toNanos(currentDelay) - wasLate, TimeUnit.MILLISECONDS.toNanos(1)); nextScheduleTime = System.nanoTime() + nextDelay; myCurrentAnimatorFuture = myService.schedule(this, nextDelay, TimeUnit.NANOSECONDS); } else { // There's a situation when a new task is submitted but current is already in progress. // For example, the task can be submitted with Animation#runWhenExpired, // but this code synchronously can fire animate, therefore myRunning increases. // If this situation happens the current one ID is abandoned, // because the value is increased somewhere else. myRunning.compareAndSet(taskId, taskId + 1); //var debugInfo = "Animation total time is " + // TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - animationStarted) + // " ms; requested time is " + // (delay + duration); //LOG.info(debugInfo); finalizeRunning(); } // we should fire events after taskId is updated // and some final activity is done by calling finalizeRunning for (Animation animation : isProceed ? expired : scheduledAnimations) { animation.fireEvent(Animation.Phase.EXPIRED); } } }, delay, TimeUnit.MILLISECONDS); return taskId; }