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