/*
|
* 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 static com.android.launcher3.LauncherState.NORMAL;
|
import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER;
|
|
import android.animation.Animator;
|
import android.animation.AnimatorListenerAdapter;
|
import android.animation.AnimatorSet;
|
import android.os.Handler;
|
import android.os.Looper;
|
import android.util.Log;
|
|
import com.android.launcher3.anim.AnimationSuccessListener;
|
import com.android.launcher3.anim.AnimatorPlaybackController;
|
import com.android.launcher3.anim.AnimatorSetBuilder;
|
import com.android.launcher3.anim.PropertySetter;
|
import com.android.launcher3.anim.PropertySetter.AnimatedPropertySetter;
|
import com.android.launcher3.compat.AccessibilityManagerCompat;
|
import com.android.launcher3.testing.TestProtocol;
|
import com.android.launcher3.uioverrides.UiFactory;
|
|
import java.io.PrintWriter;
|
import java.lang.annotation.Retention;
|
import java.lang.annotation.RetentionPolicy;
|
import java.util.ArrayList;
|
|
import androidx.annotation.IntDef;
|
|
/**
|
* 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 (becase it causes an extra configuration changed)
|
* - From all apps
|
* - From the center workspace
|
* - From another workspace
|
*/
|
public class LauncherStateManager {
|
|
public static final String TAG = "StateManager";
|
|
// We separate the state animations into "atomic" and "non-atomic" components. The atomic
|
// components may be run atomically - that is, all at once, instead of user-controlled. However,
|
// atomic components are not restricted to this purpose; they can be user-controlled alongside
|
// non atomic components as well. Note that each gesture model has exactly one atomic component,
|
// ATOMIC_OVERVIEW_SCALE_COMPONENT *or* ATOMIC_OVERVIEW_PEEK_COMPONENT.
|
@IntDef(flag = true, value = {
|
NON_ATOMIC_COMPONENT,
|
ATOMIC_OVERVIEW_SCALE_COMPONENT,
|
ATOMIC_OVERVIEW_PEEK_COMPONENT,
|
})
|
@Retention(RetentionPolicy.SOURCE)
|
public @interface AnimationComponents {}
|
public static final int NON_ATOMIC_COMPONENT = 1 << 0;
|
public static final int ATOMIC_OVERVIEW_SCALE_COMPONENT = 1 << 1;
|
public static final int ATOMIC_OVERVIEW_PEEK_COMPONENT = 1 << 2;
|
|
public static final int ANIM_ALL = NON_ATOMIC_COMPONENT | ATOMIC_OVERVIEW_SCALE_COMPONENT
|
| ATOMIC_OVERVIEW_PEEK_COMPONENT;
|
|
private final AnimationConfig mConfig = new AnimationConfig();
|
private final Handler mUiHandler;
|
private final Launcher mLauncher;
|
private final ArrayList<StateListener> mListeners = new ArrayList<>();
|
|
// Animators which are run on properties also controlled by state animations.
|
private Animator[] mStateElementAnimators;
|
|
private StateHandler[] mStateHandlers;
|
private LauncherState mState = NORMAL;
|
|
private LauncherState mLastStableState = NORMAL;
|
private LauncherState mCurrentStableState = NORMAL;
|
|
private LauncherState mRestState;
|
|
public LauncherStateManager(Launcher l) {
|
mUiHandler = new Handler(Looper.getMainLooper());
|
mLauncher = l;
|
}
|
|
public LauncherState getState() {
|
return mState;
|
}
|
|
public LauncherState getCurrentStableState() {
|
return mCurrentStableState;
|
}
|
|
public void dump(String prefix, PrintWriter writer) {
|
writer.println(prefix + "LauncherState");
|
writer.println(prefix + "\tmLastStableState:" + mLastStableState);
|
writer.println(prefix + "\tmCurrentStableState:" + mCurrentStableState);
|
writer.println(prefix + "\tmState:" + mState);
|
writer.println(prefix + "\tmRestState:" + mRestState);
|
writer.println(prefix + "\tisInTransition:" + (mConfig.mCurrentAnimation != null));
|
}
|
|
public StateHandler[] getStateHandlers() {
|
if (mStateHandlers == null) {
|
mStateHandlers = UiFactory.getStateHandler(mLauncher);
|
}
|
return mStateHandlers;
|
}
|
|
public void addStateListener(StateListener listener) {
|
mListeners.add(listener);
|
}
|
|
public void removeStateListener(StateListener listener) {
|
mListeners.remove(listener);
|
}
|
|
/**
|
* Returns true if the state changes should be animated.
|
*/
|
public boolean shouldAnimateStateChange() {
|
return !mLauncher.isForceInvisible() && mLauncher.isStarted();
|
}
|
|
/**
|
* @see #goToState(LauncherState, boolean, Runnable)
|
*/
|
public void goToState(LauncherState state) {
|
goToState(state, shouldAnimateStateChange());
|
}
|
|
/**
|
* @see #goToState(LauncherState, boolean, Runnable)
|
*/
|
public void goToState(LauncherState state, boolean animated) {
|
goToState(state, animated, 0, null);
|
}
|
|
/**
|
* Changes the Launcher state to the provided state.
|
*
|
* @param animated false if the state should change immediately without any animation,
|
* true otherwise
|
* @paras onCompleteRunnable any action to perform at the end of the transition, of null.
|
*/
|
public void goToState(LauncherState state, boolean animated, Runnable onCompleteRunnable) {
|
goToState(state, animated, 0, onCompleteRunnable);
|
}
|
|
/**
|
* Changes the Launcher state to the provided state after the given delay.
|
*/
|
public void goToState(LauncherState state, long delay, Runnable onCompleteRunnable) {
|
goToState(state, true, delay, onCompleteRunnable);
|
}
|
|
/**
|
* Changes the Launcher state to the provided state after the given delay.
|
*/
|
public void goToState(LauncherState state, long delay) {
|
goToState(state, true, delay, null);
|
}
|
|
public void reapplyState() {
|
reapplyState(false);
|
}
|
|
public void reapplyState(boolean cancelCurrentAnimation) {
|
boolean wasInAnimation = mConfig.mCurrentAnimation != null;
|
if (cancelCurrentAnimation) {
|
cancelAllStateElementAnimation();
|
cancelAnimation();
|
}
|
if (mConfig.mCurrentAnimation == null) {
|
for (StateHandler handler : getStateHandlers()) {
|
handler.setState(mState);
|
}
|
if (wasInAnimation) {
|
onStateTransitionEnd(mState);
|
}
|
}
|
}
|
|
private void goToState(LauncherState state, boolean animated, long delay,
|
final Runnable onCompleteRunnable) {
|
animated &= Utilities.areAnimationsEnabled(mLauncher);
|
if (mLauncher.isInState(state)) {
|
if (mConfig.mCurrentAnimation == null) {
|
// Run any queued runnable
|
if (onCompleteRunnable != null) {
|
onCompleteRunnable.run();
|
}
|
return;
|
} else if (!mConfig.userControlled && animated && mConfig.mTargetState == state) {
|
// We are running the same animation as requested
|
if (onCompleteRunnable != null) {
|
mConfig.mCurrentAnimation.addListener(new AnimationSuccessListener() {
|
@Override
|
public void onAnimationSuccess(Animator animator) {
|
onCompleteRunnable.run();
|
}
|
});
|
}
|
return;
|
}
|
}
|
|
// Cancel the current animation. This will reset mState to mCurrentStableState, so store it.
|
LauncherState fromState = mState;
|
mConfig.reset();
|
|
if (!animated) {
|
cancelAllStateElementAnimation();
|
onStateTransitionStart(state);
|
for (StateHandler handler : getStateHandlers()) {
|
handler.setState(state);
|
}
|
|
onStateTransitionEnd(state);
|
|
// Run any queued runnable
|
if (onCompleteRunnable != null) {
|
onCompleteRunnable.run();
|
}
|
return;
|
}
|
|
if (delay > 0) {
|
// Create the animation after the delay as some properties can change between preparing
|
// the animation and running the animation.
|
int startChangeId = mConfig.mChangeId;
|
mUiHandler.postDelayed(() -> {
|
if (mConfig.mChangeId == startChangeId) {
|
goToStateAnimated(state, fromState, onCompleteRunnable);
|
}
|
}, delay);
|
} else {
|
goToStateAnimated(state, fromState, onCompleteRunnable);
|
}
|
}
|
|
private void goToStateAnimated(LauncherState state, LauncherState fromState,
|
Runnable onCompleteRunnable) {
|
// Since state NORMAL can be reached from multiple states, just assume that the
|
// transition plays in reverse and use the same duration as previous state.
|
mConfig.duration = state == NORMAL ? fromState.transitionDuration : state.transitionDuration;
|
|
AnimatorSetBuilder builder = new AnimatorSetBuilder();
|
prepareForAtomicAnimation(fromState, state, builder);
|
AnimatorSet animation = createAnimationToNewWorkspaceInternal(
|
state, builder, onCompleteRunnable);
|
mUiHandler.post(new StartAnimRunnable(animation));
|
}
|
|
/**
|
* Prepares for a non-user controlled animation from fromState to toState. Preparations include:
|
* - Setting interpolators for various animations included in the state transition.
|
* - Setting some start values (e.g. scale) for views that are hidden but about to be shown.
|
*/
|
public void prepareForAtomicAnimation(LauncherState fromState, LauncherState toState,
|
AnimatorSetBuilder builder) {
|
toState.prepareForAtomicAnimation(mLauncher, fromState, builder);
|
}
|
|
public AnimatorSet createAtomicAnimation(LauncherState fromState, LauncherState toState,
|
AnimatorSetBuilder builder, @AnimationComponents int atomicComponent, long duration) {
|
prepareForAtomicAnimation(fromState, toState, builder);
|
AnimationConfig config = new AnimationConfig();
|
config.animComponents = atomicComponent;
|
config.duration = duration;
|
for (StateHandler handler : mLauncher.getStateManager().getStateHandlers()) {
|
handler.setStateWithAnimation(toState, builder, config);
|
}
|
return builder.build();
|
}
|
|
/**
|
* Creates a {@link AnimatorPlaybackController} that can be used for a controlled
|
* state transition. The UI is force-set to fromState before creating the controller.
|
* @param fromState the initial state for the transition.
|
* @param state the final state for the transition.
|
* @param duration intended duration for normal playback. Use higher duration for better
|
* accuracy.
|
*/
|
public AnimatorPlaybackController createAnimationToNewWorkspace(
|
LauncherState fromState, LauncherState state, long duration) {
|
// Since we are creating a state animation to a different state, temporarily prevent state
|
// change as part of config reset.
|
LauncherState originalRestState = mRestState;
|
mRestState = state;
|
mConfig.reset();
|
mRestState = originalRestState;
|
|
for (StateHandler handler : getStateHandlers()) {
|
handler.setState(fromState);
|
}
|
|
return createAnimationToNewWorkspace(state, duration);
|
}
|
|
/**
|
* Creates a {@link AnimatorPlaybackController} that can be used for a controlled
|
* state transition.
|
* @param state the final state for the transition.
|
* @param duration intended duration for normal playback. Use higher duration for better
|
* accuracy.
|
*/
|
public AnimatorPlaybackController createAnimationToNewWorkspace(
|
LauncherState state, long duration) {
|
return createAnimationToNewWorkspace(state, duration, LauncherStateManager.ANIM_ALL);
|
}
|
|
public AnimatorPlaybackController createAnimationToNewWorkspace(
|
LauncherState state, long duration, @AnimationComponents int animComponents) {
|
return createAnimationToNewWorkspace(state, new AnimatorSetBuilder(), duration, null,
|
animComponents);
|
}
|
|
public AnimatorPlaybackController createAnimationToNewWorkspace(LauncherState state,
|
AnimatorSetBuilder builder, long duration, Runnable onCancelRunnable,
|
@AnimationComponents int animComponents) {
|
mConfig.reset();
|
mConfig.userControlled = true;
|
mConfig.animComponents = animComponents;
|
mConfig.duration = duration;
|
mConfig.playbackController = AnimatorPlaybackController.wrap(
|
createAnimationToNewWorkspaceInternal(state, builder, null), duration,
|
onCancelRunnable);
|
return mConfig.playbackController;
|
}
|
|
protected AnimatorSet createAnimationToNewWorkspaceInternal(final LauncherState state,
|
AnimatorSetBuilder builder, final Runnable onCompleteRunnable) {
|
|
for (StateHandler handler : getStateHandlers()) {
|
handler.setStateWithAnimation(state, builder, mConfig);
|
}
|
|
final AnimatorSet animation = builder.build();
|
animation.addListener(new AnimationSuccessListener() {
|
|
@Override
|
public void onAnimationStart(Animator animation) {
|
// Change the internal state only when the transition actually starts
|
onStateTransitionStart(state);
|
}
|
|
@Override
|
public void onAnimationSuccess(Animator animator) {
|
// Run any queued runnables
|
if (onCompleteRunnable != null) {
|
onCompleteRunnable.run();
|
}
|
onStateTransitionEnd(state);
|
}
|
});
|
mConfig.setAnimation(animation, state);
|
return mConfig.mCurrentAnimation;
|
}
|
|
private void onStateTransitionStart(LauncherState state) {
|
if (TestProtocol.sDebugTracing) {
|
android.util.Log.d(TestProtocol.NO_DRAG_TAG,
|
"onStateTransitionStart");
|
}
|
if (mState != state) {
|
mState.onStateDisabled(mLauncher);
|
}
|
mState = state;
|
mState.onStateEnabled(mLauncher);
|
mLauncher.onStateSet(mState);
|
|
if (state.disablePageClipping) {
|
// Only disable clipping if needed, otherwise leave it as previous value.
|
mLauncher.getWorkspace().setClipChildren(false);
|
}
|
UiFactory.onLauncherStateOrResumeChanged(mLauncher);
|
|
for (int i = mListeners.size() - 1; i >= 0; i--) {
|
mListeners.get(i).onStateTransitionStart(state);
|
}
|
}
|
|
private void onStateTransitionEnd(LauncherState state) {
|
// Only change the stable states after the transitions have finished
|
if (state != mCurrentStableState) {
|
mLastStableState = state.getHistoryForState(mCurrentStableState);
|
if (TestProtocol.sDebugTracing) {
|
Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG,
|
"mCurrentStableState = " + state.getClass().getSimpleName() + " @ " +
|
android.util.Log.getStackTraceString(new Throwable()));
|
}
|
mCurrentStableState = state;
|
}
|
|
state.onStateTransitionEnd(mLauncher);
|
mLauncher.getWorkspace().setClipChildren(!state.disablePageClipping);
|
mLauncher.finishAutoCancelActionMode();
|
|
if (state == NORMAL) {
|
setRestState(null);
|
}
|
|
UiFactory.onLauncherStateOrResumeChanged(mLauncher);
|
|
for (int i = mListeners.size() - 1; i >= 0; i--) {
|
mListeners.get(i).onStateTransitionComplete(state);
|
}
|
|
AccessibilityManagerCompat.sendStateEventToTest(mLauncher, state.ordinal);
|
}
|
|
public void onWindowFocusChanged() {
|
UiFactory.onLauncherStateOrFocusChanged(mLauncher);
|
}
|
|
public LauncherState getLastState() {
|
return mLastStableState;
|
}
|
|
public void moveToRestState() {
|
if (mConfig.mCurrentAnimation != null && mConfig.userControlled) {
|
// The user is doing something. Lets not mess it up
|
return;
|
}
|
if (mState.disableRestore) {
|
goToState(getRestState());
|
// Reset history
|
mLastStableState = NORMAL;
|
}
|
}
|
|
public LauncherState getRestState() {
|
return mRestState == null ? NORMAL : mRestState;
|
}
|
|
public void setRestState(LauncherState restState) {
|
mRestState = restState;
|
}
|
|
/**
|
* Cancels the current animation.
|
*/
|
public void cancelAnimation() {
|
mConfig.reset();
|
}
|
|
public void setCurrentUserControlledAnimation(AnimatorPlaybackController controller) {
|
clearCurrentAnimation();
|
setCurrentAnimation(controller.getTarget());
|
mConfig.userControlled = true;
|
mConfig.playbackController = controller;
|
}
|
|
/**
|
* Sets the animation as the current state animation, i.e., canceled when
|
* starting another animation and may block some launcher interactions while running.
|
*
|
* @param childAnimations Set of animations with the new target is controlling.
|
*/
|
public void setCurrentAnimation(AnimatorSet anim, Animator... childAnimations) {
|
for (Animator childAnim : childAnimations) {
|
if (childAnim == null) {
|
continue;
|
}
|
if (mConfig.playbackController != null
|
&& mConfig.playbackController.getTarget() == childAnim) {
|
clearCurrentAnimation();
|
break;
|
} else if (mConfig.mCurrentAnimation == childAnim) {
|
clearCurrentAnimation();
|
break;
|
}
|
}
|
boolean reapplyNeeded = mConfig.mCurrentAnimation != null;
|
cancelAnimation();
|
if (reapplyNeeded) {
|
reapplyState();
|
// Dispatch on transition end, so that any transient property is cleared.
|
onStateTransitionEnd(mState);
|
}
|
mConfig.setAnimation(anim, null);
|
}
|
|
private void cancelAllStateElementAnimation() {
|
if (mStateElementAnimators == null) {
|
return;
|
}
|
|
for (Animator animator : mStateElementAnimators) {
|
if (animator != null) {
|
animator.cancel();
|
}
|
}
|
}
|
|
/**
|
* Cancels a currently running gesture animation
|
*/
|
public void cancelStateElementAnimation(int index) {
|
if (mStateElementAnimators == null) {
|
return;
|
}
|
if (mStateElementAnimators[index] != null) {
|
mStateElementAnimators[index].cancel();
|
}
|
}
|
|
public Animator createStateElementAnimation(int index, float... values) {
|
cancelStateElementAnimation(index);
|
LauncherAppTransitionManager latm = mLauncher.getAppTransitionManager();
|
if (mStateElementAnimators == null) {
|
mStateElementAnimators = new Animator[latm.getStateElementAnimationsCount()];
|
}
|
Animator anim = latm.createStateElementAnimation(index, values);
|
mStateElementAnimators[index] = anim;
|
anim.addListener(new AnimatorListenerAdapter() {
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
mStateElementAnimators[index] = null;
|
}
|
});
|
return anim;
|
}
|
|
private void clearCurrentAnimation() {
|
if (mConfig.mCurrentAnimation != null) {
|
mConfig.mCurrentAnimation.removeListener(mConfig);
|
mConfig.mCurrentAnimation = null;
|
}
|
mConfig.playbackController = null;
|
}
|
|
private class StartAnimRunnable implements Runnable {
|
|
private final AnimatorSet mAnim;
|
|
public StartAnimRunnable(AnimatorSet anim) {
|
if (TestProtocol.sDebugTracing) {
|
android.util.Log.d(TestProtocol.NO_DRAG_TAG,
|
"StartAnimRunnable");
|
}
|
mAnim = anim;
|
}
|
|
@Override
|
public void run() {
|
if (mConfig.mCurrentAnimation != mAnim) {
|
return;
|
}
|
mAnim.start();
|
}
|
}
|
|
public static class AnimationConfig extends AnimatorListenerAdapter {
|
public long duration;
|
public boolean userControlled;
|
public AnimatorPlaybackController playbackController;
|
public @AnimationComponents int animComponents = ANIM_ALL;
|
private PropertySetter mPropertySetter;
|
|
private AnimatorSet mCurrentAnimation;
|
private LauncherState mTargetState;
|
// Id to keep track of config changes, to tie an animation with the corresponding request
|
private int mChangeId = 0;
|
|
/**
|
* Cancels the current animation and resets config variables.
|
*/
|
public void reset() {
|
duration = 0;
|
userControlled = false;
|
animComponents = ANIM_ALL;
|
mPropertySetter = null;
|
mTargetState = null;
|
|
if (playbackController != null) {
|
playbackController.getAnimationPlayer().cancel();
|
playbackController.dispatchOnCancel();
|
} else if (mCurrentAnimation != null) {
|
mCurrentAnimation.setDuration(0);
|
mCurrentAnimation.cancel();
|
}
|
|
mCurrentAnimation = null;
|
playbackController = null;
|
mChangeId ++;
|
}
|
|
public PropertySetter getPropertySetter(AnimatorSetBuilder builder) {
|
if (mPropertySetter == null) {
|
mPropertySetter = duration == 0 ? NO_ANIM_PROPERTY_SETTER
|
: new AnimatedPropertySetter(duration, builder);
|
}
|
return mPropertySetter;
|
}
|
|
@Override
|
public void onAnimationEnd(Animator animation) {
|
if (playbackController != null && playbackController.getTarget() == animation) {
|
playbackController = null;
|
}
|
if (mCurrentAnimation == animation) {
|
mCurrentAnimation = null;
|
}
|
}
|
|
public void setAnimation(AnimatorSet animation, LauncherState targetState) {
|
mCurrentAnimation = animation;
|
mTargetState = targetState;
|
mCurrentAnimation.addListener(this);
|
}
|
|
public boolean playAtomicOverviewScaleComponent() {
|
return (animComponents & ATOMIC_OVERVIEW_SCALE_COMPONENT) != 0;
|
}
|
|
public boolean playAtomicOverviewPeekComponent() {
|
return (animComponents & ATOMIC_OVERVIEW_PEEK_COMPONENT) != 0;
|
}
|
|
public boolean playNonAtomicComponent() {
|
return (animComponents & NON_ATOMIC_COMPONENT) != 0;
|
}
|
}
|
|
public interface StateHandler {
|
|
/**
|
* Updates the UI to {@param state} without any animations
|
*/
|
void setState(LauncherState state);
|
|
/**
|
* Sets the UI to {@param state} by animating any changes.
|
*/
|
void setStateWithAnimation(LauncherState toState,
|
AnimatorSetBuilder builder, AnimationConfig config);
|
}
|
|
public interface StateListener {
|
|
void onStateTransitionStart(LauncherState toState);
|
void onStateTransitionComplete(LauncherState finalState);
|
}
|
}
|