| /* |
| * Copyright (C) 2023 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 static android.view.View.GONE; |
| import static android.view.View.INVISIBLE; |
| import static android.view.View.VISIBLE; |
| |
| import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; |
| import static com.android.launcher3.allapps.ActivityAllAppsContainerView.AdapterHolder.MAIN; |
| import static com.android.launcher3.allapps.BaseAllAppsAdapter.VIEW_TYPE_PRIVATE_SPACE_HEADER; |
| import static com.android.launcher3.allapps.PrivateProfileManager.STATE_DISABLED; |
| import static com.android.launcher3.allapps.PrivateProfileManager.STATE_ENABLED; |
| import static com.android.launcher3.allapps.PrivateProfileManager.STATE_TRANSITION; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_LOCK_TAP; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP; |
| import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP; |
| |
| import android.animation.Animator; |
| import android.animation.AnimatorListenerAdapter; |
| import android.animation.AnimatorSet; |
| import android.animation.LayoutTransition; |
| import android.animation.ValueAnimator; |
| import android.view.ViewGroup; |
| import android.widget.ImageButton; |
| import android.widget.ImageView; |
| import android.widget.RelativeLayout; |
| import android.widget.TextView; |
| |
| import androidx.recyclerview.widget.LinearSmoothScroller; |
| import androidx.recyclerview.widget.RecyclerView; |
| |
| import com.android.app.animation.Interpolators; |
| import com.android.launcher3.Flags; |
| import com.android.launcher3.R; |
| import com.android.launcher3.allapps.UserProfileManager.UserProfileState; |
| import com.android.launcher3.anim.AnimatedPropertySetter; |
| import com.android.launcher3.anim.PropertySetter; |
| import com.android.launcher3.views.RecyclerViewFastScroller; |
| |
| import java.util.List; |
| |
| /** |
| * Controller which returns views to be added to Private Space Header based upon |
| * {@link UserProfileState} |
| */ |
| public class PrivateSpaceHeaderViewController { |
| private static final int EXPAND_SCROLL_DURATION = 2000; |
| private static final int EXPAND_COLLAPSE_DURATION = 800; |
| private static final int SETTINGS_OPACITY_DURATION = 160; |
| private final ActivityAllAppsContainerView mAllApps; |
| private final PrivateProfileManager mPrivateProfileManager; |
| |
| public PrivateSpaceHeaderViewController(ActivityAllAppsContainerView allApps, |
| PrivateProfileManager privateProfileManager) { |
| this.mAllApps = allApps; |
| this.mPrivateProfileManager = privateProfileManager; |
| } |
| |
| /** Add Private Space Header view elements based upon {@link UserProfileState} */ |
| public void addPrivateSpaceHeaderViewElements(RelativeLayout parent) { |
| // Set the transition duration for the settings and lock button to animate. |
| ViewGroup settingsAndLockGroup = parent.findViewById(R.id.settingsAndLockGroup); |
| LayoutTransition settingsAndLockTransition = settingsAndLockGroup.getLayoutTransition(); |
| settingsAndLockTransition.enableTransitionType(LayoutTransition.CHANGING); |
| settingsAndLockTransition.setDuration(EXPAND_COLLAPSE_DURATION); |
| |
| //Add quietMode image and action for lock/unlock button |
| ViewGroup lockButton = |
| parent.findViewById(R.id.ps_lock_unlock_button); |
| assert lockButton != null; |
| addLockButton(parent, lockButton); |
| |
| //Trigger lock/unlock action from header. |
| addHeaderOnClickListener(parent); |
| |
| //Add image and action for private space settings button |
| ImageButton settingsButton = parent.findViewById(R.id.ps_settings_button); |
| assert settingsButton != null; |
| addPrivateSpaceSettingsButton(settingsButton); |
| |
| //Add image for private space transitioning view |
| ImageView transitionView = parent.findViewById(R.id.ps_transition_image); |
| assert transitionView != null; |
| addTransitionImage(transitionView); |
| } |
| |
| /** |
| * Adds the quietModeButton and attach onClickListener for the header to animate different |
| * states when clicked. |
| */ |
| private void addLockButton(ViewGroup psHeader, ViewGroup lockButton) { |
| TextView lockText = lockButton.findViewById(R.id.lock_text); |
| switch (mPrivateProfileManager.getCurrentState()) { |
| case STATE_ENABLED -> { |
| lockText.setVisibility(VISIBLE); |
| lockButton.setVisibility(VISIBLE); |
| lockButton.setOnClickListener(view -> lockAction(psHeader)); |
| } |
| case STATE_DISABLED -> { |
| lockText.setVisibility(GONE); |
| lockButton.setVisibility(VISIBLE); |
| lockButton.setOnClickListener(view -> unlockAction(psHeader)); |
| } |
| default -> lockButton.setVisibility(GONE); |
| } |
| } |
| |
| private void addHeaderOnClickListener(RelativeLayout header) { |
| if (mPrivateProfileManager.getCurrentState() == STATE_DISABLED) { |
| header.setOnClickListener(view -> unlockAction(header)); |
| } else { |
| header.setOnClickListener(null); |
| } |
| } |
| |
| private void unlockAction(ViewGroup psHeader) { |
| mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_UNLOCK_TAP); |
| mPrivateProfileManager.unlockPrivateProfile((() -> onPrivateProfileUnlocked(psHeader))); |
| } |
| |
| private void lockAction(ViewGroup psHeader) { |
| mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_LOCK_TAP); |
| if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation()) { |
| updatePrivateStateAnimator(false, psHeader); |
| } else { |
| mPrivateProfileManager.lockPrivateProfile(); |
| } |
| } |
| |
| private void addPrivateSpaceSettingsButton(ImageButton settingsButton) { |
| if (mPrivateProfileManager.getCurrentState() == STATE_ENABLED |
| && mPrivateProfileManager.isPrivateSpaceSettingsAvailable()) { |
| settingsButton.setVisibility(VISIBLE); |
| settingsButton.setAlpha(1f); |
| settingsButton.setOnClickListener( |
| view -> { |
| mPrivateProfileManager.logEvents(LAUNCHER_PRIVATE_SPACE_SETTINGS_TAP); |
| mPrivateProfileManager.openPrivateSpaceSettings(); |
| }); |
| } else { |
| settingsButton.setVisibility(GONE); |
| } |
| } |
| |
| private void addTransitionImage(ImageView transitionImage) { |
| if (mPrivateProfileManager.getCurrentState() == STATE_TRANSITION) { |
| transitionImage.setVisibility(VISIBLE); |
| } else { |
| transitionImage.setVisibility(GONE); |
| } |
| } |
| |
| private void onPrivateProfileUnlocked(ViewGroup header) { |
| // If we are on main adapter view, we apply the PS Container expansion animation and |
| // then scroll down to load the entire container, making animation visible. |
| ActivityAllAppsContainerView<?>.AdapterHolder mainAdapterHolder = |
| (ActivityAllAppsContainerView<?>.AdapterHolder) mAllApps.mAH.get(MAIN); |
| if (Flags.enablePrivateSpace() && Flags.privateSpaceAnimation() |
| && mAllApps.getActiveRecyclerView() == mainAdapterHolder.mRecyclerView) { |
| // Animate the text and settings icon. |
| updatePrivateStateAnimator(true, header); |
| mAllApps.getActiveRecyclerView().scrollToBottomWithMotion(EXPAND_SCROLL_DURATION); |
| } |
| } |
| |
| /** Finds the private space header to scroll to and set the private space icons to GONE. */ |
| private void collapse() { |
| AllAppsRecyclerView allAppsRecyclerView = mAllApps.getActiveRecyclerView(); |
| for (int i = allAppsRecyclerView.getChildCount() - 1; i > 0; i--) { |
| int adapterPosition = allAppsRecyclerView.getChildAdapterPosition( |
| allAppsRecyclerView.getChildAt(i)); |
| List<BaseAllAppsAdapter.AdapterItem> allAppsAdapters = allAppsRecyclerView.getApps() |
| .getAdapterItems(); |
| if (adapterPosition < 0 || adapterPosition >= allAppsAdapters.size()) { |
| continue; |
| } |
| // Scroll to the private space header. |
| if (allAppsAdapters.get(adapterPosition).viewType == VIEW_TYPE_PRIVATE_SPACE_HEADER) { |
| // Note: SmoothScroller is meant to be used once. |
| RecyclerView.SmoothScroller smoothScroller = |
| new LinearSmoothScroller(mAllApps.getContext()) { |
| @Override protected int getVerticalSnapPreference() { |
| return LinearSmoothScroller.SNAP_TO_END; |
| } |
| }; |
| smoothScroller.setTargetPosition(adapterPosition); |
| RecyclerView.LayoutManager layoutManager = allAppsRecyclerView.getLayoutManager(); |
| if (layoutManager != null) { |
| layoutManager.startSmoothScroll(smoothScroller); |
| } |
| break; |
| } |
| // Make the private space apps gone to "collapse". |
| if (allAppsAdapters.get(adapterPosition).decorationInfo != null) { |
| allAppsRecyclerView.getChildAt(i).setVisibility(GONE); |
| } |
| } |
| } |
| |
| PrivateProfileManager getPrivateProfileManager() { |
| return mPrivateProfileManager; |
| } |
| |
| /** |
| * Scrolls up to the private space header and animates the collapsing of the text. |
| */ |
| private ValueAnimator animateCollapseAnimation(ViewGroup lockButton) { |
| float from = 1; |
| float to = 0; |
| RecyclerViewFastScroller scrollBar = mAllApps.getActiveRecyclerView().getScrollbar(); |
| ValueAnimator collapseAnim = ValueAnimator.ofFloat(from, to); |
| collapseAnim.setDuration(EXPAND_COLLAPSE_DURATION); |
| collapseAnim.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| if (scrollBar != null) { |
| scrollBar.setVisibility(INVISIBLE); |
| } |
| // scroll up |
| collapse(); |
| // Animate the collapsing of the text. |
| lockButton.findViewById(R.id.lock_text).setVisibility(GONE); |
| } |
| @Override |
| public void onAnimationEnd(Animator animation) { |
| super.onAnimationEnd(animation); |
| if (scrollBar != null) { |
| scrollBar.setThumbOffsetY(-1); |
| scrollBar.setVisibility(VISIBLE); |
| } |
| mPrivateProfileManager.lockPrivateProfile(); |
| } |
| }); |
| return collapseAnim; |
| } |
| |
| /** |
| * 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, ViewGroup psHeader) { |
| PropertySetter setter = new AnimatedPropertySetter(); |
| ViewGroup lockButton = psHeader.findViewById(R.id.ps_lock_unlock_button); |
| ImageButton settingsButton = psHeader.findViewById(R.id.ps_settings_button); |
| updateSettingsGearAlpha(settingsButton, expand, setter); |
| AnimatorSet animatorSet = setter.buildAnim(); |
| animatorSet.addListener(new AnimatorListenerAdapter() { |
| @Override |
| public void onAnimationStart(Animator animation) { |
| // Animate the collapsing of the text at the same time while updating lock button. |
| lockButton.findViewById(R.id.lock_text).setVisibility(expand ? VISIBLE : GONE); |
| } |
| }); |
| // 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(lockButton)); |
| } |
| animatorSet.setDuration(EXPAND_COLLAPSE_DURATION); |
| animatorSet.start(); |
| } |
| |
| /** Change the settings gear alpha when expanded or collapsed. */ |
| private void updateSettingsGearAlpha(ImageButton settingsButton, boolean expand, |
| PropertySetter setter) { |
| float toAlpha = expand ? 1 : 0; |
| setter.setFloat(settingsButton, VIEW_ALPHA, toAlpha, Interpolators.LINEAR) |
| .setDuration(SETTINGS_OPACITY_DURATION).setStartDelay(0); |
| } |
| } |