Fix jumps in the beginning of animations

If the first draw frame of an animation is
expensive, which it often is, it causes a big
jump. Added a helper class which automatically
adjusts the animation start time if the first
frame is more than 16ms.

Change-Id: I100edbc41c2abe930a32d6bcf0a782ea9735f7f9
This commit is contained in:
Michael Jurka
2013-03-13 12:55:46 +01:00
parent b8f5c401de
commit f1ad608c28
13 changed files with 184 additions and 77 deletions
@@ -444,7 +444,8 @@ public class AppWidgetResizeFrame extends FrameLayout {
newHeight);
PropertyValuesHolder x = PropertyValuesHolder.ofInt("x", lp.x, newX);
PropertyValuesHolder y = PropertyValuesHolder.ofInt("y", lp.y, newY);
ObjectAnimator oa = LauncherAnimUtils.ofPropertyValuesHolder(lp, width, height, x, y);
ObjectAnimator oa =
LauncherAnimUtils.ofPropertyValuesHolder(lp, this, width, height, x, y);
ObjectAnimator leftOa = LauncherAnimUtils.ofFloat(mLeftHandle, "alpha", 1.0f);
ObjectAnimator rightOa = LauncherAnimUtils.ofFloat(mRightHandle, "alpha", 1.0f);
ObjectAnimator topOa = LauncherAnimUtils.ofFloat(mTopHandle, "alpha", 1.0f);
@@ -290,11 +290,7 @@ public class AppsCustomizeTabHost extends TabHost implements LauncherTransitiona
final AnimatorSet animSet = LauncherAnimUtils.createAnimatorSet();
animSet.playTogether(outAnim, inAnim);
animSet.setDuration(duration);
post(new Runnable() {
public void run() {
animSet.start();
}
});
animSet.start();
}
});
}
+3 -3
View File
@@ -241,7 +241,7 @@ public class CellLayout extends ViewGroup {
for (int i = 0; i < mDragOutlineAnims.length; i++) {
final InterruptibleInOutAnimator anim =
new InterruptibleInOutAnimator(duration, fromAlphaValue, toAlphaValue);
new InterruptibleInOutAnimator(this, duration, fromAlphaValue, toAlphaValue);
anim.getAnimator().setInterpolator(mEaseOutInterpolator);
final int thisIndex = i;
anim.getAnimator().addUpdateListener(new AnimatorUpdateListener() {
@@ -1128,7 +1128,7 @@ public class CellLayout extends ViewGroup {
return true;
}
ValueAnimator va = LauncherAnimUtils.ofFloat(0f, 1f);
ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
va.setDuration(duration);
mReorderAnimators.put(lp, va);
@@ -2324,7 +2324,7 @@ public class CellLayout extends ViewGroup {
if (finalDeltaX == 0 && finalDeltaY == 0) {
return;
}
ValueAnimator va = LauncherAnimUtils.ofFloat(0f, 1f);
ValueAnimator va = LauncherAnimUtils.ofFloat(child, 0f, 1f);
a = va;
va.setRepeatMode(ValueAnimator.REVERSE);
va.setRepeatCount(ValueAnimator.INFINITE);
+2 -2
View File
@@ -80,7 +80,7 @@ public class DragView extends View {
setScaleY(initialScale);
// Animate the view into the correct position
mAnim = LauncherAnimUtils.ofFloat(0.0f, 1.0f);
mAnim = LauncherAnimUtils.ofFloat(this, 0f, 1f);
mAnim.setDuration(150);
mAnim.addUpdateListener(new AnimatorUpdateListener() {
@Override
@@ -203,7 +203,7 @@ public class DragView extends View {
}
public void crossFade(int duration) {
ValueAnimator va = LauncherAnimUtils.ofFloat(0f, 1f);
ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1f);
va.setDuration(duration);
va.setInterpolator(new DecelerateInterpolator(1.5f));
va.addUpdateListener(new AnimatorUpdateListener() {
@@ -0,0 +1,105 @@
/*
* Copyright (C) 2013 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 com.android.launcher2;
import android.animation.ValueAnimator;
import android.animation.Animator.AnimatorListener;
import android.util.Log;
import android.view.ViewTreeObserver;
import android.view.View;
/*
* This is a helper class that listens to updates from the corresponding animation.
* For the first two frames, it adjusts the current play time of the animation to
* prevent jank at the beginning of the animation
*/
public class FirstFrameAnimatorHelper implements ValueAnimator.AnimatorUpdateListener {
private static final boolean DEBUG = false;
private static final int MAX_FIRST_FRAME_DELAY = 200;
private static final int IDEAL_FRAME_DURATION = 16;
private View mTarget;
private long mStartFrame;
private long mStartTime = -1;
private boolean mHandlingOnAnimationUpdate;
private boolean mAdjustedSecondFrameTime;
private static ViewTreeObserver.OnDrawListener sGlobalDrawListener;
private static long sGlobalFrameCounter;
public FirstFrameAnimatorHelper(ValueAnimator animator, View target) {
mTarget = target;
animator.addUpdateListener(this);
}
public static void initializeDrawListener(View view) {
sGlobalDrawListener = new ViewTreeObserver.OnDrawListener() {
private long mTime = System.currentTimeMillis();
public void onDraw() {
sGlobalFrameCounter++;
long newTime = System.currentTimeMillis();
if (DEBUG) {
Log.d("FirstFrameAnimatorHelper", "TICK " + (newTime - mTime));
}
mTime = newTime;
}
};
view.getViewTreeObserver().addOnDrawListener(sGlobalDrawListener);
}
public void onAnimationUpdate(ValueAnimator animation) {
if (mStartTime == -1) {
mStartFrame = sGlobalFrameCounter;
mStartTime = System.currentTimeMillis();
}
if (!mHandlingOnAnimationUpdate) {
mHandlingOnAnimationUpdate = true;
long frameNum = sGlobalFrameCounter - mStartFrame;
// If we haven't drawn our first frame, reset the time to t = 0
// (give up after 200ms of waiting though - might happen, for example, if we are no
// longer in the foreground and no frames are being rendered ever)
if (frameNum == 0 && System.currentTimeMillis() < mStartTime + MAX_FIRST_FRAME_DELAY) {
mTarget.getRootView().invalidate(); // make sure we'll do a draw
animation.setCurrentPlayTime(0);
// For the second frame, if the first frame took more than 16ms,
// adjust the start time and pretend it took only 16ms anyway. This
// prevents a large jump in the animation due to an expensive first frame
} else if (frameNum == 1 && !mAdjustedSecondFrameTime &&
System.currentTimeMillis() > mStartTime + 16) {
animation.setCurrentPlayTime(IDEAL_FRAME_DURATION);
mAdjustedSecondFrameTime = true;
} else {
if (frameNum > 1) {
animation.removeUpdateListener(this);
}
if (DEBUG) print(animation);
}
mHandlingOnAnimationUpdate = false;
} else {
if (DEBUG) print(animation);
}
}
public void print(ValueAnimator animation) {
float flatFraction = animation.getCurrentPlayTime() / (float) animation.getDuration();
Log.d("FirstFrameAnimatorHelper", sGlobalFrameCounter +
"(" + (sGlobalFrameCounter - mStartFrame) + ") " + mTarget + " dirty? " +
mTarget.isDirty() + " " + flatFraction + " " + this + " " + animation);
}
}
+2 -18
View File
@@ -444,15 +444,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
});
oa.setDuration(mExpandDuration);
setLayerType(LAYER_TYPE_HARDWARE, null);
buildLayer();
post(new Runnable() {
public void run() {
// Check if the animator changed in the meantime
if (oa != mOpenCloseAnimator)
return;
oa.start();
}
});
oa.start();
}
private void sendCustomAccessibilityEvent(int type, String text) {
@@ -497,15 +489,7 @@ public class Folder extends LinearLayout implements DragSource, View.OnClickList
});
oa.setDuration(mExpandDuration);
setLayerType(LAYER_TYPE_HARDWARE, null);
buildLayer();
post(new Runnable() {
public void run() {
// Check if the animator changed in the meantime
if (oa != mOpenCloseAnimator)
return;
oa.start();
}
});
oa.start();
}
void notifyDataSetChanged() {
+3 -3
View File
@@ -199,7 +199,7 @@ public class FolderIcon extends LinearLayout implements FolderListener {
if (mNeutralAnimator != null) {
mNeutralAnimator.cancel();
}
mAcceptAnimator = LauncherAnimUtils.ofFloat(0f, 1f);
mAcceptAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
mAcceptAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
final int previewSize = sPreviewSize;
@@ -228,7 +228,7 @@ public class FolderIcon extends LinearLayout implements FolderListener {
if (mAcceptAnimator != null) {
mAcceptAnimator.cancel();
}
mNeutralAnimator = LauncherAnimUtils.ofFloat(0f, 1f);
mNeutralAnimator = LauncherAnimUtils.ofFloat(mCellLayout, 0f, 1f);
mNeutralAnimator.setDuration(CONSUMPTION_ANIMATION_DURATION);
final int previewSize = sPreviewSize;
@@ -573,7 +573,7 @@ public class FolderIcon extends LinearLayout implements FolderListener {
final float transY0 = (mAvailableSpaceInPreview - d.getIntrinsicHeight()) / 2;
mAnimParams.drawable = d;
ValueAnimator va = LauncherAnimUtils.ofFloat(0f, 1.0f);
ValueAnimator va = LauncherAnimUtils.ofFloat(this, 0f, 1.0f);
va.addUpdateListener(new AnimatorUpdateListener(){
public void onAnimationUpdate(ValueAnimator animation) {
float progress = (Float) animation.getAnimatedValue();
@@ -19,6 +19,7 @@ package com.android.launcher2;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.view.View;
/**
* A convenience class for two-way animations, e.g. a fadeIn/fadeOut animation.
@@ -44,8 +45,8 @@ public class InterruptibleInOutAnimator {
// TODO: This isn't really necessary, but is here to help diagnose a bug in the drag viz
private int mDirection = STOPPED;
public InterruptibleInOutAnimator(long duration, float fromValue, float toValue) {
mAnimator = LauncherAnimUtils.ofFloat(fromValue, toValue).setDuration(duration);
public InterruptibleInOutAnimator(View view, long duration, float fromValue, float toValue) {
mAnimator = LauncherAnimUtils.ofFloat(view, fromValue, toValue).setDuration(duration);
mOriginalDuration = duration;
mOriginalFromValue = fromValue;
mOriginalToValue = toValue;
+19 -37
View File
@@ -1240,7 +1240,7 @@ public final class Launcher extends Activity
filter.addAction(Intent.ACTION_SCREEN_OFF);
filter.addAction(Intent.ACTION_USER_PRESENT);
registerReceiver(mReceiver, filter);
FirstFrameAnimatorHelper.initializeDrawListener(getWindow().getDecorView());
mAttached = true;
mVisible = true;
}
@@ -1269,10 +1269,9 @@ public final class Launcher extends Activity
final ViewTreeObserver observer = mWorkspace.getViewTreeObserver();
// We want to let Launcher draw itself at least once before we force it to build
// layers on all the workspace pages, so that transitioning to Launcher from other
// apps is nice and speedy. Usually the first call to preDraw doesn't correspond to
// a true draw so we wait until the second preDraw call to be safe
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
public boolean onPreDraw() {
// apps is nice and speedy.
observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
public void onDraw() {
// We delay the layer building a bit in order to give
// other message processing a time to run. In particular
// this avoids a delay in hiding the IME if it was
@@ -1280,8 +1279,8 @@ public final class Launcher extends Activity
// some communication back with the app.
mWorkspace.postDelayed(mBuildLayersRunnable, 500);
observer.removeOnPreDrawListener(this);
return true;
observer.removeOnDrawListener(this);
return;
}
});
}
@@ -2483,6 +2482,7 @@ public final class Launcher extends Activity
*/
private void showAppsCustomizeHelper(final boolean animated, final boolean springLoaded) {
if (mStateAnimation != null) {
mStateAnimation.setDuration(0);
mStateAnimation.cancel();
mStateAnimation = null;
}
@@ -2513,7 +2513,7 @@ public final class Launcher extends Activity
toView.setVisibility(View.VISIBLE);
toView.setAlpha(0f);
final ObjectAnimator alphaAnim = ObjectAnimator
final ObjectAnimator alphaAnim = LauncherAnimUtils
.ofFloat(toView, "alpha", 0f, 1f)
.setDuration(fadeDuration);
alphaAnim.setInterpolator(new DecelerateInterpolator(1.5f));
@@ -2578,7 +2578,6 @@ public final class Launcher extends Activity
}
boolean delayAnim = false;
final ViewTreeObserver observer;
dispatchOnLauncherTransitionPrepare(fromView, animated, false);
dispatchOnLauncherTransitionPrepare(toView, animated, false);
@@ -2588,10 +2587,7 @@ public final class Launcher extends Activity
if ((((LauncherTransitionable) toView).getContent().getMeasuredWidth() == 0) ||
(mWorkspace.getMeasuredWidth() == 0) ||
(toView.getMeasuredWidth() == 0)) {
observer = mWorkspace.getViewTreeObserver();
delayAnim = true;
} else {
observer = null;
}
final AnimatorSet stateAnimation = mStateAnimation;
@@ -2604,25 +2600,17 @@ public final class Launcher extends Activity
setPivotsForZoom(toView, scale);
dispatchOnLauncherTransitionStart(fromView, animated, false);
dispatchOnLauncherTransitionStart(toView, animated, false);
toView.post(new Runnable() {
public void run() {
// Check that mStateAnimation hasn't changed while
// we waited for a layout/draw pass
if (mStateAnimation != stateAnimation)
return;
mStateAnimation.start();
}
});
LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView);
}
};
if (delayAnim) {
final OnGlobalLayoutListener delayedStart = new OnGlobalLayoutListener() {
public void onGlobalLayout() {
toView.post(startAnimRunnable);
observer.removeOnGlobalLayoutListener(this);
}
};
observer.addOnGlobalLayoutListener(delayedStart);
final ViewTreeObserver observer = toView.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
public void onGlobalLayout() {
startAnimRunnable.run();
toView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
} else {
startAnimRunnable.run();
}
@@ -2663,6 +2651,7 @@ public final class Launcher extends Activity
final boolean springLoaded, final Runnable onCompleteRunnable) {
if (mStateAnimation != null) {
mStateAnimation.setDuration(0);
mStateAnimation.cancel();
mStateAnimation = null;
}
@@ -2697,7 +2686,7 @@ public final class Launcher extends Activity
setDuration(duration).
setInterpolator(new Workspace.ZoomInInterpolator());
final ObjectAnimator alphaAnim = ObjectAnimator
final ObjectAnimator alphaAnim = LauncherAnimUtils
.ofFloat(fromView, "alpha", 1f, 0f)
.setDuration(fadeOutDuration);
alphaAnim.setInterpolator(new AccelerateDecelerateInterpolator());
@@ -2740,14 +2729,7 @@ public final class Launcher extends Activity
}
dispatchOnLauncherTransitionStart(fromView, animated, true);
dispatchOnLauncherTransitionStart(toView, animated, true);
final Animator stateAnimation = mStateAnimation;
mWorkspace.post(new Runnable() {
public void run() {
if (stateAnimation != mStateAnimation)
return;
mStateAnimation.start();
}
});
LauncherAnimUtils.startAnimationAfterNextDraw(mStateAnimation, toView);
} else {
fromView.setVisibility(View.GONE);
dispatchOnLauncherTransitionPrepare(fromView, animated, true);
@@ -21,6 +21,8 @@ import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.ValueAnimator;
import android.view.View;
import android.view.ViewTreeObserver;
import java.util.HashSet;
@@ -47,6 +49,22 @@ public class LauncherAnimUtils {
a.addListener(sEndAnimListener);
}
// Helper method. Assumes a draw is pending, and that if the animation's duration is 0
// it should be cancelled
public static void startAnimationAfterNextDraw(final Animator animator, final View view) {
final ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnDrawListener(new ViewTreeObserver.OnDrawListener() {
public void onDraw() {
// Use this as a signal that the animation was cancelled
if (animator.getDuration() == 0) {
return;
}
animator.start();
view.getViewTreeObserver().removeOnDrawListener(this);
}
});
}
public static void onDestroyActivity() {
HashSet<Animator> animators = new HashSet<Animator>(sAnimators);
for (Animator a : animators) {
@@ -64,28 +82,40 @@ public class LauncherAnimUtils {
return anim;
}
public static ValueAnimator ofFloat(float... values) {
public static ValueAnimator ofFloat(View target, float... values) {
ValueAnimator anim = new ValueAnimator();
anim.setFloatValues(values);
cancelOnDestroyActivity(anim);
return anim;
}
public static ObjectAnimator ofFloat(Object target, String propertyName, float... values) {
public static ObjectAnimator ofFloat(View target, String propertyName, float... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setPropertyName(propertyName);
anim.setFloatValues(values);
cancelOnDestroyActivity(anim);
new FirstFrameAnimatorHelper(anim, target);
return anim;
}
public static ObjectAnimator ofPropertyValuesHolder(Object target,
public static ObjectAnimator ofPropertyValuesHolder(View target,
PropertyValuesHolder... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setValues(values);
cancelOnDestroyActivity(anim);
new FirstFrameAnimatorHelper(anim, target);
return anim;
}
public static ObjectAnimator ofPropertyValuesHolder(Object target,
View view, PropertyValuesHolder... values) {
ObjectAnimator anim = new ObjectAnimator();
anim.setTarget(target);
anim.setValues(values);
cancelOnDestroyActivity(anim);
new FirstFrameAnimatorHelper(anim, view);
return anim;
}
}
@@ -19,6 +19,7 @@ package com.android.launcher2;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.view.ViewPropertyAnimator;
import android.view.View;
@@ -123,6 +124,12 @@ public class LauncherViewPropertyAnimator extends Animator implements AnimatorLi
@Override
public void onAnimationStart(Animator animation) {
// This is the first time we get a handle to the internal ValueAnimator
// used by the ViewPropertyAnimator.
// FirstFrameAnimatorHelper hooks itself up to the updates on the animator,
// and then adjusts the play time to keep the first two frames jank-free
new FirstFrameAnimatorHelper((ValueAnimator) animation, mTarget)
.onAnimationUpdate((ValueAnimator) animation);
for (int i = 0; i < mListeners.size(); i++) {
Animator.AnimatorListener listener = mListeners.get(i);
listener.onAnimationStart(this);
@@ -77,7 +77,6 @@ public class SearchDropTargetBar extends FrameLayout implements DragController.D
// Enable the hw layers before the animation starts (will be disabled in the onAnimationEnd
// callback below)
v.setLayerType(View.LAYER_TYPE_HARDWARE, null);
v.buildLayer();
}
private void setupAnimation(ObjectAnimator anim, final View v) {
+4 -2
View File
@@ -1146,7 +1146,8 @@ public class Workspace extends SmoothPagedView
float startAlpha = getBackgroundAlpha();
if (finalAlpha != startAlpha) {
if (animated) {
mBackgroundFadeOutAnimation = LauncherAnimUtils.ofFloat(startAlpha, finalAlpha);
mBackgroundFadeOutAnimation =
LauncherAnimUtils.ofFloat(this, startAlpha, finalAlpha);
mBackgroundFadeOutAnimation.addUpdateListener(new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
setBackgroundAlpha(((Float) animation.getAnimatedValue()).floatValue());
@@ -1687,7 +1688,8 @@ public class Workspace extends SmoothPagedView
}
if (mOldBackgroundAlphas[i] != 0 ||
mNewBackgroundAlphas[i] != 0) {
ValueAnimator bgAnim = LauncherAnimUtils.ofFloat(0f, 1f).setDuration(duration);
ValueAnimator bgAnim =
LauncherAnimUtils.ofFloat(cl, 0f, 1f).setDuration(duration);
bgAnim.setInterpolator(mZoomInInterpolator);
bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() {
public void onAnimationUpdate(float a, float b) {