blob: c8c8fa4c819c32f2e6f93fac45a617965f497d20 [file] [log] [blame]
/*
* Copyright (C) 2016 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.ObjectAnimator;
import android.animation.ValueAnimator;
import android.util.Log;
import android.view.View;
import android.view.animation.LinearInterpolator;
import static com.android.launcher3.Workspace.State.NORMAL;
import static com.android.launcher3.Workspace.State.OVERVIEW;
/**
* Manages the animations that play as the user pinches to/from overview mode.
*
* It will look like this pinching in:
* - Workspace scales down
* - At some threshold 1, hotseat and QSB fade out (full animation)
* - At a later threshold 2, panel buttons fade in and scrim fades in
* - At a final threshold 3, snap to overview
*
* Pinching out:
* - Workspace scales up
* - At threshold 1, panel buttons fade out
* - At threshold 2, hotseat and QSB fade in and scrim fades out
* - At threshold 3, snap to workspace
*
* @see PinchToOverviewListener
* @see PinchThresholdManager
*/
public class PinchAnimationManager {
private static final String TAG = "PinchAnimationManager";
private static final int THRESHOLD_ANIM_DURATION = 150;
private static final LinearInterpolator INTERPOLATOR = new LinearInterpolator();
private static final int INDEX_PAGE_INDICATOR = 0;
private static final int INDEX_HOTSEAT = 1;
private static final int INDEX_OVERVIEW_PANEL_BUTTONS = 2;
private static final int INDEX_SCRIM = 3;
private final Animator[] mAnimators = new Animator[4];
private final int[] mVisiblePageRange = new int[2];
private Launcher mLauncher;
private Workspace mWorkspace;
private float mOverviewScale;
private float mOverviewTranslationY;
private int mNormalOverviewTransitionDuration;
private boolean mIsAnimating;
public PinchAnimationManager(Launcher launcher) {
mLauncher = launcher;
mWorkspace = launcher.mWorkspace;
mOverviewScale = mWorkspace.getOverviewModeShrinkFactor();
mOverviewTranslationY = mWorkspace.getOverviewModeTranslationY();
mNormalOverviewTransitionDuration = mWorkspace.getStateTransitionAnimation()
.mOverviewTransitionTime;
}
public int getNormalOverviewTransitionDuration() {
return mNormalOverviewTransitionDuration;
}
/**
* Interpolate from {@param currentProgress} to {@param toProgress}, calling
* {@link #setAnimationProgress(float)} throughout the duration. If duration is -1,
* the default overview transition duration is used.
*/
public void animateToProgress(float currentProgress, float toProgress, int duration,
final PinchThresholdManager thresholdManager) {
if (duration == -1) {
duration = mNormalOverviewTransitionDuration;
}
ValueAnimator animator = ValueAnimator.ofFloat(currentProgress, toProgress);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float pinchProgress = (Float) animation.getAnimatedValue();
setAnimationProgress(pinchProgress);
thresholdManager.updateAndAnimatePassedThreshold(pinchProgress,
PinchAnimationManager.this);
}
}
);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mIsAnimating = false;
thresholdManager.reset();
mWorkspace.onLauncherTransitionEnd(mLauncher, false, true);
}
});
animator.setDuration(duration).start();
mIsAnimating = true;
}
public boolean isAnimating() {
return mIsAnimating;
}
/**
* Animates to the specified progress. This should be called repeatedly throughout the pinch
* gesture to run animations that interpolate throughout the gesture.
* @param interpolatedProgress The progress from 0 to 1, where 0 is overview and 1 is workspace.
*/
public void setAnimationProgress(float interpolatedProgress) {
float interpolatedScale = interpolatedProgress * (1f - mOverviewScale) + mOverviewScale;
float interpolatedTranslationY = (1f - interpolatedProgress) * mOverviewTranslationY;
mWorkspace.setScaleX(interpolatedScale);
mWorkspace.setScaleY(interpolatedScale);
mWorkspace.setTranslationY(interpolatedTranslationY);
setOverviewPanelsAlpha(1f - interpolatedProgress, 0);
}
/**
* Animates certain properties based on which threshold was passed, and in what direction. The
* starting state must also be taken into account because the thresholds mean different things
* when going from workspace to overview and vice versa.
* @param threshold One of {@link PinchThresholdManager#THRESHOLD_ONE},
* {@link PinchThresholdManager#THRESHOLD_TWO}, or
* {@link PinchThresholdManager#THRESHOLD_THREE}
* @param startState {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}.
* @param goingTowards {@link Workspace.State#NORMAL} or {@link Workspace.State#OVERVIEW}.
* Note that this doesn't have to be the opposite of startState;
*/
public void animateThreshold(float threshold, Workspace.State startState,
Workspace.State goingTowards) {
if (threshold == PinchThresholdManager.THRESHOLD_ONE) {
if (startState == OVERVIEW) {
animateOverviewPanelButtons(goingTowards == OVERVIEW);
} else if (startState == NORMAL) {
animateHotseatAndPageIndicator(goingTowards == NORMAL);
animateQsb(goingTowards == NORMAL);
}
} else if (threshold == PinchThresholdManager.THRESHOLD_TWO) {
if (startState == OVERVIEW) {
animateHotseatAndPageIndicator(goingTowards == NORMAL);
animateQsb(goingTowards == NORMAL);
animateScrim(goingTowards == OVERVIEW);
} else if (startState == NORMAL) {
animateOverviewPanelButtons(goingTowards == OVERVIEW);
animateScrim(goingTowards == OVERVIEW);
}
} else if (threshold == PinchThresholdManager.THRESHOLD_THREE) {
// Passing threshold 3 ends the pinch and snaps to the new state.
if (startState == OVERVIEW && goingTowards == NORMAL) {
mLauncher.showWorkspace(true);
mWorkspace.snapToPage(mWorkspace.getCurrentPage());
} else if (startState == NORMAL && goingTowards == OVERVIEW) {
mLauncher.showOverviewMode(true);
}
} else {
Log.e(TAG, "Received unknown threshold to animate: " + threshold);
}
}
private void setOverviewPanelsAlpha(float alpha, int duration) {
mWorkspace.getVisiblePages(mVisiblePageRange);
for (int i = mVisiblePageRange[0]; i <= mVisiblePageRange[1]; i++) {
View page = mWorkspace.getPageAt(i);
if (!mWorkspace.shouldDrawChild(page)) {
continue;
}
if (duration == 0) {
((CellLayout) page).setBackgroundAlpha(alpha);
} else {
ObjectAnimator.ofFloat(page, "backgroundAlpha", alpha)
.setDuration(duration).start();
}
}
}
private void animateHotseatAndPageIndicator(boolean show) {
animateShowHideView(INDEX_HOTSEAT, mLauncher.getHotseat(), show);
if (mWorkspace.getPageIndicator() != null) {
// There aren't page indicators in landscape mode on phones, hence the null check.
animateShowHideView(INDEX_PAGE_INDICATOR, mWorkspace.getPageIndicator(), show);
}
}
private void animateQsb(boolean show) {
SearchDropTargetBar.State searchBarState = show ? SearchDropTargetBar.State.SEARCH_BAR
: SearchDropTargetBar.State.INVISIBLE;
mLauncher.getSearchDropTargetBar().animateToState(searchBarState, THRESHOLD_ANIM_DURATION);
}
private void animateOverviewPanelButtons(boolean show) {
animateShowHideView(INDEX_OVERVIEW_PANEL_BUTTONS, mLauncher.getOverviewPanel(), show);
}
private void animateScrim(boolean show) {
float endValue = show ? mWorkspace.getStateTransitionAnimation().mWorkspaceScrimAlpha : 0;
startAnimator(INDEX_SCRIM,
ObjectAnimator.ofFloat(mLauncher.getDragLayer(), "backgroundAlpha", endValue),
mNormalOverviewTransitionDuration);
}
private void animateShowHideView(int index, final View view, boolean show) {
Animator animator = new LauncherViewPropertyAnimator(view).alpha(show ? 1 : 0).withLayer();
if (show) {
view.setVisibility(View.VISIBLE);
} else {
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.INVISIBLE);
}
});
}
startAnimator(index, animator, THRESHOLD_ANIM_DURATION);
}
private void startAnimator(int index, Animator animator, long duration) {
if (mAnimators[index] != null) {
mAnimators[index].removeAllListeners();
mAnimators[index].cancel();
}
mAnimators[index] = animator;
mAnimators[index].setInterpolator(INTERPOLATOR);
mAnimators[index].setDuration(duration).start();
}
}