blob: 888cc578323e2d6763f17dd8131292c5cbc90792 [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.TimeInterpolator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.DecelerateInterpolator;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.util.Thunk;
import java.util.HashMap;
/**
* A convenience class to update a view's visibility state after an alpha animation.
*/
class AlphaUpdateListener extends AnimatorListenerAdapter implements ValueAnimator.AnimatorUpdateListener {
private static final float ALPHA_CUTOFF_THRESHOLD = 0.01f;
private View mView;
private boolean mAccessibilityEnabled;
private boolean mCanceled = false;
public AlphaUpdateListener(View v, boolean accessibilityEnabled) {
mView = v;
mAccessibilityEnabled = accessibilityEnabled;
}
@Override
public void onAnimationUpdate(ValueAnimator arg0) {
updateVisibility(mView, mAccessibilityEnabled);
}
public static void updateVisibility(View view, boolean accessibilityEnabled) {
// We want to avoid the extra layout pass by setting the views to GONE unless
// accessibility is on, in which case not setting them to GONE causes a glitch.
int invisibleState = accessibilityEnabled ? View.GONE : View.INVISIBLE;
if (view.getAlpha() < ALPHA_CUTOFF_THRESHOLD && view.getVisibility() != invisibleState) {
view.setVisibility(invisibleState);
} else if (view.getAlpha() > ALPHA_CUTOFF_THRESHOLD
&& view.getVisibility() != View.VISIBLE) {
view.setVisibility(View.VISIBLE);
}
}
@Override
public void onAnimationCancel(Animator animation) {
mCanceled = true;
}
@Override
public void onAnimationEnd(Animator arg0) {
if (mCanceled) return;
updateVisibility(mView, mAccessibilityEnabled);
}
@Override
public void onAnimationStart(Animator arg0) {
// We want the views to be visible for animation, so fade-in/out is visible
mView.setVisibility(View.VISIBLE);
}
}
/**
* This interpolator emulates the rate at which the perceived scale of an object changes
* as its distance from a camera increases. When this interpolator is applied to a scale
* animation on a view, it evokes the sense that the object is shrinking due to moving away
* from the camera.
*/
class ZInterpolator implements TimeInterpolator {
private float focalLength;
public ZInterpolator(float foc) {
focalLength = foc;
}
public float getInterpolation(float input) {
return (1.0f - focalLength / (focalLength + input)) /
(1.0f - focalLength / (focalLength + 1.0f));
}
}
/**
* The exact reverse of ZInterpolator.
*/
class InverseZInterpolator implements TimeInterpolator {
private ZInterpolator zInterpolator;
public InverseZInterpolator(float foc) {
zInterpolator = new ZInterpolator(foc);
}
public float getInterpolation(float input) {
return 1 - zInterpolator.getInterpolation(1 - input);
}
}
/**
* InverseZInterpolator compounded with an ease-out.
*/
class ZoomInInterpolator implements TimeInterpolator {
private final InverseZInterpolator inverseZInterpolator = new InverseZInterpolator(0.35f);
private final DecelerateInterpolator decelerate = new DecelerateInterpolator(3.0f);
public float getInterpolation(float input) {
return decelerate.getInterpolation(inverseZInterpolator.getInterpolation(input));
}
}
/**
* Stores the transition states for convenience.
*/
class TransitionStates {
// Raw states
final boolean oldStateIsNormal;
final boolean oldStateIsSpringLoaded;
final boolean oldStateIsNormalHidden;
final boolean oldStateIsOverviewHidden;
final boolean oldStateIsOverview;
final boolean stateIsNormal;
final boolean stateIsSpringLoaded;
final boolean stateIsNormalHidden;
final boolean stateIsOverviewHidden;
final boolean stateIsOverview;
// Convenience members
final boolean workspaceToAllApps;
final boolean overviewToAllApps;
final boolean allAppsToWorkspace;
final boolean workspaceToOverview;
final boolean overviewToWorkspace;
public TransitionStates(final Workspace.State fromState, final Workspace.State toState) {
oldStateIsNormal = (fromState == Workspace.State.NORMAL);
oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED);
oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN);
oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN);
oldStateIsOverview = (fromState == Workspace.State.OVERVIEW);
stateIsNormal = (toState == Workspace.State.NORMAL);
stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED);
stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN);
stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN);
stateIsOverview = (toState == Workspace.State.OVERVIEW);
workspaceToOverview = (oldStateIsNormal && stateIsOverview);
workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden);
overviewToWorkspace = (oldStateIsOverview && stateIsNormal);
overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden);
allAppsToWorkspace = (oldStateIsNormalHidden && stateIsNormal);
}
}
/**
* Manages the animations between each of the workspace states.
*/
public class WorkspaceStateTransitionAnimation {
public static final String TAG = "WorkspaceStateTransitionAnimation";
@Thunk static final int BACKGROUND_FADE_OUT_DURATION = 350;
final @Thunk Launcher mLauncher;
final @Thunk Workspace mWorkspace;
@Thunk AnimatorSet mStateAnimator;
@Thunk float[] mOldBackgroundAlphas;
@Thunk float[] mOldAlphas;
@Thunk float[] mNewBackgroundAlphas;
@Thunk float[] mNewAlphas;
@Thunk int mLastChildCount = -1;
@Thunk float mCurrentScale;
@Thunk float mNewScale;
@Thunk final ZoomInInterpolator mZoomInInterpolator = new ZoomInInterpolator();
@Thunk float mSpringLoadedShrinkFactor;
@Thunk float mOverviewModeShrinkFactor;
@Thunk float mWorkspaceScrimAlpha;
@Thunk int mAllAppsTransitionTime;
@Thunk int mOverviewTransitionTime;
@Thunk int mOverlayTransitionTime;
@Thunk int mSpringLoadedTransitionTime;
@Thunk boolean mWorkspaceFadeInAdjacentScreens;
public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) {
mLauncher = launcher;
mWorkspace = workspace;
DeviceProfile grid = mLauncher.getDeviceProfile();
Resources res = launcher.getResources();
mAllAppsTransitionTime = res.getInteger(R.integer.config_allAppsTransitionTime);
mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime);
mOverlayTransitionTime = res.getInteger(R.integer.config_overlayTransitionTime);
mSpringLoadedTransitionTime = mOverlayTransitionTime / 2;
mSpringLoadedShrinkFactor = mLauncher.getDeviceProfile().workspaceSpringLoadShrinkFactor;
mOverviewModeShrinkFactor =
res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100f;
mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f;
mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
}
public void snapToPageFromOverView(int whichPage) {
mWorkspace.snapToPage(whichPage, mOverviewTransitionTime, mZoomInInterpolator);
}
public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState,
boolean animated, HashMap<View, Integer> layerViews) {
AccessibilityManager am = (AccessibilityManager)
mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE);
final boolean accessibilityEnabled = am.isEnabled();
TransitionStates states = new TransitionStates(fromState, toState);
int workspaceDuration = getAnimationDuration(states);
animateWorkspace(states, animated, workspaceDuration, layerViews,
accessibilityEnabled);
animateBackgroundGradient(states, animated, BACKGROUND_FADE_OUT_DURATION);
return mStateAnimator;
}
public float getFinalScale() {
return mNewScale;
}
/**
* Reinitializes the arrays that we need for the animations on each page.
*/
private void reinitializeAnimationArrays() {
final int childCount = mWorkspace.getChildCount();
if (mLastChildCount == childCount) return;
mOldBackgroundAlphas = new float[childCount];
mOldAlphas = new float[childCount];
mNewBackgroundAlphas = new float[childCount];
mNewAlphas = new float[childCount];
}
/**
* Returns the proper animation duration for a transition.
*/
private int getAnimationDuration(TransitionStates states) {
if (states.workspaceToAllApps || states.overviewToAllApps) {
return mAllAppsTransitionTime;
} else if (states.workspaceToOverview || states.overviewToWorkspace) {
return mOverviewTransitionTime;
} else if (mLauncher.mState == Launcher.State.WORKSPACE_SPRING_LOADED
|| states.oldStateIsNormal && states.stateIsSpringLoaded) {
return mSpringLoadedTransitionTime;
} else {
return mOverlayTransitionTime;
}
}
/**
* Starts a transition animation for the workspace.
*/
private void animateWorkspace(final TransitionStates states, final boolean animated,
final int duration, final HashMap<View, Integer> layerViews,
final boolean accessibilityEnabled) {
// Reinitialize animation arrays for the current workspace state
reinitializeAnimationArrays();
// Cancel existing workspace animations and create a new animator set if requested
cancelAnimation();
if (animated) {
mStateAnimator = LauncherAnimUtils.createAnimatorSet();
}
// Update the workspace state
float finalBackgroundAlpha = (states.stateIsSpringLoaded || states.stateIsOverview) ?
1.0f : 0f;
float finalHotseatAlpha = (states.stateIsNormal || states.stateIsSpringLoaded) ? 1f : 0f;
float finalPageIndicatorAlpha = finalHotseatAlpha;
float finalOverviewPanelAlpha = states.stateIsOverview ? 1f : 0f;
float finalWorkspaceTranslationY = 0;
if (states.stateIsOverview || states.stateIsOverviewHidden) {
finalWorkspaceTranslationY = mWorkspace.getOverviewModeTranslationY();
} else if (states.stateIsSpringLoaded) {
finalWorkspaceTranslationY = mWorkspace.getSpringLoadedTranslationY();
}
final int childCount = mWorkspace.getChildCount();
final int customPageCount = mWorkspace.numCustomPages();
mNewScale = 1.0f;
if (states.oldStateIsOverview) {
mWorkspace.disableFreeScroll();
} else if (states.stateIsOverview) {
mWorkspace.enableFreeScroll();
}
if (!states.stateIsNormal) {
if (states.stateIsSpringLoaded) {
mNewScale = mSpringLoadedShrinkFactor;
} else if (states.stateIsOverview || states.stateIsOverviewHidden) {
mNewScale = mOverviewModeShrinkFactor;
}
}
int toPage = mWorkspace.getPageNearestToCenterOfScreen();
for (int i = 0; i < childCount; i++) {
final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
boolean isCurrentPage = (i == toPage);
float initialAlpha = cl.getShortcutsAndWidgets().getAlpha();
float finalAlpha;
if (states.stateIsOverviewHidden) {
finalAlpha = 0f;
} else if(states.stateIsNormalHidden) {
finalAlpha = FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP ? 1 : 0;
} else if (states.stateIsNormal && mWorkspaceFadeInAdjacentScreens) {
finalAlpha = (i == toPage || i < customPageCount) ? 1f : 0f;
} else {
finalAlpha = 1f;
}
// If we are animating to/from the small state, then hide the side pages and fade the
// current page in
if (!mWorkspace.isSwitchingState()) {
if (states.workspaceToAllApps || states.allAppsToWorkspace) {
if (states.allAppsToWorkspace && isCurrentPage) {
initialAlpha = 0f;
} else if (!isCurrentPage) {
initialAlpha = finalAlpha = 0f;
}
cl.setShortcutAndWidgetAlpha(initialAlpha);
}
}
mOldAlphas[i] = initialAlpha;
mNewAlphas[i] = finalAlpha;
if (animated) {
mOldBackgroundAlphas[i] = cl.getBackgroundAlpha();
mNewBackgroundAlphas[i] = finalBackgroundAlpha;
} else {
cl.setBackgroundAlpha(finalBackgroundAlpha);
cl.setShortcutAndWidgetAlpha(finalAlpha);
}
}
final ViewGroup overviewPanel = mLauncher.getOverviewPanel();
final View hotseat = mLauncher.getHotseat();
final View pageIndicator = mWorkspace.getPageIndicator();
if (animated) {
LauncherViewPropertyAnimator scale = new LauncherViewPropertyAnimator(mWorkspace);
scale.scaleX(mNewScale)
.scaleY(mNewScale)
.translationY(finalWorkspaceTranslationY)
.setDuration(duration)
.setInterpolator(mZoomInInterpolator);
mStateAnimator.play(scale);
for (int index = 0; index < childCount; index++) {
final int i = index;
final CellLayout cl = (CellLayout) mWorkspace.getChildAt(i);
float currentAlpha = cl.getShortcutsAndWidgets().getAlpha();
if (mOldAlphas[i] == 0 && mNewAlphas[i] == 0) {
cl.setBackgroundAlpha(mNewBackgroundAlphas[i]);
cl.setShortcutAndWidgetAlpha(mNewAlphas[i]);
} else {
if (layerViews != null) {
layerViews.put(cl, LauncherStateTransitionAnimation.BUILD_LAYER);
}
if (mOldAlphas[i] != mNewAlphas[i] || currentAlpha != mNewAlphas[i]) {
LauncherViewPropertyAnimator alphaAnim =
new LauncherViewPropertyAnimator(cl.getShortcutsAndWidgets());
alphaAnim.alpha(mNewAlphas[i])
.setDuration(duration)
.setInterpolator(mZoomInInterpolator);
mStateAnimator.play(alphaAnim);
}
if (mOldBackgroundAlphas[i] != 0 ||
mNewBackgroundAlphas[i] != 0) {
ValueAnimator bgAnim = ObjectAnimator.ofFloat(cl, "backgroundAlpha",
mOldBackgroundAlphas[i], mNewBackgroundAlphas[i]);
bgAnim.setInterpolator(mZoomInInterpolator);
bgAnim.setDuration(duration);
mStateAnimator.play(bgAnim);
}
}
}
Animator pageIndicatorAlpha;
if (pageIndicator != null) {
pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator)
.alpha(finalPageIndicatorAlpha).withLayer();
pageIndicatorAlpha.addListener(new AlphaUpdateListener(pageIndicator,
accessibilityEnabled));
} else {
// create a dummy animation so we don't need to do null checks later
pageIndicatorAlpha = ValueAnimator.ofFloat(0, 0);
}
LauncherViewPropertyAnimator hotseatAlpha = new LauncherViewPropertyAnimator(hotseat)
.alpha(finalHotseatAlpha);
hotseatAlpha.addListener(new AlphaUpdateListener(hotseat, accessibilityEnabled));
LauncherViewPropertyAnimator overviewPanelAlpha =
new LauncherViewPropertyAnimator(overviewPanel).alpha(finalOverviewPanelAlpha);
overviewPanelAlpha.addListener(new AlphaUpdateListener(overviewPanel,
accessibilityEnabled));
// For animation optimations, we may need to provide the Launcher transition
// with a set of views on which to force build layers in certain scenarios.
hotseat.setLayerType(View.LAYER_TYPE_HARDWARE, null);
overviewPanel.setLayerType(View.LAYER_TYPE_HARDWARE, null);
if (layerViews != null) {
// If layerViews is not null, we add these views, and indicate that
// the caller can manage layer state.
layerViews.put(hotseat, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
layerViews.put(overviewPanel, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
} else {
// Otherwise let the animator handle layer management.
hotseatAlpha.withLayer();
overviewPanelAlpha.withLayer();
}
if (states.workspaceToOverview) {
pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2));
hotseatAlpha.setInterpolator(new DecelerateInterpolator(2));
overviewPanelAlpha.setInterpolator(null);
} else if (states.overviewToWorkspace) {
pageIndicatorAlpha.setInterpolator(null);
hotseatAlpha.setInterpolator(null);
overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2));
}
overviewPanelAlpha.setDuration(duration);
pageIndicatorAlpha.setDuration(duration);
hotseatAlpha.setDuration(duration);
mStateAnimator.play(overviewPanelAlpha);
mStateAnimator.play(hotseatAlpha);
mStateAnimator.play(pageIndicatorAlpha);
mStateAnimator.addListener(new AnimatorListenerAdapter() {
boolean canceled = false;
@Override
public void onAnimationCancel(Animator animation) {
canceled = true;
}
@Override
public void onAnimationEnd(Animator animation) {
mStateAnimator = null;
if (canceled) return;
if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
overviewPanel.getChildAt(0).performAccessibilityAction(
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
}
}
});
} else {
overviewPanel.setAlpha(finalOverviewPanelAlpha);
AlphaUpdateListener.updateVisibility(overviewPanel, accessibilityEnabled);
hotseat.setAlpha(finalHotseatAlpha);
AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled);
if (pageIndicator != null) {
pageIndicator.setAlpha(finalPageIndicatorAlpha);
AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled);
}
mWorkspace.updateCustomContentVisibility();
mWorkspace.setScaleX(mNewScale);
mWorkspace.setScaleY(mNewScale);
mWorkspace.setTranslationY(finalWorkspaceTranslationY);
if (accessibilityEnabled && overviewPanel.getVisibility() == View.VISIBLE) {
overviewPanel.getChildAt(0).performAccessibilityAction(
AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
}
}
}
/**
* Animates the background scrim. Add to the state animator to prevent jankiness.
*
* @param states the current and final workspace states
* @param animated whether or not to set the background alpha immediately
* @duration duration of the animation
*/
private void animateBackgroundGradient(TransitionStates states,
boolean animated, int duration) {
final DragLayer dragLayer = mLauncher.getDragLayer();
final float startAlpha = dragLayer.getBackgroundAlpha();
float finalAlpha = states.stateIsNormal || states.stateIsNormalHidden ?
0 : mWorkspaceScrimAlpha;
if (finalAlpha != startAlpha) {
if (animated) {
// These properties refer to the background protection gradient used for AllApps
// and Widget tray.
ValueAnimator bgFadeOutAnimation = ValueAnimator.ofFloat(startAlpha, finalAlpha);
bgFadeOutAnimation.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
dragLayer.setBackgroundAlpha(
((Float)animation.getAnimatedValue()).floatValue());
}
});
bgFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f));
bgFadeOutAnimation.setDuration(duration);
mStateAnimator.play(bgFadeOutAnimation);
} else {
dragLayer.setBackgroundAlpha(finalAlpha);
}
}
}
/**
* Cancels the current animation.
*/
private void cancelAnimation() {
if (mStateAnimator != null) {
mStateAnimator.setDuration(0);
mStateAnimator.cancel();
}
mStateAnimator = null;
}
}