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