1 package com.android.launcher3.allapps; 2 3 import static com.android.launcher3.LauncherState.ALL_APPS_CONTENT; 4 import static com.android.launcher3.LauncherState.ALL_APPS_HEADER_EXTRA; 5 import static com.android.launcher3.LauncherState.BACKGROUND_APP; 6 import static com.android.launcher3.LauncherState.HOTSEAT_ICONS; 7 import static com.android.launcher3.LauncherState.OVERVIEW; 8 import static com.android.launcher3.LauncherState.VERTICAL_SWIPE_INDICATOR; 9 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_ALL_APPS_FADE; 10 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_OVERVIEW_SCALE; 11 import static com.android.launcher3.anim.AnimatorSetBuilder.ANIM_VERTICAL_PROGRESS; 12 import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; 13 import static com.android.launcher3.anim.Interpolators.LINEAR; 14 import static com.android.launcher3.anim.PropertySetter.NO_ANIM_PROPERTY_SETTER; 15 import static com.android.launcher3.util.SystemUiController.UI_STATE_ALL_APPS; 16 17 import android.animation.Animator; 18 import android.animation.AnimatorListenerAdapter; 19 import android.util.FloatProperty; 20 import android.util.Log; 21 import android.view.animation.Interpolator; 22 23 import com.android.launcher3.DeviceProfile; 24 import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; 25 import com.android.launcher3.Launcher; 26 import com.android.launcher3.LauncherState; 27 import com.android.launcher3.LauncherStateManager.AnimationConfig; 28 import com.android.launcher3.LauncherStateManager.StateHandler; 29 import com.android.launcher3.R; 30 import com.android.launcher3.anim.AnimationSuccessListener; 31 import com.android.launcher3.anim.AnimatorSetBuilder; 32 import com.android.launcher3.anim.PropertySetter; 33 import com.android.launcher3.anim.SpringObjectAnimator; 34 import com.android.launcher3.testing.TestProtocol; 35 import com.android.launcher3.util.Themes; 36 import com.android.launcher3.views.ScrimView; 37 38 /** 39 * Handles AllApps view transition. 40 * 1) Slides all apps view using direct manipulation 41 * 2) When finger is released, animate to either top or bottom accordingly. 42 * <p/> 43 * Algorithm: 44 * If release velocity > THRES1, snap according to the direction of movement. 45 * If release velocity < THRES1, snap according to either top or bottom depending on whether it's 46 * closer to top or closer to the page indicator. 47 */ 48 public class AllAppsTransitionController implements StateHandler, OnDeviceProfileChangeListener { 49 50 private static final float SPRING_DAMPING_RATIO = 0.9f; 51 private static final float SPRING_STIFFNESS = 600f; 52 53 public static final FloatProperty<AllAppsTransitionController> ALL_APPS_PROGRESS = 54 new FloatProperty<AllAppsTransitionController>("allAppsProgress") { 55 56 @Override 57 public Float get(AllAppsTransitionController controller) { 58 return controller.mProgress; 59 } 60 61 @Override 62 public void setValue(AllAppsTransitionController controller, float progress) { 63 controller.setProgress(progress); 64 } 65 }; 66 67 private static final int APPS_VIEW_ALPHA_CHANNEL_INDEX = 0; 68 69 private AllAppsContainerView mAppsView; 70 private ScrimView mScrimView; 71 72 private final Launcher mLauncher; 73 private final boolean mIsDarkTheme; 74 private boolean mIsVerticalLayout; 75 76 // Animation in this class is controlled by a single variable {@link mProgress}. 77 // Visually, it represents top y coordinate of the all apps container if multiplied with 78 // {@link mShiftRange}. 79 80 // When {@link mProgress} is 0, all apps container is pulled up. 81 // When {@link mProgress} is 1, all apps container is pulled down. 82 private float mShiftRange; // changes depending on the orientation 83 private float mProgress; // [0, 1], mShiftRange * mProgress = shiftCurrent 84 85 private float mScrollRangeDelta = 0; 86 AllAppsTransitionController(Launcher l)87 public AllAppsTransitionController(Launcher l) { 88 mLauncher = l; 89 mShiftRange = mLauncher.getDeviceProfile().heightPx; 90 mProgress = 1f; 91 92 mIsDarkTheme = Themes.getAttrBoolean(mLauncher, R.attr.isMainColorDark); 93 mIsVerticalLayout = mLauncher.getDeviceProfile().isVerticalBarLayout(); 94 mLauncher.addOnDeviceProfileChangeListener(this); 95 } 96 getShiftRange()97 public float getShiftRange() { 98 return mShiftRange; 99 } 100 101 @Override onDeviceProfileChanged(DeviceProfile dp)102 public void onDeviceProfileChanged(DeviceProfile dp) { 103 mIsVerticalLayout = dp.isVerticalBarLayout(); 104 setScrollRangeDelta(mScrollRangeDelta); 105 106 if (mIsVerticalLayout) { 107 mAppsView.getAlphaProperty(APPS_VIEW_ALPHA_CHANNEL_INDEX).setValue(1); 108 mLauncher.getHotseat().setTranslationY(0); 109 mLauncher.getWorkspace().getPageIndicator().setTranslationY(0); 110 } 111 } 112 113 /** 114 * Note this method should not be called outside this class. This is public because it is used 115 * in xml-based animations which also handle updating the appropriate UI. 116 * 117 * @param progress value between 0 and 1, 0 shows all apps and 1 shows workspace 118 * 119 * @see #setState(LauncherState) 120 * @see #setStateWithAnimation(LauncherState, AnimatorSetBuilder, AnimationConfig) 121 */ setProgress(float progress)122 public void setProgress(float progress) { 123 mProgress = progress; 124 mScrimView.setProgress(progress); 125 float shiftCurrent = progress * mShiftRange; 126 127 mAppsView.setTranslationY(shiftCurrent); 128 129 // Use a light system UI (dark icons) if all apps is behind at least half of the 130 // status bar. 131 boolean forceChange = shiftCurrent - mScrimView.getDragHandleSize() 132 <= mLauncher.getDeviceProfile().getInsets().top / 2; 133 if (forceChange) { 134 mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, !mIsDarkTheme); 135 } else { 136 mLauncher.getSystemUiController().updateUiState(UI_STATE_ALL_APPS, 0); 137 } 138 139 if ((OVERVIEW.getVisibleElements(mLauncher) & HOTSEAT_ICONS) != 0) { 140 // Translate hotseat with the shelf until reaching overview. 141 float overviewProgress = OVERVIEW.getVerticalProgress(mLauncher); 142 if (progress >= overviewProgress || mLauncher.isInState(BACKGROUND_APP)) { 143 float hotseatShift = (progress - overviewProgress) * mShiftRange; 144 mLauncher.getHotseat().setTranslationY(hotseatShift); 145 } 146 } 147 } 148 getProgress()149 public float getProgress() { 150 return mProgress; 151 } 152 153 /** 154 * Sets the vertical transition progress to {@param state} and updates all the dependent UI 155 * accordingly. 156 */ 157 @Override setState(LauncherState state)158 public void setState(LauncherState state) { 159 setProgress(state.getVerticalProgress(mLauncher)); 160 setAlphas(state, null, new AnimatorSetBuilder()); 161 onProgressAnimationEnd(); 162 } 163 164 /** 165 * Creates an animation which updates the vertical transition progress and updates all the 166 * dependent UI using various animation events 167 */ 168 @Override setStateWithAnimation(LauncherState toState, AnimatorSetBuilder builder, AnimationConfig config)169 public void setStateWithAnimation(LauncherState toState, 170 AnimatorSetBuilder builder, AnimationConfig config) { 171 if (TestProtocol.sDebugTracing) { 172 Log.d(TestProtocol.NO_ALLAPPS_EVENT_TAG, 173 "setStateWithAnimation " + toState.getClass().getSimpleName()); 174 } 175 float targetProgress = toState.getVerticalProgress(mLauncher); 176 if (Float.compare(mProgress, targetProgress) == 0) { 177 setAlphas(toState, config, builder); 178 // Fail fast 179 onProgressAnimationEnd(); 180 return; 181 } 182 183 if (!config.playNonAtomicComponent()) { 184 // There is no atomic component for the all apps transition, so just return early. 185 return; 186 } 187 188 Interpolator interpolator = config.userControlled ? LINEAR : toState == OVERVIEW 189 ? builder.getInterpolator(ANIM_OVERVIEW_SCALE, FAST_OUT_SLOW_IN) 190 : FAST_OUT_SLOW_IN; 191 Animator anim = createSpringAnimation(mProgress, targetProgress); 192 anim.setDuration(config.duration); 193 anim.setInterpolator(builder.getInterpolator(ANIM_VERTICAL_PROGRESS, interpolator)); 194 anim.addListener(getProgressAnimatorListener()); 195 196 builder.play(anim); 197 198 setAlphas(toState, config, builder); 199 } 200 createSpringAnimation(float... progressValues)201 public Animator createSpringAnimation(float... progressValues) { 202 return new SpringObjectAnimator<>(this, ALL_APPS_PROGRESS, 1f / mShiftRange, 203 SPRING_DAMPING_RATIO, SPRING_STIFFNESS, progressValues); 204 } 205 setAlphas(LauncherState toState, AnimationConfig config, AnimatorSetBuilder builder)206 private void setAlphas(LauncherState toState, AnimationConfig config, 207 AnimatorSetBuilder builder) { 208 setAlphas(toState.getVisibleElements(mLauncher), config, builder); 209 } 210 setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder)211 public void setAlphas(int visibleElements, AnimationConfig config, AnimatorSetBuilder builder) { 212 PropertySetter setter = config == null ? NO_ANIM_PROPERTY_SETTER 213 : config.getPropertySetter(builder); 214 boolean hasHeaderExtra = (visibleElements & ALL_APPS_HEADER_EXTRA) != 0; 215 boolean hasContent = (visibleElements & ALL_APPS_CONTENT) != 0; 216 217 Interpolator allAppsFade = builder.getInterpolator(ANIM_ALL_APPS_FADE, LINEAR); 218 setter.setViewAlpha(mAppsView.getContentView(), hasContent ? 1 : 0, allAppsFade); 219 setter.setViewAlpha(mAppsView.getScrollBar(), hasContent ? 1 : 0, allAppsFade); 220 mAppsView.getFloatingHeaderView().setContentVisibility(hasHeaderExtra, hasContent, setter, 221 allAppsFade); 222 mAppsView.getSearchUiManager().setContentVisibility(visibleElements, setter, allAppsFade); 223 224 setter.setInt(mScrimView, ScrimView.DRAG_HANDLE_ALPHA, 225 (visibleElements & VERTICAL_SWIPE_INDICATOR) != 0 ? 255 : 0, allAppsFade); 226 } 227 getProgressAnimatorListener()228 public AnimatorListenerAdapter getProgressAnimatorListener() { 229 return new AnimationSuccessListener() { 230 @Override 231 public void onAnimationSuccess(Animator animator) { 232 onProgressAnimationEnd(); 233 } 234 }; 235 } 236 237 public void setupViews(AllAppsContainerView appsView) { 238 mAppsView = appsView; 239 mScrimView = mLauncher.findViewById(R.id.scrim_view); 240 } 241 242 /** 243 * Updates the total scroll range but does not update the UI. 244 */ 245 void setScrollRangeDelta(float delta) { 246 mScrollRangeDelta = delta; 247 mShiftRange = mLauncher.getDeviceProfile().heightPx - mScrollRangeDelta; 248 249 if (mScrimView != null) { 250 mScrimView.reInitUi(); 251 } 252 } 253 254 /** 255 * Set the final view states based on the progress. 256 * TODO: This logic should go in {@link LauncherState} 257 */ 258 private void onProgressAnimationEnd() { 259 if (Float.compare(mProgress, 1f) == 0) { 260 mAppsView.reset(false /* animate */); 261 } else if (isAllAppsExpanded()) { 262 mAppsView.onScrollUpEnd(); 263 } 264 } 265 266 private boolean isAllAppsExpanded() { 267 return Float.compare(mProgress, 0f) == 0; 268 } 269 270 public void highlightWorkTabIfNecessary() { 271 if (isAllAppsExpanded()) { 272 mAppsView.highlightWorkTabIfNecessary(); 273 } 274 } 275 } 276