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.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; 20 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; 21 22 import static com.android.systemui.shared.recents.utilities.PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT; 23 import static com.android.systemui.shared.recents.utilities.Utilities.isRelativePercentDifferenceGreaterThan; 24 25 import android.content.Context; 26 import android.graphics.Bitmap; 27 import android.graphics.BitmapShader; 28 import android.graphics.Canvas; 29 import android.graphics.Color; 30 import android.graphics.ColorFilter; 31 import android.graphics.Matrix; 32 import android.graphics.Paint; 33 import android.graphics.PorterDuff; 34 import android.graphics.PorterDuffXfermode; 35 import android.graphics.Rect; 36 import android.graphics.Shader; 37 import android.graphics.drawable.Drawable; 38 import android.util.AttributeSet; 39 import android.util.FloatProperty; 40 import android.util.Property; 41 import android.view.View; 42 import android.widget.ImageView; 43 44 import androidx.annotation.Nullable; 45 import androidx.core.graphics.ColorUtils; 46 47 import com.android.launcher3.DeviceProfile; 48 import com.android.launcher3.Utilities; 49 import com.android.launcher3.util.SystemUiController; 50 import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags; 51 import com.android.launcher3.util.ViewPool; 52 import com.android.quickstep.FullscreenDrawParams; 53 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 54 import com.android.quickstep.orientation.RecentsPagedOrientationHandler; 55 import com.android.systemui.shared.recents.model.Task; 56 import com.android.systemui.shared.recents.model.ThumbnailData; 57 import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; 58 59 import java.util.Objects; 60 61 /** 62 * A task in the Recents view. 63 * 64 * @deprecated This class will be replaced by the new [TaskThumbnailView]. 65 */ 66 @Deprecated 67 public class TaskThumbnailViewDeprecated extends View implements ViewPool.Reusable { 68 69 public static final Property<TaskThumbnailViewDeprecated, Float> DIM_ALPHA = 70 new FloatProperty<TaskThumbnailViewDeprecated>("dimAlpha") { 71 @Override 72 public void setValue(TaskThumbnailViewDeprecated thumbnail, float dimAlpha) { 73 thumbnail.setDimAlpha(dimAlpha); 74 } 75 76 @Override 77 public Float get(TaskThumbnailViewDeprecated thumbnailView) { 78 return thumbnailView.mDimAlpha; 79 } 80 }; 81 82 public static final Property<TaskThumbnailViewDeprecated, Float> SPLASH_ALPHA = 83 new FloatProperty<TaskThumbnailViewDeprecated>("splashAlpha") { 84 @Override 85 public void setValue(TaskThumbnailViewDeprecated thumbnail, float splashAlpha) { 86 thumbnail.setSplashAlpha(splashAlpha); 87 } 88 89 @Override 90 public Float get(TaskThumbnailViewDeprecated thumbnailView) { 91 return thumbnailView.mSplashAlpha / 255f; 92 } 93 }; 94 95 private final RecentsViewContainer mContainer; 96 private TaskOverlay<?> mOverlay; 97 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 98 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 99 private final Paint mSplashBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 100 private final Paint mClearPaint = new Paint(); 101 private final Paint mDimmingPaintAfterClearing = new Paint(); 102 private final int mDimColor; 103 104 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 105 private final Rect mPreviewRect = new Rect(); 106 private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); 107 private FullscreenDrawParams mFullscreenParams; 108 private ImageView mSplashView; 109 private Drawable mSplashViewDrawable; 110 private TaskView mTaskView; 111 112 @Nullable 113 private Task mTask; 114 @Nullable 115 private ThumbnailData mThumbnailData; 116 @Nullable 117 protected BitmapShader mBitmapShader; 118 119 /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */ 120 private float mDimAlpha = 0f; 121 /** Controls visibility of the splash view, 0 is transparent, 255 fully opaque. */ 122 private int mSplashAlpha = 0; 123 124 private boolean mOverlayEnabled; 125 /** Used as a placeholder when the original thumbnail animates out to. */ 126 private boolean mShowSplashForSplitSelection; 127 TaskThumbnailViewDeprecated(Context context)128 public TaskThumbnailViewDeprecated(Context context) { 129 this(context, null); 130 } 131 TaskThumbnailViewDeprecated(Context context, @Nullable AttributeSet attrs)132 public TaskThumbnailViewDeprecated(Context context, @Nullable AttributeSet attrs) { 133 this(context, attrs, 0); 134 } 135 TaskThumbnailViewDeprecated(Context context, @Nullable AttributeSet attrs, int defStyleAttr)136 public TaskThumbnailViewDeprecated(Context context, @Nullable AttributeSet attrs, 137 int defStyleAttr) { 138 super(context, attrs, defStyleAttr); 139 mPaint.setFilterBitmap(true); 140 mBackgroundPaint.setColor(Color.WHITE); 141 mSplashBackgroundPaint.setColor(Color.WHITE); 142 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 143 mContainer = RecentsViewContainer.containerFromContext(context); 144 // Initialize with placeholder value. It is overridden later by TaskView 145 mFullscreenParams = new FullscreenDrawParams(context, __ -> 0f, __ -> 0f); 146 mDimColor = RecentsView.getForegroundScrimDimColor(context); 147 mDimmingPaintAfterClearing.setColor(mDimColor); 148 } 149 150 /** 151 * Updates the thumbnail to draw the provided task 152 */ bind(Task task, TaskOverlay<?> overlay, TaskView taskView)153 public void bind(Task task, TaskOverlay<?> overlay, TaskView taskView) { 154 mOverlay = overlay; 155 mOverlay.reset(); 156 mTask = task; 157 mTaskView = taskView; 158 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 159 mPaint.setColor(color); 160 mBackgroundPaint.setColor(color); 161 mSplashBackgroundPaint.setColor(color); 162 updateSplashView(mTask.icon); 163 } 164 165 /** 166 * Updates the thumbnail. 167 * 168 * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately. 169 * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)} 170 * version with {@code refreshNow} is true. The only exception is 171 * in the live tile case that we grab a screenshot when user enters Overview 172 * upon swipe up so that a usable screenshot is accessible immediately when 173 * recents animation needs to be finished / cancelled. 174 */ setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData, boolean refreshNow)175 public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData, 176 boolean refreshNow) { 177 mTask = task; 178 ThumbnailData oldThumbnailData = mThumbnailData; 179 mThumbnailData = (thumbnailData != null && thumbnailData.getThumbnail() != null) 180 ? thumbnailData : null; 181 if (mTask != null) { 182 updateSplashView(mTask.icon); 183 } 184 if (refreshNow) { 185 Long oldSnapshotId = oldThumbnailData != null ? oldThumbnailData.getSnapshotId() : null; 186 Long snapshotId = mThumbnailData != null ? mThumbnailData.getSnapshotId() : null; 187 refresh(snapshotId != null && !Objects.equals(oldSnapshotId, snapshotId)); 188 } 189 } 190 191 /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */ setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData)192 public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData) { 193 setThumbnail(task, thumbnailData, true /* refreshNow */); 194 } 195 196 /** Updates the shader, paint, matrix to redraw. */ refresh()197 public void refresh() { 198 refresh(false); 199 } 200 201 /** 202 * Updates the shader, paint, matrix to redraw. 203 * 204 * @param shouldRefreshOverlay whether to re-initialize overlay 205 */ refresh(boolean shouldRefreshOverlay)206 private void refresh(boolean shouldRefreshOverlay) { 207 if (mThumbnailData != null && mThumbnailData.getThumbnail() != null) { 208 Bitmap bm = mThumbnailData.getThumbnail(); 209 bm.prepareToDraw(); 210 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 211 mPaint.setShader(mBitmapShader); 212 updateThumbnailMatrix(); 213 if (shouldRefreshOverlay) { 214 refreshOverlay(); 215 } 216 } else { 217 mBitmapShader = null; 218 mThumbnailData = null; 219 mPaint.setShader(null); 220 mOverlay.reset(); 221 } 222 updateThumbnailPaintFilter(); 223 } 224 225 /** 226 * Sets the alpha of the dim layer on top of this view. 227 * <p> 228 * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be the 229 * extracted background color. 230 */ setDimAlpha(float dimAlpha)231 public void setDimAlpha(float dimAlpha) { 232 mDimAlpha = dimAlpha; 233 updateThumbnailPaintFilter(); 234 } 235 236 /** 237 * Sets the alpha of the splash view. 238 */ setSplashAlpha(float splashAlpha)239 public void setSplashAlpha(float splashAlpha) { 240 mSplashAlpha = (int) (Utilities.boundToRange(splashAlpha, 0f, 1f) * 255); 241 if (mSplashViewDrawable != null) { 242 mSplashViewDrawable.setAlpha(mSplashAlpha); 243 } 244 mSplashBackgroundPaint.setAlpha(mSplashAlpha); 245 invalidate(); 246 } 247 getDimAlpha()248 public float getDimAlpha() { 249 return mDimAlpha; 250 } 251 252 @SystemUiControllerFlags getSysUiStatusNavFlags()253 public int getSysUiStatusNavFlags() { 254 if (mThumbnailData != null) { 255 int flags = 0; 256 flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0 257 ? SystemUiController.FLAG_LIGHT_STATUS 258 : SystemUiController.FLAG_DARK_STATUS; 259 flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0 260 ? SystemUiController.FLAG_LIGHT_NAV 261 : SystemUiController.FLAG_DARK_NAV; 262 return flags; 263 } 264 return 0; 265 } 266 267 @Override onLayout(boolean changed, int left, int top, int right, int bottom)268 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 269 super.onLayout(changed, left, top, right, bottom); 270 updateSplashView(mSplashViewDrawable); 271 } 272 273 @Override onDraw(Canvas canvas)274 protected void onDraw(Canvas canvas) { 275 canvas.save(); 276 // Draw the insets if we're being drawn fullscreen (we do this for quick switch). 277 drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), 278 mFullscreenParams.getCurrentCornerRadius()); 279 canvas.restore(); 280 } 281 getPreviewPositionHelper()282 public PreviewPositionHelper getPreviewPositionHelper() { 283 return mPreviewPositionHelper; 284 } 285 setFullscreenParams(FullscreenDrawParams fullscreenParams)286 public void setFullscreenParams(FullscreenDrawParams fullscreenParams) { 287 mFullscreenParams = fullscreenParams; 288 invalidate(); 289 } 290 drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)291 public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, 292 float cornerRadius) { 293 if (mTask != null && mTaskView.isRunningTask() 294 && !mTaskView.getShouldShowScreenshot()) { 295 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); 296 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, 297 mDimmingPaintAfterClearing); 298 return; 299 } 300 301 // Always draw the background since the snapshots might be translucent or partially empty 302 // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss 303 // split screen). 304 canvas.drawRoundRect(x, y + 1, width, height - 1, cornerRadius, 305 cornerRadius, mBackgroundPaint); 306 307 final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null 308 || mThumbnailData == null; 309 if (drawBackgroundOnly) { 310 return; 311 } 312 313 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 314 315 // Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios. 316 if (shouldShowSplashView()) { 317 float cornerRadiusX = cornerRadius; 318 float cornerRadiusY = cornerRadius; 319 if (mShowSplashForSplitSelection) { 320 cornerRadiusX = cornerRadius / getScaleX(); 321 cornerRadiusY = cornerRadius / getScaleY(); 322 } 323 324 // Always draw background for hiding inconsistencies, even if splash view is not yet 325 // loaded (which can happen as task icons are loaded asynchronously in the background) 326 canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadiusX, 327 cornerRadiusY, mSplashBackgroundPaint); 328 if (mSplashView != null) { 329 mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1); 330 mSplashView.draw(canvas); 331 } 332 } 333 } 334 setOverlayEnabled(boolean overlayEnabled)335 public void setOverlayEnabled(boolean overlayEnabled) { 336 if (mOverlayEnabled != overlayEnabled) { 337 mOverlayEnabled = overlayEnabled; 338 339 refreshOverlay(); 340 } 341 } 342 343 /** 344 * Determine if the splash should be shown over top of the thumbnail. 345 * 346 * <p>We want to show the splash if the aspect ratio or rotation of the thumbnail would be 347 * different from the task. 348 */ shouldShowSplashView()349 public boolean shouldShowSplashView() { 350 return isThumbnailAspectRatioDifferentFromThumbnailData() 351 || isThumbnailRotationDifferentFromTask() 352 || mShowSplashForSplitSelection; 353 } 354 setShowSplashForSplitSelection(boolean showSplashForSplitSelection)355 public void setShowSplashForSplitSelection(boolean showSplashForSplitSelection) { 356 mShowSplashForSplitSelection = showSplashForSplitSelection; 357 } 358 refreshSplashView()359 protected void refreshSplashView() { 360 if (mTask != null) { 361 updateSplashView(mTask.icon); 362 invalidate(); 363 } 364 } 365 updateSplashView(Drawable icon)366 private void updateSplashView(Drawable icon) { 367 if (icon == null || icon.getConstantState() == null) { 368 mSplashViewDrawable = null; 369 mSplashView = null; 370 return; 371 } 372 mSplashViewDrawable = icon.getConstantState().newDrawable().mutate(); 373 mSplashViewDrawable.setAlpha(mSplashAlpha); 374 ImageView imageView = mSplashView == null ? new ImageView(getContext()) : mSplashView; 375 imageView.setImageDrawable(mSplashViewDrawable); 376 377 imageView.setScaleType(ImageView.ScaleType.MATRIX); 378 Matrix matrix = new Matrix(); 379 float drawableWidth = mSplashViewDrawable.getIntrinsicWidth(); 380 float drawableHeight = mSplashViewDrawable.getIntrinsicHeight(); 381 float viewWidth = getMeasuredWidth(); 382 float viewCenterX = viewWidth / 2f; 383 float viewHeight = getMeasuredHeight(); 384 float viewCenterY = viewHeight / 2f; 385 float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f; 386 float centeredDrawableTop = (viewHeight - drawableHeight) / 2f; 387 float nonGridScale = mTaskView == null ? 1 : 1 / mTaskView.getNonGridScale(); 388 float recentsMaxScale = mTaskView == null || mTaskView.getRecentsView() == null 389 ? 1 : 1 / mTaskView.getRecentsView().getMaxScaleForFullScreen(); 390 float scaleX = nonGridScale * recentsMaxScale * (1 / getScaleX()); 391 float scaleY = nonGridScale * recentsMaxScale * (1 / getScaleY()); 392 393 // Center the image in the view. 394 matrix.setTranslate(centeredDrawableLeft, centeredDrawableTop); 395 // Apply scale transformation after translation, pivoting around center of view. 396 matrix.postScale(scaleX, scaleY, viewCenterX, viewCenterY); 397 398 imageView.setImageMatrix(matrix); 399 mSplashView = imageView; 400 } 401 isThumbnailAspectRatioDifferentFromThumbnailData()402 private boolean isThumbnailAspectRatioDifferentFromThumbnailData() { 403 if (mThumbnailData == null || mThumbnailData.getThumbnail() == null) { 404 return false; 405 } 406 407 float thumbnailViewAspect = getWidth() / (float) getHeight(); 408 float thumbnailDataAspect = mThumbnailData.getThumbnail().getWidth() 409 / (float) mThumbnailData.getThumbnail().getHeight(); 410 411 return isRelativePercentDifferenceGreaterThan(thumbnailViewAspect, 412 thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); 413 } 414 415 /** 416 * Returns whether or not the current thumbnail is a different orientation to the task. 417 * <p> 418 * Used to disable modal state when screenshot doesn't match the device orientation. 419 */ isThumbnailRotationDifferentFromTask()420 public boolean isThumbnailRotationDifferentFromTask() { 421 RecentsView recents = mTaskView.getRecentsView(); 422 if (recents == null || mThumbnailData == null) { 423 return false; 424 } 425 426 if (recents.getPagedOrientationHandler() == RecentsPagedOrientationHandler.PORTRAIT) { 427 int currentRotation = recents.getPagedViewOrientedState().getRecentsActivityRotation(); 428 return (currentRotation - mThumbnailData.rotation) % 2 != 0; 429 } else { 430 return recents.getPagedOrientationHandler().getRotation() != mThumbnailData.rotation; 431 } 432 } 433 434 /** 435 * Potentially re-init the task overlay. Be cautious when calling this as the overlay may 436 * do processing on initialization. 437 */ refreshOverlay()438 private void refreshOverlay() { 439 if (mOverlayEnabled) { 440 mOverlay.initOverlay(mTask, 441 mThumbnailData != null ? mThumbnailData.getThumbnail() : null, 442 mPreviewPositionHelper.getMatrix(), 443 mPreviewPositionHelper.isOrientationChanged()); 444 } else { 445 mOverlay.reset(); 446 } 447 } 448 updateThumbnailPaintFilter()449 private void updateThumbnailPaintFilter() { 450 ColorFilter filter = getColorFilter(mDimAlpha); 451 mBackgroundPaint.setColorFilter(filter); 452 int alpha = (int) (mDimAlpha * 255); 453 mDimmingPaintAfterClearing.setAlpha(alpha); 454 if (mBitmapShader != null) { 455 mPaint.setColorFilter(filter); 456 } else { 457 mPaint.setColorFilter(null); 458 mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha)); 459 } 460 invalidate(); 461 } 462 updateThumbnailMatrix()463 private void updateThumbnailMatrix() { 464 DeviceProfile dp = mContainer.getDeviceProfile(); 465 mPreviewPositionHelper.setOrientationChanged(false); 466 if (mBitmapShader != null && mThumbnailData != null) { 467 mPreviewRect.set(0, 0, mThumbnailData.getThumbnail().getWidth(), 468 mThumbnailData.getThumbnail().getHeight()); 469 int currentRotation = mTaskView.getOrientedState().getRecentsActivityRotation(); 470 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 471 mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, 472 getMeasuredWidth(), getMeasuredHeight(), dp.isTablet, currentRotation, isRtl); 473 474 mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix()); 475 mPaint.setShader(mBitmapShader); 476 } 477 mTaskView.updateFullscreenParams(); 478 invalidate(); 479 } 480 481 @Override onSizeChanged(int w, int h, int oldw, int oldh)482 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 483 super.onSizeChanged(w, h, oldw, oldh); 484 updateThumbnailMatrix(); 485 486 refreshOverlay(); 487 } 488 getColorFilter(float dimAmount)489 private ColorFilter getColorFilter(float dimAmount) { 490 return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount); 491 } 492 493 /** 494 * Returns current thumbnail or null if none is set. 495 */ 496 @Nullable getThumbnail()497 public Bitmap getThumbnail() { 498 if (mThumbnailData == null) { 499 return null; 500 } 501 return mThumbnailData.getThumbnail(); 502 } 503 504 /** 505 * Returns whether the snapshot is real. If the device is locked for the user of the task, 506 * the snapshot used will be an app-theme generated snapshot instead of a real snapshot. 507 */ isRealSnapshot()508 public boolean isRealSnapshot() { 509 if (mThumbnailData == null) { 510 return false; 511 } 512 return mThumbnailData.isRealSnapshot && !mTask.isLocked; 513 } 514 getThumbnailMatrix()515 public Matrix getThumbnailMatrix() { 516 return mPreviewPositionHelper.getMatrix(); 517 } 518 519 @Override onRecycle()520 public void onRecycle() { 521 // Do nothing 522 } 523 } 524