1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.launcher3.taskbar.allapps; 17 18 import static com.android.app.animation.Interpolators.EMPHASIZED; 19 import static com.android.launcher3.Flags.enablePredictiveBackGesture; 20 import static com.android.launcher3.touch.AllAppsSwipeController.ALL_APPS_FADE_MANUAL; 21 import static com.android.launcher3.touch.AllAppsSwipeController.SCRIM_FADE_MANUAL; 22 23 import android.animation.Animator; 24 import android.content.Context; 25 import android.graphics.Canvas; 26 import android.graphics.Rect; 27 import android.os.Handler; 28 import android.os.Looper; 29 import android.util.AttributeSet; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.animation.Interpolator; 33 import android.window.OnBackInvokedDispatcher; 34 35 import androidx.annotation.Nullable; 36 37 import com.android.app.animation.Interpolators; 38 import com.android.launcher3.DeviceProfile; 39 import com.android.launcher3.Insettable; 40 import com.android.launcher3.R; 41 import com.android.launcher3.anim.AnimatorListeners; 42 import com.android.launcher3.anim.PendingAnimation; 43 import com.android.launcher3.config.FeatureFlags; 44 import com.android.launcher3.taskbar.allapps.TaskbarAllAppsViewController.TaskbarAllAppsCallbacks; 45 import com.android.launcher3.taskbar.overlay.TaskbarOverlayContext; 46 import com.android.launcher3.util.Themes; 47 import com.android.launcher3.views.AbstractSlideInView; 48 49 /** Wrapper for taskbar all apps with slide-in behavior. */ 50 public class TaskbarAllAppsSlideInView extends AbstractSlideInView<TaskbarOverlayContext> 51 implements Insettable, DeviceProfile.OnDeviceProfileChangeListener { 52 private final Handler mHandler; 53 54 private TaskbarAllAppsContainerView mAppsView; 55 private float mShiftRange; 56 private @Nullable Runnable mShowOnFullyAttachedToWindowRunnable; 57 58 // Initialized in init. 59 private TaskbarAllAppsCallbacks mAllAppsCallbacks; 60 TaskbarAllAppsSlideInView(Context context, AttributeSet attrs)61 public TaskbarAllAppsSlideInView(Context context, AttributeSet attrs) { 62 this(context, attrs, 0); 63 } 64 TaskbarAllAppsSlideInView(Context context, AttributeSet attrs, int defStyleAttr)65 public TaskbarAllAppsSlideInView(Context context, AttributeSet attrs, 66 int defStyleAttr) { 67 super(context, attrs, defStyleAttr); 68 mHandler = new Handler(Looper.myLooper()); 69 } 70 init(TaskbarAllAppsCallbacks callbacks)71 void init(TaskbarAllAppsCallbacks callbacks) { 72 mAllAppsCallbacks = callbacks; 73 } 74 75 /** Opens the all apps view. */ show(boolean animate)76 void show(boolean animate) { 77 if (mIsOpen || mOpenCloseAnimation.getAnimationPlayer().isRunning()) { 78 return; 79 } 80 mIsOpen = true; 81 82 addOnAttachStateChangeListener(new OnAttachStateChangeListener() { 83 @Override 84 public void onViewAttachedToWindow(View v) { 85 removeOnAttachStateChangeListener(this); 86 // Wait for view and its descendants to be fully attached before starting open. 87 mShowOnFullyAttachedToWindowRunnable = () -> showOnFullyAttachedToWindow(animate); 88 mHandler.post(mShowOnFullyAttachedToWindowRunnable); 89 } 90 91 @Override 92 public void onViewDetachedFromWindow(View v) { 93 removeOnAttachStateChangeListener(this); 94 } 95 }); 96 attachToContainer(); 97 } 98 showOnFullyAttachedToWindow(boolean animate)99 private void showOnFullyAttachedToWindow(boolean animate) { 100 mAllAppsCallbacks.onAllAppsTransitionStart(true); 101 if (!animate) { 102 mAllAppsCallbacks.onAllAppsTransitionEnd(true); 103 setTranslationShift(TRANSLATION_SHIFT_OPENED); 104 return; 105 } 106 107 setUpOpenAnimation(mAllAppsCallbacks.getOpenDuration()); 108 Animator animator = mOpenCloseAnimation.getAnimationPlayer(); 109 animator.setInterpolator(EMPHASIZED); 110 animator.addListener(AnimatorListeners.forEndCallback(() -> { 111 if (mIsOpen) { 112 mAllAppsCallbacks.onAllAppsTransitionEnd(true); 113 } 114 })); 115 animator.start(); 116 } 117 118 @Override onOpenCloseAnimationPending(PendingAnimation animation)119 protected void onOpenCloseAnimationPending(PendingAnimation animation) { 120 final boolean isOpening = mToTranslationShift == TRANSLATION_SHIFT_OPENED; 121 122 if (mActivityContext.getDeviceProfile().isPhone) { 123 final Interpolator allAppsFadeInterpolator = 124 isOpening ? ALL_APPS_FADE_MANUAL : Interpolators.reverse(ALL_APPS_FADE_MANUAL); 125 animation.setViewAlpha(mAppsView, 1 - mToTranslationShift, allAppsFadeInterpolator); 126 } 127 128 mAllAppsCallbacks.onAllAppsAnimationPending(animation, isOpening); 129 } 130 131 @Override getScrimInterpolator()132 protected Interpolator getScrimInterpolator() { 133 if (mActivityContext.getDeviceProfile().isTablet) { 134 return super.getScrimInterpolator(); 135 } 136 return mToTranslationShift == TRANSLATION_SHIFT_OPENED 137 ? SCRIM_FADE_MANUAL 138 : Interpolators.reverse(SCRIM_FADE_MANUAL); 139 } 140 141 /** The apps container inside this view. */ getAppsView()142 TaskbarAllAppsContainerView getAppsView() { 143 return mAppsView; 144 } 145 146 @Override handleClose(boolean animate)147 protected void handleClose(boolean animate) { 148 if (mShowOnFullyAttachedToWindowRunnable != null) { 149 mHandler.removeCallbacks(mShowOnFullyAttachedToWindowRunnable); 150 mShowOnFullyAttachedToWindowRunnable = null; 151 } 152 if (mIsOpen) { 153 mAllAppsCallbacks.onAllAppsTransitionStart(false); 154 } 155 handleClose(animate, mAllAppsCallbacks.getCloseDuration()); 156 } 157 158 @Override onCloseComplete()159 protected void onCloseComplete() { 160 mAllAppsCallbacks.onAllAppsTransitionEnd(false); 161 super.onCloseComplete(); 162 } 163 164 @Override getIdleInterpolator()165 protected Interpolator getIdleInterpolator() { 166 return EMPHASIZED; 167 } 168 169 @Override isOfType(int type)170 protected boolean isOfType(int type) { 171 return (type & TYPE_TASKBAR_ALL_APPS) != 0; 172 } 173 174 @Override onFinishInflate()175 protected void onFinishInflate() { 176 super.onFinishInflate(); 177 mAppsView = findViewById(R.id.apps_view); 178 if (mActivityContext.getDeviceProfile().isPhone) { 179 mAppsView.setAlpha(0); 180 } 181 mContent = mAppsView; 182 183 // Setup header protection for search bar, if enabled. 184 if (FeatureFlags.ENABLE_ALL_APPS_SEARCH_IN_TASKBAR.get()) { 185 mAppsView.setOnInvalidateHeaderListener(this::invalidate); 186 } 187 188 DeviceProfile dp = mActivityContext.getDeviceProfile(); 189 setShiftRange(dp.allAppsShiftRange); 190 } 191 192 @Override onAttachedToWindow()193 protected void onAttachedToWindow() { 194 super.onAttachedToWindow(); 195 mActivityContext.addOnDeviceProfileChangeListener(this); 196 if (enablePredictiveBackGesture()) { 197 mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(mViewOutlineProvider); 198 mAppsView.getAppsRecyclerViewContainer().setClipToOutline(true); 199 OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); 200 if (dispatcher != null) { 201 dispatcher.registerOnBackInvokedCallback( 202 OnBackInvokedDispatcher.PRIORITY_DEFAULT, this); 203 } 204 } 205 } 206 207 @Override onDetachedFromWindow()208 protected void onDetachedFromWindow() { 209 super.onDetachedFromWindow(); 210 mActivityContext.removeOnDeviceProfileChangeListener(this); 211 if (enablePredictiveBackGesture()) { 212 mAppsView.getAppsRecyclerViewContainer().setOutlineProvider(null); 213 mAppsView.getAppsRecyclerViewContainer().setClipToOutline(false); 214 OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher(); 215 if (dispatcher != null) { 216 dispatcher.unregisterOnBackInvokedCallback(this); 217 } 218 } 219 } 220 221 @Override dispatchDraw(Canvas canvas)222 protected void dispatchDraw(Canvas canvas) { 223 // We should call drawOnScrimWithBottomOffset() rather than drawOnScrimWithScale(). Because 224 // for taskbar all apps, the scrim view is a child view of AbstractSlideInView. Thus scaling 225 // down in AbstractSlideInView#onScaleProgressChanged() with SCALE_PROPERTY has already 226 // done the job - there is no need to re-apply scale effect here. But it also means we need 227 // to pass extra bottom offset to background scrim to fill the bottom gap during predictive 228 // back swipe. 229 mAppsView.drawOnScrimWithBottomOffset(canvas, getBottomOffsetPx()); 230 super.dispatchDraw(canvas); 231 } 232 233 @Override onLayout(boolean changed, int l, int t, int r, int b)234 protected void onLayout(boolean changed, int l, int t, int r, int b) { 235 super.onLayout(changed, l, t, r, b); 236 setTranslationShift(mTranslationShift); 237 } 238 239 @Override getScrimColor(Context context)240 protected int getScrimColor(Context context) { 241 return mActivityContext.getDeviceProfile().isPhone 242 ? Themes.getAttrColor(context, R.attr.allAppsScrimColor) 243 : context.getColor(R.color.widgets_picker_scrim); 244 } 245 246 @Override onControllerInterceptTouchEvent(MotionEvent ev)247 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 248 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 249 mNoIntercept = !mAppsView.shouldContainerScroll(ev) 250 || getTopOpenViewWithType( 251 mActivityContext, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null; 252 } 253 return super.onControllerInterceptTouchEvent(ev); 254 } 255 256 @Override setInsets(Rect insets)257 public void setInsets(Rect insets) { 258 mAppsView.setInsets(insets); 259 } 260 261 @Override onDeviceProfileChanged(DeviceProfile dp)262 public void onDeviceProfileChanged(DeviceProfile dp) { 263 setShiftRange(dp.allAppsShiftRange); 264 setTranslationShift(TRANSLATION_SHIFT_OPENED); 265 } 266 setShiftRange(float shiftRange)267 private void setShiftRange(float shiftRange) { 268 mShiftRange = shiftRange; 269 } 270 271 @Override getShiftRange()272 protected float getShiftRange() { 273 return mShiftRange; 274 } 275 276 @Override isEventOverContent(MotionEvent ev)277 protected boolean isEventOverContent(MotionEvent ev) { 278 return getPopupContainer().isEventOverView(mAppsView.getVisibleContainerView(), ev); 279 } 280 281 /** 282 * In taskbar all apps search mode, we should scale down content inside all apps, rather 283 * than the whole all apps bottom sheet, to indicate we will navigate back within the all apps. 284 */ 285 @Override shouldAnimateContentViewInBackSwipe()286 public boolean shouldAnimateContentViewInBackSwipe() { 287 return mAllAppsCallbacks.canHandleSearchBackInvoked(); 288 } 289 290 @Override onUserSwipeToDismissProgressChanged()291 protected void onUserSwipeToDismissProgressChanged() { 292 super.onUserSwipeToDismissProgressChanged(); 293 mAppsView.setClipChildren(!mIsDismissInProgress); 294 mAppsView.getAppsRecyclerViewContainer().setClipChildren(!mIsDismissInProgress); 295 } 296 297 @Override onBackInvoked()298 public void onBackInvoked() { 299 if (mAllAppsCallbacks.handleSearchBackInvoked()) { 300 // We need to scale back taskbar all apps if we navigate back within search inside all 301 // apps 302 post(this::animateSwipeToDismissProgressToStart); 303 } else { 304 super.onBackInvoked(); 305 } 306 } 307 } 308