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; }