diff --git a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java index cc0289ec1a..c255eb54a7 100644 --- a/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java +++ b/src/com/android/launcher3/allapps/ActivityAllAppsContainerView.java @@ -517,7 +517,7 @@ public class ActivityAllAppsContainerView switchToTab(ActivityAllAppsContainerView.AdapterHolder.MAIN); // Scroll to bottom if (mPrivateProfileManager != null) { - mPrivateProfileManager.scrollForViewToBeVisibleInContainer( + mPrivateProfileManager.scrollForHeaderToBeVisibleInContainer( getActiveAppsRecyclerView(), getPersonalAppList().getAdapterItems(), mPrivateProfileManager.getPsHeaderHeight(), diff --git a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java index 2190e1a5af..3ba1eedc5c 100644 --- a/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java +++ b/src/com/android/launcher3/allapps/BaseAllAppsAdapter.java @@ -21,6 +21,7 @@ import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_NOTHING; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_LEFT; import static com.android.launcher3.allapps.SectionDecorationInfo.ROUND_TOP_RIGHT; import static com.android.launcher3.allapps.UserProfileManager.STATE_DISABLED; +import static com.android.launcher3.allapps.UserProfileManager.STATE_ENABLED; import android.content.Context; import android.view.LayoutInflater; @@ -40,7 +41,6 @@ import com.android.launcher3.Flags; import com.android.launcher3.LauncherPrefs; import com.android.launcher3.R; import com.android.launcher3.allapps.search.SearchAdapterProvider; -import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.model.data.AppInfo; import com.android.launcher3.views.ActivityContext; @@ -262,6 +262,17 @@ public abstract class BaseAllAppsAdapter ex icon.reset(); icon.applyFromApplicationInfo(adapterItem.itemInfo); icon.setOnFocusChangeListener(mIconFocusListener); + PrivateProfileManager privateProfileManager = mApps.getPrivateProfileManager(); + // Set the alpha of the private space icon to 0 upon expanding the header so the + // alpha can animate -> 1. + if (icon.getAlpha() == 0 || icon.getAlpha() == 1) { + icon.setAlpha(privateProfileManager != null + && privateProfileManager.isPrivateSpaceItem(adapterItem) + && privateProfileManager.getAnimationScrolling() + && privateProfileManager.getAnimate() + && privateProfileManager.getCurrentState() == STATE_ENABLED + ? 0 : 1); + } break; } case VIEW_TYPE_EMPTY_SEARCH: { diff --git a/src/com/android/launcher3/allapps/PrivateProfileManager.java b/src/com/android/launcher3/allapps/PrivateProfileManager.java index eb967bc68e..1d221ee7aa 100644 --- a/src/com/android/launcher3/allapps/PrivateProfileManager.java +++ b/src/com/android/launcher3/allapps/PrivateProfileManager.java @@ -40,6 +40,7 @@ import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.LayoutTransition; +import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.Intent; @@ -52,6 +53,7 @@ import android.widget.ImageView; import android.widget.RelativeLayout; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearSmoothScroller; @@ -91,13 +93,27 @@ public class PrivateProfileManager extends UserProfileManager { private static final int SETTINGS_OPACITY_DURATION = 160; private final ActivityAllAppsContainerView mAllApps; private final Predicate mPrivateProfileMatcher; + private final int mPsHeaderHeight; + private final RecyclerView.OnScrollListener mOnIdleScrollListener = + new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + mAnimationScrolling = false; + } + } + }; private Set mPreInstalledSystemPackages = new HashSet<>(); private Intent mAppInstallerIntent = new Intent(); private PrivateAppsSectionDecorator mPrivateAppsSectionDecorator; private boolean mPrivateSpaceSettingsAvailable; private boolean mIsAnimationRunning; - private int mHeaderHeight; private boolean mAnimate; + private boolean mAnimationScrolling; + private Runnable mOnPSHeaderAdded; + @Nullable + private RelativeLayout mPSHeader; public PrivateProfileManager(UserManager userManager, ActivityAllAppsContainerView allApps, @@ -107,6 +123,8 @@ public class PrivateProfileManager extends UserProfileManager { mAllApps = allApps; mPrivateProfileMatcher = (user) -> userCache.getUserInfo(user).isPrivate(); UI_HELPER_EXECUTOR.post(this::initializeInBackgroundThread); + mPsHeaderHeight = mAllApps.getContext().getResources().getDimensionPixelSize( + R.dimen.ps_header_height); } /** Adds Private Space Header to the layout. */ @@ -172,19 +190,26 @@ public class PrivateProfileManager extends UserProfileManager { .get(mAllApps.mActivityContext).getValue(PRIVATE_SPACE_HIDE_WHEN_LOCKED_URI, 0); } - /** Resets the current state of Private Profile, w.r.t. to Launcher. */ + /** + * Resets the current state of Private Profile, w.r.t. to Launcher. The decorator should only + * be applied upon expand before animating. When collapsing, reset() will remove the decorator + * when animation is not running. + */ public void reset() { int previousState = getCurrentState(); boolean isEnabled = !mAllApps.getAppsStore() .hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED); int updatedState = isEnabled ? STATE_ENABLED : STATE_DISABLED; setCurrentState(updatedState); - resetPrivateSpaceDecorator(updatedState); + if (mPSHeader != null) { + mPSHeader.setAlpha(1); + } if (transitioningFromLockedToUnlocked(previousState, updatedState)) { postUnlock(); } else if (transitioningFromUnlockedToLocked(previousState, updatedState)){ executeLock(); } + resetPrivateSpaceDecorator(updatedState); } /** Opens the Private Space Settings Page. */ @@ -263,7 +288,7 @@ public class PrivateProfileManager extends UserProfileManager { mainAdapterHolder.mRecyclerView.addItemDecoration(mPrivateAppsSectionDecorator); } else { // Remove Private Space Decorator from the Recycler view. - if (mPrivateAppsSectionDecorator != null) { + if (mPrivateAppsSectionDecorator != null && !mIsAnimationRunning) { mainAdapterHolder.mRecyclerView.removeItemDecoration(mPrivateAppsSectionDecorator); } } @@ -289,10 +314,13 @@ public class PrivateProfileManager extends UserProfileManager { /** Collapses the private space before the app list has been updated. */ void executeLock() { - MAIN_EXECUTOR.execute(this::collapsePrivateSpace); + MAIN_EXECUTOR.execute(() -> updatePrivateStateAnimator(false)); } void setAnimationRunning(boolean isAnimationRunning) { + if (!isAnimationRunning) { + mAnimate = false; + } mIsAnimationRunning = isAnimationRunning; } @@ -325,8 +353,13 @@ public class PrivateProfileManager extends UserProfileManager { /** Add Private Space Header view elements based upon {@link UserProfileState} */ public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { + mPSHeader = parent; + if (mOnPSHeaderAdded != null) { + MAIN_EXECUTOR.execute(mOnPSHeaderAdded); + mOnPSHeaderAdded = null; + } // Set the transition duration for the settings and lock button to animate. - ViewGroup settingAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); + ViewGroup settingAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); if (mAnimate) { enableLayoutTransition(settingAndLockGroup); } else { @@ -335,16 +368,15 @@ public class PrivateProfileManager extends UserProfileManager { } //Add quietMode image and action for lock/unlock button - ViewGroup lockButton = - parent.findViewById(R.id.ps_lock_unlock_button); + ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button); assert lockButton != null; addLockButton(lockButton); //Trigger lock/unlock action from header. - addHeaderOnClickListener(parent); + addHeaderOnClickListener(mPSHeader); //Add image and action for private space settings button - ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); + ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button); assert settingsButton != null; addPrivateSpaceSettingsButton(settingsButton); @@ -352,7 +384,6 @@ public class PrivateProfileManager extends UserProfileManager { ImageView transitionView = parent.findViewById(R.id.ps_transition_image); assert transitionView != null; addTransitionImage(transitionView); - mHeaderHeight = parent.getHeight(); } /** @@ -436,16 +467,18 @@ public class PrivateProfileManager extends UserProfileManager { smoothScroller.setTargetPosition(i); RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); if (layoutManager != null) { - layoutManager.startSmoothScroll(smoothScroller); + startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller); + currentItem.decorationInfo = null; } break; } // Make the private space apps gone to "collapse". - if (currentItem.decorationInfo != null) { + if (isPrivateSpaceItem(currentItem)) { RecyclerView.ViewHolder viewHolder = allAppsRecyclerView.findViewHolderForAdapterPosition(i); if (viewHolder != null) { viewHolder.itemView.setVisibility(GONE); + currentItem.decorationInfo = null; } } } @@ -455,7 +488,7 @@ public class PrivateProfileManager extends UserProfileManager { * Upon expanding, only scroll to the item position in the adapter that allows the header to be * visible. */ - public int scrollForViewToBeVisibleInContainer( + public int scrollForHeaderToBeVisibleInContainer( AllAppsRecyclerView allAppsRecyclerView, List appListAdapterItems, int psHeaderHeight, @@ -495,7 +528,7 @@ public class PrivateProfileManager extends UserProfileManager { smoothScroller.setTargetPosition(itemToScrollTo); RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); if (layoutManager != null) { - layoutManager.startSmoothScroll(smoothScroller); + startAnimationScroll(allAppsRecyclerView, layoutManager, smoothScroller); } } return itemToScrollTo; @@ -531,27 +564,56 @@ public class PrivateProfileManager extends UserProfileManager { return collapseAnim; } + private ValueAnimator animateAlphaOfIcons(boolean isExpanding) { + float from = isExpanding ? 0 : 1; + float to = isExpanding ? 1 : 0; + AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); + List allAppsAdapterItems = + mAllApps.getActiveRecyclerView().getApps().getAdapterItems(); + ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to); + alphaAnim.setDuration(EXPAND_COLLAPSE_DURATION); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + float newAlpha = (float) valueAnimator.getAnimatedValue(); + for (int i = 0; i < allAppsAdapterItems.size(); i++) { + BaseAllAppsAdapter.AdapterItem currentItem = allAppsAdapterItems.get(i); + if (isPrivateSpaceItem(currentItem) && + currentItem.viewType != VIEW_TYPE_PRIVATE_SPACE_HEADER) { + RecyclerView.ViewHolder viewHolder = + allAppsRecyclerView.findViewHolderForAdapterPosition(i); + if (viewHolder != null) { + viewHolder.itemView.setAlpha(newAlpha); + } + } + } + } + }); + return alphaAnim; + } + /** * Using PropertySetter{@link PropertySetter}, we can update the view's attributes within an * animation. At the moment, collapsing, setting alpha changes, and animating the text is done * here. */ - private void updatePrivateStateAnimator(boolean expand, @Nullable ViewGroup psHeader) { - if (psHeader == null) { + private void updatePrivateStateAnimator(boolean expand) { + if (mPSHeader == null) { + mOnPSHeaderAdded = () -> updatePrivateStateAnimator(expand); + setAnimationRunning(false); return; } - ViewGroup settingsAndLockGroup = psHeader.findViewById(R.id.settingsAndLockGroup); - ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); + ViewGroup settingsAndLockGroup = mPSHeader.findViewById(R.id.settingsAndLockGroup); + ViewGroup lockButton = mPSHeader.findViewById(R.id.ps_lock_unlock_button); if (settingsAndLockGroup.getLayoutTransition() == null) { // Set a new transition if the current ViewGroup does not already contain one as each // transition should only happen once when applied. enableLayoutTransition(settingsAndLockGroup); } - - PropertySetter setter = new AnimatedPropertySetter(); - ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); - updateSettingsGearAlpha(settingsButton, expand, setter); - AnimatorSet animatorSet = setter.buildAnim(); + PropertySetter headerSetter = new AnimatedPropertySetter(); + ImageButton settingsButton = mPSHeader.findViewById(R.id.ps_settings_button); + updateSettingsGearAlpha(settingsButton, expand, headerSetter); + AnimatorSet animatorSet = headerSetter.buildAnim(); animatorSet.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -562,22 +624,50 @@ public class PrivateProfileManager extends UserProfileManager { }); animatorSet.addListener(forEndCallback(() -> { setAnimationRunning(false); - mAnimate = false; if (!expand) { // Call onAppsUpdated() because it may be canceled when this animation occurs. mAllApps.getPersonalAppList().onAppsUpdated(); + if (isPrivateSpaceHidden()) { + // TODO (b/325455879): Figure out if we can avoid this. + mAllApps.getActiveRecyclerView().getAdapter().notifyDataSetChanged(); + } } })); - // Play the collapsing together of the stateAnimator to avoid being unable to scroll to the - // header. Otherwise the smooth scrolling will scroll higher when played with the state - // animator. - if (!expand) { - animatorSet.playTogether(animateCollapseAnimation()); + if (expand) { + animatorSet.playTogether(animateAlphaOfIcons(true)); + } else { + if (isPrivateSpaceHidden()) { + animatorSet.playSequentially(animateAlphaOfIcons(false), + animateCollapseAnimation(), fadeOutHeaderAlpha()); + } else { + animatorSet.playSequentially(animateAlphaOfIcons(false), + animateCollapseAnimation()); + } } animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); animatorSet.start(); } + /** Fades out the private space container. */ + private ValueAnimator fadeOutHeaderAlpha() { + if (mPSHeader == null) { + return new ValueAnimator(); + } + float from = 1; + float to = 0; + ValueAnimator alphaAnim = ObjectAnimator.ofFloat(from, to); + alphaAnim.setDuration(EXPAND_COLLAPSE_DURATION); + alphaAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator valueAnimator) { + if (mPSHeader != null) { + mPSHeader.setAlpha((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(); @@ -617,10 +707,9 @@ public class PrivateProfileManager extends UserProfileManager { // Animate the text and settings icon. DeviceProfile deviceProfile = ActivityContext.lookupContext(mAllApps.getContext()).getDeviceProfile(); - scrollForViewToBeVisibleInContainer(mainAdapterHolder.mRecyclerView, adapterItems, + scrollForHeaderToBeVisibleInContainer(mainAdapterHolder.mRecyclerView, adapterItems, getPsHeaderHeight(), deviceProfile.allAppsCellHeightPx); - ViewGroup psHeader = getPsHeader(mainAdapterHolder.mRecyclerView, adapterItems); - updatePrivateStateAnimator(true, psHeader); + updatePrivateStateAnimator(true); } } @@ -635,36 +724,28 @@ public class PrivateProfileManager extends UserProfileManager { }); } - private void collapsePrivateSpace() { - AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); - AlphabeticalAppsList appList = allAppsRecyclerView.getApps(); - if (appList == null) { - return; - } - ViewGroup psHeader = getPsHeader(allAppsRecyclerView, appList.getAdapterItems()); - assert psHeader != null; - updatePrivateStateAnimator(false, psHeader); + /** Starts the smooth scroll with the provided smoothScroller and add idle listener. */ + private void startAnimationScroll(AllAppsRecyclerView allAppsRecyclerView, + RecyclerView.LayoutManager layoutManager, RecyclerView.SmoothScroller smoothScroller) { + mAnimationScrolling = true; + layoutManager.startSmoothScroll(smoothScroller); + allAppsRecyclerView.removeOnScrollListener(mOnIdleScrollListener); + allAppsRecyclerView.addOnScrollListener(mOnIdleScrollListener); + } + + boolean getAnimate() { + return mAnimate; + } + + boolean getAnimationScrolling() { + return mAnimationScrolling; } int getPsHeaderHeight() { - return mHeaderHeight; + return mPsHeaderHeight; } - /** Get the private space header from the adapter items. */ - @Nullable - private ViewGroup getPsHeader(AllAppsRecyclerView allAppsRecyclerView, - List adapterItems){ - ViewGroup psHeader = null; - for (int i = 0; i < adapterItems.size(); i++) { - BaseAllAppsAdapter.AdapterItem currentItem = adapterItems.get(i); - if (currentItem.viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { - RecyclerView.ViewHolder viewHolder = - allAppsRecyclerView.findViewHolderForAdapterPosition(i); - if (viewHolder != null) { - psHeader = (ViewGroup) viewHolder.itemView; - } - } - } - return psHeader; + boolean isPrivateSpaceItem(BaseAllAppsAdapter.AdapterItem item) { + return item.decorationInfo != null; } } diff --git a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java index 476bda5ada..e9d2f6e61a 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateProfileManagerTest.java @@ -26,7 +26,6 @@ import static com.android.launcher3.util.rule.TestStabilityRule.LOCAL; import static com.android.launcher3.util.rule.TestStabilityRule.PLATFORM_POSTSUBMIT; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; @@ -39,6 +38,7 @@ import android.content.Intent; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.content.res.Resources; import android.os.Process; import android.os.UserHandle; import android.os.UserManager; @@ -97,6 +97,8 @@ public class PrivateProfileManagerTest { private LauncherApps mLauncherApps; @Mock private AllAppsRecyclerView mAllAppsRecyclerView; + @Mock + private Resources mResources; @Before public void setUp() { @@ -106,6 +108,7 @@ public class PrivateProfileManagerTest { when(mUserCache.getUserInfo(Process.myUserHandle())).thenReturn(MAIN_ICON_INFO); when(mUserCache.getUserInfo(PRIVATE_HANDLE)).thenReturn(PRIVATE_ICON_INFO); when(mAllApps.getContext()).thenReturn(mContext); + when(mAllApps.getContext().getResources()).thenReturn(mResources); when(mAllApps.getAppsStore()).thenReturn(mAllAppsStore); when(mAllApps.getActiveRecyclerView()).thenReturn(mAllAppsRecyclerView); when(mContext.getPackageManager()).thenReturn(mPackageManager); @@ -145,6 +148,7 @@ public class PrivateProfileManagerTest { public void quietModeFlagPresent_privateSpaceIsResetToDisabled() { PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); doNothing().when(privateProfileManager).resetPrivateSpaceDecorator(anyInt()); + doNothing().when(privateProfileManager).executeLock(); when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)) .thenReturn(false, true); @@ -181,6 +185,7 @@ public class PrivateProfileManagerTest { public void transitioningToLocked_resetCallsExecuteLock() throws Exception { PrivateProfileManager privateProfileManager = spy(mPrivateProfileManager); doNothing().when(privateProfileManager).resetPrivateSpaceDecorator(anyInt()); + doNothing().when(privateProfileManager).executeLock(); when(mAllAppsStore.hasModelFlag(FLAG_PRIVATE_PROFILE_QUIET_MODE_ENABLED)) .thenReturn(true); doNothing().when(privateProfileManager).expandPrivateSpace(); diff --git a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java index 7ac276acb3..351b921e97 100644 --- a/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java +++ b/tests/src/com/android/launcher3/allapps/PrivateSpaceHeaderViewTest.java @@ -303,7 +303,7 @@ public class PrivateSpaceHeaderViewTest { assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(position, - privateProfileManager.scrollForViewToBeVisibleInContainer( + privateProfileManager.scrollForHeaderToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), PS_HEADER_HEIGHT, @@ -339,7 +339,7 @@ public class PrivateSpaceHeaderViewTest { assertEquals(NUM_PRIVATE_SPACE_APPS + 1 + 1, mAlphabeticalAppsList.getAdapterItems().size()); assertEquals(position, - privateProfileManager.scrollForViewToBeVisibleInContainer( + privateProfileManager.scrollForHeaderToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), BIGGER_PS_HEADER_HEIGHT, @@ -369,7 +369,7 @@ public class PrivateSpaceHeaderViewTest { // The number of adapterItems should be the private space apps + one main app. assertEquals(NUM_PRIVATE_SPACE_APPS + 1, mAlphabeticalAppsList.getAdapterItems().size()); - assertEquals(SCROLL_NO_WHERE, privateProfileManager.scrollForViewToBeVisibleInContainer( + assertEquals(SCROLL_NO_WHERE, privateProfileManager.scrollForHeaderToBeVisibleInContainer( new AllAppsRecyclerView(mContext), mAlphabeticalAppsList.getAdapterItems(), BIGGER_PS_HEADER_HEIGHT,