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 104 public TaskView(Context context) { 105 this(context, null); 106 } 107 108 public TaskView(Context context, AttributeSet attrs) { 109 this(context, attrs, 0); 110 } 111 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 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 */ 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 146 public Task getTask() { 147 return mTask; 148 } 149 150 public TaskThumbnailView getThumbnail() { 151 return mSnapshotView; 152 } 153 154 public IconView getIconView() { 155 return mIconView; 156 } 157 158 public void launchTask(boolean animate) { 159 launchTask(animate, (result) -> { 160 if (!result) { 161 notifyTaskLaunchFailed(TAG); 162 } 163 }, getHandler()); 164 } 165 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 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 193 public void onTaskDataUnloaded() { 194 mSnapshotView.setThumbnail(null, null); 195 mIconView.setDrawable(null); 196 mIconView.setOnLongClickListener(null); 197 } 198 199 @Override 200 public void onTaskWindowingModeChanged() { 201 // Do nothing 202 } 203 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 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 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 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 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 253 public static float getCurveScaleForInterpolation(float linearInterpolation) { 254 float curveInterpolation = CURVE_INTERPOLATOR.getInterpolation(linearInterpolation); 255 return getCurveScaleForCurveInterpolation(curveInterpolation); 256 } 257 258 private static float getCurveScaleForCurveInterpolation(float curveInterpolation) { 259 return 1 - curveInterpolation * EDGE_SCALE_DOWN_FACTOR; 260 } 261 262 private void setCurveScale(float curveScale) { 263 mCurveScale = curveScale; 264 onScaleChanged(); 265 } 266 267 public float getCurveScale() { 268 return mCurveScale; 269 } 270 271 public void setZoomScale(float adjacentScale) { 272 mZoomScale = adjacentScale; 273 onScaleChanged(); 274 } 275 276 private void onScaleChanged() { 277 float scale = mCurveScale * mZoomScale; 278 setScaleX(scale); 279 setScaleY(scale); 280 } 281 282 @Override 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 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 299 public void getOutline(View view, Outline outline) { 300 outline.setRoundRect(0, mMarginTop, view.getWidth(), 301 view.getHeight(), mRadius); 302 } 303 } 304 305 @Override 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 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 355 private RecentsView getRecentsView() { 356 return (RecentsView) getParent(); 357 } 358 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