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