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