• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2017 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 android.widget.Toast.LENGTH_SHORT;
20 
21 import static com.android.quickstep.views.TaskThumbnailView.DIM_ALPHA_MULTIPLIER;
22 
23 import android.animation.Animator;
24 import android.animation.AnimatorListenerAdapter;
25 import android.animation.ObjectAnimator;
26 import android.animation.TimeInterpolator;
27 import android.app.ActivityOptions;
28 import android.content.Context;
29 import android.content.res.Resources;
30 import android.graphics.Outline;
31 import android.os.Bundle;
32 import android.os.Handler;
33 import android.util.AttributeSet;
34 import android.util.FloatProperty;
35 import android.util.Log;
36 import android.util.Property;
37 import android.view.View;
38 import android.view.ViewOutlineProvider;
39 import android.view.accessibility.AccessibilityNodeInfo;
40 import android.widget.FrameLayout;
41 import android.widget.Toast;
42 
43 import com.android.launcher3.BaseActivity;
44 import com.android.launcher3.BaseDraggingActivity;
45 import com.android.launcher3.R;
46 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Direction;
47 import com.android.launcher3.userevent.nano.LauncherLogProto.Action.Touch;
48 import com.android.quickstep.TaskSystemShortcut;
49 import com.android.quickstep.TaskUtils;
50 import com.android.quickstep.views.RecentsView.PageCallbacks;
51 import com.android.quickstep.views.RecentsView.ScrollState;
52 import com.android.systemui.shared.recents.model.Task;
53 import com.android.systemui.shared.recents.model.Task.TaskCallbacks;
54 import com.android.systemui.shared.recents.model.ThumbnailData;
55 import com.android.systemui.shared.system.ActivityManagerWrapper;
56 
57 import java.util.function.Consumer;
58 
59 /**
60  * A task in the Recents view.
61  */
62 public class TaskView extends FrameLayout implements TaskCallbacks, PageCallbacks {
63 
64     private static final String TAG = TaskView.class.getSimpleName();
65 
66     /** A curve of x from 0 to 1, where 0 is the center of the screen and 1 is the edge. */
67     private static final TimeInterpolator CURVE_INTERPOLATOR
68             = x -> (float) -Math.cos(x * Math.PI) / 2f + .5f;
69 
70     /**
71      * The alpha of a black scrim on a page in the carousel as it leaves the screen.
72      * In the resting position of the carousel, the adjacent pages have about half this scrim.
73      */
74     private static final float MAX_PAGE_SCRIM_ALPHA = 0.4f;
75 
76     /**
77      * How much to scale down pages near the edge of the screen.
78      */
79     private static final float EDGE_SCALE_DOWN_FACTOR = 0.03f;
80 
81     public static final long SCALE_ICON_DURATION = 120;
82     private static final long DIM_ANIM_DURATION = 700;
83 
84     public static final Property<TaskView, Float> ZOOM_SCALE =
85             new FloatProperty<TaskView>("zoomScale") {
86                 @Override
87                 public void setValue(TaskView taskView, float v) {
88                     taskView.setZoomScale(v);
89                 }
90 
91                 @Override
92                 public Float get(TaskView taskView) {
93                     return taskView.mZoomScale;
94                 }
95             };
96 
97     private Task mTask;
98     private TaskThumbnailView mSnapshotView;
99     private IconView mIconView;
100     private float mCurveScale;
101     private float mZoomScale;
102     private Animator mDimAlphaAnim;
103 
TaskView(Context context)104     public TaskView(Context context) {
105         this(context, null);
106     }
107 
TaskView(Context context, AttributeSet attrs)108     public TaskView(Context context, AttributeSet attrs) {
109         this(context, attrs, 0);
110     }
111 
TaskView(Context context, AttributeSet attrs, int defStyleAttr)112     public TaskView(Context context, AttributeSet attrs, int defStyleAttr) {
113         super(context, attrs, defStyleAttr);
114         setOnClickListener((view) -> {
115             if (getTask() == null) {
116                 return;
117             }
118             launchTask(true /* animate */);
119             BaseActivity.fromContext(context).getUserEventDispatcher().logTaskLaunchOrDismiss(
120                     Touch.TAP, Direction.NONE, getRecentsView().indexOfChild(this),
121                     TaskUtils.getComponentKeyForTask(getTask().key));
122         });
123         setOutlineProvider(new TaskOutlineProvider(getResources()));
124     }
125 
126     @Override
onFinishInflate()127     protected void onFinishInflate() {
128         super.onFinishInflate();
129         mSnapshotView = findViewById(R.id.snapshot);
130         mIconView = findViewById(R.id.icon);
131     }
132 
133     /**
134      * Updates this task view to the given {@param task}.
135      */
bind(Task task)136     public void bind(Task task) {
137         if (mTask != null) {
138             mTask.removeCallback(this);
139         }
140         mTask = task;
141         mSnapshotView.bind();
142         task.addCallback(this);
143         setContentDescription(task.titleDescription);
144     }
145 
getTask()146     public Task getTask() {
147         return mTask;
148     }
149 
getThumbnail()150     public TaskThumbnailView getThumbnail() {
151         return mSnapshotView;
152     }
153 
getIconView()154     public IconView getIconView() {
155         return mIconView;
156     }
157 
launchTask(boolean animate)158     public void launchTask(boolean animate) {
159         launchTask(animate, (result) -> {
160             if (!result) {
161                 notifyTaskLaunchFailed(TAG);
162             }
163         }, getHandler());
164     }
165 
launchTask(boolean animate, Consumer<Boolean> resultCallback, Handler resultCallbackHandler)166     public void launchTask(boolean animate, Consumer<Boolean> resultCallback,
167             Handler resultCallbackHandler) {
168         if (mTask != null) {
169             final ActivityOptions opts;
170             if (animate) {
171                 opts = BaseDraggingActivity.fromContext(getContext())
172                         .getActivityLaunchOptions(this);
173             } else {
174                 opts = ActivityOptions.makeCustomAnimation(getContext(), 0, 0);
175             }
176             ActivityManagerWrapper.getInstance().startActivityFromRecentsAsync(mTask.key,
177                     opts, resultCallback, resultCallbackHandler);
178         }
179     }
180 
181     @Override
onTaskDataLoaded(Task task, ThumbnailData thumbnailData)182     public void onTaskDataLoaded(Task task, ThumbnailData thumbnailData) {
183         mSnapshotView.setThumbnail(task, thumbnailData);
184         mIconView.setDrawable(task.icon);
185         mIconView.setOnClickListener(icon -> TaskMenuView.showForTask(this));
186         mIconView.setOnLongClickListener(icon -> {
187             requestDisallowInterceptTouchEvent(true);
188             return TaskMenuView.showForTask(this);
189         });
190     }
191 
192     @Override
onTaskDataUnloaded()193     public void onTaskDataUnloaded() {
194         mSnapshotView.setThumbnail(null, null);
195         mIconView.setDrawable(null);
196         mIconView.setOnLongClickListener(null);
197     }
198 
199     @Override
onTaskWindowingModeChanged()200     public void onTaskWindowingModeChanged() {
201         // Do nothing
202     }
203 
animateIconToScaleAndDim(float scale)204     public void animateIconToScaleAndDim(float scale) {
205         mIconView.animate().scaleX(scale).scaleY(scale).setDuration(SCALE_ICON_DURATION).start();
206         mDimAlphaAnim = ObjectAnimator.ofFloat(mSnapshotView, DIM_ALPHA_MULTIPLIER, 1 - scale,
207                 scale);
208         mDimAlphaAnim.setDuration(DIM_ANIM_DURATION);
209         mDimAlphaAnim.addListener(new AnimatorListenerAdapter() {
210             @Override
211             public void onAnimationEnd(Animator animation) {
212                 mDimAlphaAnim = null;
213             }
214         });
215         mDimAlphaAnim.start();
216     }
217 
setIconScaleAndDim(float iconScale)218     protected void setIconScaleAndDim(float iconScale) {
219         mIconView.animate().cancel();
220         mIconView.setScaleX(iconScale);
221         mIconView.setScaleY(iconScale);
222         if (mDimAlphaAnim != null) {
223             mDimAlphaAnim.cancel();
224         }
225         mSnapshotView.setDimAlphaMultipler(iconScale);
226     }
227 
resetVisualProperties()228     public void resetVisualProperties() {
229         setZoomScale(1);
230         setTranslationX(0f);
231         setTranslationY(0f);
232         setTranslationZ(0);
233         setAlpha(1f);
234         setIconScaleAndDim(1);
235     }
236 
237     @Override
onPageScroll(ScrollState scrollState)238     public void onPageScroll(ScrollState scrollState) {
239         float curveInterpolation =
240                 CURVE_INTERPOLATOR.getInterpolation(scrollState.linearInterpolation);
241 
242         mSnapshotView.setDimAlpha(curveInterpolation * MAX_PAGE_SCRIM_ALPHA);
243         setCurveScale(getCurveScaleForCurveInterpolation(curveInterpolation));
244     }
245 
246     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)247     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
248         super.onLayout(changed, left, top, right, bottom);
249         setPivotX((right - left) * 0.5f);
250         setPivotY(mSnapshotView.getTop() + mSnapshotView.getHeight() * 0.5f);
251     }
252 
getCurveScaleForInterpolation(float linearInterpolation)253     public static float getCurveScaleForInterpolation(float linearInterpolation) {
254         float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation);
255         return getCurveScaleForCurveInterpolation(curveInterpolation);
256     }
257 
getCurveScaleForCurveInterpolation(float curveInterpolation)258     private static float getCurveScaleForCurveInterpolation(float curveInterpolation) {
259         return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR;
260     }
261 
setCurveScale(float curveScale)262     private void setCurveScale(float curveScale) {
263         mCurveScale = curveScale;
264         onScaleChanged();
265     }
266 
getCurveScale()267     public float getCurveScale() {
268         return mCurveScale;
269     }
270 
setZoomScale(float adjacentScale)271     public void setZoomScale(float adjacentScale) {
272         mZoomScale = adjacentScale;
273         onScaleChanged();
274     }
275 
onScaleChanged()276     private void onScaleChanged() {
277         float scale = mCurveScale * mZoomScale;
278         setScaleX(scale);
279         setScaleY(scale);
280     }
281 
282     @Override
hasOverlappingRendering()283     public boolean hasOverlappingRendering() {
284         // TODO: Clip-out the icon region from the thumbnail, since they are overlapping.
285         return false;
286     }
287 
288     private static final class TaskOutlineProvider extends ViewOutlineProvider {
289 
290         private final int mMarginTop;
291         private final float mRadius;
292 
TaskOutlineProvider(Resources res)293         TaskOutlineProvider(Resources res) {
294             mMarginTop = res.getDimensionPixelSize(R.dimen.task_thumbnail_top_margin);
295             mRadius = res.getDimension(R.dimen.task_corner_radius);
296         }
297 
298         @Override
getOutline(View view, Outline outline)299         public void getOutline(View view, Outline outline) {
300             outline.setRoundRect(0, mMarginTop, view.getWidth(),
301                     view.getHeight(), mRadius);
302         }
303     }
304 
305     @Override
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)306     public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
307         super.onInitializeAccessibilityNodeInfo(info);
308 
309         info.addAction(
310                 new AccessibilityNodeInfo.AccessibilityAction(R.string.accessibility_close_task,
311                         getContext().getText(R.string.accessibility_close_task)));
312 
313         final Context context = getContext();
314         final BaseDraggingActivity activity = BaseDraggingActivity.fromContext(context);
315         for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) {
316             OnClickListener onClickListener = menuOption.getOnClickListener(activity, this);
317             if (onClickListener != null) {
318                 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(menuOption.labelResId,
319                         context.getText(menuOption.labelResId)));
320             }
321         }
322 
323         final RecentsView recentsView = getRecentsView();
324         final AccessibilityNodeInfo.CollectionItemInfo itemInfo =
325                 AccessibilityNodeInfo.CollectionItemInfo.obtain(
326                         0, 1, recentsView.getChildCount() - recentsView.indexOfChild(this) - 1, 1,
327                         false);
328         info.setCollectionItemInfo(itemInfo);
329     }
330 
331     @Override
performAccessibilityAction(int action, Bundle arguments)332     public boolean performAccessibilityAction(int action, Bundle arguments) {
333         if (action == R.string.accessibility_close_task) {
334             getRecentsView().dismissTask(this, true /*animateTaskView*/,
335                     true /*removeTask*/);
336             return true;
337         }
338 
339         for (TaskSystemShortcut menuOption : TaskMenuView.MENU_OPTIONS) {
340             if (action == menuOption.labelResId) {
341                 OnClickListener onClickListener = menuOption.getOnClickListener(
342                         BaseDraggingActivity.fromContext(getContext()), this);
343                 if (onClickListener != null) {
344                     onClickListener.onClick(this);
345                 }
346                 return true;
347             }
348         }
349 
350         if (getRecentsView().performTaskAccessibilityActionExtra(action)) return true;
351 
352         return super.performAccessibilityAction(action, arguments);
353     }
354 
getRecentsView()355     private RecentsView getRecentsView() {
356         return (RecentsView) getParent();
357     }
358 
notifyTaskLaunchFailed(String tag)359     public void notifyTaskLaunchFailed(String tag) {
360         String msg = "Failed to launch task";
361         if (mTask != null) {
362             msg += " (task=" + mTask.key.baseIntent + " userId=" + mTask.key.userId + ")";
363         }
364         Log.w(tag, msg);
365         Toast.makeText(getContext(), R.string.activity_not_available, LENGTH_SHORT).show();
366     }
367 }
368