1 /* 2 * Copyright (C) 2014 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.systemui.recents.views; 18 19 import android.content.Context; 20 import android.content.res.Configuration; 21 import android.content.res.Resources; 22 import android.graphics.Bitmap; 23 import android.graphics.BitmapShader; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.ColorMatrix; 27 import android.graphics.ColorMatrixColorFilter; 28 import android.graphics.LightingColorFilter; 29 import android.graphics.Matrix; 30 import android.graphics.Paint; 31 import android.graphics.Rect; 32 import android.graphics.Shader; 33 import android.util.AttributeSet; 34 import android.view.View; 35 import android.view.ViewDebug; 36 37 import com.android.systemui.R; 38 import com.android.systemui.recents.events.EventBus; 39 import com.android.systemui.recents.events.ui.TaskSnapshotChangedEvent; 40 import com.android.systemui.recents.misc.Utilities; 41 import com.android.systemui.recents.model.Task; 42 import com.android.systemui.recents.model.ThumbnailData; 43 import java.io.PrintWriter; 44 45 46 /** 47 * The task thumbnail view. It implements an image view that allows for animating the dim and 48 * alpha of the thumbnail image. 49 */ 50 public class TaskViewThumbnail extends View { 51 52 private static final ColorMatrix TMP_FILTER_COLOR_MATRIX = new ColorMatrix(); 53 private static final ColorMatrix TMP_BRIGHTNESS_COLOR_MATRIX = new ColorMatrix(); 54 55 private Task mTask; 56 57 private int mDisplayOrientation = Configuration.ORIENTATION_UNDEFINED; 58 private Rect mDisplayRect = new Rect(); 59 60 // Drawing 61 @ViewDebug.ExportedProperty(category="recents") 62 protected Rect mTaskViewRect = new Rect(); 63 @ViewDebug.ExportedProperty(category="recents") 64 protected Rect mThumbnailRect = new Rect(); 65 @ViewDebug.ExportedProperty(category="recents") 66 protected float mThumbnailScale; 67 private float mFullscreenThumbnailScale = 1f; 68 /** The height, in pixels, of the task view's title bar. */ 69 private int mTitleBarHeight; 70 private boolean mSizeToFit = false; 71 private boolean mOverlayHeaderOnThumbnailActionBar = true; 72 private ThumbnailData mThumbnailData; 73 74 protected int mCornerRadius; 75 @ViewDebug.ExportedProperty(category="recents") 76 private float mDimAlpha; 77 private Matrix mMatrix = new Matrix(); 78 private Paint mDrawPaint = new Paint(); 79 protected Paint mLockedPaint = new Paint(); 80 protected Paint mBgFillPaint = new Paint(); 81 protected BitmapShader mBitmapShader; 82 protected boolean mUserLocked = false; 83 private LightingColorFilter mLightingColorFilter = new LightingColorFilter(0xffffffff, 0); 84 85 // Clip the top of the thumbnail against the opaque header bar that overlaps this view 86 private View mTaskBar; 87 88 // Visibility optimization, if the thumbnail height is less than the height of the header 89 // bar for the task view, then just mark this thumbnail view as invisible 90 @ViewDebug.ExportedProperty(category="recents") 91 private boolean mInvisible; 92 93 @ViewDebug.ExportedProperty(category="recents") 94 private boolean mDisabledInSafeMode; 95 TaskViewThumbnail(Context context)96 public TaskViewThumbnail(Context context) { 97 this(context, null); 98 } 99 TaskViewThumbnail(Context context, AttributeSet attrs)100 public TaskViewThumbnail(Context context, AttributeSet attrs) { 101 this(context, attrs, 0); 102 } 103 TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr)104 public TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr) { 105 this(context, attrs, defStyleAttr, 0); 106 } 107 TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)108 public TaskViewThumbnail(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 109 super(context, attrs, defStyleAttr, defStyleRes); 110 mDrawPaint.setColorFilter(mLightingColorFilter); 111 mDrawPaint.setFilterBitmap(true); 112 mDrawPaint.setAntiAlias(true); 113 Resources res = getResources(); 114 mCornerRadius = res.getDimensionPixelSize(R.dimen.recents_task_view_rounded_corners_radius); 115 mBgFillPaint.setColor(Color.WHITE); 116 mLockedPaint.setColor(Color.WHITE); 117 mTitleBarHeight = res.getDimensionPixelSize(R.dimen.recents_grid_task_view_header_height); 118 } 119 120 /** 121 * Called when the task view frame changes, allowing us to move the contents of the header 122 * to match the frame changes. 123 */ onTaskViewSizeChanged(int width, int height)124 public void onTaskViewSizeChanged(int width, int height) { 125 // Return early if the bounds have not changed 126 if (mTaskViewRect.width() == width && mTaskViewRect.height() == height) { 127 return; 128 } 129 130 mTaskViewRect.set(0, 0, width, height); 131 setLeftTopRightBottom(0, 0, width, height); 132 updateThumbnailMatrix(); 133 } 134 135 @Override onDraw(Canvas canvas)136 protected void onDraw(Canvas canvas) { 137 if (mInvisible) { 138 return; 139 } 140 141 int viewWidth = mTaskViewRect.width(); 142 int viewHeight = mTaskViewRect.height(); 143 int thumbnailWidth = Math.min(viewWidth, 144 (int) (mThumbnailRect.width() * mThumbnailScale)); 145 int thumbnailHeight = Math.min(viewHeight, 146 (int) (mThumbnailRect.height() * mThumbnailScale)); 147 148 if (mUserLocked) { 149 canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius, 150 mLockedPaint); 151 } else if (mBitmapShader != null && thumbnailWidth > 0 && thumbnailHeight > 0) { 152 int topOffset = 0; 153 if (mTaskBar != null && mOverlayHeaderOnThumbnailActionBar) { 154 topOffset = mTaskBar.getHeight() - mCornerRadius; 155 } 156 157 // Draw the background, there will be some small overdraw with the thumbnail 158 if (thumbnailWidth < viewWidth) { 159 // Portrait thumbnail on a landscape task view 160 canvas.drawRoundRect(Math.max(0, thumbnailWidth - mCornerRadius), topOffset, 161 viewWidth, viewHeight, 162 mCornerRadius, mCornerRadius, mBgFillPaint); 163 } 164 if (thumbnailHeight < viewHeight) { 165 // Landscape thumbnail on a portrait task view 166 canvas.drawRoundRect(0, Math.max(topOffset, thumbnailHeight - mCornerRadius), 167 viewWidth, viewHeight, 168 mCornerRadius, mCornerRadius, mBgFillPaint); 169 } 170 171 // Draw the thumbnail 172 canvas.drawRoundRect(0, topOffset, thumbnailWidth, thumbnailHeight, 173 mCornerRadius, mCornerRadius, mDrawPaint); 174 } else { 175 canvas.drawRoundRect(0, 0, viewWidth, viewHeight, mCornerRadius, mCornerRadius, 176 mBgFillPaint); 177 } 178 } 179 180 /** Sets the thumbnail to a given bitmap. */ setThumbnail(ThumbnailData thumbnailData)181 void setThumbnail(ThumbnailData thumbnailData) { 182 if (thumbnailData != null && thumbnailData.thumbnail != null) { 183 Bitmap bm = thumbnailData.thumbnail; 184 bm.prepareToDraw(); 185 mFullscreenThumbnailScale = thumbnailData.scale; 186 mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); 187 mDrawPaint.setShader(mBitmapShader); 188 mThumbnailRect.set(0, 0, 189 bm.getWidth() - thumbnailData.insets.left - thumbnailData.insets.right, 190 bm.getHeight() - thumbnailData.insets.top - thumbnailData.insets.bottom); 191 mThumbnailData = thumbnailData; 192 updateThumbnailMatrix(); 193 updateThumbnailPaintFilter(); 194 } else { 195 mBitmapShader = null; 196 mDrawPaint.setShader(null); 197 mThumbnailRect.setEmpty(); 198 mThumbnailData = null; 199 } 200 } 201 202 /** Updates the paint to draw the thumbnail. */ updateThumbnailPaintFilter()203 void updateThumbnailPaintFilter() { 204 if (mInvisible) { 205 return; 206 } 207 int mul = (int) ((1.0f - mDimAlpha) * 255); 208 if (mBitmapShader != null) { 209 if (mDisabledInSafeMode) { 210 // Brightness: C-new = C-old*(1-amount) + amount 211 TMP_FILTER_COLOR_MATRIX.setSaturation(0); 212 float scale = 1f - mDimAlpha; 213 float[] mat = TMP_BRIGHTNESS_COLOR_MATRIX.getArray(); 214 mat[0] = scale; 215 mat[6] = scale; 216 mat[12] = scale; 217 mat[4] = mDimAlpha * 255f; 218 mat[9] = mDimAlpha * 255f; 219 mat[14] = mDimAlpha * 255f; 220 TMP_FILTER_COLOR_MATRIX.preConcat(TMP_BRIGHTNESS_COLOR_MATRIX); 221 ColorMatrixColorFilter filter = new ColorMatrixColorFilter(TMP_FILTER_COLOR_MATRIX); 222 mDrawPaint.setColorFilter(filter); 223 mBgFillPaint.setColorFilter(filter); 224 mLockedPaint.setColorFilter(filter); 225 } else { 226 mLightingColorFilter.setColorMultiply(Color.argb(255, mul, mul, mul)); 227 mDrawPaint.setColorFilter(mLightingColorFilter); 228 mDrawPaint.setColor(0xFFffffff); 229 mBgFillPaint.setColorFilter(mLightingColorFilter); 230 mLockedPaint.setColorFilter(mLightingColorFilter); 231 } 232 } else { 233 int grey = mul; 234 mDrawPaint.setColorFilter(null); 235 mDrawPaint.setColor(Color.argb(255, grey, grey, grey)); 236 } 237 if (!mInvisible) { 238 invalidate(); 239 } 240 } 241 242 /** 243 * Updates the scale of the bitmap relative to this view. 244 */ updateThumbnailMatrix()245 public void updateThumbnailMatrix() { 246 mThumbnailScale = 1f; 247 if (mBitmapShader != null && mThumbnailData != null) { 248 // We consider this a stack task if it is not freeform (ie. has no bounds) or has been 249 // dragged into the stack from the freeform workspace 250 boolean isStackTask = !mTask.isFreeformTask() || mTask.bounds == null; 251 int xOffset, yOffset = 0; 252 if (mTaskViewRect.isEmpty()) { 253 // If we haven't measured , skip the thumbnail drawing and only draw the background 254 // color 255 mThumbnailScale = 0f; 256 } else if (mSizeToFit) { 257 // Make sure we fill the entire space regardless of the orientation. 258 float viewAspectRatio = (float) mTaskViewRect.width() / 259 (float) (mTaskViewRect.height() - mTitleBarHeight); 260 float thumbnailAspectRatio = 261 (float) mThumbnailRect.width() / (float) mThumbnailRect.height(); 262 if (viewAspectRatio > thumbnailAspectRatio) { 263 mThumbnailScale = 264 (float) mTaskViewRect.width() / (float) mThumbnailRect.width(); 265 } else { 266 mThumbnailScale = (float) (mTaskViewRect.height() - mTitleBarHeight) 267 / (float) mThumbnailRect.height(); 268 } 269 } else if (isStackTask) { 270 float invThumbnailScale = 1f / mFullscreenThumbnailScale; 271 if (mDisplayOrientation == Configuration.ORIENTATION_PORTRAIT) { 272 if (mThumbnailData.orientation == Configuration.ORIENTATION_PORTRAIT) { 273 // If we are in the same orientation as the screenshot, just scale it to the 274 // width of the task view 275 mThumbnailScale = (float) mTaskViewRect.width() / mThumbnailRect.width(); 276 } else { 277 // Scale the landscape thumbnail up to app size, then scale that to the task 278 // view size to match other portrait screenshots 279 mThumbnailScale = invThumbnailScale * 280 ((float) mTaskViewRect.width() / mDisplayRect.width()); 281 } 282 } else { 283 // Otherwise, scale the screenshot to fit 1:1 in the current orientation 284 mThumbnailScale = invThumbnailScale; 285 } 286 } else { 287 // Otherwise, if this is a freeform task with task bounds, then scale the thumbnail 288 // to fit the entire bitmap into the task bounds 289 mThumbnailScale = Math.min( 290 (float) mTaskViewRect.width() / mThumbnailRect.width(), 291 (float) mTaskViewRect.height() / mThumbnailRect.height()); 292 } 293 mMatrix.setTranslate(-mThumbnailData.insets.left * mFullscreenThumbnailScale, 294 -mThumbnailData.insets.top * mFullscreenThumbnailScale); 295 mMatrix.postScale(mThumbnailScale, mThumbnailScale); 296 mBitmapShader.setLocalMatrix(mMatrix); 297 } 298 if (!mInvisible) { 299 invalidate(); 300 } 301 } 302 303 /** Sets whether the thumbnail should be resized to fit the task view in all orientations. */ setSizeToFit(boolean flag)304 public void setSizeToFit(boolean flag) { 305 mSizeToFit = flag; 306 } 307 308 /** 309 * Sets whether the header should overlap (and hide) the action bar in the thumbnail, or 310 * be stacked just above it. 311 */ setOverlayHeaderOnThumbnailActionBar(boolean flag)312 public void setOverlayHeaderOnThumbnailActionBar(boolean flag) { 313 mOverlayHeaderOnThumbnailActionBar = flag; 314 } 315 316 /** Updates the clip rect based on the given task bar. */ updateClipToTaskBar(View taskBar)317 void updateClipToTaskBar(View taskBar) { 318 mTaskBar = taskBar; 319 invalidate(); 320 } 321 322 /** Updates the visibility of the the thumbnail. */ updateThumbnailVisibility(int clipBottom)323 void updateThumbnailVisibility(int clipBottom) { 324 boolean invisible = mTaskBar != null && (getHeight() - clipBottom) <= mTaskBar.getHeight(); 325 if (invisible != mInvisible) { 326 mInvisible = invisible; 327 if (!mInvisible) { 328 updateThumbnailPaintFilter(); 329 } 330 } 331 } 332 333 /** 334 * Sets the dim alpha, only used when we are not using hardware layers. 335 * (see RecentsConfiguration.useHardwareLayers) 336 */ setDimAlpha(float dimAlpha)337 public void setDimAlpha(float dimAlpha) { 338 mDimAlpha = dimAlpha; 339 updateThumbnailPaintFilter(); 340 } 341 342 /** 343 * Returns the {@link Paint} used to draw a task screenshot, or {@link #mLockedPaint} if the 344 * thumbnail shouldn't be drawn because it belongs to a locked user. 345 */ getDrawPaint()346 protected Paint getDrawPaint() { 347 if (mUserLocked) { 348 return mLockedPaint; 349 } 350 return mDrawPaint; 351 } 352 353 /** 354 * Binds the thumbnail view to the task. 355 */ bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect)356 void bindToTask(Task t, boolean disabledInSafeMode, int displayOrientation, Rect displayRect) { 357 mTask = t; 358 mDisabledInSafeMode = disabledInSafeMode; 359 mDisplayOrientation = displayOrientation; 360 mDisplayRect.set(displayRect); 361 if (t.colorBackground != 0) { 362 mBgFillPaint.setColor(t.colorBackground); 363 } 364 if (t.colorPrimary != 0) { 365 mLockedPaint.setColor(t.colorPrimary); 366 } 367 mUserLocked = t.isLocked; 368 EventBus.getDefault().register(this); 369 } 370 371 /** 372 * Called when the bound task's data has loaded and this view should update to reflect the 373 * changes. 374 */ onTaskDataLoaded(ThumbnailData thumbnailData)375 void onTaskDataLoaded(ThumbnailData thumbnailData) { 376 setThumbnail(thumbnailData); 377 } 378 379 /** Unbinds the thumbnail view from the task */ unbindFromTask()380 void unbindFromTask() { 381 mTask = null; 382 setThumbnail(null); 383 EventBus.getDefault().unregister(this); 384 } 385 onBusEvent(TaskSnapshotChangedEvent event)386 public final void onBusEvent(TaskSnapshotChangedEvent event) { 387 if (mTask == null || event.taskId != mTask.key.id || event.thumbnailData == null 388 || event.thumbnailData.thumbnail == null) { 389 return; 390 } 391 setThumbnail(event.thumbnailData); 392 } 393 dump(String prefix, PrintWriter writer)394 public void dump(String prefix, PrintWriter writer) { 395 writer.print(prefix); writer.print("TaskViewThumbnail"); 396 writer.print(" mTaskViewRect="); writer.print(Utilities.dumpRect(mTaskViewRect)); 397 writer.print(" mThumbnailRect="); writer.print(Utilities.dumpRect(mThumbnailRect)); 398 writer.print(" mThumbnailScale="); writer.print(mThumbnailScale); 399 writer.print(" mDimAlpha="); writer.print(mDimAlpha); 400 writer.println(); 401 } 402 } 403