blob: e4cf7c756f4f873181865f38aaa40b4ecb6b4b4b [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.support.animation;
import android.os.SystemClock;
import android.support.v4.util.SimpleArrayMap;
import android.view.Choreographer;
import java.util.ArrayList;
/**
* This custom, static handler handles the timing pulse that is shared by all active
* ValueAnimators. This approach ensures that the setting of animation values will happen on the
* same thread that animations start on, and that all animations will share the same times for
* calculating their values, which makes synchronizing animations possible.
*
* The handler uses the Choreographer by default for doing periodic callbacks. A custom
* AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
* may be independent of UI frame update. This could be useful in testing.
*
* @hide
*/
class AnimationHandler {
/**
* Callbacks that receives notifications for animation timing
*/
interface AnimationFrameCallback {
/**
* Run animation based on the frame time.
* @param frameTime The frame start time
*/
boolean doAnimationFrame(long frameTime);
}
/**
* Internal per-thread collections used to avoid set collisions as animations start and end
* while being processed.
* @hide
*/
private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
new SimpleArrayMap<>();
public static final ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();
private final ArrayList<AnimationFrameCallback> mAnimationCallbacks = new ArrayList<>();
private AnimationFrameCallbackProvider mProvider;
private long mCurrentFrameTime = 0;
private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
mCurrentFrameTime = System.currentTimeMillis();
doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
getProvider().postFrameCallback(this);
}
}
};
private boolean mListDirty = false;
public static AnimationHandler getInstance() {
if (sAnimatorHandler.get() == null) {
sAnimatorHandler.set(new AnimationHandler());
}
return sAnimatorHandler.get();
}
public static long getFrameTime() {
if (sAnimatorHandler.get() == null) {
return 0;
}
return sAnimatorHandler.get().mCurrentFrameTime;
}
/**
* By default, the Choreographer is used to provide timing for frame callbacks. A custom
* provider can be used here to provide different timing pulse.
*/
public void setProvider(AnimationFrameCallbackProvider provider) {
if (provider == null) {
mProvider = new MyFrameCallbackProvider();
} else {
mProvider = provider;
}
}
private AnimationFrameCallbackProvider getProvider() {
if (mProvider == null) {
mProvider = new MyFrameCallbackProvider();
}
return mProvider;
}
/**
* Register to get a callback on the next frame after the delay.
*/
public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
if (mAnimationCallbacks.size() == 0) {
getProvider().postFrameCallback(mFrameCallback);
}
if (!mAnimationCallbacks.contains(callback)) {
mAnimationCallbacks.add(callback);
}
if (delay > 0) {
mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
}
}
/**
* Removes the given callback from the list, so it will no longer be called for frame related
* timing.
*/
public void removeCallback(AnimationFrameCallback callback) {
mDelayedCallbackStartTime.remove(callback);
int id = mAnimationCallbacks.indexOf(callback);
if (id >= 0) {
mAnimationCallbacks.set(id, null);
mListDirty = true;
}
}
private void doAnimationFrame(long frameTime) {
long currentTime = SystemClock.uptimeMillis();
for (int i = 0; i < mAnimationCallbacks.size(); i++) {
final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
if (callback == null) {
continue;
}
if (isCallbackDue(callback, currentTime)) {
callback.doAnimationFrame(frameTime);
}
}
cleanUpList();
}
/**
* Remove the callbacks from mDelayedCallbackStartTime once they have passed the initial delay
* so that they can start getting frame callbacks.
*
* @return true if they have passed the initial delay or have no delay, false otherwise.
*/
private boolean isCallbackDue(AnimationFrameCallback callback, long currentTime) {
Long startTime = mDelayedCallbackStartTime.get(callback);
if (startTime == null) {
return true;
}
if (startTime < currentTime) {
mDelayedCallbackStartTime.remove(callback);
return true;
}
return false;
}
private void cleanUpList() {
if (mListDirty) {
for (int i = mAnimationCallbacks.size() - 1; i >= 0; i--) {
if (mAnimationCallbacks.get(i) == null) {
mAnimationCallbacks.remove(i);
}
}
mListDirty = false;
}
}
/**
* Default provider of timing pulse that uses Choreographer for frame callbacks.
*/
private static class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {
final Choreographer mChoreographer = Choreographer.getInstance();
@Override
public void postFrameCallback(Choreographer.FrameCallback callback) {
mChoreographer.postFrameCallback(callback);
}
}
/**
* The intention for having this interface is to increase the testability of ValueAnimator.
* Specifically, we can have a custom implementation of the interface below and provide
* timing pulse without using Choreographer. That way we could use any arbitrary interval for
* our timing pulse in the tests.
*
* @hide
*/
public interface AnimationFrameCallbackProvider {
void postFrameCallback(Choreographer.FrameCallback callback);
}
}