diff --git a/aconfig/launcher_search.aconfig b/aconfig/launcher_search.aconfig
index 31d8d348da..b243922a29 100644
--- a/aconfig/launcher_search.aconfig
+++ b/aconfig/launcher_search.aconfig
@@ -42,3 +42,11 @@ flag {
description: "This flag disables drag and drop for Private Space Items."
bug: "289223923"
}
+
+
+flag {
+ name: "private_space_floating_mask_view"
+ namespace: "launcher_search"
+ description: "This flag enables the floating mask view as part of the Private Space animation. "
+ bug: "339850589"
+}
diff --git a/res/drawable/bg_ps_mask_left_corner.xml b/res/drawable/bg_ps_mask_left_corner.xml
new file mode 100644
index 0000000000..43eeedb0b4
--- /dev/null
+++ b/res/drawable/bg_ps_mask_left_corner.xml
@@ -0,0 +1,30 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/bg_ps_mask_right_corner.xml b/res/drawable/bg_ps_mask_right_corner.xml
new file mode 100644
index 0000000000..d63b866f9c
--- /dev/null
+++ b/res/drawable/bg_ps_mask_right_corner.xml
@@ -0,0 +1,30 @@
+
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/private_space_mask_view.xml b/res/layout/private_space_mask_view.xml
new file mode 100644
index 0000000000..44e2797020
--- /dev/null
+++ b/res/layout/private_space_mask_view.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 54edab469f..74fadda131 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -536,6 +536,8 @@
8dp
6dp
10dp
+ 28dp
+ 16dp
136px
diff --git a/src/com/android/launcher3/allapps/FloatingMaskView.java b/src/com/android/launcher3/allapps/FloatingMaskView.java
new file mode 100644
index 0000000000..606eb0328e
--- /dev/null
+++ b/src/com/android/launcher3/allapps/FloatingMaskView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 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.allapps;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+
+import androidx.constraintlayout.widget.ConstraintLayout;
+
+import com.android.launcher3.R;
+import com.android.launcher3.views.ActivityContext;
+
+public class FloatingMaskView extends ConstraintLayout {
+
+ private final ActivityContext mActivityContext;
+ private ImageView mBottomBox;
+
+ public FloatingMaskView(Context context) {
+ this(context, null, 0);
+ }
+
+ public FloatingMaskView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FloatingMaskView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mActivityContext = ActivityContext.lookupContext(context);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ mBottomBox = findViewById(R.id.bottom_box);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ ViewGroup.MarginLayoutParams lp = (ViewGroup.MarginLayoutParams) getLayoutParams();
+ AllAppsRecyclerView allAppsContainerView =
+ mActivityContext.getAppsView().getActiveRecyclerView();
+ if (lp != null) {
+ lp.rightMargin = allAppsContainerView.getPaddingRight();
+ lp.leftMargin = allAppsContainerView.getPaddingLeft();
+ mBottomBox.setMinimumHeight(allAppsContainerView.getPaddingBottom());
+ }
+ }
+}
diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java
index 53f9cfcb71..27340a3678 100644
--- a/src/com/android/launcher3/allapps/PrivateProfileManager.java
+++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java
@@ -60,6 +60,7 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
@@ -96,14 +97,17 @@ public class PrivateProfileManager extends UserProfileManager {
private static final int TEXT_UNLOCK_OPACITY_DURATION = 300;
private static final int TEXT_LOCK_OPACITY_DURATION = 50;
private static final int APP_OPACITY_DURATION = 400;
+ private static final int MASK_VIEW_DURATION = 200;
private static final int APP_OPACITY_DELAY = 400;
private static final int SETTINGS_AND_LOCK_GROUP_TRANSITION_DELAY = 400;
private static final int SETTINGS_OPACITY_DELAY = 400;
private static final int LOCK_TEXT_OPACITY_DELAY = 500;
+ private static final int MASK_VIEW_DELAY = 400;
private static final int NO_DELAY = 0;
private final ActivityAllAppsContainerView> mAllApps;
private final Predicate mPrivateProfileMatcher;
private final int mPsHeaderHeight;
+ private final int mFloatingMaskViewCornerRadius;
private final RecyclerView.OnScrollListener mOnIdleScrollListener =
new RecyclerView.OnScrollListener() {
@Override
@@ -123,6 +127,7 @@ public class PrivateProfileManager extends UserProfileManager {
private Runnable mOnPSHeaderAdded;
@Nullable
private RelativeLayout mPSHeader;
+ private ConstraintLayout mFloatingMaskView;
private final String mLockedStateContentDesc;
private final String mUnLockedStateContentDesc;
@@ -142,6 +147,8 @@ public class PrivateProfileManager extends UserProfileManager {
.getString(R.string.ps_container_lock_button_content_description);
mUnLockedStateContentDesc = mAllApps.getContext()
.getString(R.string.ps_container_unlock_button_content_description);
+ mFloatingMaskViewCornerRadius = mAllApps.getContext().getResources().getDimensionPixelSize(
+ R.dimen.ps_floating_mask_corner_radius);
}
/** Adds Private Space Header to the layout. */
@@ -219,6 +226,7 @@ public class PrivateProfileManager extends UserProfileManager {
.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED);
int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED;
setCurrentState(updatedState);
+ mFloatingMaskView = null;
if (mPSHeader != null) {
mPSHeader.setAlpha(1);
}
@@ -494,12 +502,15 @@ public class PrivateProfileManager extends UserProfileManager {
RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager();
if (layoutManager != null) {
startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller);
- currentItem.decorationInfo = null;
+ // Preserve decorator if floating mask view exists.
+ if (mFloatingMaskView == null) {
+ currentItem.decorationInfo = null;
+ }
}
break;
}
// Make the private space apps gone to "collapse".
- if (isPrivateSpaceItem(currentItem)) {
+ if (mFloatingMaskView == null && isPrivateSpaceItem(currentItem)) {
RecyclerView.ViewHolder viewHolder =
allAppsRecyclerView.findViewHolderForAdapterPosition(i);
if (viewHolder != null) {
@@ -637,6 +648,7 @@ public class PrivateProfileManager extends UserProfileManager {
setAnimationRunning(false);
return;
}
+ attachFloatingMaskView(expand);
ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup);
if (settingsAndLockGroup.getLayoutTransition() == null) {
// Set a new transition if the current ViewGroup does not already contain one as each
@@ -662,6 +674,11 @@ public class PrivateProfileManager extends UserProfileManager {
mPSHeader.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE);
setAnimationRunning(true);
}
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ detachFloatingMaskView();
+ }
});
animatorSet.addListener(forEndCallback(() -> {
setAnimationRunning(false);
@@ -681,13 +698,17 @@ public class PrivateProfileManager extends UserProfileManager {
}
}));
if (expand) {
- animatorSet.playTogether(animateAlphaOfIcons(true));
+ animatorSet.playTogether(animateAlphaOfIcons(true),
+ translateFloatingMaskView(false));
} else {
if (isPrivateSpaceHidden()) {
- animatorSet.playSequentially(animateAlphaOfIcons(false),
- animateCollapseAnimation(), fadeOutHeaderAlpha());
+ animatorSet.playSequentially(translateFloatingMaskView(false),
+ animateAlphaOfIcons(false),
+ animateCollapseAnimation(),
+ fadeOutHeaderAlpha());
} else {
- animatorSet.playSequentially(animateAlphaOfIcons(false),
+ animatorSet.playSequentially(translateFloatingMaskView(true),
+ animateAlphaOfIcons(false),
animateCollapseAnimation());
}
}
@@ -715,6 +736,27 @@ public class PrivateProfileManager extends UserProfileManager {
return alphaAnim;
}
+ /** Fades out the private space container. */
+ private ValueAnimator translateFloatingMaskView(boolean animateIn) {
+ if (!Flags.privateSpaceFloatingMaskView() || mFloatingMaskView == null) {
+ return new ValueAnimator();
+ }
+ // Translate base on the height amount. Translates out on expand and in on collapse.
+ float floatingMaskViewHeight = getFloatingMaskViewHeight();
+ float from = animateIn ? floatingMaskViewHeight : 0;
+ float to = animateIn ? 0 : floatingMaskViewHeight;
+ ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to);
+ alphaAnim.setDuration(MASK_VIEW_DURATION);
+ alphaAnim.setStartDelay(MASK_VIEW_DELAY);
+ alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
+ @Override
+ public void onAnimationUpdate(ValueAnimator valueAnimator) {
+ mFloatingMaskView.setTranslationY((float) valueAnimator.getAnimatedValue());
+ }
+ });
+ return alphaAnim;
+ }
+
/** Animates the layout changes when the text of the button becomes visible/gone. */
private void enableLayoutTransition(ViewGroup settingsAndLockGroup) {
LayoutTransition settingsAndLockTransition = new LayoutTransition();
@@ -806,6 +848,28 @@ public class PrivateProfileManager extends UserProfileManager {
});
}
+ private void attachFloatingMaskView(boolean expand) {
+ if (!Flags.privateSpaceFloatingMaskView()) {
+ return;
+ }
+ mFloatingMaskView = (FloatingMaskView) mAllApps.getLayoutInflater().inflate(
+ R.layout.private_space_mask_view, mAllApps, false);
+ mAllApps.addView(mFloatingMaskView);
+ // Translate off the screen first if its collapsing so this header view isn't visible to
+ // user when animation starts.
+ if (!expand) {
+ mFloatingMaskView.setTranslationY(getFloatingMaskViewHeight());
+ }
+ mFloatingMaskView.setVisibility(VISIBLE);
+ }
+
+ private void detachFloatingMaskView() {
+ if (mFloatingMaskView != null) {
+ mAllApps.removeView(mFloatingMaskView);
+ }
+ mFloatingMaskView = null;
+ }
+
/** Starts the smooth scroll with the provided smoothScroller and add idle listener. */
private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView,
RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) {
@@ -815,6 +879,10 @@ public class PrivateProfileManager extends UserProfileManager {
allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener);
}
+ private float getFloatingMaskViewHeight() {
+ return mFloatingMaskViewCornerRadius + getMainRecyclerView().getPaddingBottom();
+ }
+
AllAppsRecyclerView getMainRecyclerView() {
return mAllApps.mAH.get(ActivityAllAppsContainerView.AdapterHolder.MAIN).mRecyclerView;
}