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 com.android.launcher3.config.FeatureFlags.ENABLE_QUICKSTEP_LIVE_TILE; 20 import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; 21 22 import android.content.Context; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapShader; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.ColorFilter; 28 import android.graphics.ColorMatrix; 29 import android.graphics.ColorMatrixColorFilter; 30 import android.graphics.Insets; 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.RectF; 37 import android.graphics.Shader; 38 import android.os.Build; 39 import android.util.AttributeSet; 40 import android.util.FloatProperty; 41 import android.util.Property; 42 import android.view.Surface; 43 import android.view.View; 44 45 import androidx.annotation.RequiresApi; 46 47 import com.android.launcher3.BaseActivity; 48 import com.android.launcher3.DeviceProfile; 49 import com.android.launcher3.R; 50 import com.android.launcher3.Utilities; 51 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 52 import com.android.launcher3.util.MainThreadInitializedObject; 53 import com.android.launcher3.util.SystemUiController; 54 import com.android.launcher3.util.Themes; 55 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 56 import com.android.quickstep.views.TaskView.FullscreenDrawParams; 57 import com.android.systemui.plugins.OverviewScreenshotActions; 58 import com.android.systemui.plugins.PluginListener; 59 import com.android.systemui.shared.recents.model.Task; 60 import com.android.systemui.shared.recents.model.ThumbnailData; 61 62 /** 63 * A task in the Recents view. 64 */ 65 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> { 66 67 private static final ColorMatrix COLOR_MATRIX = new ColorMatrix(); 68 private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix(); 69 70 private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS = 71 new MainThreadInitializedObject<>(FullscreenDrawParams::new); 72 73 public static final Property<TaskThumbnailView, Float> DIM_ALPHA = 74 new FloatProperty<TaskThumbnailView>("dimAlpha") { 75 @Override 76 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { 77 thumbnail.setDimAlpha(dimAlpha); 78 } 79 80 @Override 81 public Float get(TaskThumbnailView thumbnailView) { 82 return thumbnailView.mDimAlpha; 83 } 84 }; 85 86 private final BaseActivity mActivity; 87 private TaskOverlay mOverlay; 88 private final boolean mIsDarkTextTheme; 89 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 90 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 91 private final Paint mClearPaint = new Paint(); 92 private final Paint mDimmingPaintAfterClearing = new Paint(); 93 94 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 95 private final Rect mPreviewRect = new Rect(); 96 private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); 97 private TaskView.FullscreenDrawParams mFullscreenParams; 98 99 private Task mTask; 100 private ThumbnailData mThumbnailData; 101 protected BitmapShader mBitmapShader; 102 103 private float mDimAlpha = 1f; 104 private float mDimAlphaMultiplier = 1f; 105 private float mSaturation = 1f; 106 107 private boolean mOverlayEnabled; 108 private OverviewScreenshotActions mOverviewScreenshotActionsPlugin; 109 TaskThumbnailView(Context context)110 public TaskThumbnailView(Context context) { 111 this(context, null); 112 } 113 TaskThumbnailView(Context context, AttributeSet attrs)114 public TaskThumbnailView(Context context, AttributeSet attrs) { 115 this(context, attrs, 0); 116 } 117 TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr)118 public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { 119 super(context, attrs, defStyleAttr); 120 mPaint.setFilterBitmap(true); 121 mBackgroundPaint.setColor(Color.WHITE); 122 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 123 mDimmingPaintAfterClearing.setColor(Color.BLACK); 124 mActivity = BaseActivity.fromContext(context); 125 mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); 126 // Initialize with dummy value. It is overridden later by TaskView 127 mFullscreenParams = TEMP_PARAMS.get(context); 128 } 129 130 /** 131 * Updates the thumbnail to draw the provided task 132 * @param task 133 */ bind(Task task)134 public void bind(Task task) { 135 getTaskOverlay().reset(); 136 mTask = task; 137 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 138 mPaint.setColor(color); 139 mBackgroundPaint.setColor(color); 140 } 141 142 /** 143 * Updates the thumbnail. 144 * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately. 145 * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)} 146 * version with {@code refreshNow} is true. The only exception is 147 * in the live tile case that we grab a screenshot when user enters Overview 148 * upon swipe up so that a usable screenshot is accessible immediately when 149 * recents animation needs to be finished / cancelled. 150 */ setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow)151 public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) { 152 mTask = task; 153 mThumbnailData = 154 (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null; 155 if (refreshNow) { 156 refresh(); 157 } 158 } 159 160 /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */ setThumbnail(Task task, ThumbnailData thumbnailData)161 public void setThumbnail(Task task, ThumbnailData thumbnailData) { 162 setThumbnail(task, thumbnailData, true /* refreshNow */); 163 } 164 165 /** Updates the shader, paint, matrix to redraw. */ refresh()166 public void refresh() { 167 if (mThumbnailData != null && mThumbnailData.thumbnail != null) { 168 Bitmap bm = mThumbnailData.thumbnail; 169 bm.prepareToDraw(); 170 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 171 mPaint.setShader(mBitmapShader); 172 updateThumbnailMatrix(); 173 } else { 174 mBitmapShader = null; 175 mThumbnailData = null; 176 mPaint.setShader(null); 177 getTaskOverlay().reset(); 178 } 179 if (mOverviewScreenshotActionsPlugin != null) { 180 mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity); 181 } 182 updateThumbnailPaintFilter(); 183 } 184 setDimAlphaMultipler(float dimAlphaMultipler)185 public void setDimAlphaMultipler(float dimAlphaMultipler) { 186 mDimAlphaMultiplier = dimAlphaMultipler; 187 setDimAlpha(mDimAlpha); 188 } 189 190 /** 191 * Sets the alpha of the dim layer on top of this view. 192 * <p> 193 * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. 194 */ setDimAlpha(float dimAlpha)195 public void setDimAlpha(float dimAlpha) { 196 mDimAlpha = dimAlpha; 197 updateThumbnailPaintFilter(); 198 } 199 getTaskOverlay()200 public TaskOverlay getTaskOverlay() { 201 if (mOverlay == null) { 202 mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this); 203 } 204 return mOverlay; 205 } 206 getDimAlpha()207 public float getDimAlpha() { 208 return mDimAlpha; 209 } 210 getInsets(Rect fallback)211 public Rect getInsets(Rect fallback) { 212 if (mThumbnailData != null) { 213 return mThumbnailData.insets; 214 } 215 return fallback; 216 } 217 218 /** 219 * Get the scaled insets that are being used to draw the task view. This is a subsection of 220 * the full snapshot. 221 * @return the insets in snapshot bitmap coordinates. 222 */ 223 @RequiresApi(api = Build.VERSION_CODES.Q) getScaledInsets()224 public Insets getScaledInsets() { 225 if (mThumbnailData == null) { 226 return Insets.NONE; 227 } 228 229 RectF bitmapRect = new RectF( 230 0, 0, 231 mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight()); 232 RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); 233 234 // The position helper matrix tells us how to transform the bitmap to fit the view, the 235 // inverse tells us where the view would be in the bitmaps coordinates. The insets are the 236 // difference between the bitmap bounds and the projected view bounds. 237 Matrix boundsToBitmapSpace = new Matrix(); 238 mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace); 239 RectF boundsInBitmapSpace = new RectF(); 240 boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); 241 242 return Insets.of( 243 Math.round(boundsInBitmapSpace.left), 244 Math.round(boundsInBitmapSpace.top), 245 Math.round(bitmapRect.right - boundsInBitmapSpace.right), 246 Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom)); 247 } 248 249 getSysUiStatusNavFlags()250 public int getSysUiStatusNavFlags() { 251 if (mThumbnailData != null) { 252 int flags = 0; 253 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 254 ? SystemUiController.FLAG_LIGHT_STATUS 255 : SystemUiController.FLAG_DARK_STATUS; 256 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 257 ? SystemUiController.FLAG_LIGHT_NAV 258 : SystemUiController.FLAG_DARK_NAV; 259 return flags; 260 } 261 return 0; 262 } 263 264 @Override onDraw(Canvas canvas)265 protected void onDraw(Canvas canvas) { 266 RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets; 267 canvas.save(); 268 canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale); 269 canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top); 270 // Draw the insets if we're being drawn fullscreen (we do this for quick switch). 271 drawOnCanvas(canvas, 272 -currentDrawnInsets.left, 273 -currentDrawnInsets.top, 274 getMeasuredWidth() + currentDrawnInsets.right, 275 getMeasuredHeight() + currentDrawnInsets.bottom, 276 mFullscreenParams.mCurrentDrawnCornerRadius); 277 canvas.restore(); 278 } 279 280 @Override onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, Context context)281 public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, 282 Context context) { 283 mOverviewScreenshotActionsPlugin = overviewScreenshotActions; 284 mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity); 285 } 286 287 @Override onPluginDisconnected(OverviewScreenshotActions plugin)288 public void onPluginDisconnected(OverviewScreenshotActions plugin) { 289 if (mOverviewScreenshotActionsPlugin != null) { 290 mOverviewScreenshotActionsPlugin = null; 291 } 292 } 293 294 @Override onAttachedToWindow()295 protected void onAttachedToWindow() { 296 super.onAttachedToWindow(); 297 PluginManagerWrapper.INSTANCE.get(getContext()) 298 .addPluginListener(this, OverviewScreenshotActions.class); 299 } 300 301 @Override onDetachedFromWindow()302 protected void onDetachedFromWindow() { 303 super.onDetachedFromWindow(); 304 PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this); 305 } 306 getPreviewPositionHelper()307 public PreviewPositionHelper getPreviewPositionHelper() { 308 return mPreviewPositionHelper; 309 } 310 setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)311 public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { 312 mFullscreenParams = fullscreenParams; 313 invalidate(); 314 } 315 drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)316 public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, 317 float cornerRadius) { 318 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 319 if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) { 320 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); 321 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, 322 mDimmingPaintAfterClearing); 323 return; 324 } 325 } 326 327 // Draw the background in all cases, except when the thumbnail data is opaque 328 final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null 329 || mThumbnailData == null; 330 if (drawBackgroundOnly || mPreviewPositionHelper.mClipBottom > 0 331 || mThumbnailData.isTranslucent) { 332 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); 333 if (drawBackgroundOnly) { 334 return; 335 } 336 } 337 338 if (mPreviewPositionHelper.mClipBottom > 0) { 339 canvas.save(); 340 canvas.clipRect(x, y, width, mPreviewPositionHelper.mClipBottom); 341 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 342 canvas.restore(); 343 } else { 344 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 345 } 346 } 347 getTaskView()348 public TaskView getTaskView() { 349 return (TaskView) getParent(); 350 } 351 setOverlayEnabled(boolean overlayEnabled)352 public void setOverlayEnabled(boolean overlayEnabled) { 353 if (mOverlayEnabled != overlayEnabled) { 354 mOverlayEnabled = overlayEnabled; 355 updateOverlay(); 356 } 357 } 358 updateOverlay()359 private void updateOverlay() { 360 if (mOverlayEnabled) { 361 getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix, 362 mPreviewPositionHelper.mIsOrientationChanged); 363 } else { 364 getTaskOverlay().reset(); 365 } 366 } 367 updateThumbnailPaintFilter()368 private void updateThumbnailPaintFilter() { 369 int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); 370 ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation); 371 mBackgroundPaint.setColorFilter(filter); 372 mDimmingPaintAfterClearing.setAlpha(255 - mul); 373 if (mBitmapShader != null) { 374 mPaint.setColorFilter(filter); 375 } else { 376 mPaint.setColorFilter(null); 377 mPaint.setColor(Color.argb(255, mul, mul, mul)); 378 } 379 invalidate(); 380 } 381 updateThumbnailMatrix()382 private void updateThumbnailMatrix() { 383 mPreviewPositionHelper.mClipBottom = -1; 384 mPreviewPositionHelper.mIsOrientationChanged = false; 385 if (mBitmapShader != null && mThumbnailData != null) { 386 mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(), 387 mThumbnailData.thumbnail.getHeight()); 388 int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState() 389 .getRecentsActivityRotation(); 390 mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, 391 getMeasuredWidth(), getMeasuredHeight(), mActivity.getDeviceProfile(), 392 currentRotation); 393 394 mBitmapShader.setLocalMatrix(mPreviewPositionHelper.mMatrix); 395 mPaint.setShader(mBitmapShader); 396 } 397 getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper); 398 invalidate(); 399 400 // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay 401 // as overlay could modify the views in the overlay as a side effect of its update. 402 post(this::updateOverlay); 403 } 404 405 @Override onSizeChanged(int w, int h, int oldw, int oldh)406 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 407 super.onSizeChanged(w, h, oldw, oldh); 408 updateThumbnailMatrix(); 409 } 410 411 /** 412 * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 - 413 * leave unchanged. 414 */ getColorFilter(int intensity, boolean shouldLighten, float saturation)415 private static ColorFilter getColorFilter(int intensity, boolean shouldLighten, 416 float saturation) { 417 intensity = Utilities.boundToRange(intensity, 0, 255); 418 419 if (intensity == 255 && saturation == 1) { 420 return null; 421 } 422 423 final float intensityScale = intensity / 255f; 424 COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1); 425 426 if (saturation != 1) { 427 SATURATION_COLOR_MATRIX.setSaturation(saturation); 428 COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX); 429 } 430 431 if (shouldLighten) { 432 final float[] colorArray = COLOR_MATRIX.getArray(); 433 final int colorAdd = 255 - intensity; 434 colorArray[4] = colorAdd; 435 colorArray[9] = colorAdd; 436 colorArray[14] = colorAdd; 437 } 438 439 return new ColorMatrixColorFilter(COLOR_MATRIX); 440 } 441 getThumbnail()442 public Bitmap getThumbnail() { 443 if (mThumbnailData == null) { 444 return null; 445 } 446 return mThumbnailData.thumbnail; 447 } 448 449 /** 450 * Returns whether the snapshot is real. If the device is locked for the user of the task, 451 * the snapshot used will be an app-theme generated snapshot instead of a real snapshot. 452 */ isRealSnapshot()453 public boolean isRealSnapshot() { 454 if (mThumbnailData == null) { 455 return false; 456 } 457 return mThumbnailData.isRealSnapshot && !mTask.isLocked; 458 } 459 460 /** 461 * Utility class to position the thumbnail in the TaskView 462 */ 463 public static class PreviewPositionHelper { 464 465 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 466 private final RectF mClippedInsets = new RectF(); 467 private final Matrix mMatrix = new Matrix(); 468 private float mClipBottom = -1; 469 private boolean mIsOrientationChanged; 470 getMatrix()471 public Matrix getMatrix() { 472 return mMatrix; 473 } 474 475 /** 476 * Updates the matrix based on the provided parameters 477 */ updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation)478 public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData, 479 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) { 480 boolean isRotated = false; 481 boolean isOrientationDifferent; 482 mClipBottom = -1; 483 484 int thumbnailRotation = thumbnailData.rotation; 485 int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); 486 Rect thumbnailInsets = getBoundedInsets( 487 dp.getInsets(), thumbnailData.insets, deltaRotate); 488 489 float scale = thumbnailData.scale; 490 final float thumbnailWidth = thumbnailPosition.width() 491 - (thumbnailInsets.left + thumbnailInsets.right) * scale; 492 final float thumbnailHeight = thumbnailPosition.height() 493 - (thumbnailInsets.top + thumbnailInsets.bottom) * scale; 494 495 final float thumbnailScale; 496 497 // Landscape vs portrait change 498 boolean windowingModeSupportsRotation = !dp.isMultiWindowMode 499 && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; 500 isOrientationDifferent = isOrientationChange(deltaRotate) 501 && windowingModeSupportsRotation; 502 if (canvasWidth == 0) { 503 // If we haven't measured , skip the thumbnail drawing and only draw the background 504 // color 505 thumbnailScale = 0f; 506 } else { 507 // Rotate the screenshot if not in multi-window mode 508 isRotated = deltaRotate > 0 && windowingModeSupportsRotation; 509 // Scale the screenshot to always fit the width of the card. 510 thumbnailScale = isOrientationDifferent 511 ? canvasWidth / thumbnailHeight 512 : canvasWidth / thumbnailWidth; 513 } 514 515 Rect splitScreenInsets = dp.getInsets(); 516 if (!isRotated) { 517 // No Rotation 518 if (dp.isMultiWindowMode) { 519 mClippedInsets.offsetTo(splitScreenInsets.left * scale, 520 splitScreenInsets.top * scale); 521 } else { 522 mClippedInsets.offsetTo(thumbnailInsets.left * scale, 523 thumbnailInsets.top * scale); 524 } 525 mMatrix.setTranslate( 526 -thumbnailInsets.left * scale, 527 -thumbnailInsets.top * scale); 528 } else { 529 setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition); 530 } 531 532 final float widthWithInsets; 533 final float heightWithInsets; 534 if (isOrientationDifferent) { 535 widthWithInsets = thumbnailPosition.height() * thumbnailScale; 536 heightWithInsets = thumbnailPosition.width() * thumbnailScale; 537 } else { 538 widthWithInsets = thumbnailPosition.width() * thumbnailScale; 539 heightWithInsets = thumbnailPosition.height() * thumbnailScale; 540 } 541 mClippedInsets.left *= thumbnailScale; 542 mClippedInsets.top *= thumbnailScale; 543 544 if (dp.isMultiWindowMode) { 545 mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale; 546 mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale; 547 } else { 548 mClippedInsets.right = Math.max(0, 549 widthWithInsets - mClippedInsets.left - canvasWidth); 550 mClippedInsets.bottom = Math.max(0, 551 heightWithInsets - mClippedInsets.top - canvasHeight); 552 } 553 554 mMatrix.postScale(thumbnailScale, thumbnailScale); 555 556 float bitmapHeight = Math.max(0, 557 (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale); 558 if (Math.round(bitmapHeight) < canvasHeight) { 559 mClipBottom = bitmapHeight; 560 } 561 mIsOrientationChanged = isOrientationDifferent; 562 } 563 getBoundedInsets(Rect activityInsets, Rect insets, int deltaRotation)564 private Rect getBoundedInsets(Rect activityInsets, Rect insets, int deltaRotation) { 565 if (deltaRotation != 0) { 566 return insets; 567 } 568 return new Rect(Math.min(insets.left, activityInsets.left), 569 Math.min(insets.top, activityInsets.top), 570 Math.min(insets.right, activityInsets.right), 571 Math.min(insets.bottom, activityInsets.bottom)); 572 } 573 getRotationDelta(int oldRotation, int newRotation)574 private int getRotationDelta(int oldRotation, int newRotation) { 575 int delta = newRotation - oldRotation; 576 if (delta < 0) delta += 4; 577 return delta; 578 } 579 580 /** 581 * @param deltaRotation the number of 90 degree turns from the current orientation 582 * @return {@code true} if the change in rotation results in a shift from landscape to 583 * portrait or vice versa, {@code false} otherwise 584 */ isOrientationChange(int deltaRotation)585 private boolean isOrientationChange(int deltaRotation) { 586 return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; 587 } 588 setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale, Rect thumbnailPosition)589 private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale, 590 Rect thumbnailPosition) { 591 int newLeftInset = 0; 592 int newTopInset = 0; 593 int translateX = 0; 594 int translateY = 0; 595 596 mMatrix.setRotate(90 * deltaRotate); 597 switch (deltaRotate) { /* Counter-clockwise */ 598 case Surface.ROTATION_90: 599 newLeftInset = thumbnailInsets.bottom; 600 newTopInset = thumbnailInsets.left; 601 translateX = thumbnailPosition.height(); 602 break; 603 case Surface.ROTATION_270: 604 newLeftInset = thumbnailInsets.top; 605 newTopInset = thumbnailInsets.right; 606 translateY = thumbnailPosition.width(); 607 break; 608 case Surface.ROTATION_180: 609 newLeftInset = -thumbnailInsets.top; 610 newTopInset = -thumbnailInsets.left; 611 translateX = thumbnailPosition.width(); 612 translateY = thumbnailPosition.height(); 613 break; 614 } 615 mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale); 616 mMatrix.postTranslate(translateX - mClippedInsets.left, 617 translateY - mClippedInsets.top); 618 } 619 620 /** 621 * Insets to used for clipping the thumbnail (in case it is drawing outside its own space) 622 */ getInsetsToDrawInFullscreen()623 public RectF getInsetsToDrawInFullscreen() { 624 return mClippedInsets; 625 } 626 } 627 } 628