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.launcher3.Utilities.comp; 23 import static com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 24 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; 25 26 import android.content.Context; 27 import android.graphics.Bitmap; 28 import android.graphics.BitmapShader; 29 import android.graphics.Canvas; 30 import android.graphics.Color; 31 import android.graphics.ColorFilter; 32 import android.graphics.Insets; 33 import android.graphics.Matrix; 34 import android.graphics.Paint; 35 import android.graphics.PorterDuff; 36 import android.graphics.PorterDuffXfermode; 37 import android.graphics.Rect; 38 import android.graphics.RectF; 39 import android.graphics.Shader; 40 import android.os.Build; 41 import android.util.AttributeSet; 42 import android.util.FloatProperty; 43 import android.util.Property; 44 import android.view.Surface; 45 import android.view.View; 46 47 import androidx.annotation.Nullable; 48 import androidx.annotation.RequiresApi; 49 import androidx.core.graphics.ColorUtils; 50 51 import com.android.launcher3.BaseActivity; 52 import com.android.launcher3.DeviceProfile; 53 import com.android.launcher3.Utilities; 54 import com.android.launcher3.util.MainThreadInitializedObject; 55 import com.android.launcher3.util.SystemUiController; 56 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 57 import com.android.quickstep.views.TaskView.FullscreenDrawParams; 58 import com.android.systemui.shared.recents.model.Task; 59 import com.android.systemui.shared.recents.model.ThumbnailData; 60 61 /** 62 * A task in the Recents view. 63 */ 64 public class TaskThumbnailView extends View { 65 private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS = 66 new MainThreadInitializedObject<>(FullscreenDrawParams::new); 67 68 public static final Property<TaskThumbnailView, Float> DIM_ALPHA = 69 new FloatProperty<TaskThumbnailView>("dimAlpha") { 70 @Override 71 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { 72 thumbnail.setDimAlpha(dimAlpha); 73 } 74 75 @Override 76 public Float get(TaskThumbnailView thumbnailView) { 77 return thumbnailView.mDimAlpha; 78 } 79 }; 80 81 private final BaseActivity mActivity; 82 @Nullable 83 private TaskOverlay mOverlay; 84 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 85 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 86 private final Paint mClearPaint = new Paint(); 87 private final Paint mDimmingPaintAfterClearing = new Paint(); 88 private final int mDimColor; 89 90 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 91 private final Rect mPreviewRect = new Rect(); 92 private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); 93 private TaskView.FullscreenDrawParams mFullscreenParams; 94 95 @Nullable 96 private Task mTask; 97 @Nullable 98 private ThumbnailData mThumbnailData; 99 @Nullable 100 protected BitmapShader mBitmapShader; 101 102 /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */ 103 private float mDimAlpha = 0f; 104 105 private boolean mOverlayEnabled; 106 TaskThumbnailView(Context context)107 public TaskThumbnailView(Context context) { 108 this(context, null); 109 } 110 TaskThumbnailView(Context context, @Nullable AttributeSet attrs)111 public TaskThumbnailView(Context context, @Nullable AttributeSet attrs) { 112 this(context, attrs, 0); 113 } 114 TaskThumbnailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)115 public TaskThumbnailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { 116 super(context, attrs, defStyleAttr); 117 mPaint.setFilterBitmap(true); 118 mBackgroundPaint.setColor(Color.WHITE); 119 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 120 mActivity = BaseActivity.fromContext(context); 121 // Initialize with placeholder value. It is overridden later by TaskView 122 mFullscreenParams = TEMP_PARAMS.get(context); 123 124 mDimColor = RecentsView.getForegroundScrimDimColor(context); 125 mDimmingPaintAfterClearing.setColor(mDimColor); 126 } 127 128 /** 129 * Updates the thumbnail to draw the provided task 130 * @param task 131 */ bind(Task task)132 public void bind(Task task) { 133 getTaskOverlay().reset(); 134 mTask = task; 135 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 136 mPaint.setColor(color); 137 mBackgroundPaint.setColor(color); 138 } 139 140 /** 141 * Updates the thumbnail. 142 * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately. 143 * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)} 144 * version with {@code refreshNow} is true. The only exception is 145 * in the live tile case that we grab a screenshot when user enters Overview 146 * upon swipe up so that a usable screenshot is accessible immediately when 147 * recents animation needs to be finished / cancelled. 148 */ setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData, boolean refreshNow)149 public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData, 150 boolean refreshNow) { 151 mTask = task; 152 boolean thumbnailWasNull = mThumbnailData == null; 153 mThumbnailData = 154 (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null; 155 if (refreshNow) { 156 refresh(thumbnailWasNull && mThumbnailData != null); 157 } 158 } 159 160 /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */ setThumbnail(@ullable Task task, @Nullable ThumbnailData thumbnailData)161 public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData) { 162 setThumbnail(task, thumbnailData, true /* refreshNow */); 163 } 164 165 /** Updates the shader, paint, matrix to redraw. */ refresh()166 public void refresh() { 167 refresh(false); 168 } 169 170 /** 171 * Updates the shader, paint, matrix to redraw. 172 * @param shouldRefreshOverlay whether to re-initialize overlay 173 */ refresh(boolean shouldRefreshOverlay)174 private void refresh(boolean shouldRefreshOverlay) { 175 if (mThumbnailData != null && mThumbnailData.thumbnail != null) { 176 Bitmap bm = mThumbnailData.thumbnail; 177 bm.prepareToDraw(); 178 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 179 mPaint.setShader(mBitmapShader); 180 updateThumbnailMatrix(); 181 if (shouldRefreshOverlay) { 182 refreshOverlay(); 183 } 184 } else { 185 mBitmapShader = null; 186 mThumbnailData = null; 187 mPaint.setShader(null); 188 getTaskOverlay().reset(); 189 } 190 updateThumbnailPaintFilter(); 191 } 192 193 /** 194 * Sets the alpha of the dim layer on top of this view. 195 * <p> 196 * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be the 197 * extracted background color. 198 * 199 */ setDimAlpha(float dimAlpha)200 public void setDimAlpha(float dimAlpha) { 201 mDimAlpha = dimAlpha; 202 updateThumbnailPaintFilter(); 203 } 204 getTaskOverlay()205 public TaskOverlay getTaskOverlay() { 206 if (mOverlay == null) { 207 mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this); 208 } 209 return mOverlay; 210 } 211 getDimAlpha()212 public float getDimAlpha() { 213 return mDimAlpha; 214 } 215 216 /** 217 * Get the scaled insets that are being used to draw the task view. This is a subsection of 218 * the full snapshot. 219 * @return the insets in snapshot bitmap coordinates. 220 */ 221 @RequiresApi(api = Build.VERSION_CODES.Q) getScaledInsets()222 public Insets getScaledInsets() { 223 if (mThumbnailData == null) { 224 return Insets.NONE; 225 } 226 227 RectF bitmapRect = new RectF( 228 0, 0, 229 mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight()); 230 RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); 231 232 // The position helper matrix tells us how to transform the bitmap to fit the view, the 233 // inverse tells us where the view would be in the bitmaps coordinates. The insets are the 234 // difference between the bitmap bounds and the projected view bounds. 235 Matrix boundsToBitmapSpace = new Matrix(); 236 mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace); 237 RectF boundsInBitmapSpace = new RectF(); 238 boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); 239 240 DeviceProfile dp = mActivity.getDeviceProfile(); 241 int leftInset = TaskView.clipLeft(dp) ? Math.round(boundsInBitmapSpace.left) : 0; 242 int topInset = TaskView.clipTop(dp) ? Math.round(boundsInBitmapSpace.top) : 0; 243 int rightInset = TaskView.clipRight(dp) ? Math.round( 244 bitmapRect.right - boundsInBitmapSpace.right) : 0; 245 int bottomInset = TaskView.clipBottom(dp) 246 ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0; 247 return Insets.of(leftInset, topInset, rightInset, bottomInset); 248 } 249 250 getSysUiStatusNavFlags()251 public int getSysUiStatusNavFlags() { 252 if (mThumbnailData != null) { 253 int flags = 0; 254 flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0 255 ? SystemUiController.FLAG_LIGHT_STATUS 256 : SystemUiController.FLAG_DARK_STATUS; 257 flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0 258 ? SystemUiController.FLAG_LIGHT_NAV 259 : SystemUiController.FLAG_DARK_NAV; 260 return flags; 261 } 262 return 0; 263 } 264 265 @Override onDraw(Canvas canvas)266 protected void onDraw(Canvas canvas) { 267 RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets; 268 canvas.save(); 269 canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale); 270 canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top); 271 // Draw the insets if we're being drawn fullscreen (we do this for quick switch). 272 drawOnCanvas(canvas, 273 -currentDrawnInsets.left, 274 -currentDrawnInsets.top, 275 getMeasuredWidth() + currentDrawnInsets.right, 276 getMeasuredHeight() + currentDrawnInsets.bottom, 277 mFullscreenParams.mCurrentDrawnCornerRadius); 278 canvas.restore(); 279 } 280 getPreviewPositionHelper()281 public PreviewPositionHelper getPreviewPositionHelper() { 282 return mPreviewPositionHelper; 283 } 284 setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)285 public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { 286 mFullscreenParams = fullscreenParams; 287 invalidate(); 288 } 289 drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)290 public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, 291 float cornerRadius) { 292 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 293 if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) { 294 // TODO(b/189265196): Temporary fix to align the surface with the cutout perfectly. 295 // Round up only when the live tile task is displayed in Overview. 296 float rounding = comp(mFullscreenParams.mFullscreenProgress); 297 float left = x + rounding / 2; 298 float top = y + rounding / 2; 299 float right = width - rounding; 300 float bottom = height - rounding; 301 302 canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius, 303 mClearPaint); 304 canvas.drawRoundRect(left, top, right, bottom, cornerRadius, cornerRadius, 305 mDimmingPaintAfterClearing); 306 return; 307 } 308 } 309 310 // Always draw the background since the snapshots might be translucent or partially empty 311 // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss 312 // split screen). 313 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); 314 315 final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null 316 || mThumbnailData == null; 317 if (drawBackgroundOnly) { 318 return; 319 } 320 321 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 322 } 323 getTaskView()324 public TaskView getTaskView() { 325 return (TaskView) getParent(); 326 } 327 setOverlayEnabled(boolean overlayEnabled)328 public void setOverlayEnabled(boolean overlayEnabled) { 329 if (mOverlayEnabled != overlayEnabled) { 330 mOverlayEnabled = overlayEnabled; 331 332 refreshOverlay(); 333 } 334 } 335 336 /** 337 * Potentially re-init the task overlay. Be cautious when calling this as the overlay may 338 * do processing on initialization. 339 */ refreshOverlay()340 private void refreshOverlay() { 341 if (mOverlayEnabled) { 342 getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix, 343 mPreviewPositionHelper.mIsOrientationChanged); 344 } else { 345 getTaskOverlay().reset(); 346 } 347 } 348 updateThumbnailPaintFilter()349 private void updateThumbnailPaintFilter() { 350 ColorFilter filter = getColorFilter(mDimAlpha); 351 mBackgroundPaint.setColorFilter(filter); 352 int alpha = (int) (mDimAlpha * 255); 353 mDimmingPaintAfterClearing.setAlpha(alpha); 354 if (mBitmapShader != null) { 355 mPaint.setColorFilter(filter); 356 } else { 357 mPaint.setColorFilter(null); 358 mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha)); 359 } 360 invalidate(); 361 } 362 updateThumbnailMatrix()363 private void updateThumbnailMatrix() { 364 mPreviewPositionHelper.mIsOrientationChanged = false; 365 if (mBitmapShader != null && mThumbnailData != null) { 366 mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(), 367 mThumbnailData.thumbnail.getHeight()); 368 int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState() 369 .getRecentsActivityRotation(); 370 boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 371 mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, 372 getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(), 373 currentRotation, isRtl); 374 375 mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix); 376 mPaint.setShader(mBitmapShader); 377 } 378 getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper); 379 invalidate(); 380 } 381 382 @Override onSizeChanged(int w, int h, int oldw, int oldh)383 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 384 super.onSizeChanged(w, h, oldw, oldh); 385 updateThumbnailMatrix(); 386 387 refreshOverlay(); 388 } 389 getColorFilter(float dimAmount)390 private ColorFilter getColorFilter(float dimAmount) { 391 return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount); 392 } 393 394 /** 395 * Returns current thumbnail or null if none is set. 396 */ 397 @Nullable getThumbnail()398 public Bitmap getThumbnail() { 399 if (mThumbnailData == null) { 400 return null; 401 } 402 return mThumbnailData.thumbnail; 403 } 404 405 /** 406 * Returns whether the snapshot is real. If the device is locked for the user of the task, 407 * the snapshot used will be an app-theme generated snapshot instead of a real snapshot. 408 */ isRealSnapshot()409 public boolean isRealSnapshot() { 410 if (mThumbnailData == null) { 411 return false; 412 } 413 return mThumbnailData.isRealSnapshot && !mTask.isLocked; 414 } 415 416 /** 417 * Utility class to position the thumbnail in the TaskView 418 */ 419 public static class PreviewPositionHelper { 420 421 private static final RectF EMPTY_RECT_F = new RectF(); 422 423 // Contains the portion of the thumbnail that is unclipped when fullscreen progress = 1. 424 private final RectF mClippedInsets = new RectF(); 425 private final Matrix mMatrix = new Matrix(); 426 private boolean mIsOrientationChanged; 427 getMatrix()428 public Matrix getMatrix() { 429 return mMatrix; 430 } 431 432 /** 433 * Updates the matrix based on the provided parameters 434 */ updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation, boolean isRtl)435 public void updateThumbnailMatrix(Rect thumbnailBounds, ThumbnailData thumbnailData, 436 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation, 437 boolean isRtl) { 438 boolean isRotated = false; 439 boolean isOrientationDifferent; 440 441 int thumbnailRotation = thumbnailData.rotation; 442 int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); 443 RectF thumbnailClipHint = new RectF(); 444 if (TaskView.clipLeft(dp)) { 445 thumbnailClipHint.left = thumbnailData.insets.left; 446 } 447 if (TaskView.clipRight(dp)) { 448 thumbnailClipHint.right = thumbnailData.insets.right; 449 } 450 if (TaskView.clipTop(dp)) { 451 thumbnailClipHint.top = thumbnailData.insets.top; 452 } 453 if (TaskView.clipBottom(dp)) { 454 thumbnailClipHint.bottom = thumbnailData.insets.bottom; 455 } 456 457 float scale = thumbnailData.scale; 458 final float thumbnailScale; 459 460 // Landscape vs portrait change. 461 // Note: Disable rotation in grid layout. 462 boolean windowingModeSupportsRotation = !dp.isMultiWindowMode 463 && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN 464 && !dp.overviewShowAsGrid; 465 isOrientationDifferent = isOrientationChange(deltaRotate) 466 && windowingModeSupportsRotation; 467 if (canvasWidth == 0 || canvasHeight == 0 || scale == 0) { 468 // If we haven't measured , skip the thumbnail drawing and only draw the background 469 // color 470 thumbnailScale = 0f; 471 } else { 472 // Rotate the screenshot if not in multi-window mode 473 isRotated = deltaRotate > 0 && windowingModeSupportsRotation; 474 475 float surfaceWidth = thumbnailBounds.width() / scale; 476 float surfaceHeight = thumbnailBounds.height() / scale; 477 float availableWidth = surfaceWidth 478 - (thumbnailClipHint.left + thumbnailClipHint.right); 479 float availableHeight = surfaceHeight 480 - (thumbnailClipHint.top + thumbnailClipHint.bottom); 481 482 float canvasAspect = canvasWidth / (float) canvasHeight; 483 float availableAspect = isRotated 484 ? availableHeight / availableWidth 485 : availableWidth / availableHeight; 486 boolean isAspectLargelyDifferent = Utilities.isRelativePercentDifferenceGreaterThan( 487 canvasAspect, availableAspect, 0.1f); 488 if (isRotated && isAspectLargelyDifferent) { 489 // Do not rotate thumbnail if it would not improve fit 490 isRotated = false; 491 isOrientationDifferent = false; 492 } 493 494 if (isAspectLargelyDifferent) { 495 // Crop letterbox insets if insets isn't already clipped 496 if (!TaskView.clipLeft(dp)) { 497 thumbnailClipHint.left = thumbnailData.letterboxInsets.left; 498 } 499 if (!TaskView.clipRight(dp)) { 500 thumbnailClipHint.right = thumbnailData.letterboxInsets.right; 501 } 502 if (!TaskView.clipTop(dp)) { 503 thumbnailClipHint.top = thumbnailData.letterboxInsets.top; 504 } 505 if (!TaskView.clipBottom(dp)) { 506 thumbnailClipHint.bottom = thumbnailData.letterboxInsets.bottom; 507 } 508 availableWidth = surfaceWidth 509 - (thumbnailClipHint.left + thumbnailClipHint.right); 510 availableHeight = surfaceHeight 511 - (thumbnailClipHint.top + thumbnailClipHint.bottom); 512 } 513 514 final float targetW, targetH; 515 if (isOrientationDifferent) { 516 targetW = canvasHeight; 517 targetH = canvasWidth; 518 } else { 519 targetW = canvasWidth; 520 targetH = canvasHeight; 521 } 522 float targetAspect = targetW / targetH; 523 524 // Update the clipHint such that 525 // > the final clipped position has same aspect ratio as requested by canvas 526 // > first fit the width and crop the extra height 527 // > if that will leave empty space, fit the height and crop the width instead 528 float croppedWidth = availableWidth; 529 float croppedHeight = croppedWidth / targetAspect; 530 if (croppedHeight > availableHeight) { 531 croppedHeight = availableHeight; 532 if (croppedHeight < targetH) { 533 croppedHeight = Math.min(targetH, surfaceHeight); 534 } 535 croppedWidth = croppedHeight * targetAspect; 536 537 // One last check in case the task aspect radio messed up something 538 if (croppedWidth > surfaceWidth) { 539 croppedWidth = surfaceWidth; 540 croppedHeight = croppedWidth / targetAspect; 541 } 542 } 543 544 // Update the clip hints. Align to 0,0, crop the remaining. 545 if (isRtl) { 546 thumbnailClipHint.left += availableWidth - croppedWidth; 547 if (thumbnailClipHint.right < 0) { 548 thumbnailClipHint.left += thumbnailClipHint.right; 549 thumbnailClipHint.right = 0; 550 } 551 } else { 552 thumbnailClipHint.right += availableWidth - croppedWidth; 553 if (thumbnailClipHint.left < 0) { 554 thumbnailClipHint.right += thumbnailClipHint.left; 555 thumbnailClipHint.left = 0; 556 } 557 } 558 thumbnailClipHint.bottom += availableHeight - croppedHeight; 559 if (thumbnailClipHint.top < 0) { 560 thumbnailClipHint.bottom += thumbnailClipHint.top; 561 thumbnailClipHint.top = 0; 562 } else if (thumbnailClipHint.bottom < 0) { 563 thumbnailClipHint.top += thumbnailClipHint.bottom; 564 thumbnailClipHint.bottom = 0; 565 } 566 567 thumbnailScale = targetW / (croppedWidth * scale); 568 } 569 570 Rect splitScreenInsets = dp.getInsets(); 571 if (!isRotated) { 572 // No Rotation 573 if (dp.isMultiWindowMode) { 574 mClippedInsets.offsetTo(splitScreenInsets.left * scale, 575 splitScreenInsets.top * scale); 576 } else { 577 mClippedInsets.offsetTo(thumbnailClipHint.left * scale, 578 thumbnailClipHint.top * scale); 579 } 580 mMatrix.setTranslate( 581 -thumbnailClipHint.left * scale, 582 -thumbnailClipHint.top * scale); 583 } else { 584 setThumbnailRotation(deltaRotate, thumbnailClipHint, scale, thumbnailBounds, dp); 585 } 586 587 final float widthWithInsets; 588 final float heightWithInsets; 589 if (isOrientationDifferent) { 590 widthWithInsets = thumbnailBounds.height() * thumbnailScale; 591 heightWithInsets = thumbnailBounds.width() * thumbnailScale; 592 } else { 593 widthWithInsets = thumbnailBounds.width() * thumbnailScale; 594 heightWithInsets = thumbnailBounds.height() * thumbnailScale; 595 } 596 mClippedInsets.left *= thumbnailScale; 597 mClippedInsets.top *= thumbnailScale; 598 599 if (dp.isMultiWindowMode) { 600 mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale; 601 mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale; 602 } else { 603 mClippedInsets.right = Math.max(0, 604 widthWithInsets - mClippedInsets.left - canvasWidth); 605 mClippedInsets.bottom = Math.max(0, 606 heightWithInsets - mClippedInsets.top - canvasHeight); 607 } 608 609 mMatrix.postScale(thumbnailScale, thumbnailScale); 610 mIsOrientationChanged = isOrientationDifferent; 611 } 612 getRotationDelta(int oldRotation, int newRotation)613 private int getRotationDelta(int oldRotation, int newRotation) { 614 int delta = newRotation - oldRotation; 615 if (delta < 0) delta += 4; 616 return delta; 617 } 618 619 /** 620 * @param deltaRotation the number of 90 degree turns from the current orientation 621 * @return {@code true} if the change in rotation results in a shift from landscape to 622 * portrait or vice versa, {@code false} otherwise 623 */ isOrientationChange(int deltaRotation)624 private boolean isOrientationChange(int deltaRotation) { 625 return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; 626 } 627 setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale, Rect thumbnailPosition, DeviceProfile dp)628 private void setThumbnailRotation(int deltaRotate, RectF thumbnailInsets, float scale, 629 Rect thumbnailPosition, DeviceProfile dp) { 630 float newLeftInset = 0; 631 float newTopInset = 0; 632 float translateX = 0; 633 float translateY = 0; 634 635 mMatrix.setRotate(90 * deltaRotate); 636 switch (deltaRotate) { /* Counter-clockwise */ 637 case Surface.ROTATION_90: 638 newLeftInset = thumbnailInsets.bottom; 639 newTopInset = thumbnailInsets.left; 640 translateX = thumbnailPosition.height(); 641 break; 642 case Surface.ROTATION_270: 643 newLeftInset = thumbnailInsets.top; 644 newTopInset = thumbnailInsets.right; 645 translateY = thumbnailPosition.width(); 646 break; 647 case Surface.ROTATION_180: 648 newLeftInset = -thumbnailInsets.top; 649 newTopInset = -thumbnailInsets.left; 650 translateX = thumbnailPosition.width(); 651 translateY = thumbnailPosition.height(); 652 break; 653 } 654 mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale); 655 mMatrix.postTranslate(translateX, translateY); 656 if (TaskView.useFullThumbnail(dp)) { 657 mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top); 658 } 659 } 660 661 /** 662 * Insets to used for clipping the thumbnail (in case it is drawing outside its own space) 663 */ getInsetsToDrawInFullscreen(DeviceProfile dp)664 public RectF getInsetsToDrawInFullscreen(DeviceProfile dp) { 665 return TaskView.useFullThumbnail(dp) ? mClippedInsets : EMPTY_RECT_F; 666 } 667 } 668 } 669