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.quickstep.views.TaskThumbnailView.DIM_ALPHA; 20 21 import android.animation.Animator; 22 import android.animation.AnimatorSet; 23 import android.animation.ObjectAnimator; 24 import android.content.Context; 25 import android.graphics.Outline; 26 import android.graphics.Rect; 27 import android.graphics.drawable.ShapeDrawable; 28 import android.graphics.drawable.shapes.RectShape; 29 import android.util.AttributeSet; 30 import android.view.Gravity; 31 import android.view.MotionEvent; 32 import android.view.View; 33 import android.view.ViewOutlineProvider; 34 import android.widget.LinearLayout; 35 import android.widget.TextView; 36 37 import androidx.annotation.Nullable; 38 39 import com.android.launcher3.AbstractFloatingView; 40 import com.android.launcher3.BaseDraggingActivity; 41 import com.android.launcher3.DeviceProfile; 42 import com.android.launcher3.R; 43 import com.android.launcher3.anim.AnimationSuccessListener; 44 import com.android.launcher3.anim.Interpolators; 45 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider; 46 import com.android.launcher3.popup.SystemShortcut; 47 import com.android.launcher3.touch.PagedOrientationHandler; 48 import com.android.launcher3.views.BaseDragLayer; 49 import com.android.quickstep.TaskOverlayFactory; 50 import com.android.quickstep.TaskUtils; 51 import com.android.quickstep.util.TaskCornerRadius; 52 import com.android.quickstep.views.TaskView.TaskIdAttributeContainer; 53 54 /** 55 * Contains options for a recent task when long-pressing its icon. 56 */ 57 public class TaskMenuView extends AbstractFloatingView { 58 59 private static final Rect sTempRect = new Rect(); 60 61 private static final int REVEAL_OPEN_DURATION = 150; 62 private static final int REVEAL_CLOSE_DURATION = 100; 63 64 private BaseDraggingActivity mActivity; 65 private TextView mTaskName; 66 @Nullable 67 private AnimatorSet mOpenCloseAnimator; 68 private TaskView mTaskView; 69 private TaskIdAttributeContainer mTaskContainer; 70 private LinearLayout mOptionLayout; 71 TaskMenuView(Context context, AttributeSet attrs)72 public TaskMenuView(Context context, AttributeSet attrs) { 73 this(context, attrs, 0); 74 } 75 TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr)76 public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) { 77 super(context, attrs, defStyleAttr); 78 79 mActivity = BaseDraggingActivity.fromContext(context); 80 setClipToOutline(true); 81 } 82 83 @Override onFinishInflate()84 protected void onFinishInflate() { 85 super.onFinishInflate(); 86 mTaskName = findViewById(R.id.task_name); 87 mOptionLayout = findViewById(R.id.menu_option_layout); 88 } 89 90 @Override onControllerInterceptTouchEvent(MotionEvent ev)91 public boolean onControllerInterceptTouchEvent(MotionEvent ev) { 92 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 93 BaseDragLayer dl = mActivity.getDragLayer(); 94 if (!dl.isEventOverView(this, ev)) { 95 // TODO: log this once we have a new container type for it? 96 close(true); 97 return true; 98 } 99 } 100 return false; 101 } 102 103 @Override handleClose(boolean animate)104 protected void handleClose(boolean animate) { 105 if (animate) { 106 animateClose(); 107 } else { 108 closeComplete(); 109 } 110 } 111 112 @Override isOfType(int type)113 protected boolean isOfType(int type) { 114 return (type & TYPE_TASK_MENU) != 0; 115 } 116 117 @Override getOutlineProvider()118 public ViewOutlineProvider getOutlineProvider() { 119 return new ViewOutlineProvider() { 120 @Override 121 public void getOutline(View view, Outline outline) { 122 outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), 123 TaskCornerRadius.get(view.getContext())); 124 } 125 }; 126 } 127 128 public void onRotationChanged() { 129 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 130 mOpenCloseAnimator.end(); 131 } 132 if (mIsOpen) { 133 mOptionLayout.removeAllViews(); 134 if (!populateAndLayoutMenu()) { 135 close(false); 136 } 137 } 138 } 139 140 public static boolean showForTask(TaskIdAttributeContainer taskContainer) { 141 BaseDraggingActivity activity = BaseDraggingActivity.fromContext( 142 taskContainer.getTaskView().getContext()); 143 final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate( 144 R.layout.task_menu, activity.getDragLayer(), false); 145 return taskMenuView.populateAndShowForTask(taskContainer); 146 } 147 148 private boolean populateAndShowForTask(TaskIdAttributeContainer taskContainer) { 149 if (isAttachedToWindow()) { 150 return false; 151 } 152 mActivity.getDragLayer().addView(this); 153 mTaskView = taskContainer.getTaskView(); 154 mTaskContainer = taskContainer; 155 if (!populateAndLayoutMenu()) { 156 return false; 157 } 158 post(this::animateOpen); 159 return true; 160 } 161 162 /** @return true if successfully able to populate task view menu, false otherwise */ 163 private boolean populateAndLayoutMenu() { 164 if (mTaskContainer.getTask().icon == null) { 165 // Icon may not be loaded 166 return false; 167 } 168 addMenuOptions(mTaskContainer); 169 orientAroundTaskView(mTaskContainer); 170 return true; 171 } 172 173 private void addMenuOptions(TaskIdAttributeContainer taskContainer) { 174 mTaskName.setText(TaskUtils.getTitle(getContext(), taskContainer.getTask())); 175 mTaskName.setOnClickListener(v -> close(true)); 176 TaskOverlayFactory.getEnabledShortcuts(mTaskView, taskContainer) 177 .forEach(this::addMenuOption); 178 } 179 180 private void addMenuOption(SystemShortcut menuOption) { 181 LinearLayout menuOptionView = (LinearLayout) mActivity.getLayoutInflater().inflate( 182 R.layout.task_view_menu_option, this, false); 183 menuOption.setIconAndLabelFor( 184 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text)); 185 LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams(); 186 mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp, 187 menuOptionView, mActivity.getDeviceProfile()); 188 // Set an onClick listener on each menu option. The onClick method is responsible for 189 // ending LiveTile mode on the thumbnail if needed. 190 menuOptionView.setOnClickListener(menuOption::onClick); 191 mOptionLayout.addView(menuOptionView); 192 } 193 194 private void orientAroundTaskView(TaskIdAttributeContainer taskContainer) { 195 RecentsView recentsView = mActivity.getOverviewPanel(); 196 PagedOrientationHandler orientationHandler = recentsView.getPagedOrientationHandler(); 197 measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); 198 199 // Get Position 200 DeviceProfile deviceProfile = mActivity.getDeviceProfile(); 201 mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskContainer.getThumbnailView(), 202 sTempRect); 203 Rect insets = mActivity.getDragLayer().getInsets(); 204 BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams(); 205 int padding = getResources() 206 .getDimensionPixelSize(R.dimen.task_menu_vertical_padding); 207 params.width = orientationHandler 208 .getTaskMenuWidth(taskContainer.getThumbnailView(), 209 deviceProfile, taskContainer.getStagePosition()) - (2 * padding); 210 // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start 211 params.gravity = Gravity.LEFT; 212 setLayoutParams(params); 213 setScaleX(mTaskView.getScaleX()); 214 setScaleY(mTaskView.getScaleY()); 215 216 // Set divider spacing 217 ShapeDrawable divider = new ShapeDrawable(new RectShape()); 218 divider.getPaint().setColor(getResources().getColor(android.R.color.transparent)); 219 int dividerSpacing = (int) getResources().getDimension(R.dimen.task_menu_spacing); 220 mOptionLayout.setShowDividers(SHOW_DIVIDER_MIDDLE); 221 222 orientationHandler.setTaskOptionsMenuLayoutOrientation( 223 deviceProfile, mOptionLayout, dividerSpacing, divider); 224 float thumbnailAlignedX = sTempRect.left - insets.left; 225 float thumbnailAlignedY = sTempRect.top - insets.top; 226 // Changing pivot to make computations easier 227 // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set, 228 // which would render the X and Y position set here incorrect 229 setPivotX(0); 230 setPivotY(0); 231 setRotation(orientationHandler.getDegreesRotated()); 232 233 // Margin that insets the menuView inside the taskView 234 float taskInsetMargin = getResources().getDimension(R.dimen.task_card_margin); 235 setTranslationX(orientationHandler.getTaskMenuX(thumbnailAlignedX, 236 mTaskContainer.getThumbnailView(), deviceProfile, taskInsetMargin)); 237 setTranslationY(orientationHandler.getTaskMenuY( 238 thumbnailAlignedY, mTaskContainer.getThumbnailView(), 239 mTaskContainer.getStagePosition(), this, taskInsetMargin)); 240 } 241 242 private void animateOpen() { 243 animateOpenOrClosed(false); 244 mIsOpen = true; 245 } 246 247 private void animateClose() { 248 animateOpenOrClosed(true); 249 } 250 251 private void animateOpenOrClosed(boolean closing) { 252 if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) { 253 mOpenCloseAnimator.end(); 254 } 255 mOpenCloseAnimator = new AnimatorSet(); 256 257 final Animator revealAnimator = createOpenCloseOutlineProvider() 258 .createRevealAnimator(this, closing); 259 revealAnimator.setInterpolator(Interpolators.DEACCEL); 260 mOpenCloseAnimator.playTogether(revealAnimator, 261 ObjectAnimator.ofFloat( 262 mTaskContainer.getThumbnailView(), DIM_ALPHA, 263 closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA), 264 ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1)); 265 mOpenCloseAnimator.addListener(new AnimationSuccessListener() { 266 @Override 267 public void onAnimationStart(Animator animation) { 268 setVisibility(VISIBLE); 269 } 270 271 @Override 272 public void onAnimationSuccess(Animator animator) { 273 if (closing) { 274 closeComplete(); 275 } 276 } 277 }); 278 mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION); 279 mOpenCloseAnimator.start(); 280 } 281 282 private void closeComplete() { 283 mIsOpen = false; 284 mActivity.getDragLayer().removeView(this); 285 } 286 287 private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() { 288 float radius = TaskCornerRadius.get(mContext); 289 Rect fromRect = new Rect(0, 0, getWidth(), 0); 290 Rect toRect = new Rect(0, 0, getWidth(), getHeight()); 291 return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect); 292 } 293 294 } 295