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.content.res.Configuration; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapShader; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.ColorFilter; 29 import android.graphics.ColorMatrix; 30 import android.graphics.ColorMatrixColorFilter; 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.util.AttributeSet; 39 import android.util.FloatProperty; 40 import android.util.Property; 41 import android.view.View; 42 import android.view.ViewGroup; 43 44 import com.android.launcher3.BaseActivity; 45 import com.android.launcher3.DeviceProfile; 46 import com.android.launcher3.R; 47 import com.android.launcher3.Utilities; 48 import com.android.launcher3.config.FeatureFlags; 49 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 50 import com.android.launcher3.util.SystemUiController; 51 import com.android.launcher3.util.Themes; 52 import com.android.quickstep.TaskOverlayFactory; 53 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 54 import com.android.quickstep.util.TaskCornerRadius; 55 import com.android.systemui.plugins.OverviewScreenshotActions; 56 import com.android.systemui.plugins.PluginListener; 57 import com.android.systemui.shared.recents.model.Task; 58 import com.android.systemui.shared.recents.model.ThumbnailData; 59 60 /** 61 * A task in the Recents view. 62 */ 63 public class TaskThumbnailView extends View implements PluginListener<OverviewScreenshotActions> { 64 65 private final static ColorMatrix COLOR_MATRIX = new ColorMatrix(); 66 private final static ColorMatrix SATURATION_COLOR_MATRIX = new ColorMatrix(); 67 private final static RectF EMPTY_RECT_F = new RectF(); 68 69 public static final Property<TaskThumbnailView, Float> DIM_ALPHA = 70 new FloatProperty<TaskThumbnailView>("dimAlpha") { 71 @Override 72 public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { 73 thumbnail.setDimAlpha(dimAlpha); 74 } 75 76 @Override 77 public Float get(TaskThumbnailView thumbnailView) { 78 return thumbnailView.mDimAlpha; 79 } 80 }; 81 82 private final BaseActivity mActivity; 83 private final TaskOverlay mOverlay; 84 private final boolean mIsDarkTextTheme; 85 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 86 private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 87 private final Paint mClearPaint = new Paint(); 88 private final Paint mDimmingPaintAfterClearing = new Paint(); 89 90 private final Matrix mMatrix = new Matrix(); 91 92 private float mClipBottom = -1; 93 // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. 94 private RectF mClippedInsets = new RectF(); 95 private TaskView.FullscreenDrawParams mFullscreenParams; 96 97 private Task mTask; 98 private ThumbnailData mThumbnailData; 99 protected BitmapShader mBitmapShader; 100 101 private float mDimAlpha = 1f; 102 private float mDimAlphaMultiplier = 1f; 103 private float mSaturation = 1f; 104 105 private boolean mOverlayEnabled; 106 private boolean mRotated; 107 private OverviewScreenshotActions mOverviewScreenshotActionsPlugin; 108 TaskThumbnailView(Context context)109 public TaskThumbnailView(Context context) { 110 this(context, null); 111 } 112 TaskThumbnailView(Context context, AttributeSet attrs)113 public TaskThumbnailView(Context context, AttributeSet attrs) { 114 this(context, attrs, 0); 115 } 116 TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr)117 public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { 118 super(context, attrs, defStyleAttr); 119 mOverlay = TaskOverlayFactory.INSTANCE.get(context).createOverlay(this); 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 mFullscreenParams = new TaskView.FullscreenDrawParams(TaskCornerRadius.get(context)); 127 } 128 bind(Task task)129 public void bind(Task task) { 130 mOverlay.reset(); 131 mTask = task; 132 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 133 mPaint.setColor(color); 134 mBackgroundPaint.setColor(color); 135 } 136 137 /** 138 * Updates this thumbnail. 139 */ setThumbnail(Task task, ThumbnailData thumbnailData)140 public void setThumbnail(Task task, ThumbnailData thumbnailData) { 141 mTask = task; 142 if (thumbnailData != null && thumbnailData.thumbnail != null) { 143 Bitmap bm = thumbnailData.thumbnail; 144 bm.prepareToDraw(); 145 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 146 mPaint.setShader(mBitmapShader); 147 mThumbnailData = thumbnailData; 148 updateThumbnailMatrix(); 149 } else { 150 mBitmapShader = null; 151 mThumbnailData = null; 152 mPaint.setShader(null); 153 mOverlay.reset(); 154 } 155 156 if (mOverviewScreenshotActionsPlugin != null) { 157 mOverviewScreenshotActionsPlugin 158 .setupActions((ViewGroup) getTaskView(), getThumbnail(), mActivity); 159 } 160 updateThumbnailPaintFilter(); 161 } 162 setDimAlphaMultipler(float dimAlphaMultipler)163 public void setDimAlphaMultipler(float dimAlphaMultipler) { 164 mDimAlphaMultiplier = dimAlphaMultipler; 165 setDimAlpha(mDimAlpha); 166 } 167 168 /** 169 * Sets the alpha of the dim layer on top of this view. 170 * <p> 171 * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. 172 */ setDimAlpha(float dimAlpha)173 public void setDimAlpha(float dimAlpha) { 174 mDimAlpha = dimAlpha; 175 updateThumbnailPaintFilter(); 176 } 177 setSaturation(float saturation)178 public void setSaturation(float saturation) { 179 mSaturation = saturation; 180 updateThumbnailPaintFilter(); 181 } 182 getDimAlpha()183 public float getDimAlpha() { 184 return mDimAlpha; 185 } 186 getInsets(Rect fallback)187 public Rect getInsets(Rect fallback) { 188 if (mThumbnailData != null) { 189 return mThumbnailData.insets; 190 } 191 return fallback; 192 } 193 getSysUiStatusNavFlags()194 public int getSysUiStatusNavFlags() { 195 if (mThumbnailData != null) { 196 int flags = 0; 197 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 198 ? SystemUiController.FLAG_LIGHT_STATUS 199 : SystemUiController.FLAG_DARK_STATUS; 200 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 201 ? SystemUiController.FLAG_LIGHT_NAV 202 : SystemUiController.FLAG_DARK_NAV; 203 return flags; 204 } 205 return 0; 206 } 207 208 @Override onDraw(Canvas canvas)209 protected void onDraw(Canvas canvas) { 210 RectF currentDrawnInsets = mFullscreenParams.mCurrentDrawnInsets; 211 canvas.save(); 212 canvas.translate(currentDrawnInsets.left, currentDrawnInsets.top); 213 canvas.scale(mFullscreenParams.mScale, mFullscreenParams.mScale); 214 // Draw the insets if we're being drawn fullscreen (we do this for quick switch). 215 drawOnCanvas(canvas, 216 -currentDrawnInsets.left, 217 -currentDrawnInsets.top, 218 getMeasuredWidth() + currentDrawnInsets.right, 219 getMeasuredHeight() + currentDrawnInsets.bottom, 220 mFullscreenParams.mCurrentDrawnCornerRadius); 221 canvas.restore(); 222 } 223 224 @Override onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, Context context)225 public void onPluginConnected(OverviewScreenshotActions overviewScreenshotActions, 226 Context context) { 227 mOverviewScreenshotActionsPlugin = overviewScreenshotActions; 228 mOverviewScreenshotActionsPlugin.setupActions(getTaskView(), getThumbnail(), mActivity); 229 } 230 231 @Override onPluginDisconnected(OverviewScreenshotActions plugin)232 public void onPluginDisconnected(OverviewScreenshotActions plugin) { 233 if (mOverviewScreenshotActionsPlugin != null) { 234 mOverviewScreenshotActionsPlugin = null; 235 } 236 } 237 238 @Override onAttachedToWindow()239 protected void onAttachedToWindow() { 240 super.onAttachedToWindow(); 241 PluginManagerWrapper.INSTANCE.get(getContext()) 242 .addPluginListener(this, OverviewScreenshotActions.class); 243 } 244 245 @Override onDetachedFromWindow()246 protected void onDetachedFromWindow() { 247 super.onDetachedFromWindow(); 248 PluginManagerWrapper.INSTANCE.get(getContext()).removePluginListener(this); 249 } 250 getInsetsToDrawInFullscreen(boolean isMultiWindowMode)251 public RectF getInsetsToDrawInFullscreen(boolean isMultiWindowMode) { 252 // Don't show insets in multi window mode. 253 return isMultiWindowMode ? EMPTY_RECT_F : mClippedInsets; 254 } 255 setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams)256 public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { 257 mFullscreenParams = fullscreenParams; 258 invalidate(); 259 } 260 drawOnCanvas(Canvas canvas, float x, float y, float width, float height, float cornerRadius)261 public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, 262 float cornerRadius) { 263 if (ENABLE_QUICKSTEP_LIVE_TILE.get()) { 264 if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) { 265 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); 266 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, 267 mDimmingPaintAfterClearing); 268 return; 269 } 270 } 271 272 // Draw the background in all cases, except when the thumbnail data is opaque 273 final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null 274 || mThumbnailData == null; 275 if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { 276 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); 277 if (drawBackgroundOnly) { 278 return; 279 } 280 } 281 282 if (mClipBottom > 0) { 283 canvas.save(); 284 canvas.clipRect(x, y, width, mClipBottom); 285 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 286 canvas.restore(); 287 } else { 288 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 289 } 290 } 291 getTaskView()292 public TaskView getTaskView() { 293 return (TaskView) getParent(); 294 } 295 setOverlayEnabled(boolean overlayEnabled)296 public void setOverlayEnabled(boolean overlayEnabled) { 297 if (mOverlayEnabled != overlayEnabled) { 298 mOverlayEnabled = overlayEnabled; 299 updateOverlay(); 300 } 301 } 302 updateOverlay()303 private void updateOverlay() { 304 // The overlay doesn't really work when the screenshot is rotated, so don't add it. 305 if (mOverlayEnabled && !mRotated && mBitmapShader != null && mThumbnailData != null) { 306 mOverlay.initOverlay(mTask, mThumbnailData, mMatrix); 307 } else { 308 mOverlay.reset(); 309 } 310 } 311 updateThumbnailPaintFilter()312 private void updateThumbnailPaintFilter() { 313 int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); 314 ColorFilter filter = getColorFilter(mul, mIsDarkTextTheme, mSaturation); 315 mBackgroundPaint.setColorFilter(filter); 316 mDimmingPaintAfterClearing.setAlpha(255 - mul); 317 if (mBitmapShader != null) { 318 mPaint.setColorFilter(filter); 319 } else { 320 mPaint.setColorFilter(null); 321 mPaint.setColor(Color.argb(255, mul, mul, mul)); 322 } 323 invalidate(); 324 } 325 updateThumbnailMatrix()326 private void updateThumbnailMatrix() { 327 boolean isRotated = false; 328 mClipBottom = -1; 329 if (mBitmapShader != null && mThumbnailData != null) { 330 float scale = mThumbnailData.scale; 331 Rect thumbnailInsets = mThumbnailData.insets; 332 final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - 333 (thumbnailInsets.left + thumbnailInsets.right) * scale; 334 final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - 335 (thumbnailInsets.top + thumbnailInsets.bottom) * scale; 336 337 final float thumbnailScale; 338 final DeviceProfile profile = mActivity.getDeviceProfile(); 339 340 if (getMeasuredWidth() == 0) { 341 // If we haven't measured , skip the thumbnail drawing and only draw the background 342 // color 343 thumbnailScale = 0f; 344 } else { 345 final Configuration configuration = 346 getContext().getResources().getConfiguration(); 347 // Rotate the screenshot if not in multi-window mode 348 isRotated = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && 349 configuration.orientation != mThumbnailData.orientation && 350 !mActivity.isInMultiWindowMode() && 351 mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; 352 // Scale the screenshot to always fit the width of the card. 353 thumbnailScale = isRotated 354 ? getMeasuredWidth() / thumbnailHeight 355 : getMeasuredWidth() / thumbnailWidth; 356 } 357 358 if (isRotated) { 359 int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; 360 mMatrix.setRotate(90 * rotationDir); 361 int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top; 362 int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right; 363 mClippedInsets.offsetTo(newLeftInset * scale, newTopInset * scale); 364 if (rotationDir == -1) { 365 // Crop the right/bottom side of the screenshot rather than left/top 366 float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight(); 367 mClippedInsets.offset(0, excessHeight); 368 } 369 mMatrix.postTranslate(-mClippedInsets.left, -mClippedInsets.top); 370 // Move the screenshot to the thumbnail window (rotation moved it out). 371 if (rotationDir == 1) { 372 mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0); 373 } else { 374 mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth()); 375 } 376 } else { 377 mClippedInsets.offsetTo(thumbnailInsets.left * scale, thumbnailInsets.top * scale); 378 mMatrix.setTranslate(-mClippedInsets.left, -mClippedInsets.top); 379 } 380 381 final float widthWithInsets; 382 final float heightWithInsets; 383 if (isRotated) { 384 widthWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale; 385 heightWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale; 386 } else { 387 widthWithInsets = mThumbnailData.thumbnail.getWidth() * thumbnailScale; 388 heightWithInsets = mThumbnailData.thumbnail.getHeight() * thumbnailScale; 389 } 390 mClippedInsets.left *= thumbnailScale; 391 mClippedInsets.top *= thumbnailScale; 392 mClippedInsets.right = widthWithInsets - mClippedInsets.left - getMeasuredWidth(); 393 mClippedInsets.bottom = heightWithInsets - mClippedInsets.top - getMeasuredHeight(); 394 395 mMatrix.postScale(thumbnailScale, thumbnailScale); 396 mBitmapShader.setLocalMatrix(mMatrix); 397 398 float bitmapHeight = Math.max((isRotated ? thumbnailWidth : thumbnailHeight) 399 * thumbnailScale, 0); 400 if (Math.round(bitmapHeight) < getMeasuredHeight()) { 401 mClipBottom = bitmapHeight; 402 } 403 mPaint.setShader(mBitmapShader); 404 } 405 406 mRotated = isRotated; 407 invalidate(); 408 409 // Update can be called from {@link #onSizeChanged} during layout, post handling of overlay 410 // as overlay could modify the views in the overlay as a side effect of its update. 411 post(this::updateOverlay); 412 } 413 414 @Override onSizeChanged(int w, int h, int oldw, int oldh)415 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 416 super.onSizeChanged(w, h, oldw, oldh); 417 updateThumbnailMatrix(); 418 } 419 420 /** 421 * @param intensity multiplier for color values. 0 - make black (white if shouldLighten), 255 - 422 * leave unchanged. 423 */ getColorFilter(int intensity, boolean shouldLighten, float saturation)424 private static ColorFilter getColorFilter(int intensity, boolean shouldLighten, 425 float saturation) { 426 intensity = Utilities.boundToRange(intensity, 0, 255); 427 428 if (intensity == 255 && saturation == 1) { 429 return null; 430 } 431 432 final float intensityScale = intensity / 255f; 433 COLOR_MATRIX.setScale(intensityScale, intensityScale, intensityScale, 1); 434 435 if (saturation != 1) { 436 SATURATION_COLOR_MATRIX.setSaturation(saturation); 437 COLOR_MATRIX.postConcat(SATURATION_COLOR_MATRIX); 438 } 439 440 if (shouldLighten) { 441 final float[] colorArray = COLOR_MATRIX.getArray(); 442 final int colorAdd = 255 - intensity; 443 colorArray[4] = colorAdd; 444 colorArray[9] = colorAdd; 445 colorArray[14] = colorAdd; 446 } 447 448 return new ColorMatrixColorFilter(COLOR_MATRIX); 449 } 450 getThumbnail()451 public Bitmap getThumbnail() { 452 if (mThumbnailData == null) { 453 return null; 454 } 455 return mThumbnailData.thumbnail; 456 } 457 } 458