blob: e2474900da0501064b559999b8f0849fcacf70a4 [file] [log] [blame]
/*
* Copyright (C) 2015 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.launcher3;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.PropertyValuesHolder;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.res.Resources;
import android.util.Log;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import com.android.launcher3.allapps.AllAppsContainerView;
import com.android.launcher3.allapps.AllAppsTransitionController;
import com.android.launcher3.anim.AnimationLayerSet;
import com.android.launcher3.anim.CircleRevealOutlineProvider;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.util.Thunk;
import com.android.launcher3.widget.WidgetsContainerView;
/**
* TODO: figure out what kind of tests we can write for this
*
* Things to test when changing the following class.
* - Home from workspace
* - from center screen
* - from other screens
* - Home from all apps
* - from center screen
* - from other screens
* - Back from all apps
* - from center screen
* - from other screens
* - Launch app from workspace and quit
* - with back
* - with home
* - Launch app from all apps and quit
* - with back
* - with home
* - Go to a screen that's not the default, then all
* apps, and launch and app, and go back
* - with back
* -with home
* - On workspace, long press power and go back
* - with back
* - with home
* - On all apps, long press power and go back
* - with back
* - with home
* - On workspace, power off
* - On all apps, power off
* - Launch an app and turn off the screen while in that app
* - Go back with home key
* - Go back with back key TODO: make this not go to workspace
* - From all apps
* - From workspace
* - Enter and exit car mode (becuase it causes an extra configuration changed)
* - From all apps
* - From the center workspace
* - From another workspace
*/
public class LauncherStateTransitionAnimation {
/**
* animation used for the widget tray
*/
public static final int CIRCULAR_REVEAL = 0;
/**
* animation used for all apps tray
*/
public static final int PULLUP = 1;
private static final float FINAL_REVEAL_ALPHA_FOR_WIDGETS = 0.3f;
/**
* Private callbacks made during transition setup.
*/
private static class PrivateTransitionCallbacks {
private final float materialRevealViewFinalAlpha;
PrivateTransitionCallbacks(float revealAlpha) {
materialRevealViewFinalAlpha = revealAlpha;
}
float getMaterialRevealViewStartFinalRadius() {
return 0;
}
AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(View revealView,
View buttonView) {
return null;
}
void onTransitionComplete() {}
}
public static final String TAG = "LSTAnimation";
public static final int SINGLE_FRAME_DELAY = 16;
@Thunk Launcher mLauncher;
@Thunk AnimatorSet mCurrentAnimation;
AllAppsTransitionController mAllAppsController;
public LauncherStateTransitionAnimation(Launcher l, AllAppsTransitionController allAppsController) {
mLauncher = l;
mAllAppsController = allAppsController;
}
/**
* Starts an animation to the apps view.
*/
public void startAnimationToAllApps(final boolean animated) {
final AllAppsContainerView toView = mLauncher.getAppsView();
final View buttonView = mLauncher.getStartViewForAllAppsRevealAnimation();
PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) {
@Override
public float getMaterialRevealViewStartFinalRadius() {
int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize;
return allAppsButtonSize / 2;
}
@Override
public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
final View revealView, final View allAppsButtonView) {
return new AnimatorListenerAdapter() {
public void onAnimationStart(Animator animation) {
allAppsButtonView.setVisibility(View.INVISIBLE);
}
public void onAnimationEnd(Animator animation) {
allAppsButtonView.setVisibility(View.VISIBLE);
}
};
}
@Override
void onTransitionComplete() {
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
}
};
// Only animate the search bar if animating from spring loaded mode back to all apps
startAnimationToOverlay(
Workspace.State.NORMAL_HIDDEN, buttonView, toView, animated, PULLUP, cb);
}
/**
* Starts an animation to the widgets view.
*/
public void startAnimationToWidgets(final boolean animated) {
final WidgetsContainerView toView = mLauncher.getWidgetsView();
final View buttonView = mLauncher.getWidgetsButton();
startAnimationToOverlay(
Workspace.State.OVERVIEW_HIDDEN, buttonView, toView, animated, CIRCULAR_REVEAL,
new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS){
@Override
void onTransitionComplete() {
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
}
});
}
/**
* Starts an animation to the workspace from the current overlay view.
*/
public void startAnimationToWorkspace(final Launcher.State fromState,
final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
final boolean animated, final Runnable onCompleteRunnable) {
if (toWorkspaceState != Workspace.State.NORMAL &&
toWorkspaceState != Workspace.State.SPRING_LOADED &&
toWorkspaceState != Workspace.State.OVERVIEW) {
Log.e(TAG, "Unexpected call to startAnimationToWorkspace");
}
if (fromState == Launcher.State.APPS || fromState == Launcher.State.APPS_SPRING_LOADED
|| mAllAppsController.isTransitioning()) {
startAnimationToWorkspaceFromAllApps(fromWorkspaceState, toWorkspaceState,
animated, PULLUP, onCompleteRunnable);
} else if (fromState == Launcher.State.WIDGETS ||
fromState == Launcher.State.WIDGETS_SPRING_LOADED) {
startAnimationToWorkspaceFromWidgets(fromWorkspaceState, toWorkspaceState,
animated, onCompleteRunnable);
} else {
startAnimationToNewWorkspaceState(fromWorkspaceState, toWorkspaceState,
animated, onCompleteRunnable);
}
}
/**
* Creates and starts a new animation to a particular overlay view.
*/
private void startAnimationToOverlay(
final Workspace.State toWorkspaceState,
final View buttonView, final BaseContainerView toView,
final boolean animated, int animType, final PrivateTransitionCallbacks pCb) {
final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
final Resources res = mLauncher.getResources();
final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime);
final int revealDurationSlide = res.getInteger(R.integer.config_overlaySlideRevealTime);
final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger);
final AnimationLayerSet layerViews = new AnimationLayerSet();
// If for some reason our views aren't initialized, don't animate
boolean initialized = buttonView != null;
// Cancel the current animation
cancelAnimation();
final View contentView = toView.getContentView();
playCommonTransitionAnimations(toWorkspaceState,
animated, initialized, animation, layerViews);
if (!animated || !initialized) {
if (toWorkspaceState == Workspace.State.NORMAL_HIDDEN) {
mAllAppsController.finishPullUp();
}
toView.setTranslationX(0.0f);
toView.setTranslationY(0.0f);
toView.setScaleX(1.0f);
toView.setScaleY(1.0f);
toView.setAlpha(1.0f);
toView.setVisibility(View.VISIBLE);
// Show the content view
contentView.setVisibility(View.VISIBLE);
pCb.onTransitionComplete();
return;
}
if (animType == CIRCULAR_REVEAL) {
// Setup the reveal view animation
final View revealView = toView.getRevealView();
int width = revealView.getMeasuredWidth();
int height = revealView.getMeasuredHeight();
float revealRadius = (float) Math.hypot(width / 2, height / 2);
revealView.setVisibility(View.VISIBLE);
revealView.setAlpha(0f);
revealView.setTranslationY(0f);
revealView.setTranslationX(0f);
// Calculate the final animation values
int[] buttonViewToPanelDelta =
Utilities.getCenterDeltaInScreenSpace(revealView, buttonView);
final float revealViewToAlpha = pCb.materialRevealViewFinalAlpha;
final float revealViewToXDrift = buttonViewToPanelDelta[0];
final float revealViewToYDrift = buttonViewToPanelDelta[1];
// Create the animators
PropertyValuesHolder panelAlpha =
PropertyValuesHolder.ofFloat(View.ALPHA, revealViewToAlpha, 1f);
PropertyValuesHolder panelDriftY =
PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, revealViewToYDrift, 0);
PropertyValuesHolder panelDriftX =
PropertyValuesHolder.ofFloat(View.TRANSLATION_X, revealViewToXDrift, 0);
ObjectAnimator panelAlphaAndDrift = ObjectAnimator.ofPropertyValuesHolder(revealView,
panelAlpha, panelDriftY, panelDriftX);
panelAlphaAndDrift.setDuration(revealDuration);
panelAlphaAndDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
// Play the animation
layerViews.addView(revealView);
animation.play(panelAlphaAndDrift);
// Setup the animation for the content view
contentView.setVisibility(View.VISIBLE);
contentView.setAlpha(0f);
contentView.setTranslationY(revealViewToYDrift);
layerViews.addView(contentView);
// Create the individual animators
ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY",
revealViewToYDrift, 0);
pageDrift.setDuration(revealDuration);
pageDrift.setInterpolator(new LogDecelerateInterpolator(100, 0));
pageDrift.setStartDelay(itemsAlphaStagger);
animation.play(pageDrift);
ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 0f, 1f);
itemsAlpha.setDuration(revealDuration);
itemsAlpha.setInterpolator(new AccelerateInterpolator(1.5f));
itemsAlpha.setStartDelay(itemsAlphaStagger);
animation.play(itemsAlpha);
float startRadius = pCb.getMaterialRevealViewStartFinalRadius();
AnimatorListenerAdapter listener = pCb.getMaterialRevealViewAnimatorListener(
revealView, buttonView);
Animator reveal = new CircleRevealOutlineProvider(width / 2, height / 2,
startRadius, revealRadius).createRevealAnimator(revealView);
reveal.setDuration(revealDuration);
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
if (listener != null) {
reveal.addListener(listener);
}
animation.play(reveal);
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Hide the reveal view
revealView.setVisibility(View.INVISIBLE);
// This can hold unnecessary references to views.
cleanupAnimation();
pCb.onTransitionComplete();
}
});
toView.bringToFront();
toView.setVisibility(View.VISIBLE);
animation.addListener(layerViews);
toView.post(new StartAnimRunnable(animation, toView));
mCurrentAnimation = animation;
} else if (animType == PULLUP) {
if (!FeatureFlags.LAUNCHER3_PHYSICS) {
// We are animating the content view alpha, so ensure we have a layer for it.
layerViews.addView(contentView);
}
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
cleanupAnimation();
pCb.onTransitionComplete();
}
});
boolean shouldPost = mAllAppsController.animateToAllApps(animation, revealDurationSlide);
Runnable startAnimRunnable = new StartAnimRunnable(animation, toView);
mCurrentAnimation = animation;
mCurrentAnimation.addListener(layerViews);
if (shouldPost) {
toView.post(startAnimRunnable);
} else {
startAnimRunnable.run();
}
}
}
/**
* Plays animations used by various transitions.
*/
private void playCommonTransitionAnimations(
Workspace.State toWorkspaceState,
boolean animated, boolean initialized, AnimatorSet animation,
AnimationLayerSet layerViews) {
// Create the workspace animation.
// NOTE: this call apparently also sets the state for the workspace if !animated
Animator workspaceAnim = mLauncher.startWorkspaceStateChangeAnimation(toWorkspaceState,
animated, layerViews);
if (animated && initialized) {
// Play the workspace animation
if (workspaceAnim != null) {
animation.play(workspaceAnim);
}
}
}
/**
* Starts an animation to the workspace from the apps view.
*/
private void startAnimationToWorkspaceFromAllApps(final Workspace.State fromWorkspaceState,
final Workspace.State toWorkspaceState, final boolean animated, int type,
final Runnable onCompleteRunnable) {
// No alpha anim from all apps
PrivateTransitionCallbacks cb = new PrivateTransitionCallbacks(1f) {
@Override
float getMaterialRevealViewStartFinalRadius() {
int allAppsButtonSize = mLauncher.getDeviceProfile().allAppsButtonVisualSize;
return allAppsButtonSize / 2;
}
@Override
public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
final View revealView, final View allAppsButtonView) {
return new AnimatorListenerAdapter() {
public void onAnimationStart(Animator animation) {
// We set the alpha instead of visibility to ensure that the focus does not
// get taken from the all apps view
allAppsButtonView.setVisibility(View.VISIBLE);
allAppsButtonView.setAlpha(0f);
}
public void onAnimationEnd(Animator animation) {
// Hide the reveal view
revealView.setVisibility(View.INVISIBLE);
// Show the all apps button, and focus it
allAppsButtonView.setAlpha(1f);
}
};
}
@Override
void onTransitionComplete() {
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
}
};
// Only animate the search bar if animating to spring loaded mode from all apps
startAnimationToWorkspaceFromOverlay(fromWorkspaceState, toWorkspaceState,
mLauncher.getStartViewForAllAppsRevealAnimation(), mLauncher.getAppsView(),
animated, type, onCompleteRunnable, cb);
}
/**
* Starts an animation to the workspace from the widgets view.
*/
private void startAnimationToWorkspaceFromWidgets(final Workspace.State fromWorkspaceState,
final Workspace.State toWorkspaceState, final boolean animated,
final Runnable onCompleteRunnable) {
final WidgetsContainerView widgetsView = mLauncher.getWidgetsView();
PrivateTransitionCallbacks cb =
new PrivateTransitionCallbacks(FINAL_REVEAL_ALPHA_FOR_WIDGETS) {
@Override
public AnimatorListenerAdapter getMaterialRevealViewAnimatorListener(
final View revealView, final View widgetsButtonView) {
return new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animation) {
// Hide the reveal view
revealView.setVisibility(View.INVISIBLE);
}
};
}
@Override
void onTransitionComplete() {
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
}
};
startAnimationToWorkspaceFromOverlay(
fromWorkspaceState, toWorkspaceState,
mLauncher.getWidgetsButton(), widgetsView,
animated, CIRCULAR_REVEAL, onCompleteRunnable, cb);
}
/**
* Starts an animation to the workspace from another workspace state, e.g. normal to overview.
*/
private void startAnimationToNewWorkspaceState(final Workspace.State fromWorkspaceState,
final Workspace.State toWorkspaceState, final boolean animated,
final Runnable onCompleteRunnable) {
final View fromWorkspace = mLauncher.getWorkspace();
final AnimationLayerSet layerViews = new AnimationLayerSet();
final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
// Cancel the current animation
cancelAnimation();
playCommonTransitionAnimations(toWorkspaceState, animated, animated, animation, layerViews);
mLauncher.getUserEventDispatcher().resetElapsedContainerMillis();
if (animated) {
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
// This can hold unnecessary references to views.
cleanupAnimation();
}
});
animation.addListener(layerViews);
fromWorkspace.post(new StartAnimRunnable(animation, null));
mCurrentAnimation = animation;
} else /* if (!animated) */ {
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
mCurrentAnimation = null;
}
}
/**
* Creates and starts a new animation to the workspace.
*/
private void startAnimationToWorkspaceFromOverlay(
final Workspace.State fromWorkspaceState, final Workspace.State toWorkspaceState,
final View buttonView, final BaseContainerView fromView,
final boolean animated, int animType, final Runnable onCompleteRunnable,
final PrivateTransitionCallbacks pCb) {
final AnimatorSet animation = LauncherAnimUtils.createAnimatorSet();
final Resources res = mLauncher.getResources();
final int revealDuration = res.getInteger(R.integer.config_overlayRevealTime);
final int revealDurationSlide = res.getInteger(R.integer.config_overlaySlideRevealTime);
final int itemsAlphaStagger = res.getInteger(R.integer.config_overlayItemsAlphaStagger);
final View toView = mLauncher.getWorkspace();
final View revealView = fromView.getRevealView();
final View contentView = fromView.getContentView();
final AnimationLayerSet layerViews = new AnimationLayerSet();
// If for some reason our views aren't initialized, don't animate
boolean initialized = buttonView != null;
// Cancel the current animation
cancelAnimation();
playCommonTransitionAnimations(toWorkspaceState,
animated, initialized, animation, layerViews);
if (!animated || !initialized) {
if (fromWorkspaceState == Workspace.State.NORMAL_HIDDEN) {
mAllAppsController.finishPullDown();
}
fromView.setVisibility(View.GONE);
pCb.onTransitionComplete();
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
return;
}
if (animType == CIRCULAR_REVEAL) {
// hideAppsCustomizeHelper is called in some cases when it is already hidden
// don't perform all these no-op animations. In particularly, this was causing
// the all-apps button to pop in and out.
if (fromView.getVisibility() == View.VISIBLE) {
int width = revealView.getMeasuredWidth();
int height = revealView.getMeasuredHeight();
float revealRadius = (float) Math.hypot(width / 2, height / 2);
revealView.setVisibility(View.VISIBLE);
revealView.setAlpha(1f);
revealView.setTranslationY(0);
layerViews.addView(revealView);
// Calculate the final animation values
int[] buttonViewToPanelDelta = Utilities.getCenterDeltaInScreenSpace(revealView, buttonView);
final float revealViewToXDrift = buttonViewToPanelDelta[0];
final float revealViewToYDrift = buttonViewToPanelDelta[1];
// The vertical motion of the apps panel should be delayed by one frame
// from the conceal animation in order to give the right feel. We correspondingly
// shorten the duration so that the slide and conceal end at the same time.
TimeInterpolator decelerateInterpolator = new LogDecelerateInterpolator(100, 0);
ObjectAnimator panelDriftY = ObjectAnimator.ofFloat(revealView, "translationY",
0, revealViewToYDrift);
panelDriftY.setDuration(revealDuration - SINGLE_FRAME_DELAY);
panelDriftY.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
panelDriftY.setInterpolator(decelerateInterpolator);
animation.play(panelDriftY);
ObjectAnimator panelDriftX = ObjectAnimator.ofFloat(revealView, "translationX",
0, revealViewToXDrift);
panelDriftX.setDuration(revealDuration - SINGLE_FRAME_DELAY);
panelDriftX.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
panelDriftX.setInterpolator(decelerateInterpolator);
animation.play(panelDriftX);
// Setup animation for the reveal panel alpha
if (pCb.materialRevealViewFinalAlpha != 1f) {
ObjectAnimator panelAlpha = ObjectAnimator.ofFloat(revealView, "alpha",
1f, pCb.materialRevealViewFinalAlpha);
panelAlpha.setDuration(revealDuration);
panelAlpha.setInterpolator(decelerateInterpolator);
animation.play(panelAlpha);
}
// Setup the animation for the content view
layerViews.addView(contentView);
// Create the individual animators
ObjectAnimator pageDrift = ObjectAnimator.ofFloat(contentView, "translationY",
0, revealViewToYDrift);
contentView.setTranslationY(0);
pageDrift.setDuration(revealDuration - SINGLE_FRAME_DELAY);
pageDrift.setInterpolator(decelerateInterpolator);
pageDrift.setStartDelay(itemsAlphaStagger + SINGLE_FRAME_DELAY);
animation.play(pageDrift);
contentView.setAlpha(1f);
ObjectAnimator itemsAlpha = ObjectAnimator.ofFloat(contentView, "alpha", 1f, 0f);
itemsAlpha.setDuration(100);
itemsAlpha.setInterpolator(decelerateInterpolator);
animation.play(itemsAlpha);
// Invalidate the scrim throughout the animation to ensure the highlight
// cutout is correct throughout.
ValueAnimator invalidateScrim = ValueAnimator.ofFloat(0f, 1f);
invalidateScrim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mLauncher.getDragLayer().invalidateScrim();
}
});
animation.play(invalidateScrim);
// Animate the all apps button
float finalRadius = pCb.getMaterialRevealViewStartFinalRadius();
AnimatorListenerAdapter listener =
pCb.getMaterialRevealViewAnimatorListener(revealView, buttonView);
Animator reveal = new CircleRevealOutlineProvider(width / 2, height / 2,
revealRadius, finalRadius).createRevealAnimator(revealView);
reveal.setInterpolator(new LogDecelerateInterpolator(100, 0));
reveal.setDuration(revealDuration);
reveal.setStartDelay(itemsAlphaStagger);
if (listener != null) {
reveal.addListener(listener);
}
animation.play(reveal);
}
animation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
fromView.setVisibility(View.GONE);
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
// Reset page transforms
if (contentView != null) {
contentView.setTranslationX(0);
contentView.setTranslationY(0);
contentView.setAlpha(1);
}
// This can hold unnecessary references to views.
cleanupAnimation();
pCb.onTransitionComplete();
}
});
mCurrentAnimation = animation;
mCurrentAnimation.addListener(layerViews);
fromView.post(new StartAnimRunnable(animation, null));
} else if (animType == PULLUP) {
// We are animating the content view alpha, so ensure we have a layer for it
layerViews.addView(contentView);
animation.addListener(new AnimatorListenerAdapter() {
boolean canceled = false;
@Override
public void onAnimationCancel(Animator animation) {
canceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
if (canceled) return;
// Run any queued runnables
if (onCompleteRunnable != null) {
onCompleteRunnable.run();
}
cleanupAnimation();
pCb.onTransitionComplete();
}
});
boolean shouldPost = mAllAppsController.animateToWorkspace(animation, revealDurationSlide);
Runnable startAnimRunnable = new StartAnimRunnable(animation, toView);
mCurrentAnimation = animation;
mCurrentAnimation.addListener(layerViews);
if (shouldPost) {
fromView.post(startAnimRunnable);
} else {
startAnimRunnable.run();
}
}
return;
}
/**
* Cancels the current animation.
*/
private void cancelAnimation() {
if (mCurrentAnimation != null) {
mCurrentAnimation.setDuration(0);
mCurrentAnimation.cancel();
mCurrentAnimation = null;
}
}
@Thunk void cleanupAnimation() {
mCurrentAnimation = null;
}
private class StartAnimRunnable implements Runnable {
private final AnimatorSet mAnim;
private final View mViewToFocus;
public StartAnimRunnable(AnimatorSet anim, View viewToFocus) {
mAnim = anim;
mViewToFocus = viewToFocus;
}
@Override
public void run() {
if (mCurrentAnimation != mAnim) {
return;
}
if (mViewToFocus != null) {
mViewToFocus.requestFocus();
}
mAnim.start();
}
}
}