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.Rect;
26 import android.graphics.drawable.Drawable;
27 import android.util.AttributeSet;
28 import android.view.Gravity;
29 import android.view.MotionEvent;
30 import android.view.View;
31 import android.view.ViewGroup;
32 import android.widget.LinearLayout;
33 import android.widget.TextView;
34 
35 import com.android.launcher3.AbstractFloatingView;
36 import com.android.launcher3.BaseDraggingActivity;
37 import com.android.launcher3.FastBitmapDrawable;
38 import com.android.launcher3.R;
39 import com.android.launcher3.anim.AnimationSuccessListener;
40 import com.android.launcher3.anim.Interpolators;
41 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider;
42 import com.android.launcher3.popup.SystemShortcut;
43 import com.android.launcher3.touch.PagedOrientationHandler;
44 import com.android.launcher3.util.Themes;
45 import com.android.launcher3.views.BaseDragLayer;
46 import com.android.quickstep.TaskOverlayFactory;
47 import com.android.quickstep.TaskUtils;
48 import com.android.quickstep.views.IconView.OnScaleUpdateListener;
49 
50 /**
51  * Contains options for a recent task when long-pressing its icon.
52  */
53 public class TaskMenuView extends AbstractFloatingView {
54 
55     private static final Rect sTempRect = new Rect();
56 
57     private final OnScaleUpdateListener mTaskViewIconScaleListener = new OnScaleUpdateListener() {
58         @Override
59         public void onScaleUpdate(float scale) {
60             final Drawable drawable = mTaskIcon.getDrawable();
61             if (drawable instanceof FastBitmapDrawable) {
62                 if (scale != ((FastBitmapDrawable) drawable).getScale()) {
63                     mMenuIconDrawable.setScale(scale);
64                 }
65             }
66         }
67     };
68 
69     private final OnScaleUpdateListener mMenuIconScaleListener = new OnScaleUpdateListener() {
70         @Override
71         public void onScaleUpdate(float scale) {
72             final Drawable taskViewDrawable = mTaskView.getIconView().getDrawable();
73             if (taskViewDrawable instanceof FastBitmapDrawable) {
74                 final float currentScale = ((FastBitmapDrawable) taskViewDrawable).getScale();
75                 if (currentScale != scale) {
76                     ((FastBitmapDrawable) taskViewDrawable).setScale(scale);
77                 }
78             }
79         }
80     };
81 
82     private static final int REVEAL_OPEN_DURATION = 150;
83     private static final int REVEAL_CLOSE_DURATION = 100;
84 
85     private final float mThumbnailTopMargin;
86     private BaseDraggingActivity mActivity;
87     private TextView mTaskName;
88     private IconView mTaskIcon;
89     private AnimatorSet mOpenCloseAnimator;
90     private TaskView mTaskView;
91     private LinearLayout mOptionLayout;
92     private FastBitmapDrawable mMenuIconDrawable;
93 
TaskMenuView(Context context, AttributeSet attrs)94     public TaskMenuView(Context context, AttributeSet attrs) {
95         this(context, attrs, 0);
96     }
97 
TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr)98     public TaskMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
99         super(context, attrs, defStyleAttr);
100 
101         mActivity = BaseDraggingActivity.fromContext(context);
102         mThumbnailTopMargin = getResources().getDimension(R.dimen.task_thumbnail_top_margin);
103     }
104 
105     @Override
onFinishInflate()106     protected void onFinishInflate() {
107         super.onFinishInflate();
108         mTaskName = findViewById(R.id.task_name);
109         mTaskIcon = findViewById(R.id.task_icon);
110         mOptionLayout = findViewById(R.id.menu_option_layout);
111     }
112 
113     @Override
onControllerInterceptTouchEvent(MotionEvent ev)114     public boolean onControllerInterceptTouchEvent(MotionEvent ev) {
115         if (ev.getAction() == MotionEvent.ACTION_DOWN) {
116             BaseDragLayer dl = mActivity.getDragLayer();
117             if (!dl.isEventOverView(this, ev)) {
118                 // TODO: log this once we have a new container type for it?
119                 close(true);
120                 return true;
121             }
122         }
123         return false;
124     }
125 
126     @Override
handleClose(boolean animate)127     protected void handleClose(boolean animate) {
128         if (animate) {
129             animateClose();
130         } else {
131             closeComplete();
132         }
133     }
134 
135     @Override
logActionCommand(int command)136     public void logActionCommand(int command) {
137         // TODO
138     }
139 
140     @Override
onDetachedFromWindow()141     protected void onDetachedFromWindow() {
142         super.onDetachedFromWindow();
143 
144         // Remove all scale listeners when menu is removed
145         mTaskView.getIconView().removeUpdateScaleListener(mTaskViewIconScaleListener);
146         mTaskIcon.removeUpdateScaleListener(mMenuIconScaleListener);
147     }
148 
149     @Override
isOfType(int type)150     protected boolean isOfType(int type) {
151         return (type & TYPE_TASK_MENU) != 0;
152     }
153 
setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler)154     public void setPosition(float x, float y, PagedOrientationHandler pagedOrientationHandler) {
155         float adjustedY = y + mThumbnailTopMargin;
156         // Changing pivot to make computations easier
157         // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
158         // which would render the X and Y position set here incorrect
159         setPivotX(0);
160         setPivotY(0);
161         setRotation(pagedOrientationHandler.getDegreesRotated());
162         setX(pagedOrientationHandler.getTaskMenuX(x, mTaskView.getThumbnail()));
163         setY(pagedOrientationHandler.getTaskMenuY(adjustedY, mTaskView.getThumbnail()));
164     }
165 
onRotationChanged()166     public void onRotationChanged() {
167         if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
168             mOpenCloseAnimator.end();
169         }
170         if (mIsOpen) {
171             mOptionLayout.removeAllViews();
172             populateAndLayoutMenu();
173         }
174     }
175 
showForTask(TaskView taskView)176     public static TaskMenuView showForTask(TaskView taskView) {
177         BaseDraggingActivity activity = BaseDraggingActivity.fromContext(taskView.getContext());
178         final TaskMenuView taskMenuView = (TaskMenuView) activity.getLayoutInflater().inflate(
179                         R.layout.task_menu, activity.getDragLayer(), false);
180         return taskMenuView.populateAndShowForTask(taskView) ? taskMenuView : null;
181     }
182 
populateAndShowForTask(TaskView taskView)183     private boolean populateAndShowForTask(TaskView taskView) {
184         if (isAttachedToWindow()) {
185             return false;
186         }
187         mActivity.getDragLayer().addView(this);
188         mTaskView = taskView;
189         populateAndLayoutMenu();
190         post(this::animateOpen);
191         return true;
192     }
193 
populateAndLayoutMenu()194     private void populateAndLayoutMenu() {
195         addMenuOptions(mTaskView);
196         orientAroundTaskView(mTaskView);
197     }
198 
addMenuOptions(TaskView taskView)199     private void addMenuOptions(TaskView taskView) {
200         Drawable icon = taskView.getTask().icon.getConstantState().newDrawable();
201         mTaskIcon.setDrawable(icon);
202         mTaskIcon.setOnClickListener(v -> close(true));
203         mTaskName.setText(TaskUtils.getTitle(getContext(), taskView.getTask()));
204         mTaskName.setOnClickListener(v -> close(true));
205 
206         // Set the icons to match scale by listening to each other's changes
207         mMenuIconDrawable = icon instanceof FastBitmapDrawable ? (FastBitmapDrawable) icon : null;
208         taskView.getIconView().addUpdateScaleListener(mTaskViewIconScaleListener);
209         mTaskIcon.addUpdateScaleListener(mMenuIconScaleListener);
210 
211         // Move the icon and text up half an icon size to lay over the TaskView
212         LinearLayout.LayoutParams params =
213                 (LinearLayout.LayoutParams) mTaskIcon.getLayoutParams();
214         params.topMargin = (int) -mThumbnailTopMargin;
215         mTaskIcon.setLayoutParams(params);
216 
217         TaskOverlayFactory.getEnabledShortcuts(taskView).forEach(this::addMenuOption);
218     }
219 
addMenuOption(SystemShortcut menuOption)220     private void addMenuOption(SystemShortcut menuOption) {
221         ViewGroup menuOptionView = (ViewGroup) mActivity.getLayoutInflater().inflate(
222                 R.layout.task_view_menu_option, this, false);
223         menuOption.setIconAndLabelFor(
224                 menuOptionView.findViewById(R.id.icon), menuOptionView.findViewById(R.id.text));
225         LayoutParams lp = (LayoutParams) menuOptionView.getLayoutParams();
226         mTaskView.getPagedOrientationHandler().setLayoutParamsForTaskMenuOptionItem(lp);
227         menuOptionView.setOnClickListener(menuOption);
228         mOptionLayout.addView(menuOptionView);
229     }
230 
orientAroundTaskView(TaskView taskView)231     private void orientAroundTaskView(TaskView taskView) {
232         PagedOrientationHandler orientationHandler = taskView.getPagedOrientationHandler();
233         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
234         mActivity.getDragLayer().getDescendantRectRelativeToSelf(taskView, sTempRect);
235         Rect insets = mActivity.getDragLayer().getInsets();
236         BaseDragLayer.LayoutParams params = (BaseDragLayer.LayoutParams) getLayoutParams();
237         params.width = orientationHandler.getTaskMenuWidth(taskView.getThumbnail());
238         // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
239         params.gravity = Gravity.LEFT;
240         setLayoutParams(params);
241         setScaleX(taskView.getScaleX());
242         setScaleY(taskView.getScaleY());
243         mOptionLayout.setOrientation(orientationHandler
244                 .getTaskMenuLayoutOrientation(mOptionLayout));
245         setPosition(sTempRect.left - insets.left, sTempRect.top - insets.top,
246             taskView.getPagedOrientationHandler());
247     }
248 
animateOpen()249     private void animateOpen() {
250         animateOpenOrClosed(false);
251         mIsOpen = true;
252     }
253 
animateClose()254     private void animateClose() {
255         animateOpenOrClosed(true);
256     }
257 
animateOpenOrClosed(boolean closing)258     private void animateOpenOrClosed(boolean closing) {
259         if (mOpenCloseAnimator != null && mOpenCloseAnimator.isRunning()) {
260             mOpenCloseAnimator.end();
261         }
262         mOpenCloseAnimator = new AnimatorSet();
263 
264         final Animator revealAnimator = createOpenCloseOutlineProvider()
265                 .createRevealAnimator(this, closing);
266         revealAnimator.setInterpolator(Interpolators.DEACCEL);
267         mOpenCloseAnimator.play(revealAnimator);
268         mOpenCloseAnimator.play(ObjectAnimator.ofFloat(mTaskView.getThumbnail(), DIM_ALPHA,
269                 closing ? 0 : TaskView.MAX_PAGE_SCRIM_ALPHA));
270         mOpenCloseAnimator.addListener(new AnimationSuccessListener() {
271             @Override
272             public void onAnimationStart(Animator animation) {
273                 setVisibility(VISIBLE);
274             }
275 
276             @Override
277             public void onAnimationSuccess(Animator animator) {
278                 if (closing) {
279                     closeComplete();
280                 }
281             }
282         });
283         mOpenCloseAnimator.play(ObjectAnimator.ofFloat(this, ALPHA, closing ? 0 : 1));
284         mOpenCloseAnimator.setDuration(closing ? REVEAL_CLOSE_DURATION: REVEAL_OPEN_DURATION);
285         mOpenCloseAnimator.start();
286     }
287 
closeComplete()288     private void closeComplete() {
289         mIsOpen = false;
290         mActivity.getDragLayer().removeView(this);
291     }
292 
createOpenCloseOutlineProvider()293     private RoundedRectRevealOutlineProvider createOpenCloseOutlineProvider() {
294         float radius = Themes.getDialogCornerRadius(getContext());
295         Rect fromRect = new Rect(0, 0, getWidth(), 0);
296         Rect toRect = new Rect(0, 0, getWidth(), getHeight());
297         return new RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect);
298     }
299 
findMenuItemByText(String text)300     public View findMenuItemByText(String text) {
301         for (int i = mOptionLayout.getChildCount() - 1; i >= 0; --i) {
302             final ViewGroup menuOptionView = (ViewGroup) mOptionLayout.getChildAt(i);
303             if (text.equals(menuOptionView.<TextView>findViewById(R.id.text).getText())) {
304                 return menuOptionView;
305             }
306         }
307         return null;
308     }
309 }
310