1 /*
2  * Copyright (C) 2021 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;
17 
18 import static com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY;
19 import static com.android.launcher3.Utilities.squaredHypot;
20 import static com.android.launcher3.anim.Interpolators.LINEAR;
21 import static com.android.quickstep.AnimatedFloat.VALUE;
22 
23 import android.graphics.Rect;
24 import android.util.FloatProperty;
25 import android.view.MotionEvent;
26 import android.view.View;
27 import android.view.ViewTreeObserver;
28 import android.view.ViewTreeObserver.OnPreDrawListener;
29 
30 import com.android.launcher3.BubbleTextView;
31 import com.android.launcher3.DeviceProfile;
32 import com.android.launcher3.LauncherAppState;
33 import com.android.launcher3.Utilities;
34 import com.android.launcher3.anim.AnimatorPlaybackController;
35 import com.android.launcher3.anim.PendingAnimation;
36 import com.android.launcher3.folder.FolderIcon;
37 import com.android.launcher3.model.data.ItemInfo;
38 import com.android.launcher3.util.MultiValueAlpha;
39 import com.android.quickstep.AnimatedFloat;
40 
41 /**
42  * Handles properties/data collection, then passes the results to TaskbarView to render.
43  */
44 public class TaskbarViewController {
45     private static final Runnable NO_OP = () -> { };
46 
47     public static final int ALPHA_INDEX_HOME = 0;
48     public static final int ALPHA_INDEX_KEYGUARD = 1;
49     public static final int ALPHA_INDEX_STASH = 2;
50     public static final int ALPHA_INDEX_RECENTS_DISABLED = 3;
51     public static final int ALPHA_INDEX_NOTIFICATION_EXPANDED = 4;
52     private static final int NUM_ALPHA_CHANNELS = 5;
53 
54     private final TaskbarActivityContext mActivity;
55     private final TaskbarView mTaskbarView;
56     private final MultiValueAlpha mTaskbarIconAlpha;
57     private final AnimatedFloat mTaskbarIconScaleForStash = new AnimatedFloat(this::updateScale);
58     private final AnimatedFloat mTaskbarIconTranslationYForHome = new AnimatedFloat(
59             this::updateTranslationY);
60     private final AnimatedFloat mTaskbarIconTranslationYForStash = new AnimatedFloat(
61             this::updateTranslationY);
62     private AnimatedFloat mTaskbarNavButtonTranslationY;
63 
64     private final TaskbarModelCallbacks mModelCallbacks;
65 
66     // Initialized in init.
67     private TaskbarControllers mControllers;
68 
69     // Animation to align icons with Launcher, created lazily. This allows the controller to be
70     // active only during the animation and does not need to worry about layout changes.
71     private AnimatorPlaybackController mIconAlignControllerLazy = null;
72     private Runnable mOnControllerPreCreateCallback = NO_OP;
73 
TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView)74     public TaskbarViewController(TaskbarActivityContext activity, TaskbarView taskbarView) {
75         mActivity = activity;
76         mTaskbarView = taskbarView;
77         mTaskbarIconAlpha = new MultiValueAlpha(mTaskbarView, NUM_ALPHA_CHANNELS);
78         mTaskbarIconAlpha.setUpdateVisibility(true);
79         mModelCallbacks = new TaskbarModelCallbacks(activity, mTaskbarView);
80     }
81 
init(TaskbarControllers controllers)82     public void init(TaskbarControllers controllers) {
83         mControllers = controllers;
84         mTaskbarView.init(new TaskbarViewCallbacks());
85         mTaskbarView.getLayoutParams().height = mActivity.getDeviceProfile().taskbarSize;
86 
87         mTaskbarIconScaleForStash.updateValue(1f);
88 
89         mModelCallbacks.init(controllers);
90         if (mActivity.isUserSetupComplete()) {
91             // Only load the callbacks if user setup is completed
92             LauncherAppState.getInstance(mActivity).getModel().addCallbacksAndLoad(mModelCallbacks);
93         }
94         mTaskbarNavButtonTranslationY =
95                 controllers.navbarButtonsViewController.getTaskbarNavButtonTranslationY();
96     }
97 
onDestroy()98     public void onDestroy() {
99         LauncherAppState.getInstance(mActivity).getModel().removeCallbacks(mModelCallbacks);
100     }
101 
areIconsVisible()102     public boolean areIconsVisible() {
103         return mTaskbarView.areIconsVisible();
104     }
105 
getTaskbarIconAlpha()106     public MultiValueAlpha getTaskbarIconAlpha() {
107         return mTaskbarIconAlpha;
108     }
109 
110     /**
111      * Should be called when the IME visibility changes, so we can make Taskbar not steal touches.
112      */
setImeIsVisible(boolean isImeVisible)113     public void setImeIsVisible(boolean isImeVisible) {
114         mTaskbarView.setTouchesEnabled(!isImeVisible);
115     }
116 
117     /**
118      * Should be called when the recents button is disabled, so we can hide taskbar icons as well.
119      */
setRecentsButtonDisabled(boolean isDisabled)120     public void setRecentsButtonDisabled(boolean isDisabled) {
121         // TODO: check TaskbarStashController#supportsStashing(), to stash instead of setting alpha.
122         mTaskbarIconAlpha.getProperty(ALPHA_INDEX_RECENTS_DISABLED).setValue(isDisabled ? 0 : 1);
123     }
124 
125     /**
126      * Sets OnClickListener and OnLongClickListener for the given view.
127      */
setClickAndLongClickListenersForIcon(View icon)128     public void setClickAndLongClickListenersForIcon(View icon) {
129         mTaskbarView.setClickAndLongClickListenersForIcon(icon);
130     }
131 
132     /**
133      * Adds one time pre draw listener to the taskbar view, it is called before
134      * drawing a frame and invoked only once
135      * @param listener callback that will be invoked before drawing the next frame
136      */
addOneTimePreDrawListener(Runnable listener)137     public void addOneTimePreDrawListener(Runnable listener) {
138         mTaskbarView.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
139             @Override
140             public boolean onPreDraw() {
141                 final ViewTreeObserver viewTreeObserver = mTaskbarView.getViewTreeObserver();
142                 if (viewTreeObserver.isAlive()) {
143                     listener.run();
144                     viewTreeObserver.removeOnPreDrawListener(this);
145                 }
146                 return true;
147             }
148         });
149     }
150 
getIconLayoutBounds()151     public Rect getIconLayoutBounds() {
152         return mTaskbarView.getIconLayoutBounds();
153     }
154 
getIconViews()155     public View[] getIconViews() {
156         return mTaskbarView.getIconViews();
157     }
158 
getTaskbarIconScaleForStash()159     public AnimatedFloat getTaskbarIconScaleForStash() {
160         return mTaskbarIconScaleForStash;
161     }
162 
getTaskbarIconTranslationYForStash()163     public AnimatedFloat getTaskbarIconTranslationYForStash() {
164         return mTaskbarIconTranslationYForStash;
165     }
166 
167     /**
168      * Applies scale properties for the entire TaskbarView (rather than individual icons).
169      */
updateScale()170     private void updateScale() {
171         float scale = mTaskbarIconScaleForStash.value;
172         mTaskbarView.setScaleX(scale);
173         mTaskbarView.setScaleY(scale);
174     }
175 
updateTranslationY()176     private void updateTranslationY() {
177         mTaskbarView.setTranslationY(mTaskbarIconTranslationYForHome.value
178                 + mTaskbarIconTranslationYForStash.value);
179     }
180 
181     /**
182      * Sets the taskbar icon alignment relative to Launcher hotseat icons
183      * @param alignmentRatio [0, 1]
184      *                       0 => not aligned
185      *                       1 => fully aligned
186      */
setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp)187     public void setLauncherIconAlignment(float alignmentRatio, DeviceProfile launcherDp) {
188         if (mIconAlignControllerLazy == null) {
189             mIconAlignControllerLazy = createIconAlignmentController(launcherDp);
190         }
191         mIconAlignControllerLazy.setPlayFraction(alignmentRatio);
192         if (alignmentRatio <= 0 || alignmentRatio >= 1) {
193             // Cleanup lazy controller so that it is created again in next animation
194             mIconAlignControllerLazy = null;
195         }
196     }
197 
198     /**
199      * Creates an animation for aligning the taskbar icons with the provided Launcher device profile
200      */
createIconAlignmentController(DeviceProfile launcherDp)201     private AnimatorPlaybackController createIconAlignmentController(DeviceProfile launcherDp) {
202         mOnControllerPreCreateCallback.run();
203         PendingAnimation setter = new PendingAnimation(100);
204         Rect hotseatPadding = launcherDp.getHotseatLayoutPadding(mActivity);
205         float scaleUp = ((float) launcherDp.iconSizePx) / mActivity.getDeviceProfile().iconSizePx;
206         int hotseatCellSize =
207                 (launcherDp.availableWidthPx - hotseatPadding.left - hotseatPadding.right)
208                         / launcherDp.numShownHotseatIcons;
209 
210         int offsetY = launcherDp.getTaskbarOffsetY();
211         setter.setFloat(mTaskbarIconTranslationYForHome, VALUE, -offsetY, LINEAR);
212         setter.setFloat(mTaskbarNavButtonTranslationY, VALUE, -offsetY, LINEAR);
213 
214         int collapsedHeight = mActivity.getDefaultTaskbarWindowHeight();
215         int expandedHeight = Math.max(collapsedHeight,
216                 mActivity.getDeviceProfile().taskbarSize + offsetY);
217         setter.addOnFrameListener(anim -> mActivity.setTaskbarWindowHeight(
218                 anim.getAnimatedFraction() > 0 ? expandedHeight : collapsedHeight));
219 
220         int count = mTaskbarView.getChildCount();
221         for (int i = 0; i < count; i++) {
222             View child = mTaskbarView.getChildAt(i);
223             ItemInfo info = (ItemInfo) child.getTag();
224             setter.setFloat(child, SCALE_PROPERTY, scaleUp, LINEAR);
225 
226             float childCenter = (child.getLeft() + child.getRight()) / 2;
227             float hotseatIconCenter = hotseatPadding.left + hotseatCellSize * info.screenId
228                     + hotseatCellSize / 2;
229             setter.setFloat(child, ICON_TRANSLATE_X, hotseatIconCenter - childCenter, LINEAR);
230         }
231 
232         AnimatorPlaybackController controller = setter.createPlaybackController();
233         mOnControllerPreCreateCallback = () -> controller.setPlayFraction(0);
234         return controller;
235     }
236 
onRotationChanged(DeviceProfile deviceProfile)237     public void onRotationChanged(DeviceProfile deviceProfile) {
238         if (areIconsVisible()) {
239             // We only translate on rotation when on home
240             return;
241         }
242         mTaskbarNavButtonTranslationY.updateValue(-deviceProfile.getTaskbarOffsetY());
243     }
244 
245     /**
246      * Returns whether the given MotionEvent, *in screen coorindates*, is within any Taskbar item's
247      * touch bounds.
248      */
isEventOverAnyItem(MotionEvent ev)249     public boolean isEventOverAnyItem(MotionEvent ev) {
250         return mTaskbarView.isEventOverAnyItem(ev);
251     }
252 
253     /**
254      * Callbacks for {@link TaskbarView} to interact with its controller.
255      */
256     public class TaskbarViewCallbacks {
257         private final float mSquaredTouchSlop = Utilities.squaredTouchSlop(mActivity);
258 
259         private float mDownX, mDownY;
260         private boolean mCanceledStashHint;
261 
getIconOnClickListener()262         public View.OnClickListener getIconOnClickListener() {
263             return mActivity.getItemOnClickListener();
264         }
265 
getIconOnLongClickListener()266         public View.OnLongClickListener getIconOnLongClickListener() {
267             return mControllers.taskbarDragController::startDragOnLongClick;
268         }
269 
getBackgroundOnLongClickListener()270         public View.OnLongClickListener getBackgroundOnLongClickListener() {
271             return view -> mControllers.taskbarStashController
272                     .updateAndAnimateIsManuallyStashedInApp(true);
273         }
274 
275         /**
276          * Get the first chance to handle TaskbarView#onTouchEvent, and return whether we want to
277          * consume the touch so TaskbarView treats it as an ACTION_CANCEL.
278          */
onTouchEvent(MotionEvent motionEvent)279         public boolean onTouchEvent(MotionEvent motionEvent) {
280             final float x = motionEvent.getRawX();
281             final float y = motionEvent.getRawY();
282             switch (motionEvent.getAction()) {
283                 case MotionEvent.ACTION_DOWN:
284                     mDownX = x;
285                     mDownY = y;
286                     mControllers.taskbarStashController.startStashHint(/* animateForward = */ true);
287                     mCanceledStashHint = false;
288                     break;
289                 case MotionEvent.ACTION_MOVE:
290                     if (!mCanceledStashHint
291                             && squaredHypot(mDownX - x, mDownY - y) > mSquaredTouchSlop) {
292                         mControllers.taskbarStashController.startStashHint(
293                                 /* animateForward= */ false);
294                         mCanceledStashHint = true;
295                         return true;
296                     }
297                     break;
298                 case MotionEvent.ACTION_UP:
299                 case MotionEvent.ACTION_CANCEL:
300                     if (!mCanceledStashHint) {
301                         mControllers.taskbarStashController.startStashHint(
302                                 /* animateForward= */ false);
303                     }
304                     break;
305             }
306             return false;
307         }
308     }
309 
310     public static final FloatProperty<View> ICON_TRANSLATE_X =
311             new FloatProperty<View>("taskbarAligmentTranslateX") {
312 
313                 @Override
314                 public void setValue(View view, float v) {
315                     if (view instanceof BubbleTextView) {
316                         ((BubbleTextView) view).setTranslationXForTaskbarAlignmentAnimation(v);
317                     } else if (view instanceof FolderIcon) {
318                         ((FolderIcon) view).setTranslationForTaskbarAlignmentAnimation(v);
319                     } else {
320                         view.setTranslationX(v);
321                     }
322                 }
323 
324                 @Override
325                 public Float get(View view) {
326                     if (view instanceof BubbleTextView) {
327                         return ((BubbleTextView) view)
328                                 .getTranslationXForTaskbarAlignmentAnimation();
329                     } else if (view instanceof FolderIcon) {
330                         return ((FolderIcon) view).getTranslationXForTaskbarAlignmentAnimation();
331                     }
332                     return view.getTranslationX();
333                 }
334             };
335 }
336