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