Files
Lawnchair/src/com/android/launcher3/WorkspaceStateTransitionAnimation.java
Adam Cohen 2e6da1539b Refactoring DeviceProfile
-> Pulling out the parts of device profile which can (and need to be)
   initialized and accessed without access to an Activity context,
   ie. the invariant bits.
-> The invariant bits are stored in InvariantDeviceProfile which is
   initialized statically from LauncherAppState.
-> The DeviceProfile contains the Activity context-dependent bits,
   and we will create one of these for each Activity instance, and
   this instance is accessed through the Launcher activity.
-> It's possible that we can continue to refactor this such that
   all appropriate dimensions can be computed without an Activity
   context (by only specifying orientation). This would be an
   extension of this CL and allow us to know exactly how launcher
   will look in both orientations from any context.

Sets the stage for some improvements around b/19514688

Change-Id: Ia7daccf14d8ca2b9cb340b8780b684769e9f1892
2015-05-21 16:16:40 -07:00

532 lines
23 KiB
Java

/*
* 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.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.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;
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 onAnimationEnd(Animator arg0) {
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));
}
}
/**
* Manages the animations between each of the workspace states.
*/
public class WorkspaceStateTransitionAnimation {
public static final String TAG = "WorkspaceStateTransitionAnimation";
public static final int SCROLL_TO_CURRENT_PAGE = -1;
@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();
// These properties refer to the background protection gradient used for AllApps and Customize
@Thunk ValueAnimator mBackgroundFadeInAnimation;
@Thunk ValueAnimator mBackgroundFadeOutAnimation;
@Thunk float mSpringLoadedShrinkFactor;
@Thunk float mOverviewModeShrinkFactor;
@Thunk float mWorkspaceScrimAlpha;
@Thunk int mAllAppsTransitionTime;
@Thunk int mOverviewTransitionTime;
@Thunk int mOverlayTransitionTime;
@Thunk boolean mWorkspaceFadeInAdjacentScreens;
public WorkspaceStateTransitionAnimation(Launcher launcher, Workspace workspace) {
mLauncher = launcher;
mWorkspace = workspace;
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = mLauncher.getDeviceProfile();
Resources res = launcher.getResources();
mAllAppsTransitionTime = res.getInteger(R.integer.config_workspaceUnshrinkTime);
mOverviewTransitionTime = res.getInteger(R.integer.config_overviewTransitionTime);
mOverlayTransitionTime = res.getInteger(R.integer.config_appsCustomizeWorkspaceShrinkTime);
mSpringLoadedShrinkFactor =
res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100f;
mWorkspaceScrimAlpha = res.getInteger(R.integer.config_workspaceScrimAlpha) / 100f;
mOverviewModeShrinkFactor = grid.getOverviewModeScale();
mWorkspaceFadeInAdjacentScreens = grid.shouldFadeAdjacentWorkspaceScreens();
}
public AnimatorSet getAnimationToState(Workspace.State fromState, Workspace.State toState,
int toPage, boolean animated,
HashMap<View, Integer> layerViews) {
getAnimation(fromState, toState, toPage, animated, layerViews);
return mStateAnimator;
}
public float getFinalScale() {
return mNewScale;
}
/**
* Starts a transition animation for the workspace.
*/
private void getAnimation(final Workspace.State fromState, final Workspace.State toState,
int toPage, final boolean animated,
final HashMap<View, Integer> layerViews) {
AccessibilityManager am = (AccessibilityManager)
mLauncher.getSystemService(Context.ACCESSIBILITY_SERVICE);
final boolean accessibilityEnabled = am.isEnabled();
// 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
final boolean oldStateIsNormal = (fromState == Workspace.State.NORMAL);
final boolean oldStateIsSpringLoaded = (fromState == Workspace.State.SPRING_LOADED);
final boolean oldStateIsNormalHidden = (fromState == Workspace.State.NORMAL_HIDDEN);
final boolean oldStateIsOverviewHidden = (fromState == Workspace.State.OVERVIEW_HIDDEN);
final boolean oldStateIsOverview = (fromState == Workspace.State.OVERVIEW);
final boolean stateIsNormal = (toState == Workspace.State.NORMAL);
final boolean stateIsSpringLoaded = (toState == Workspace.State.SPRING_LOADED);
final boolean stateIsNormalHidden = (toState == Workspace.State.NORMAL_HIDDEN);
final boolean stateIsOverviewHidden = (toState == Workspace.State.OVERVIEW_HIDDEN);
final boolean stateIsOverview = (toState == Workspace.State.OVERVIEW);
final boolean workspaceToAllApps = (oldStateIsNormal && stateIsNormalHidden);
final boolean overviewToAllApps = (oldStateIsOverview && stateIsOverviewHidden);
final boolean allAppsToWorkspace = (stateIsNormalHidden && stateIsNormal);
final boolean workspaceToOverview = (oldStateIsNormal && stateIsOverview);
final boolean overviewToWorkspace = (oldStateIsOverview && stateIsNormal);
float finalBackgroundAlpha = (stateIsSpringLoaded || stateIsOverview) ? 1.0f : 0f;
float finalHotseatAndPageIndicatorAlpha = (stateIsNormal || stateIsSpringLoaded) ? 1f : 0f;
float finalOverviewPanelAlpha = stateIsOverview ? 1f : 0f;
// We keep the search bar visible on the workspace and in AllApps now
boolean showSearchBar = stateIsNormal ||
(mLauncher.isAllAppsSearchOverridden() && stateIsNormalHidden);
float finalSearchBarAlpha = showSearchBar ? 1f : 0f;
float finalWorkspaceTranslationY = stateIsOverview || stateIsOverviewHidden ?
mWorkspace.getOverviewModeTranslationY() : 0;
final int childCount = mWorkspace.getChildCount();
final int customPageCount = mWorkspace.numCustomPages();
mNewScale = 1.0f;
if (oldStateIsOverview) {
mWorkspace.disableFreeScroll();
} else if (stateIsOverview) {
mWorkspace.enableFreeScroll();
}
if (!stateIsNormal) {
if (stateIsSpringLoaded) {
mNewScale = mSpringLoadedShrinkFactor;
} else if (stateIsOverview || stateIsOverviewHidden) {
mNewScale = mOverviewModeShrinkFactor;
}
}
final int duration;
if (workspaceToAllApps || overviewToAllApps) {
duration = mAllAppsTransitionTime;
} else if (workspaceToOverview || overviewToWorkspace) {
duration = mOverviewTransitionTime;
} else {
duration = mOverlayTransitionTime;
}
if (toPage == SCROLL_TO_CURRENT_PAGE) {
toPage = mWorkspace.getPageNearestToCenterOfScreen();
}
mWorkspace.snapToPage(toPage, duration, mZoomInInterpolator);
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 (stateIsNormalHidden || stateIsOverviewHidden) {
finalAlpha = 0f;
} else if (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 (workspaceToAllApps || allAppsToWorkspace) {
if (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 View searchBar = mLauncher.getOrCreateQsbBar();
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 =
LauncherAnimUtils.ofFloat(cl, 0f, 1f);
bgAnim.setInterpolator(mZoomInInterpolator);
bgAnim.setDuration(duration);
bgAnim.addUpdateListener(new LauncherAnimatorUpdateListener() {
public void onAnimationUpdate(float a, float b) {
cl.setBackgroundAlpha(
a * mOldBackgroundAlphas[i] +
b * mNewBackgroundAlphas[i]);
}
});
mStateAnimator.play(bgAnim);
}
}
}
Animator pageIndicatorAlpha = null;
if (pageIndicator != null) {
pageIndicatorAlpha = new LauncherViewPropertyAnimator(pageIndicator)
.alpha(finalHotseatAndPageIndicatorAlpha).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(finalHotseatAndPageIndicatorAlpha);
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 (workspaceToOverview) {
pageIndicatorAlpha.setInterpolator(new DecelerateInterpolator(2));
hotseatAlpha.setInterpolator(new DecelerateInterpolator(2));
overviewPanelAlpha.setInterpolator(null);
} else if (overviewToWorkspace) {
pageIndicatorAlpha.setInterpolator(null);
hotseatAlpha.setInterpolator(null);
overviewPanelAlpha.setInterpolator(new DecelerateInterpolator(2));
}
overviewPanelAlpha.setDuration(duration);
pageIndicatorAlpha.setDuration(duration);
hotseatAlpha.setDuration(duration);
// TODO: This should really be coordinated with the SearchDropTargetBar, otherwise the
// bar has no idea that it is hidden, and this has no idea what state the bar is
// actually in.
if (searchBar != null) {
LauncherViewPropertyAnimator searchBarAlpha = new LauncherViewPropertyAnimator(searchBar)
.alpha(finalSearchBarAlpha);
searchBarAlpha.addListener(new AlphaUpdateListener(searchBar, accessibilityEnabled));
searchBar.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(searchBar, LauncherStateTransitionAnimation.BUILD_AND_SET_LAYER);
} else {
// Otherwise let the animator handle layer management.
searchBarAlpha.withLayer();
}
searchBarAlpha.setDuration(duration);
mStateAnimator.play(searchBarAlpha);
}
mStateAnimator.play(overviewPanelAlpha);
mStateAnimator.play(hotseatAlpha);
mStateAnimator.play(pageIndicatorAlpha);
mStateAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mStateAnimator = null;
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(finalHotseatAndPageIndicatorAlpha);
AlphaUpdateListener.updateVisibility(hotseat, accessibilityEnabled);
if (pageIndicator != null) {
pageIndicator.setAlpha(finalHotseatAndPageIndicatorAlpha);
AlphaUpdateListener.updateVisibility(pageIndicator, accessibilityEnabled);
}
if (searchBar != null) {
searchBar.setAlpha(finalSearchBarAlpha);
AlphaUpdateListener.updateVisibility(searchBar, 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);
}
}
if (stateIsNormal) {
animateBackgroundGradient(0f, animated);
} else {
animateBackgroundGradient(mWorkspaceScrimAlpha, animated);
}
}
/**
* 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];
}
/**
* Animates the background scrim.
* TODO(winsonc): Is there a better place for this?
*
* @param finalAlpha the final alpha for the background scrim
* @param animated whether or not to set the background alpha immediately
*/
private void animateBackgroundGradient(float finalAlpha, boolean animated) {
// Cancel any running background animations
cancelAnimator(mBackgroundFadeInAnimation);
cancelAnimator(mBackgroundFadeOutAnimation);
final DragLayer dragLayer = mLauncher.getDragLayer();
final float startAlpha = dragLayer.getBackgroundAlpha();
if (finalAlpha != startAlpha) {
if (animated) {
mBackgroundFadeOutAnimation =
LauncherAnimUtils.ofFloat(mWorkspace, startAlpha, finalAlpha);
mBackgroundFadeOutAnimation.addUpdateListener(
new ValueAnimator.AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
dragLayer.setBackgroundAlpha(
((Float)animation.getAnimatedValue()).floatValue());
}
});
mBackgroundFadeOutAnimation.setInterpolator(new DecelerateInterpolator(1.5f));
mBackgroundFadeOutAnimation.setDuration(BACKGROUND_FADE_OUT_DURATION);
mBackgroundFadeOutAnimation.start();
} else {
dragLayer.setBackgroundAlpha(finalAlpha);
}
}
}
/**
* Cancels the current animation.
*/
private void cancelAnimation() {
cancelAnimator(mStateAnimator);
mStateAnimator = null;
}
/**
* Cancels the specified animation.
*/
private void cancelAnimator(Animator animator) {
if (animator != null) {
animator.setDuration(0);
animator.cancel();
}
}
}