Add unfold animation to launcher icons and widgets
Adds unfold animation to launcher which translates
icons and widgets from the center to the sides on
foldable devices.
Bug: 193794541
Test: manual
Change-Id: I9d6e018a0451d342f02dddea47bc180781c31d43
diff --git a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
index 2009cd7..069af96 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/QuickstepLauncher.java
@@ -36,9 +36,13 @@
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
+import android.hardware.SensorManager;
+import android.hardware.devicestate.DeviceStateManager;
import android.view.HapticFeedbackConstants;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.launcher3.BaseQuickstepLauncher;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Launcher;
@@ -75,9 +79,14 @@
import com.android.quickstep.SysUINavigationMode.Mode;
import com.android.quickstep.SystemUiProxy;
import com.android.quickstep.TaskUtils;
+import com.android.quickstep.util.LauncherUnfoldAnimationController;
+import com.android.quickstep.util.ProxyScreenStatusProvider;
import com.android.quickstep.util.QuickstepOnboardingPrefs;
import com.android.quickstep.views.RecentsView;
import com.android.quickstep.views.TaskView;
+import com.android.unfold.UnfoldTransitionFactory;
+import com.android.unfold.UnfoldTransitionProgressProvider;
+import com.android.unfold.config.UnfoldTransitionConfig;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -97,10 +106,51 @@
private FixedContainerItems mAllAppsPredictions;
private HotseatPredictionController mHotseatPredictionController;
+ @Nullable
+ private LauncherUnfoldAnimationController mLauncherUnfoldAnimationController;
+
@Override
protected void setupViews() {
super.setupViews();
mHotseatPredictionController = new HotseatPredictionController(this);
+
+ final UnfoldTransitionConfig config = UnfoldTransitionFactory.createConfig(this);
+ if (config.isEnabled()) {
+ final UnfoldTransitionProgressProvider unfoldTransitionProgressProvider =
+ UnfoldTransitionFactory.createUnfoldTransitionProgressProvider(
+ this,
+ config,
+ ProxyScreenStatusProvider.INSTANCE,
+ getSystemService(DeviceStateManager.class),
+ getSystemService(SensorManager.class),
+ getMainThreadHandler(),
+ getMainExecutor()
+ );
+
+ mLauncherUnfoldAnimationController = new LauncherUnfoldAnimationController(
+ this,
+ getWindowManager(),
+ unfoldTransitionProgressProvider
+ );
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ if (mLauncherUnfoldAnimationController != null) {
+ mLauncherUnfoldAnimationController.onResume();
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ if (mLauncherUnfoldAnimationController != null) {
+ mLauncherUnfoldAnimationController.onPause();
+ }
+
+ super.onPause();
}
@Override
@@ -231,6 +281,10 @@
public void onDestroy() {
super.onDestroy();
mHotseatPredictionController.destroy();
+
+ if (mLauncherUnfoldAnimationController != null) {
+ mLauncherUnfoldAnimationController.onDestroy();
+ }
}
@Override
diff --git a/quickstep/src/com/android/quickstep/TouchInteractionService.java b/quickstep/src/com/android/quickstep/TouchInteractionService.java
index 61622ee..4979206 100644
--- a/quickstep/src/com/android/quickstep/TouchInteractionService.java
+++ b/quickstep/src/com/android/quickstep/TouchInteractionService.java
@@ -100,6 +100,7 @@
import com.android.quickstep.util.ActiveGestureLog;
import com.android.quickstep.util.AssistantUtilities;
import com.android.quickstep.util.ProtoTracer;
+import com.android.quickstep.util.ProxyScreenStatusProvider;
import com.android.quickstep.util.SplitScreenBounds;
import com.android.systemui.plugins.OverscrollPlugin;
import com.android.systemui.plugins.PluginListener;
@@ -268,6 +269,12 @@
MAIN_EXECUTOR.execute(() -> SplitScreenBounds.INSTANCE.setSecondaryWindowBounds(wb));
}
+ @BinderThread
+ @Override
+ public void onScreenTurnedOn() {
+ MAIN_EXECUTOR.execute(ProxyScreenStatusProvider.INSTANCE::onScreenTurnedOn);
+ }
+
@Override
public void onRotationProposal(int rotation, boolean isValid) {
executeForTaskbarManager(() -> mTaskbarManager.onRotationProposal(rotation, isValid));
diff --git a/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
new file mode 100644
index 0000000..ea1ece8
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/LauncherUnfoldAnimationController.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2021 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.quickstep.util;
+
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+
+import com.android.launcher3.Launcher;
+import com.android.unfold.UnfoldTransitionProgressProvider;
+import com.android.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener;
+
+/**
+ * Controls animations that are happening during unfolding foldable devices
+ */
+public class LauncherUnfoldAnimationController {
+
+ private final Launcher mLauncher;
+ private final UnfoldTransitionProgressProvider mUnfoldTransitionProgressProvider;
+ private final UnfoldMoveFromCenterWorkspaceAnimator mMoveFromCenterWorkspaceAnimation;
+
+ private final AnimationListener mAnimationListener = new AnimationListener();
+
+ private boolean mIsTransitionRunning = false;
+ private boolean mIsReadyToPlayAnimation = false;
+
+ public LauncherUnfoldAnimationController(
+ Launcher launcher,
+ WindowManager windowManager,
+ UnfoldTransitionProgressProvider unfoldTransitionProgressProvider) {
+ mLauncher = launcher;
+ mUnfoldTransitionProgressProvider = unfoldTransitionProgressProvider;
+ mMoveFromCenterWorkspaceAnimation = new UnfoldMoveFromCenterWorkspaceAnimator(launcher,
+ windowManager);
+ mUnfoldTransitionProgressProvider.addCallback(mAnimationListener);
+ }
+
+ /**
+ * Called when launcher is resumed
+ */
+ public void onResume() {
+ final ViewTreeObserver obs = mLauncher.getWorkspace().getViewTreeObserver();
+ obs.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ if (obs.isAlive()) {
+ onPreDrawAfterResume();
+ obs.removeOnPreDrawListener(this);
+ }
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Called when launcher activity is paused
+ */
+ public void onPause() {
+ mIsReadyToPlayAnimation = false;
+
+ if (mIsTransitionRunning) {
+ mIsTransitionRunning = false;
+ mMoveFromCenterWorkspaceAnimation.onTransitionFinished();
+ }
+ }
+
+ /**
+ * Called when launcher activity is destroyed
+ */
+ public void onDestroy() {
+ mUnfoldTransitionProgressProvider.removeCallback(mAnimationListener);
+ }
+
+ /**
+ * Called after performing layouting of the views after configuration change
+ */
+ private void onPreDrawAfterResume() {
+ mIsReadyToPlayAnimation = true;
+
+ if (mIsTransitionRunning) {
+ mMoveFromCenterWorkspaceAnimation.onTransitionStarted();
+ }
+ }
+
+ private class AnimationListener implements TransitionProgressListener {
+
+ @Override
+ public void onTransitionStarted() {
+ mIsTransitionRunning = true;
+
+ if (mIsReadyToPlayAnimation) {
+ mMoveFromCenterWorkspaceAnimation.onTransitionStarted();
+ }
+ }
+
+ @Override
+ public void onTransitionFinished() {
+ if (mIsReadyToPlayAnimation) {
+ mMoveFromCenterWorkspaceAnimation.onTransitionFinished();
+ }
+
+ mIsTransitionRunning = false;
+ }
+
+ @Override
+ public void onTransitionProgress(float progress) {
+ mMoveFromCenterWorkspaceAnimation.onTransitionProgress(progress);
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java b/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java
new file mode 100644
index 0000000..9eda8f4
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/ProxyScreenStatusProvider.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2021 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.quickstep.util;
+
+import androidx.annotation.NonNull;
+
+import com.android.unfold.updates.screen.ScreenStatusProvider;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Screen status provider implementation that exposes methods to provide screen
+ * status updates to listeners. It is used to receive screen turned on event from
+ * SystemUI to Launcher.
+ */
+public class ProxyScreenStatusProvider implements ScreenStatusProvider {
+
+ public static final ProxyScreenStatusProvider INSTANCE = new ProxyScreenStatusProvider();
+ private final List<ScreenListener> mListeners = new ArrayList<>();
+
+ /**
+ * Called when the screen is on and ready (windows are drawn and screen blocker is removed)
+ */
+ public void onScreenTurnedOn() {
+ mListeners.forEach(ScreenListener::onScreenTurnedOn);
+ }
+
+ @Override
+ public void addCallback(@NonNull ScreenListener listener) {
+ mListeners.add(listener);
+ }
+
+ @Override
+ public void removeCallback(@NonNull ScreenListener listener) {
+ mListeners.remove(listener);
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
new file mode 100644
index 0000000..126e044
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/util/UnfoldMoveFromCenterWorkspaceAnimator.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2021 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.quickstep.util;
+
+import android.annotation.NonNull;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import com.android.launcher3.BubbleTextView;
+import com.android.launcher3.CellLayout;
+import com.android.launcher3.Hotseat;
+import com.android.launcher3.Launcher;
+import com.android.launcher3.ShortcutAndWidgetContainer;
+import com.android.launcher3.Workspace;
+import com.android.launcher3.widget.NavigableAppWidgetHostView;
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator;
+import com.android.systemui.shared.animation.UnfoldMoveFromCenterAnimator.TranslationApplier;
+import com.android.unfold.UnfoldTransitionProgressProvider;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Animation that moves launcher icons and widgets from center to the sides (final position)
+ */
+public class UnfoldMoveFromCenterWorkspaceAnimator
+ implements UnfoldTransitionProgressProvider.TransitionProgressListener {
+
+ private final Launcher mLauncher;
+ private final UnfoldMoveFromCenterAnimator mMoveFromCenterAnimation;
+
+ private final Map<ViewGroup, Boolean> mOriginalClipToPadding = new HashMap<>();
+ private final Map<ViewGroup, Boolean> mOriginalClipChildren = new HashMap<>();
+
+ public UnfoldMoveFromCenterWorkspaceAnimator(Launcher launcher, WindowManager windowManager) {
+ mLauncher = launcher;
+ mMoveFromCenterAnimation = new UnfoldMoveFromCenterAnimator(windowManager,
+ new WorkspaceViewsTranslationApplier());
+ }
+
+ @Override
+ public void onTransitionStarted() {
+ mMoveFromCenterAnimation.updateDisplayProperties();
+
+ Workspace workspace = mLauncher.getWorkspace();
+ Hotseat hotseat = mLauncher.getHotseat();
+
+ // App icons and widgets
+ workspace
+ .forEachVisiblePage(page -> {
+ final CellLayout cellLayout = (CellLayout) page;
+ ShortcutAndWidgetContainer itemsContainer = cellLayout
+ .getShortcutsAndWidgets();
+ disableClipping(cellLayout);
+
+ for (int i = 0; i < itemsContainer.getChildCount(); i++) {
+ View child = itemsContainer.getChildAt(i);
+ mMoveFromCenterAnimation.registerViewForAnimation(child);
+ }
+ });
+
+ disableClipping(workspace);
+
+ // Hotseat icons
+ ViewGroup hotseatIcons = hotseat.getShortcutsAndWidgets();
+ disableClipping(hotseat);
+
+ for (int i = 0; i < hotseatIcons.getChildCount(); i++) {
+ View child = hotseatIcons.getChildAt(i);
+ mMoveFromCenterAnimation.registerViewForAnimation(child);
+ }
+
+ onTransitionProgress(0f);
+ }
+
+ @Override
+ public void onTransitionProgress(float progress) {
+ mMoveFromCenterAnimation.onTransitionProgress(progress);
+ }
+
+ @Override
+ public void onTransitionFinished() {
+ mMoveFromCenterAnimation.onTransitionFinished();
+ mMoveFromCenterAnimation.clearRegisteredViews();
+
+ restoreClipping(mLauncher.getWorkspace());
+ mLauncher.getWorkspace().forEachVisiblePage(page -> restoreClipping((CellLayout) page));
+ restoreClipping(mLauncher.getHotseat());
+
+ mOriginalClipChildren.clear();
+ mOriginalClipToPadding.clear();
+ }
+
+ private void disableClipping(ViewGroup view) {
+ mOriginalClipToPadding.put(view, view.getClipToPadding());
+ mOriginalClipChildren.put(view, view.getClipChildren());
+ view.setClipToPadding(false);
+ view.setClipChildren(false);
+ }
+
+ private void restoreClipping(ViewGroup view) {
+ final Boolean originalClipToPadding = mOriginalClipToPadding.get(view);
+ if (originalClipToPadding != null) {
+ view.setClipToPadding(originalClipToPadding);
+ }
+ final Boolean originalClipChildren = mOriginalClipChildren.get(view);
+ if (originalClipChildren != null) {
+ view.setClipChildren(originalClipChildren);
+ }
+ }
+
+ private static class WorkspaceViewsTranslationApplier implements TranslationApplier {
+
+ @Override
+ public void apply(@NonNull View view, float x, float y) {
+ if (view instanceof NavigableAppWidgetHostView) {
+ ((NavigableAppWidgetHostView) view).setTranslationForMoveFromCenterAnimation(x, y);
+ } else if (view instanceof BubbleTextView) {
+ ((BubbleTextView) view).setTranslationForMoveFromCenterAnimation(x, y);
+ } else {
+ view.setTranslationX(x);
+ view.setTranslationY(y);
+ }
+ }
+ }
+}
diff --git a/src/com/android/launcher3/BubbleTextView.java b/src/com/android/launcher3/BubbleTextView.java
index f800cf6..a90fda2 100644
--- a/src/com/android/launcher3/BubbleTextView.java
+++ b/src/com/android/launcher3/BubbleTextView.java
@@ -90,6 +90,8 @@
private final PointF mTranslationForReorderBounce = new PointF(0, 0);
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
+ private final PointF mTranslationForMoveFromCenterAnimation = new PointF(0, 0);
+
private float mScaleForReorderBounce = 1f;
private static final Property<BubbleTextView, Float> DOT_SCALE_PROPERTY
@@ -799,8 +801,10 @@
}
private void updateTranslation() {
- super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x);
- super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y);
+ super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x
+ + mTranslationForMoveFromCenterAnimation.x);
+ super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y
+ + mTranslationForMoveFromCenterAnimation.y);
}
public void setReorderBounceOffset(float x, float y) {
@@ -833,6 +837,11 @@
return mScaleForReorderBounce;
}
+ public void setTranslationForMoveFromCenterAnimation(float x, float y) {
+ mTranslationForMoveFromCenterAnimation.set(x, y);
+ updateTranslation();
+ }
+
public View getView() {
return this;
}
diff --git a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
index d12fe74..241c937 100644
--- a/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/NavigableAppWidgetHostView.java
@@ -49,6 +49,8 @@
*/
private final PointF mTranslationForCentering = new PointF(0, 0);
+ private final PointF mTranslationForMoveFromCenterAnimation = new PointF(0, 0);
+
private final PointF mTranslationForReorderBounce = new PointF(0, 0);
private final PointF mTranslationForReorderPreview = new PointF(0, 0);
private float mScaleForReorderBounce = 1f;
@@ -167,9 +169,9 @@
private void updateTranslation() {
super.setTranslationX(mTranslationForReorderBounce.x + mTranslationForReorderPreview.x
- + mTranslationForCentering.x);
+ + mTranslationForCentering.x + mTranslationForMoveFromCenterAnimation.x);
super.setTranslationY(mTranslationForReorderBounce.y + mTranslationForReorderPreview.y
- + mTranslationForCentering.y);
+ + mTranslationForCentering.y + mTranslationForMoveFromCenterAnimation.y);
}
public void setTranslationForCentering(float x, float y) {
@@ -177,6 +179,11 @@
updateTranslation();
}
+ public void setTranslationForMoveFromCenterAnimation(float x, float y) {
+ mTranslationForMoveFromCenterAnimation.set(x, y);
+ updateTranslation();
+ }
+
public void setReorderBounceOffset(float x, float y) {
mTranslationForReorderBounce.set(x, y);
updateTranslation();