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