| /* |
| * Copyright (C) 2017 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.quickstep.views; |
| |
| import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; |
| import static android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS; |
| |
| import static com.android.systemui.shared.recents.utilities.PreviewPositionHelper.MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT; |
| import static com.android.systemui.shared.recents.utilities.Utilities.isRelativePercentDifferenceGreaterThan; |
| |
| import android.content.Context; |
| import android.graphics.Bitmap; |
| import android.graphics.BitmapShader; |
| import android.graphics.Canvas; |
| import android.graphics.Color; |
| import android.graphics.ColorFilter; |
| import android.graphics.Insets; |
| import android.graphics.Matrix; |
| import android.graphics.Paint; |
| import android.graphics.PorterDuff; |
| import android.graphics.PorterDuffXfermode; |
| import android.graphics.Rect; |
| import android.graphics.RectF; |
| import android.graphics.Shader; |
| import android.graphics.drawable.Drawable; |
| import android.os.Build; |
| import android.util.AttributeSet; |
| import android.util.FloatProperty; |
| import android.util.Property; |
| import android.view.View; |
| import android.widget.ImageView; |
| |
| import androidx.annotation.Nullable; |
| import androidx.annotation.RequiresApi; |
| import androidx.core.graphics.ColorUtils; |
| |
| import com.android.launcher3.BaseActivity; |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.Utilities; |
| import com.android.launcher3.touch.PagedOrientationHandler; |
| import com.android.launcher3.util.MainThreadInitializedObject; |
| import com.android.launcher3.util.SystemUiController; |
| import com.android.launcher3.util.SystemUiController.SystemUiControllerFlags; |
| import com.android.quickstep.TaskOverlayFactory.TaskOverlay; |
| import com.android.quickstep.views.TaskView.FullscreenDrawParams; |
| import com.android.systemui.shared.recents.model.Task; |
| import com.android.systemui.shared.recents.model.ThumbnailData; |
| import com.android.systemui.shared.recents.utilities.PreviewPositionHelper; |
| |
| /** |
| * A task in the Recents view. |
| */ |
| public class TaskThumbnailView extends View { |
| private static final MainThreadInitializedObject<FullscreenDrawParams> TEMP_PARAMS = |
| new MainThreadInitializedObject<>(FullscreenDrawParams::new); |
| |
| public static final Property<TaskThumbnailView, Float> DIM_ALPHA = |
| new FloatProperty<TaskThumbnailView>("dimAlpha") { |
| @Override |
| public void setValue(TaskThumbnailView thumbnail, float dimAlpha) { |
| thumbnail.setDimAlpha(dimAlpha); |
| } |
| |
| @Override |
| public Float get(TaskThumbnailView thumbnailView) { |
| return thumbnailView.mDimAlpha; |
| } |
| }; |
| |
| private final BaseActivity mActivity; |
| @Nullable |
| private TaskOverlay mOverlay; |
| private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint mBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint mSplashBackgroundPaint = new Paint(Paint.ANTI_ALIAS_FLAG); |
| private final Paint mClearPaint = new Paint(); |
| private final Paint mDimmingPaintAfterClearing = new Paint(); |
| private final int mDimColor; |
| |
| // Contains the portion of the thumbnail that is clipped when fullscreen progress = 0. |
| private final Rect mPreviewRect = new Rect(); |
| private final PreviewPositionHelper mPreviewPositionHelper = new PreviewPositionHelper(); |
| private TaskView.FullscreenDrawParams mFullscreenParams; |
| private ImageView mSplashView; |
| private Drawable mSplashViewDrawable; |
| |
| @Nullable |
| private Task mTask; |
| @Nullable |
| private ThumbnailData mThumbnailData; |
| @Nullable |
| protected BitmapShader mBitmapShader; |
| |
| /** How much this thumbnail is dimmed, 0 not dimmed at all, 1 totally dimmed. */ |
| private float mDimAlpha = 0f; |
| /** Controls visibility of the splash view, 0 is transparent, 255 fully opaque. */ |
| private int mSplashAlpha = 0; |
| |
| private boolean mOverlayEnabled; |
| |
| public TaskThumbnailView(Context context) { |
| this(context, null); |
| } |
| |
| public TaskThumbnailView(Context context, @Nullable AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public TaskThumbnailView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mPaint.setFilterBitmap(true); |
| mBackgroundPaint.setColor(Color.WHITE); |
| mSplashBackgroundPaint.setColor(Color.WHITE); |
| mClearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); |
| mActivity = BaseActivity.fromContext(context); |
| // Initialize with placeholder value. It is overridden later by TaskView |
| mFullscreenParams = TEMP_PARAMS.get(context); |
| |
| mDimColor = RecentsView.getForegroundScrimDimColor(context); |
| mDimmingPaintAfterClearing.setColor(mDimColor); |
| } |
| |
| /** |
| * Updates the thumbnail to draw the provided task |
| * @param task |
| */ |
| public void bind(Task task) { |
| getTaskOverlay().reset(); |
| mTask = task; |
| int color = task == null ? Color.BLACK : task.colorBackground | 0xFF000000; |
| mPaint.setColor(color); |
| mBackgroundPaint.setColor(color); |
| mSplashBackgroundPaint.setColor(color); |
| updateSplashView(mTask.icon); |
| } |
| |
| /** |
| * Updates the thumbnail. |
| * @param refreshNow whether the {@code thumbnailData} will be used to redraw immediately. |
| * In most cases, we use the {@link #setThumbnail(Task, ThumbnailData)} |
| * version with {@code refreshNow} is true. The only exception is |
| * in the live tile case that we grab a screenshot when user enters Overview |
| * upon swipe up so that a usable screenshot is accessible immediately when |
| * recents animation needs to be finished / cancelled. |
| */ |
| public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData, |
| boolean refreshNow) { |
| mTask = task; |
| boolean thumbnailWasNull = mThumbnailData == null; |
| mThumbnailData = |
| (thumbnailData != null && thumbnailData.thumbnail != null) ? thumbnailData : null; |
| if (mTask != null) { |
| updateSplashView(mTask.icon); |
| } |
| if (refreshNow) { |
| refresh(thumbnailWasNull && mThumbnailData != null); |
| } |
| } |
| |
| /** See {@link #setThumbnail(Task, ThumbnailData, boolean)} */ |
| public void setThumbnail(@Nullable Task task, @Nullable ThumbnailData thumbnailData) { |
| setThumbnail(task, thumbnailData, true /* refreshNow */); |
| } |
| |
| /** Updates the shader, paint, matrix to redraw. */ |
| public void refresh() { |
| refresh(false); |
| } |
| |
| /** |
| * Updates the shader, paint, matrix to redraw. |
| * @param shouldRefreshOverlay whether to re-initialize overlay |
| */ |
| private void refresh(boolean shouldRefreshOverlay) { |
| if (mThumbnailData != null && mThumbnailData.thumbnail != null) { |
| Bitmap bm = mThumbnailData.thumbnail; |
| bm.prepareToDraw(); |
| mBitmapShader = new BitmapShader(bm, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); |
| mPaint.setShader(mBitmapShader); |
| updateThumbnailMatrix(); |
| if (shouldRefreshOverlay) { |
| refreshOverlay(); |
| } |
| } else { |
| mBitmapShader = null; |
| mThumbnailData = null; |
| mPaint.setShader(null); |
| getTaskOverlay().reset(); |
| } |
| updateThumbnailPaintFilter(); |
| } |
| |
| /** |
| * Sets the alpha of the dim layer on top of this view. |
| * <p> |
| * If dimAlpha is 0, no dimming is applied; if dimAlpha is 1, the thumbnail will be the |
| * extracted background color. |
| * |
| */ |
| public void setDimAlpha(float dimAlpha) { |
| mDimAlpha = dimAlpha; |
| updateThumbnailPaintFilter(); |
| } |
| |
| /** |
| * Sets the alpha of the splash view. |
| */ |
| public void setSplashAlpha(float splashAlpha) { |
| mSplashAlpha = (int) (Utilities.boundToRange(splashAlpha, 0f, 1f) * 255); |
| if (mSplashViewDrawable != null) { |
| mSplashViewDrawable.setAlpha(mSplashAlpha); |
| } |
| mSplashBackgroundPaint.setAlpha(mSplashAlpha); |
| invalidate(); |
| } |
| |
| public TaskOverlay getTaskOverlay() { |
| if (mOverlay == null) { |
| mOverlay = getTaskView().getRecentsView().getTaskOverlayFactory().createOverlay(this); |
| } |
| return mOverlay; |
| } |
| |
| public float getDimAlpha() { |
| return mDimAlpha; |
| } |
| |
| /** |
| * Get the scaled insets that are being used to draw the task view. This is a subsection of |
| * the full snapshot. |
| * @return the insets in snapshot bitmap coordinates. |
| */ |
| @RequiresApi(api = Build.VERSION_CODES.Q) |
| public Insets getScaledInsets() { |
| if (mThumbnailData == null) { |
| return Insets.NONE; |
| } |
| |
| RectF bitmapRect = new RectF( |
| 0, 0, |
| mThumbnailData.thumbnail.getWidth(), mThumbnailData.thumbnail.getHeight()); |
| RectF viewRect = new RectF(0, 0, getMeasuredWidth(), getMeasuredHeight()); |
| |
| // The position helper matrix tells us how to transform the bitmap to fit the view, the |
| // inverse tells us where the view would be in the bitmaps coordinates. The insets are the |
| // difference between the bitmap bounds and the projected view bounds. |
| Matrix boundsToBitmapSpace = new Matrix(); |
| mPreviewPositionHelper.getMatrix().invert(boundsToBitmapSpace); |
| RectF boundsInBitmapSpace = new RectF(); |
| boundsToBitmapSpace.mapRect(boundsInBitmapSpace, viewRect); |
| |
| DeviceProfile dp = mActivity.getDeviceProfile(); |
| int bottomInset = dp.isTablet |
| ? Math.round(bitmapRect.bottom - boundsInBitmapSpace.bottom) : 0; |
| return Insets.of(0, 0, 0, bottomInset); |
| } |
| |
| |
| @SystemUiControllerFlags |
| public int getSysUiStatusNavFlags() { |
| if (mThumbnailData != null) { |
| int flags = 0; |
| flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_STATUS_BARS) != 0 |
| ? SystemUiController.FLAG_LIGHT_STATUS |
| : SystemUiController.FLAG_DARK_STATUS; |
| flags |= (mThumbnailData.appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0 |
| ? SystemUiController.FLAG_LIGHT_NAV |
| : SystemUiController.FLAG_DARK_NAV; |
| return flags; |
| } |
| return 0; |
| } |
| |
| @Override |
| protected void onLayout(boolean changed, int left, int top, int right, int bottom) { |
| super.onLayout(changed, left, top, right, bottom); |
| updateSplashView(mSplashViewDrawable); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| canvas.save(); |
| // Draw the insets if we're being drawn fullscreen (we do this for quick switch). |
| drawOnCanvas(canvas, 0, 0, getMeasuredWidth(), getMeasuredHeight(), |
| mFullscreenParams.mCurrentDrawnCornerRadius); |
| canvas.restore(); |
| } |
| |
| public PreviewPositionHelper getPreviewPositionHelper() { |
| return mPreviewPositionHelper; |
| } |
| |
| public void setFullscreenParams(TaskView.FullscreenDrawParams fullscreenParams) { |
| mFullscreenParams = fullscreenParams; |
| getTaskOverlay().setFullscreenParams(fullscreenParams); |
| invalidate(); |
| } |
| |
| public void drawOnCanvas(Canvas canvas, float x, float y, float width, float height, |
| float cornerRadius) { |
| if (mTask != null && getTaskView().isRunningTask() && !getTaskView().showScreenshot()) { |
| canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mClearPaint); |
| canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, |
| mDimmingPaintAfterClearing); |
| return; |
| } |
| |
| // Always draw the background since the snapshots might be translucent or partially empty |
| // (For example, tasks been reparented out of dismissing split root when drag-to-dismiss |
| // split screen). |
| canvas.drawRoundRect(x, y + 1, width, height - 1, cornerRadius, |
| cornerRadius, mBackgroundPaint); |
| |
| final boolean drawBackgroundOnly = mTask == null || mTask.isLocked || mBitmapShader == null |
| || mThumbnailData == null; |
| if (drawBackgroundOnly) { |
| return; |
| } |
| |
| canvas.drawRoundRect(x, y, width, height, cornerRadius, cornerRadius, mPaint); |
| |
| // Draw splash above thumbnail to hide inconsistencies in rotation and aspect ratios. |
| if (shouldShowSplashView()) { |
| // Always draw background for hiding inconsistencies, even if splash view is not yet |
| // loaded (which can happen as task icons are loaded asynchronously in the background) |
| canvas.drawRoundRect(x, y, width + 1, height + 1, cornerRadius, |
| cornerRadius, mSplashBackgroundPaint); |
| if (mSplashView != null) { |
| mSplashView.layout((int) x, (int) (y + 1), (int) width, (int) height - 1); |
| mSplashView.draw(canvas); |
| } |
| } |
| } |
| |
| public TaskView getTaskView() { |
| return (TaskView) getParent(); |
| } |
| |
| public void setOverlayEnabled(boolean overlayEnabled) { |
| if (mOverlayEnabled != overlayEnabled) { |
| mOverlayEnabled = overlayEnabled; |
| |
| refreshOverlay(); |
| } |
| } |
| |
| /** |
| * Determine if the splash should be shown over top of the thumbnail. |
| * |
| * <p>We want to show the splash if the aspect ratio or rotation of the thumbnail would be |
| * different from the task. |
| */ |
| public boolean shouldShowSplashView() { |
| return isThumbnailAspectRatioDifferentFromThumbnailData() |
| || isThumbnailRotationDifferentFromTask(); |
| } |
| |
| protected void refreshSplashView() { |
| if (mTask != null) { |
| updateSplashView(mTask.icon); |
| invalidate(); |
| } |
| } |
| |
| private void updateSplashView(Drawable icon) { |
| if (icon == null || icon.getConstantState() == null) { |
| mSplashViewDrawable = null; |
| mSplashView = null; |
| return; |
| } |
| mSplashViewDrawable = icon.getConstantState().newDrawable().mutate(); |
| mSplashViewDrawable.setAlpha(mSplashAlpha); |
| ImageView imageView = mSplashView == null ? new ImageView(getContext()) : mSplashView; |
| imageView.setImageDrawable(mSplashViewDrawable); |
| |
| imageView.setScaleType(ImageView.ScaleType.MATRIX); |
| Matrix matrix = new Matrix(); |
| |
| float drawableWidth = mSplashViewDrawable.getIntrinsicWidth(); |
| float drawableHeight = mSplashViewDrawable.getIntrinsicHeight(); |
| float viewWidth = getMeasuredWidth(); |
| float viewCenterX = viewWidth / 2f; |
| float viewHeight = getMeasuredHeight(); |
| float viewCenterY = viewHeight / 2f; |
| float centeredDrawableLeft = (viewWidth - drawableWidth) / 2f; |
| float centeredDrawableTop = (viewHeight - drawableHeight) / 2f; |
| float nonGridScale = getTaskView() == null ? 1 : 1 / getTaskView().getNonGridScale(); |
| float recentsMaxScale = getTaskView() == null || getTaskView().getRecentsView() == null |
| ? 1 : 1 / getTaskView().getRecentsView().getMaxScaleForFullScreen(); |
| float scale = nonGridScale * recentsMaxScale; |
| |
| // Center the image in the view. |
| matrix.setTranslate(centeredDrawableLeft, centeredDrawableTop); |
| // Apply scale transformation after translation, pivoting around center of view. |
| matrix.postScale(scale, scale, viewCenterX, viewCenterY); |
| |
| imageView.setImageMatrix(matrix); |
| mSplashView = imageView; |
| } |
| |
| private boolean isThumbnailAspectRatioDifferentFromThumbnailData() { |
| if (mThumbnailData == null || mThumbnailData.thumbnail == null) { |
| return false; |
| } |
| |
| float thumbnailViewAspect = getWidth() / (float) getHeight(); |
| float thumbnailDataAspect = |
| mThumbnailData.thumbnail.getWidth() / (float) mThumbnailData.thumbnail.getHeight(); |
| |
| return isRelativePercentDifferenceGreaterThan(thumbnailViewAspect, |
| thumbnailDataAspect, MAX_PCT_BEFORE_ASPECT_RATIOS_CONSIDERED_DIFFERENT); |
| } |
| |
| private boolean isThumbnailRotationDifferentFromTask() { |
| RecentsView recents = getTaskView().getRecentsView(); |
| if (recents == null || mThumbnailData == null) { |
| return false; |
| } |
| |
| if (recents.getPagedOrientationHandler() == PagedOrientationHandler.PORTRAIT) { |
| int currentRotation = recents.getPagedViewOrientedState().getRecentsActivityRotation(); |
| return (currentRotation - mThumbnailData.rotation) % 2 != 0; |
| } else { |
| return recents.getPagedOrientationHandler().getRotation() != mThumbnailData.rotation; |
| } |
| } |
| |
| /** |
| * Potentially re-init the task overlay. Be cautious when calling this as the overlay may |
| * do processing on initialization. |
| */ |
| private void refreshOverlay() { |
| if (mOverlayEnabled) { |
| getTaskOverlay().initOverlay(mTask, mThumbnailData, mPreviewPositionHelper.getMatrix(), |
| mPreviewPositionHelper.isOrientationChanged()); |
| } else { |
| getTaskOverlay().reset(); |
| } |
| } |
| |
| private void updateThumbnailPaintFilter() { |
| ColorFilter filter = getColorFilter(mDimAlpha); |
| mBackgroundPaint.setColorFilter(filter); |
| int alpha = (int) (mDimAlpha * 255); |
| mDimmingPaintAfterClearing.setAlpha(alpha); |
| if (mBitmapShader != null) { |
| mPaint.setColorFilter(filter); |
| } else { |
| mPaint.setColorFilter(null); |
| mPaint.setColor(ColorUtils.blendARGB(Color.BLACK, mDimColor, alpha)); |
| } |
| invalidate(); |
| } |
| |
| private void updateThumbnailMatrix() { |
| DeviceProfile dp = mActivity.getDeviceProfile(); |
| mPreviewPositionHelper.setOrientationChanged(false); |
| if (mBitmapShader != null && mThumbnailData != null) { |
| mPreviewRect.set(0, 0, mThumbnailData.thumbnail.getWidth(), |
| mThumbnailData.thumbnail.getHeight()); |
| int currentRotation = getTaskView().getRecentsView().getPagedViewOrientedState() |
| .getRecentsActivityRotation(); |
| boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; |
| mPreviewPositionHelper.updateThumbnailMatrix(mPreviewRect, mThumbnailData, |
| getMeasuredWidth(), getMeasuredHeight(), dp.widthPx, dp.heightPx, |
| dp.taskbarSize, dp.isTablet, currentRotation, isRtl); |
| |
| mBitmapShader.setLocalMatrix(mPreviewPositionHelper.getMatrix()); |
| mPaint.setShader(mBitmapShader); |
| } |
| getTaskView().updateCurrentFullscreenParams(mPreviewPositionHelper); |
| invalidate(); |
| } |
| |
| @Override |
| protected void onSizeChanged(int w, int h, int oldw, int oldh) { |
| super.onSizeChanged(w, h, oldw, oldh); |
| updateThumbnailMatrix(); |
| |
| refreshOverlay(); |
| } |
| |
| private ColorFilter getColorFilter(float dimAmount) { |
| return Utilities.makeColorTintingColorFilter(mDimColor, dimAmount); |
| } |
| |
| /** |
| * Returns current thumbnail or null if none is set. |
| */ |
| @Nullable |
| public Bitmap getThumbnail() { |
| if (mThumbnailData == null) { |
| return null; |
| } |
| return mThumbnailData.thumbnail; |
| } |
| |
| /** |
| * Returns whether the snapshot is real. If the device is locked for the user of the task, |
| * the snapshot used will be an app-theme generated snapshot instead of a real snapshot. |
| */ |
| public boolean isRealSnapshot() { |
| if (mThumbnailData == null) { |
| return false; |
| } |
| return mThumbnailData.isRealSnapshot && !mTask.isLocked; |
| } |
| } |