1 /* 2 * Copyright (C) 2018 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 17 package com.android.quickstep.views; 18 19 import static com.android.app.animation.Interpolators.EMPHASIZED; 20 import static com.android.launcher3.Flags.enableOverviewIconMenu; 21 import static com.android.launcher3.util.MultiPropertyFactory.MULTI_PROPERTY_VALUE; 22 import static com.android.launcher3.util.SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT; 23 import static com.android.quickstep.views.TaskThumbnailViewDeprecated.DIM_ALPHA; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorSet; 27 import android.animation.ObjectAnimator; 28 import android.animation.ValueAnimator; 29 import android.content.Context; 30 import android.graphics.Outline; 31 import android.graphics.Rect; 32 import android.graphics.drawable.GradientDrawable; 33 import android.graphics.drawable.ShapeDrawable; 34 import android.graphics.drawable.shapes.RectShape; 35 import android.util.AttributeSet; 36 import android.view.Gravity; 37 import android.view.MotionEvent; 38 import android.view.View; 39 import android.view.ViewOutlineProvider; 40 import android.widget.LinearLayout; 41 import android.widget.TextView; 42 43 import androidx.annotation.Nullable; 44 45 import com.android.app.animation.Interpolators; 46 import com.android.launcher3.AbstractFloatingView; 47 import com.android.launcher3.DeviceProfile; 48 import com.android.launcher3.R; 49 import com.android.launcher3.anim.AnimationSuccessListener; 50 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; 51 import com.android.launcher3.popup.SystemShortcut; 52 import com.android.launcher3.views.BaseDragLayer; 53 import com.android.quickstep.TaskOverlayFactory; 54 import com.android.quickstep.TaskUtils; 55 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 56 import com.android.quickstep.util.TaskCornerRadius; 57 import com.android.quickstep.views.TaskView.TaskContainer; 58 59 /** 60 * Contains options for a recent task when long-pressing its icon. 61 */ 62 public class TaskMenuView extends AbstractFloatingView { 63 64 private static final Rect sTempRect = new Rect(); 65 66 private static final int REVEAL_OPEN_DURATION = enableOverviewIconMenu() ? 417 : 150; 67 private static final int REVEAL_CLOSE_DURATION = enableOverviewIconMenu() ? 333 : 100; 68 69 private RecentsViewContainer mContainer; 70 private TextView mTaskName; 71 @Nullable 72 private AnimatorSet mOpenCloseAnimator; 73 @Nullable 74 private ValueAnimator mRevealAnimator; 75 @Nullable private Runnable mOnClosingStartCallback; 76 private TaskView mTaskView; 77 private TaskContainer mTaskContainer; 78 private LinearLayout mOptionLayout; 79 private float mMenuTranslationYBeforeOpen; 80 private float mMenuTranslationXBeforeOpen; 81 TaskMenuView(Context context, AttributeSet attrs)82 public TaskMenuView(Context context, AttributeSet attrs) { 83 this(context, attrs, 0); 84 } 85 TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr)86 public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { 87 super(context, attrs, defStyleAttr); 88 89 mContainer = RecentsViewContainer.containerFromContext(context); 90 setClipToOutline(true); 91 } 92 93 @Override onFinishInflate()94 protected void onFinishInflate() { 95 super.onFinishInflate(); 96 mTaskName = findViewById(R.id.task_name); 97 mOptionLayout = findViewById(R.id.menu_option_layout); 98 } 99 100 @Override onControllerInterceptTouchEvent(MotionEvent ev)101 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 102 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 103 BaseDragLayer dl = mContainer.getDragLayer(); 104 if (!dl.isEventOverView(this, ev)) { 105 // TODO: log this once we have a new container type for it? 106 close(true); 107 return true; 108 } 109 } 110 return false; 111 } 112 113 @Override handleClose(boolean animate)114 protected void handleClose(boolean animate) { 115 animateClose(); 116 } 117 118 @Override isOfType(int type)119 protected boolean isOfType(int type) { 120 return (type & TYPE_TASK_MENU) != 0; 121 } 122 123 @Override getOutlineProvider()124 public ViewOutlineProvider getOutlineProvider() { 125 return new ViewOutlineProvider() { 126 @Override 127 public void getOutline(View view, Outline outline) { 128 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 129 TaskCornerRadius.get(view.getContext())); 130 } 131 }; 132 } 133 134 @Override 135 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 136 if (!(enableOverviewIconMenu() 137 && ((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView))) { 138 // TODO(b/326952853): Cap menu height for grid bottom row in a way that doesn't break 139 // additionalTranslationY. 140 int maxMenuHeight = calculateMaxHeight(); 141 if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) { 142 heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST); 143 } 144 } 145 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 146 } 147 148 public void onRotationChanged() { 149 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 150 mOpenCloseAnimator.end(); 151 } 152 if (mIsOpen) { 153 mOptionLayout.removeAllViews(); 154 if (enableOverviewIconMenu() || !populateAndLayoutMenu()) { 155 close(false); 156 } 157 } 158 } 159 160 /** 161 * Show a task menu for the given taskContainer. 162 */ 163 public static boolean showForTask(TaskContainer taskContainer, 164 @Nullable Runnable onClosingStartCallback) { 165 RecentsViewContainer container = RecentsViewContainer.containerFromContext( 166 taskContainer.getTaskView().getContext()); 167 final TaskMenuView taskMenuView = (TaskMenuView) container.getLayoutInflater().inflate( 168 R.layout.task_menu, container.getDragLayer(), false); 169 taskMenuView.setOnClosingStartCallback(onClosingStartCallback); 170 return taskMenuView.populateAndShowForTask(taskContainer); 171 } 172 173 /** 174 * Show a task menu for the given taskContainer. 175 */ 176 public static boolean showForTask(TaskContainer taskContainer) { 177 return showForTask(taskContainer, null); 178 } 179 180 private boolean populateAndShowForTask(TaskContainer taskContainer) { 181 if (isAttachedToWindow()) { 182 return false; 183 } 184 mContainer.getDragLayer().addView(this); 185 mTaskView = taskContainer.getTaskView(); 186 mTaskContainer = taskContainer; 187 if (!populateAndLayoutMenu()) { 188 return false; 189 } 190 post(this::animateOpen); 191 return true; 192 } 193 194 /** @return true if successfully able to populate task view menu, false otherwise */ 195 private boolean populateAndLayoutMenu() { 196 addMenuOptions(mTaskContainer); 197 orientAroundTaskView(mTaskContainer); 198 return true; 199 } 200 201 private void addMenuOptions(TaskContainer taskContainer) { 202 if (enableOverviewIconMenu()) { 203 removeView(mTaskName); 204 } else { 205 mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask())); 206 mTaskName.setOnClickListener(v -> close(true)); 207 } 208 TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer) 209 .forEach(this::addMenuOption); 210 } 211 212 private void addMenuOption(SystemShortcut menuOption) { 213 LinearLayout menuOptionView = (LinearLayout) mContainer.getLayoutInflater().inflate( 214 R.layout.task_view_menu_option, this, false); 215 if (enableOverviewIconMenu()) { 216 ((GradientDrawable) menuOptionView.getBackground()).setCornerRadius(0); 217 } 218 menuOption.setIconAndLabelFor( 219 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); 220 LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams(); 221 mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp, 222 menuOptionView, mContainer.getDeviceProfile()); 223 // Set an onClick listener on each menu option. The onClick method is responsible for 224 // ending LiveTile mode on the thumbnail if needed. 225 menuOptionView.setOnClickListener(menuOption::onClick); 226 mOptionLayout.addView(menuOptionView); 227 } 228 229 private void orientAroundTaskView(TaskContainer taskContainer) { 230 RecentsView recentsView = mContainer.getOverviewPanel(); 231 RecentsPagedOrientationHandler orientationHandler = 232 recentsView.getPagedOrientationHandler(); 233 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 234 235 // Get Position 236 DeviceProfile deviceProfile = mContainer.getDeviceProfile(); 237 mContainer.getDragLayer().getDescendantRectRelativeToSelf( 238 enableOverviewIconMenu() 239 ? getIconView().findViewById(R.id.icon_view_menu_anchor) 240 : taskContainer.getThumbnailViewDeprecated(), 241 sTempRect); 242 Rect insets = mContainer.getDragLayer().getInsets(); 243 BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); 244 params.width = orientationHandler.getTaskMenuWidth( 245 taskContainer.getThumbnailViewDeprecated(), deviceProfile, 246 taskContainer.getStagePosition()); 247 // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start 248 params.gravity = Gravity.LEFT; 249 setLayoutParams(params); 250 setScaleX(mTaskView.getScaleX()); 251 setScaleY(mTaskView.getScaleY()); 252 253 // Set divider spacing 254 ShapeDrawable divider = new ShapeDrawable(new RectShape()); 255 divider.getPaint().setColor(getResources().getColor(android.R.color.transparent)); 256 int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing); 257 mOptionLayout.setShowDividers( 258 enableOverviewIconMenu() ? SHOW_DIVIDER_NONE : SHOW_DIVIDER_MIDDLE); 259 260 orientationHandler.setTaskOptionsMenuLayoutOrientation( 261 deviceProfile, mOptionLayout, dividerSpacing, divider); 262 float thumbnailAlignedX = sTempRect.left - insets.left; 263 float thumbnailAlignedY = sTempRect.top - insets.top; 264 265 // Changing pivot to make computations easier 266 // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, 267 // which would render the X and Y position set here incorrect 268 setPivotX(0); 269 setPivotY(0); 270 setRotation(orientationHandler.getDegreesRotated()); 271 272 if (enableOverviewIconMenu()) { 273 setTranslationX(thumbnailAlignedX); 274 setTranslationY(thumbnailAlignedY); 275 } else { 276 // Margin that insets the menuView inside the taskView 277 float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); 278 setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX, 279 mTaskContainer.getThumbnailViewDeprecated(), deviceProfile, taskInsetMargin, 280 getIconView())); 281 setTranslationY(orientationHandler.getTaskMenuY( 282 thumbnailAlignedY, mTaskContainer.getThumbnailViewDeprecated(), 283 mTaskContainer.getStagePosition(), this, taskInsetMargin, 284 getIconView())); 285 } 286 } 287 288 private void animateOpen() { 289 mMenuTranslationYBeforeOpen = getTranslationY(); 290 mMenuTranslationXBeforeOpen = getTranslationX(); 291 animateOpenOrClosed(false); 292 mIsOpen = true; 293 } 294 295 private View getIconView() { 296 return mTaskContainer.getIconView().asView(); 297 } 298 299 private void animateClose() { 300 animateOpenOrClosed(true); 301 } 302 303 private void animateOpenOrClosed(boolean closing) { 304 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 305 mOpenCloseAnimator.cancel(); 306 } 307 mOpenCloseAnimator = new AnimatorSet(); 308 // If we're opening, we just start from the beginning as a new `TaskMenuView` is created 309 // each time we do the open animation so there will never be a partial value here. 310 float revealAnimationStartProgress = 0f; 311 if (closing && mRevealAnimator != null) { 312 revealAnimationStartProgress = 1f - mRevealAnimator.getAnimatedFraction(); 313 } 314 mRevealAnimator = createOpenCloseOutlineProvider() 315 .createRevealAnimator(this, closing, revealAnimationStartProgress); 316 mRevealAnimator.setInterpolator(enableOverviewIconMenu() ? Interpolators.EMPHASIZED 317 : Interpolators.DECELERATE); 318 319 if (enableOverviewIconMenu()) { 320 IconAppChipView iconAppChip = (IconAppChipView) mTaskContainer.getIconView().asView(); 321 322 float additionalTranslationY = 0; 323 if (((RecentsView) mContainer.getOverviewPanel()).isOnGridBottomRow(mTaskView)) { 324 // Animate menu up for enough room to display full menu when task on bottom row. 325 float menuBottom = getHeight() + mMenuTranslationYBeforeOpen; 326 float taskBottom = mTaskView.getHeight() + mTaskView.getPersistentTranslationY(); 327 float taskbarTop = mContainer.getDeviceProfile().heightPx 328 - mContainer.getDeviceProfile().getOverviewActionsClaimedSpaceBelow(); 329 float midpoint = (taskBottom + taskbarTop) / 2f; 330 additionalTranslationY = -Math.max(menuBottom - midpoint, 0); 331 } 332 ObjectAnimator translationYAnim = ObjectAnimator.ofFloat(this, TRANSLATION_Y, 333 closing ? mMenuTranslationYBeforeOpen 334 : mMenuTranslationYBeforeOpen + additionalTranslationY); 335 translationYAnim.setInterpolator(EMPHASIZED); 336 337 ObjectAnimator menuTranslationYAnim = ObjectAnimator.ofFloat( 338 iconAppChip.getMenuTranslationY(), 339 MULTI_PROPERTY_VALUE, closing ? 0 : additionalTranslationY); 340 menuTranslationYAnim.setInterpolator(EMPHASIZED); 341 342 float additionalTranslationX = 0; 343 if (mContainer.getDeviceProfile().isLandscape 344 && mTaskContainer.getStagePosition() == STAGE_POSITION_BOTTOM_OR_RIGHT) { 345 // Animate menu and icon when split task would display off the side of the screen. 346 additionalTranslationX = Math.max( 347 getTranslationX() + getWidth() - (mContainer.getDeviceProfile().widthPx 348 - getResources().getDimensionPixelSize( 349 R.dimen.task_menu_edge_padding) * 2), 0); 350 } 351 352 ObjectAnimator translationXAnim = ObjectAnimator.ofFloat(this, TRANSLATION_X, 353 closing ? mMenuTranslationXBeforeOpen 354 : mMenuTranslationXBeforeOpen - additionalTranslationX); 355 translationXAnim.setInterpolator(EMPHASIZED); 356 357 ObjectAnimator menuTranslationXAnim = ObjectAnimator.ofFloat( 358 iconAppChip.getMenuTranslationX(), 359 MULTI_PROPERTY_VALUE, closing ? 0 : -additionalTranslationX); 360 menuTranslationXAnim.setInterpolator(EMPHASIZED); 361 362 mOpenCloseAnimator.playTogether(translationYAnim, translationXAnim, 363 menuTranslationXAnim, menuTranslationYAnim); 364 } 365 mOpenCloseAnimator.playTogether(mRevealAnimator, 366 ObjectAnimator.ofFloat( 367 mTaskContainer.getThumbnailViewDeprecated(), DIM_ALPHA, 368 closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA), 369 ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); 370 mOpenCloseAnimator.addListener(new AnimationSuccessListener() { 371 @Override 372 public void onAnimationStart(Animator animation) { 373 setVisibility(VISIBLE); 374 if (closing && mOnClosingStartCallback != null) { 375 mOnClosingStartCallback.run(); 376 } 377 } 378 379 @Override 380 public void onAnimationSuccess(Animator animator) { 381 if (closing) { 382 closeComplete(); 383 } 384 } 385 }); 386 mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); 387 mOpenCloseAnimator.start(); 388 } 389 390 private void closeComplete() { 391 mIsOpen = false; 392 mContainer.getDragLayer().removeView(this); 393 mRevealAnimator = null; 394 } 395 396 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { 397 float radius = TaskCornerRadius.get(mContext); 398 Rect fromRect = new Rect( 399 enableOverviewIconMenu() && isLayoutRtl() ? getWidth() : 0, 400 0, 401 enableOverviewIconMenu() && !isLayoutRtl() ? 0 : getWidth(), 402 0); 403 Rect toRect = new Rect(0, 0, getWidth(), getHeight()); 404 return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); 405 } 406 407 /** 408 * Calculates max height based on how much space we have available. 409 * If not enough space then the view will scroll. The maximum menu size will sit inside the task 410 * with a margin on the top and bottom. 411 */ 412 private int calculateMaxHeight() { 413 float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); 414 return mTaskView.getPagedOrientationHandler().getTaskMenuHeight(taskInsetMargin, 415 mContainer.getDeviceProfile(), getTranslationX(), getTranslationY()); 416 } 417 418 private void setOnClosingStartCallback(Runnable onClosingStartCallback) { 419 mOnClosingStartCallback = onClosingStartCallback; 420 } 421 } 422