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; 56 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 57 import com.android.quickstep.views.TaskView.FullscreenDrawParams; 58 import com.android.systemui.plugins.OverviewScreenshotActions; 59 import com.android.systemui.plugins.PluginListener; 60 import com.android.systemui.shared.recents.model.Task; 61 import com.android.systemui.shared.recents.model.ThumbnailData; 62 import com.android.systemui.shared.system.ConfigurationCompat; 63 64 /** 65 * A task in the Recents view. 66 */ 67 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> { 68 69 private static final ColorMatrix COLOR_MATRIX = new ColorMatrix(); 70 private static final ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix(); 71 72 private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS = 73 new MainThreadInitializedObject<>(FullscreenDrawParams::new); 74 75 public static final Property<TaskThumbnailView, Float> DIM_ALPHA = 76 new FloatProperty<TaskThumbnailView>("dimAlpha") { 77 @Override 78 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { 79 thumbnail.setDimAlpha(dimAlpha); 80 } 81 82 @Override 83 public Float get(TaskThumbnailView thumbnailView) { 84 return thumbnailView.mDimAlpha; 85 } 86 }; 87 88 private final BaseActivity mActivity; 89 private final TaskOverlay mOverlay; 90 private final boolean mIsDarkTextTheme; 91 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 92 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 93 private final Paint mClearPaint = new Paint(); 94 private final Paint mDimmingPaintAfterClearing = new Paint(); 95 96 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 97 private final Rect mPreviewRect = new Rect(); 98 private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); 99 private TaskView.FullscreenDrawParams mFullscreenParams; 100 101 private Task mTask; 102 private ThumbnailData mThumbnailData; 103 protected BitmapShader mBitmapShader; 104 105 private float mDimAlpha = 1f; 106 private float mDimAlphaMultiplier = 1f; 107 private float mSaturation = 1f; 108 109 private boolean mOverlayEnabled; 110 private OverviewScreenshotActions mOverviewScreenshotActionsPlugin; 111 TaskThumbnailView(Context context)112 public TaskThumbnailView(Context context) { 113 this(context, null); 114 } 115 TaskThumbnailView(Context context, AttributeSet attrs)116 public TaskThumbnailView(Context context, AttributeSet attrs) { 117 this(context, attrs, 0); 118 } 119 TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr)120 public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { 121 super(context, attrs, defStyleAttr); 122 mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this); 123 mPaint.setFilterBitmap(true); 124 mBackgroundPaint.setColor(Color.WHITE); 125 mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 126 mDimmingPaintAfterClearing.setColor(Color.BLACK); 127 mActivity = BaseActivity.fromContext(context); 128 mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); 129 // Initialize with dummy value. It is overridden later by TaskView 130 mFullscreenParams = TEMP_PARAMS.get(context); 131 } 132 133 /** 134 * Updates the thumbnail to draw the provided task 135 * @param task 136 */ bind(Task task)137 public void bind(Task task) { 138 mOverlay.reset(); 139 mTask = task; 140 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 141 mPaint.setColor(color); 142 mBackgroundPaint.setColor(color); 143 } 144 145 /** 146 * Updates the thumbnail. 147 * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately. 148 * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)} 149 * version with {@code refreshNow} is true. The only exception is 150 * in the live tile case that we grab a screenshot when user enters Overview 151 * upon swipe up so that a usable screenshot is accessible immediately when 152 * recents animation needs to be finished / cancelled. 153 */ setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow)154 public void setThumbnail(Task task, ThumbnailData thumbnailData, boolean refreshNow) { 155 mTask = task; 156 mThumbnailData = 157 (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null; 158 if (refreshNow) { 159 refresh(); 160 } 161 } 162 163 /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */ setThumbnail(Task task, ThumbnailData thumbnailData)164 public void setThumbnail(Task task, ThumbnailData thumbnailData) { 165 setThumbnail(task, thumbnailData, true /* refreshNow */); 166 } 167 168 /** Updates the shader, paint, matrix to redraw. */ refresh()169 public void refresh() { 170 if (mThumbnailData != null && mThumbnailData.thumbnail != null) { 171 Bitmap bm = mThumbnailData.thumbnail; 172 bm.prepareToDraw(); 173 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 174 mPaint.setShader(mBitmapShader); 175 updateThumbnailMatrix(); 176 } else { 177 mBitmapShader = null; 178 mThumbnailData = null; 179 mPaint.setShader(null); 180 mOverlay.reset(); 181 } 182 if (mOverviewScreenshotActionsPlugin != null) { 183 mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity); 184 } 185 updateThumbnailPaintFilter(); 186 } 187 setDimAlphaMultipler(float dimAlphaMultipler)188 public void setDimAlphaMultipler(float dimAlphaMultipler) { 189 mDimAlphaMultiplier = dimAlphaMultipler; 190 setDimAlpha(mDimAlpha); 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 black. 197 */ setDimAlpha(float dimAlpha)198 public void setDimAlpha(float dimAlpha) { 199 mDimAlpha = dimAlpha; 200 updateThumbnailPaintFilter(); 201 } 202 getTaskOverlay()203 public TaskOverlay getTaskOverlay() { 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 && mBitmapShader != null && mThumbnailData != null) { 361 mOverlay.initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.mMatrix, 362 mPreviewPositionHelper.mIsOrientationChanged); 363 } else { 364 mOverlay.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 = ConfigurationCompat.getWindowConfigurationRotation( 389 mActivity.getResources().getConfiguration()); 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. 451 */ isRealSnapshot()452 public boolean isRealSnapshot() { 453 if (mThumbnailData == null) { 454 return false; 455 } 456 return mThumbnailData.isRealSnapshot; 457 } 458 459 /** 460 * Utility class to position the thumbnail in the TaskView 461 */ 462 public static class PreviewPositionHelper { 463 464 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 465 private final RectF mClippedInsets = new RectF(); 466 private final Matrix mMatrix = new Matrix(); 467 private float mClipBottom = -1; 468 private boolean mIsOrientationChanged; 469 getMatrix()470 public Matrix getMatrix() { 471 return mMatrix; 472 } 473 474 /** 475 * Updates the matrix based on the provided parameters 476 */ updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData, int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation)477 public void updateThumbnailMatrix(Rect thumbnailPosition, ThumbnailData thumbnailData, 478 int canvasWidth, int canvasHeight, DeviceProfile dp, int currentRotation) { 479 boolean isRotated = false; 480 boolean isOrientationDifferent; 481 mClipBottom = -1; 482 483 float scale = thumbnailData.scale; 484 Rect activityInsets = dp.getInsets(); 485 Rect thumbnailInsets = getBoundedInsets(activityInsets, thumbnailData.insets); 486 final float thumbnailWidth = thumbnailPosition.width() 487 - (thumbnailInsets.left + thumbnailInsets.right) * scale; 488 final float thumbnailHeight = thumbnailPosition.height() 489 - (thumbnailInsets.top + thumbnailInsets.bottom) * scale; 490 491 final float thumbnailScale; 492 int thumbnailRotation = thumbnailData.rotation; 493 int deltaRotate = getRotationDelta(currentRotation, thumbnailRotation); 494 495 // Landscape vs portrait change 496 boolean windowingModeSupportsRotation = !dp.isMultiWindowMode 497 && thumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; 498 isOrientationDifferent = isOrientationChange(deltaRotate) 499 && windowingModeSupportsRotation; 500 if (canvasWidth == 0) { 501 // If we haven't measured , skip the thumbnail drawing and only draw the background 502 // color 503 thumbnailScale = 0f; 504 } else { 505 // Rotate the screenshot if not in multi-window mode 506 isRotated = deltaRotate > 0 && windowingModeSupportsRotation; 507 // Scale the screenshot to always fit the width of the card. 508 thumbnailScale = isOrientationDifferent 509 ? canvasWidth / thumbnailHeight 510 : canvasWidth / thumbnailWidth; 511 } 512 513 Rect splitScreenInsets = dp.getInsets(); 514 if (!isRotated) { 515 // No Rotation 516 if (dp.isMultiWindowMode) { 517 mClippedInsets.offsetTo(splitScreenInsets.left * scale, 518 splitScreenInsets.top * scale); 519 } else { 520 mClippedInsets.offsetTo(thumbnailInsets.left * scale, 521 thumbnailInsets.top * scale); 522 } 523 mMatrix.setTranslate( 524 -thumbnailInsets.left * scale, 525 -thumbnailInsets.top * scale); 526 } else { 527 setThumbnailRotation(deltaRotate, thumbnailInsets, scale, thumbnailPosition); 528 } 529 530 final float widthWithInsets; 531 final float heightWithInsets; 532 if (isOrientationDifferent) { 533 widthWithInsets = thumbnailPosition.height() * thumbnailScale; 534 heightWithInsets = thumbnailPosition.width() * thumbnailScale; 535 } else { 536 widthWithInsets = thumbnailPosition.width() * thumbnailScale; 537 heightWithInsets = thumbnailPosition.height() * thumbnailScale; 538 } 539 mClippedInsets.left *= thumbnailScale; 540 mClippedInsets.top *= thumbnailScale; 541 542 if (dp.isMultiWindowMode) { 543 mClippedInsets.right = splitScreenInsets.right * scale * thumbnailScale; 544 mClippedInsets.bottom = splitScreenInsets.bottom * scale * thumbnailScale; 545 } else { 546 mClippedInsets.right = Math.max(0, 547 widthWithInsets - mClippedInsets.left - canvasWidth); 548 mClippedInsets.bottom = Math.max(0, 549 heightWithInsets - mClippedInsets.top - canvasHeight); 550 } 551 552 mMatrix.postScale(thumbnailScale, thumbnailScale); 553 554 float bitmapHeight = Math.max(0, 555 (isOrientationDifferent ? thumbnailWidth : thumbnailHeight) * thumbnailScale); 556 if (Math.round(bitmapHeight) < canvasHeight) { 557 mClipBottom = bitmapHeight; 558 } 559 mIsOrientationChanged = isOrientationDifferent; 560 } 561 getBoundedInsets(Rect activityInsets, Rect insets)562 private Rect getBoundedInsets(Rect activityInsets, Rect insets) { 563 return new Rect(Math.min(insets.left, activityInsets.left), 564 Math.min(insets.top, activityInsets.top), 565 Math.min(insets.right, activityInsets.right), 566 Math.min(insets.bottom, activityInsets.bottom)); 567 } 568 getRotationDelta(int oldRotation, int newRotation)569 private int getRotationDelta(int oldRotation, int newRotation) { 570 int delta = newRotation - oldRotation; 571 if (delta < 0) delta += 4; 572 return delta; 573 } 574 575 /** 576 * @param deltaRotation the number of 90 degree turns from the current orientation 577 * @return {@code true} if the change in rotation results in a shift from landscape to 578 * portrait or vice versa, {@code false} otherwise 579 */ isOrientationChange(int deltaRotation)580 private boolean isOrientationChange(int deltaRotation) { 581 return deltaRotation == Surface.ROTATION_90 || deltaRotation == Surface.ROTATION_270; 582 } 583 setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale, Rect thumbnailPosition)584 private void setThumbnailRotation(int deltaRotate, Rect thumbnailInsets, float scale, 585 Rect thumbnailPosition) { 586 int newLeftInset = 0; 587 int newTopInset = 0; 588 int translateX = 0; 589 int translateY = 0; 590 591 mMatrix.setRotate(90 * deltaRotate); 592 switch (deltaRotate) { /* Counter-clockwise */ 593 case Surface.ROTATION_90: 594 newLeftInset = thumbnailInsets.bottom; 595 newTopInset = thumbnailInsets.left; 596 translateX = thumbnailPosition.height(); 597 break; 598 case Surface.ROTATION_270: 599 newLeftInset = thumbnailInsets.top; 600 newTopInset = thumbnailInsets.right; 601 translateY = thumbnailPosition.width(); 602 break; 603 case Surface.ROTATION_180: 604 newLeftInset = -thumbnailInsets.top; 605 newTopInset = -thumbnailInsets.left; 606 translateX = thumbnailPosition.width(); 607 translateY = thumbnailPosition.height(); 608 break; 609 } 610 mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale); 611 mMatrix.postTranslate(translateX - mClippedInsets.left, 612 translateY - mClippedInsets.top); 613 } 614 615 /** 616 * Insets to used for clipping the thumbnail (in case it is drawing outside its own space) 617 */ getInsetsToDrawInFullscreen()618 public RectF getInsetsToDrawInFullscreen() { 619 return mClippedInsets; 620 } 621 } 622 } 623