• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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