package com.android.launcher3.allapps; import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT; import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA; import static com.android.launcher3.LauncherState.BACKGROUND_APP; import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; import static com.android.launcher3.LauncherState.OVERVIEW; import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR; import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE; import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.util.FloatProperty; import android.util.Log; import android.view.animation.Interpolator; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.LauncherStateManager.AnimationConfig; import com.android.launcher3.LauncherStateManager.StateHandler; import com.android.launcher3.R; import com.android.launcher3.anim.AnimationSuccessListener; import com.android.launcher3.anim.AnimatorSetBuilder; import com.android.launcher3.anim.PropertySetter; import com.android.launcher3.anim.SpringObjectAnimator; import com.android.launcher3.testing.TestProtocol; import com.android.launcher3.util.Themes; import com.android.launcher3.views.ScrimView; /** * Handles AllApps view transition. * 1) Slides all apps view using direct manipulation * 2) When finger is released, animate to either top or bottom accordingly. *

* Algorithm: * If release velocity > THRES1, snap according to the direction of movement. * If release velocity < THRES1, snap according to either top or bottom depending on whether it's * closer to top or closer to the page indicator. */ public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener { private static final float SPRING_DAMPING_RATIO = 0.9f; private static final float SPRING_STIFFNESS = 600f; public static final FloatProperty ALL_APPS_PROGRESS = new FloatProperty("allAppsProgress") { @Override public Float get(AllAppsTransitionController controller) { return controller.mProgress; } @Override public void setValue(AllAppsTransitionController controller, float progress) { controller.setProgress(progress); } }; private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0; private AllAppsContainerView mAppsView; private ScrimView mScrimView; private final Launcher mLauncher; private final boolean mIsDarkTheme; private boolean mIsVerticalLayout; // Animation in this class is controlled by a single variable {@link mProgress}. // Visually, it represents top y coordinate of the all apps container if multiplied with // {@link mShiftRange}. // When {@link mProgress} is 0, all apps container is pulled up. // When {@link mProgress} is 1, all apps container is pulled down. private float mShiftRange; // changes depending on the orientation private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent private float mScrollRangeDelta = 0; public AllAppsTransitionController(Launcher l) { mLauncher = l; mShiftRange = mLauncher.getDeviceProfile().heightPx; mProgress = 1f; mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark); mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout(); mLauncher.addOnDeviceProfileChangeListener(this); } public float getShiftRange() { return mShiftRange; } @Override public void onDeviceProfileChanged(DeviceProfile dp) { mIsVerticalLayout = dp.isVerticalBarLayout(); setScrollRangeDelta(mScrollRangeDelta); if (mIsVerticalLayout) { mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1); mLauncher.getHotseat().setTranslationY(0); mLauncher.getWorkspace().getPageIndicator().setTranslationY(0); } } /** * Note this method should not be called outside this class. This is public because it is used * in xml-based animations which also handle updating the appropriate UI. * * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace * * @see #setState(LauncherState) * @see #setStateWithAnimation(LauncherState, AnimatorSetBuilder, AnimationConfig) */ public void setProgress(float progress) { mProgress = progress; mScrimView.setProgress(progress); float shiftCurrent = progress * mShiftRange; mAppsView.setTranslationY(shiftCurrent); // Use a light system UI (dark icons) if all apps is behind at least half of the // status bar. boolean forceChange = shiftCurrent - mScrimView.getDragHandleSize() <= mLauncher.getDeviceProfile().getInsets().top / 2; if (forceChange) { mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, !mIsDarkTheme); } else { mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0); } if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) { // Translate hotseat with the shelf until reaching overview. float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher); if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) { float hotseatShift = (progress - overviewProgress) * mShiftRange; mLauncher.getHotseat().setTranslationY(hotseatShift); } } } public float getProgress() { return mProgress; } /** * Sets the vertical transition progress to {@param state} and updates all the dependent UI * accordingly. */ @Override public void setState(LauncherState state) { setProgress(state.getVerticalProgress(mLauncher)); setAlphas(state, null, new AnimatorSetBuilder()); onProgressAnimationEnd(); } /** * Creates an animation which updates the vertical transition progress and updates all the * dependent UI using various animation events */ @Override public void setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder, AnimationConfig config) { if (TestProtocol.sDebugTracing) { Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, "setStateWithAnimation " + toState.getClass().getSimpleName()); } float targetProgress = toState.getVerticalProgress(mLauncher); if (Float.compare(mProgress, targetProgress) == 0) { setAlphas(toState, config, builder); // Fail fast onProgressAnimationEnd(); return; } if (!config.playNonAtomicComponent()) { // There is no atomic component for the all apps transition, so just return early. return; } Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN) : FAST_OUT_SLOW_IN; Animator anim = createSpringAnimation(mProgress, targetProgress); anim.setDuration(config.duration); anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator)); anim.addListener(getProgressAnimatorListener()); builder.play(anim); setAlphas(toState, config, builder); } public Animator createSpringAnimation(float... progressValues) { return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange, SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues); } private void setAlphas(LauncherState toState, AnimationConfig config, AnimatorSetBuilder builder) { setAlphas(toState.getVisibleElements(mLauncher), config, builder); } public void setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder) { PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER : config.getPropertySetter(builder); boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0; boolean hasContent = (visibleElements & ALL_APPS_CONTENT) != 0; Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR); setter.setViewAlpha(mAppsView.getContentView(), hasContent ? 1 : 0, allAppsFade); setter.setViewAlpha(mAppsView.getScrollBar(), hasContent ? 1 : 0, allAppsFade); mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasContent, setter, allAppsFade); mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade); setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA, (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade); } public AnimatorListenerAdapter getProgressAnimatorListener() { return new AnimationSuccessListener() { @Override public void onAnimationSuccess(Animator animator) { onProgressAnimationEnd(); } }; } public void setupViews(AllAppsContainerView appsView) { mAppsView = appsView; mScrimView = mLauncher.findViewById(R.id.scrim_view); } /** * Updates the total scroll range but does not update the UI. */ void setScrollRangeDelta(float delta) { mScrollRangeDelta = delta; mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta; if (mScrimView != null) { mScrimView.reInitUi(); } } /** * Set the final view states based on the progress. * TODO: This logic should go in {@link LauncherState} */ private void onProgressAnimationEnd() { if (Float.compare(mProgress, 1f) == 0) { mAppsView.reset(false /* animate */); } else if (isAllAppsExpanded()) { mAppsView.onScrollUpEnd(); } } private boolean isAllAppsExpanded() { return Float.compare(mProgress, 0f) == 0; } public void highlightWorkTabIfNecessary() { if (isAllAppsExpanded()) { mAppsView.highlightWorkTabIfNecessary(); } } }