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.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_FULLSCREEN; 20 21 import android.content.Context; 22 import android.content.res.Configuration; 23 import android.graphics.Bitmap; 24 import android.graphics.BitmapShader; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.LightingColorFilter; 28 import android.graphics.Matrix; 29 import android.graphics.Paint; 30 import android.graphics.Rect; 31 import android.graphics.Shader; 32 import android.support.v4.graphics.ColorUtils; 33 import android.util.AttributeSet; 34 import android.util.FloatProperty; 35 import android.util.Property; 36 import android.view.View; 37 38 import com.android.launcher3.BaseActivity; 39 import com.android.launcher3.DeviceProfile; 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.launcher3.config.FeatureFlags; 43 import com.android.launcher3.util.SystemUiController; 44 import com.android.launcher3.util.Themes; 45 import com.android.quickstep.TaskOverlayFactory; 46 import com.android.quickstep.TaskOverlayFactory.TaskOverlay; 47 import com.android.systemui.shared.recents.model.Task; 48 import com.android.systemui.shared.recents.model.ThumbnailData; 49 50 /** 51 * A task in the Recents view. 52 */ 53 public class TaskThumbnailView extends View { 54 55 private static final LightingColorFilter[] sDimFilterCache = new LightingColorFilter[256]; 56 private static final LightingColorFilter[] sHighlightFilterCache = new LightingColorFilter[256]; 57 58 public static final Property<TaskThumbnailView, Float> DIM_ALPHA_MULTIPLIER = 59 new FloatProperty<TaskThumbnailView>("dimAlphaMultiplier") { 60 @Override 61 public void setValue(TaskThumbnailView thumbnail, float dimAlphaMultiplier) { 62 thumbnail.setDimAlphaMultipler(dimAlphaMultiplier); 63 } 64 65 @Override 66 public Float get(TaskThumbnailView thumbnailView) { 67 return thumbnailView.mDimAlphaMultiplier; 68 } 69 }; 70 71 private final float mCornerRadius; 72 73 private final BaseActivity mActivity; 74 private final TaskOverlay mOverlay; 75 private final boolean mIsDarkTextTheme; 76 private final Paint mPaint = new Paint(); 77 private final Paint mBackgroundPaint = new Paint(); 78 79 private final Matrix mMatrix = new Matrix(); 80 81 private float mClipBottom = -1; 82 83 private Task mTask; 84 private ThumbnailData mThumbnailData; 85 protected BitmapShader mBitmapShader; 86 87 private float mDimAlpha = 1f; 88 private float mDimAlphaMultiplier = 1f; 89 90 public TaskThumbnailView(Context context) { 91 this(context, null); 92 } 93 94 public TaskThumbnailView(Context context, AttributeSet attrs) { 95 this(context, attrs, 0); 96 } 97 98 public TaskThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { 99 super(context, attrs, defStyleAttr); 100 mCornerRadius = getResources().getDimension(R.dimen.task_corner_radius); 101 mOverlay = TaskOverlayFactory.get(context).createOverlay(this); 102 mPaint.setFilterBitmap(true); 103 mBackgroundPaint.setColor(Color.WHITE); 104 mActivity = BaseActivity.fromContext(context); 105 mIsDarkTextTheme = Themes.getAttrBoolean(mActivity, R.attr.isWorkspaceDarkText); 106 } 107 108 public void bind() { 109 mOverlay.reset(); 110 } 111 112 /** 113 * Updates this thumbnail. 114 */ 115 public void setThumbnail(Task task, ThumbnailData thumbnailData) { 116 mTask = task; 117 int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; 118 mPaint.setColor(color); 119 mBackgroundPaint.setColor(color); 120 121 if (thumbnailData != null && thumbnailData.thumbnail != null) { 122 Bitmap bm = thumbnailData.thumbnail; 123 bm.prepareToDraw(); 124 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 125 mPaint.setShader(mBitmapShader); 126 mThumbnailData = thumbnailData; 127 updateThumbnailMatrix(); 128 } else { 129 mBitmapShader = null; 130 mThumbnailData = null; 131 mPaint.setShader(null); 132 mOverlay.reset(); 133 } 134 updateThumbnailPaintFilter(); 135 } 136 137 public void setDimAlphaMultipler(float dimAlphaMultipler) { 138 mDimAlphaMultiplier = dimAlphaMultipler; 139 setDimAlpha(mDimAlpha); 140 } 141 142 /** 143 * Sets the alpha of the dim layer on top of this view. 144 * 145 * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be black. 146 */ 147 public void setDimAlpha(float dimAlpha) { 148 mDimAlpha = dimAlpha; 149 updateThumbnailPaintFilter(); 150 } 151 152 public Rect getInsets() { 153 if (mThumbnailData != null) { 154 return mThumbnailData.insets; 155 } 156 return new Rect(); 157 } 158 159 public int getSysUiStatusNavFlags() { 160 if (mThumbnailData != null) { 161 int flags = 0; 162 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_STATUS_BAR) != 0 163 ? SystemUiController.FLAG_LIGHT_STATUS 164 : SystemUiController.FLAG_DARK_STATUS; 165 flags |= (mThumbnailData.systemUiVisibility & SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR) != 0 166 ? SystemUiController.FLAG_LIGHT_NAV 167 : SystemUiController.FLAG_DARK_NAV; 168 return flags; 169 } 170 return 0; 171 } 172 173 @Override 174 protected void onDraw(Canvas canvas) { 175 drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), mCornerRadius); 176 } 177 178 public float getCornerRadius() { 179 return mCornerRadius; 180 } 181 182 public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, 183 float cornerRadius) { 184 // Draw the background in all cases, except when the thumbnail data is opaque 185 final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null 186 || mThumbnailData == null; 187 if (drawBackgroundOnly || mClipBottom > 0 || mThumbnailData.isTranslucent) { 188 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mBackgroundPaint); 189 if (drawBackgroundOnly) { 190 return; 191 } 192 } 193 194 if (mClipBottom > 0) { 195 canvas.save(); 196 canvas.clipRect(x, y, width, mClipBottom); 197 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 198 canvas.restore(); 199 } else { 200 canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); 201 } 202 } 203 204 private void updateThumbnailPaintFilter() { 205 int mul = (int) ((1 - mDimAlpha * mDimAlphaMultiplier) * 255); 206 if (mBitmapShader != null) { 207 LightingColorFilter filter = getDimmingColorFilter(mul, mIsDarkTextTheme); 208 mPaint.setColorFilter(filter); 209 mBackgroundPaint.setColorFilter(filter); 210 } else { 211 mPaint.setColorFilter(null); 212 mPaint.setColor(Color.argb(255, mul, mul, mul)); 213 } 214 invalidate(); 215 } 216 217 private void updateThumbnailMatrix() { 218 boolean rotate = false; 219 mClipBottom = -1; 220 if (mBitmapShader != null && mThumbnailData != null) { 221 float scale = mThumbnailData.scale; 222 Rect thumbnailInsets = mThumbnailData.insets; 223 final float thumbnailWidth = mThumbnailData.thumbnail.getWidth() - 224 (thumbnailInsets.left + thumbnailInsets.right) * scale; 225 final float thumbnailHeight = mThumbnailData.thumbnail.getHeight() - 226 (thumbnailInsets.top + thumbnailInsets.bottom) * scale; 227 228 final float thumbnailScale; 229 final DeviceProfile profile = mActivity.getDeviceProfile(); 230 231 if (getMeasuredWidth() == 0) { 232 // If we haven't measured , skip the thumbnail drawing and only draw the background 233 // color 234 thumbnailScale = 0f; 235 } else { 236 final Configuration configuration = 237 getContext().getResources().getConfiguration(); 238 // Rotate the screenshot if not in multi-window mode 239 rotate = FeatureFlags.OVERVIEW_USE_SCREENSHOT_ORIENTATION && 240 configuration.orientation != mThumbnailData.orientation && 241 !mActivity.isInMultiWindowModeCompat() && 242 mThumbnailData.windowingMode == WINDOWING_MODE_FULLSCREEN; 243 // Scale the screenshot to always fit the width of the card. 244 thumbnailScale = rotate 245 ? getMeasuredWidth() / thumbnailHeight 246 : getMeasuredWidth() / thumbnailWidth; 247 } 248 249 if (rotate) { 250 int rotationDir = profile.isVerticalBarLayout() && !profile.isSeascape() ? -1 : 1; 251 mMatrix.setRotate(90 * rotationDir); 252 int newLeftInset = rotationDir == 1 ? thumbnailInsets.bottom : thumbnailInsets.top; 253 int newTopInset = rotationDir == 1 ? thumbnailInsets.left : thumbnailInsets.right; 254 mMatrix.postTranslate(-newLeftInset * scale, -newTopInset * scale); 255 if (rotationDir == -1) { 256 // Crop the right/bottom side of the screenshot rather than left/top 257 float excessHeight = thumbnailWidth * thumbnailScale - getMeasuredHeight(); 258 mMatrix.postTranslate(0, -excessHeight); 259 } 260 // Move the screenshot to the thumbnail window (rotation moved it out). 261 if (rotationDir == 1) { 262 mMatrix.postTranslate(mThumbnailData.thumbnail.getHeight(), 0); 263 } else { 264 mMatrix.postTranslate(0, mThumbnailData.thumbnail.getWidth()); 265 } 266 } else { 267 mMatrix.setTranslate(-mThumbnailData.insets.left * scale, 268 -mThumbnailData.insets.top * scale); 269 } 270 mMatrix.postScale(thumbnailScale, thumbnailScale); 271 mBitmapShader.setLocalMatrix(mMatrix); 272 273 float bitmapHeight = Math.max((rotate ? thumbnailWidth : thumbnailHeight) 274 * thumbnailScale, 0); 275 if (Math.round(bitmapHeight) < getMeasuredHeight()) { 276 mClipBottom = bitmapHeight; 277 } 278 mPaint.setShader(mBitmapShader); 279 } 280 281 if (rotate) { 282 // The overlay doesn't really work when the screenshot is rotated, so don't add it. 283 mOverlay.reset(); 284 } else { 285 mOverlay.setTaskInfo(mTask, mThumbnailData, mMatrix); 286 } 287 invalidate(); 288 } 289 290 @Override 291 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 292 super.onSizeChanged(w, h, oldw, oldh); 293 updateThumbnailMatrix(); 294 } 295 296 private static LightingColorFilter getDimmingColorFilter(int intensity, boolean shouldLighten) { 297 intensity = Utilities.boundToRange(intensity, 0, 255); 298 if (intensity == 255) { 299 return null; 300 } 301 if (shouldLighten) { 302 if (sHighlightFilterCache[intensity] == null) { 303 int colorAdd = 255 - intensity; 304 sHighlightFilterCache[intensity] = new LightingColorFilter( 305 Color.argb(255, intensity, intensity, intensity), 306 Color.argb(255, colorAdd, colorAdd, colorAdd)); 307 } 308 return sHighlightFilterCache[intensity]; 309 } else { 310 if (sDimFilterCache[intensity] == null) { 311 sDimFilterCache[intensity] = new LightingColorFilter( 312 Color.argb(255, intensity, intensity, intensity), 0); 313 } 314 return sDimFilterCache[intensity]; 315 } 316 } 317 } 318