1 /* 2 * Copyright (C) 2024 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.task.thumbnail 18 19 import android.content.Context 20 import android.graphics.Color 21 import android.graphics.Matrix 22 import android.graphics.Outline 23 import android.graphics.Path 24 import android.graphics.Rect 25 import android.graphics.drawable.ShapeDrawable 26 import android.util.AttributeSet 27 import android.util.Log 28 import android.view.LayoutInflater 29 import android.view.View 30 import android.view.ViewOutlineProvider 31 import android.widget.FrameLayout 32 import androidx.annotation.ColorInt 33 import androidx.core.view.isInvisible 34 import com.android.launcher3.Flags.enableDesktopExplodedView 35 import com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA 36 import com.android.launcher3.R 37 import com.android.launcher3.util.MultiPropertyFactory 38 import com.android.launcher3.util.ViewPool 39 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.BackgroundOnly 40 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.LiveTile 41 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Snapshot 42 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.SnapshotSplash 43 import com.android.quickstep.task.thumbnail.TaskThumbnailUiState.Uninitialized 44 import com.android.quickstep.views.FixedSizeImageView 45 import com.android.quickstep.views.TaskThumbnailViewHeader 46 47 class TaskThumbnailView : FrameLayout, ViewPool.Reusable { <lambda>null48 private val scrimView: View by lazy { findViewById(R.id.task_thumbnail_scrim) } <lambda>null49 private val liveTileView: LiveTileView by lazy { findViewById(R.id.task_thumbnail_live_tile) } <lambda>null50 private val thumbnailView: FixedSizeImageView by lazy { findViewById(R.id.task_thumbnail) } <lambda>null51 private val splashBackground: View by lazy { findViewById(R.id.splash_background) } <lambda>null52 private val splashIcon: FixedSizeImageView by lazy { findViewById(R.id.splash_icon) } <lambda>null53 private val dimAlpha: MultiPropertyFactory<View> by lazy { 54 MultiPropertyFactory(scrimView, VIEW_ALPHA, ScrimViewAlpha.entries.size, ::maxOf) 55 } 56 private val outlinePath = Path() 57 private var onSizeChanged: ((width: Int, height: Int) -> Unit)? = null 58 59 private var taskThumbnailViewHeader: TaskThumbnailViewHeader? = null 60 61 private var uiState: TaskThumbnailUiState = Uninitialized 62 63 /** 64 * Sets the outline bounds of the view. Default to use view's bound as outline when set to null. 65 */ 66 var outlineBounds: Rect? = null 67 set(value) { 68 field = value 69 invalidateOutline() 70 } 71 72 private val bounds = Rect() 73 74 var cornerRadius: Float = 0f 75 set(value) { 76 field = value 77 invalidateOutline() 78 } 79 80 constructor(context: Context) : super(context) 81 82 constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) 83 84 constructor( 85 context: Context, 86 attrs: AttributeSet?, 87 defStyleAttr: Int, 88 ) : super(context, attrs, defStyleAttr) 89 onFinishInflatenull90 override fun onFinishInflate() { 91 super.onFinishInflate() 92 maybeCreateHeader() 93 } 94 onAttachedToWindownull95 override fun onAttachedToWindow() { 96 super.onAttachedToWindow() 97 clipToOutline = true 98 outlineProvider = 99 object : ViewOutlineProvider() { 100 override fun getOutline(view: View, outline: Outline) { 101 val outlineRect = outlineBounds ?: bounds 102 outlinePath.apply { 103 rewind() 104 addRoundRect( 105 outlineRect.left.toFloat(), 106 outlineRect.top.toFloat(), 107 outlineRect.right.toFloat(), 108 outlineRect.bottom.toFloat(), 109 cornerRadius / scaleX, 110 cornerRadius / scaleY, 111 Path.Direction.CW, 112 ) 113 } 114 outline.setPath(outlinePath) 115 } 116 } 117 } 118 onRecyclenull119 override fun onRecycle() { 120 uiState = Uninitialized 121 onSizeChanged = null 122 outlineBounds = null 123 resetViews() 124 } 125 setStatenull126 fun setState(state: TaskThumbnailUiState, taskId: Int? = null) { 127 if (uiState == state) return 128 logDebug("taskId: $taskId - uiState changed from: $uiState to: $state") 129 uiState = state 130 resetViews() 131 when (state) { 132 is Uninitialized -> {} 133 is LiveTile -> drawLiveWindow(state) 134 is SnapshotSplash -> drawSnapshotSplash(state) 135 is BackgroundOnly -> drawBackground(state.backgroundColor) 136 } 137 } 138 139 /** 140 * Updates the alpha of the dim layer on top of this view. If dimAlpha is 0, no dimming is 141 * applied; if dimAlpha is 1, the thumbnail will be the extracted background color. 142 * 143 * @param tintAmount The amount of alpha that will be applied to the dim layer. 144 */ updateTintAmountnull145 fun updateTintAmount(tintAmount: Float) { 146 dimAlpha[ScrimViewAlpha.TintAmount.ordinal].value = tintAmount 147 } 148 updateMenuOpenProgressnull149 fun updateMenuOpenProgress(progress: Float) { 150 dimAlpha[ScrimViewAlpha.MenuProgress.ordinal].value = progress * MAX_SCRIM_ALPHA 151 } 152 updateSplashAlphanull153 fun updateSplashAlpha(value: Float) { 154 splashBackground.alpha = value 155 splashIcon.alpha = value 156 } 157 doOnSizeChangenull158 fun doOnSizeChange(action: (width: Int, height: Int) -> Unit) { 159 onSizeChanged = action 160 } 161 onSizeChangednull162 override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { 163 super.onSizeChanged(w, h, oldw, oldh) 164 onSizeChanged?.invoke(width, height) 165 bounds.set(0, 0, w, h) 166 invalidateOutline() 167 } 168 setScaleXnull169 override fun setScaleX(scaleX: Float) { 170 super.setScaleX(scaleX) 171 // Splash icon should ignore scale on TTV 172 splashIcon.scaleX = 1 / scaleX 173 } 174 setScaleYnull175 override fun setScaleY(scaleY: Float) { 176 super.setScaleY(scaleY) 177 // Splash icon should ignore scale on TTV 178 splashIcon.scaleY = 1 / scaleY 179 } 180 resetViewsnull181 private fun resetViews() { 182 liveTileView.isInvisible = true 183 thumbnailView.isInvisible = true 184 thumbnailView.setImageBitmap(null) 185 splashBackground.alpha = 0f 186 splashIcon.alpha = 0f 187 splashIcon.setImageDrawable(null) 188 scrimView.alpha = 0f 189 setBackgroundColor(Color.BLACK) 190 taskThumbnailViewHeader?.isInvisible = true 191 } 192 drawBackgroundnull193 private fun drawBackground(@ColorInt background: Int) { 194 setBackgroundColor(background) 195 } 196 drawLiveWindownull197 private fun drawLiveWindow(liveTile: LiveTile) { 198 liveTileView.isInvisible = false 199 200 if (liveTile is LiveTile.WithHeader) { 201 taskThumbnailViewHeader?.isInvisible = false 202 taskThumbnailViewHeader?.setHeader(liveTile.header) 203 } 204 } 205 drawSnapshotSplashnull206 private fun drawSnapshotSplash(snapshotSplash: SnapshotSplash) { 207 drawSnapshot(snapshotSplash.snapshot) 208 209 splashBackground.setBackgroundColor(snapshotSplash.snapshot.backgroundColor) 210 val icon = snapshotSplash.splash?.constantState?.newDrawable()?.mutate() ?: ShapeDrawable() 211 splashIcon.setImageDrawable(icon) 212 } 213 drawSnapshotnull214 private fun drawSnapshot(snapshot: Snapshot) { 215 if (snapshot is Snapshot.WithHeader) { 216 taskThumbnailViewHeader?.isInvisible = false 217 taskThumbnailViewHeader?.setHeader(snapshot.header) 218 } 219 220 drawBackground(snapshot.backgroundColor) 221 thumbnailView.setImageBitmap(snapshot.bitmap) 222 thumbnailView.isInvisible = false 223 } 224 setImageMatrixnull225 fun setImageMatrix(matrix: Matrix) { 226 if (uiState is SnapshotSplash) { 227 thumbnailView.imageMatrix = matrix 228 } 229 } 230 logDebugnull231 private fun logDebug(message: String) { 232 Log.d(TAG, "[TaskThumbnailView@${Integer.toHexString(hashCode())}] $message") 233 } 234 maybeCreateHeadernull235 private fun maybeCreateHeader() { 236 if (enableDesktopExplodedView() && taskThumbnailViewHeader == null) { 237 taskThumbnailViewHeader = 238 LayoutInflater.from(context) 239 .inflate(R.layout.task_thumbnail_view_header, this, false) 240 as TaskThumbnailViewHeader 241 addView(taskThumbnailViewHeader) 242 } 243 } 244 245 private companion object { 246 const val TAG = "TaskThumbnailView" 247 private const val MAX_SCRIM_ALPHA = 0.4f 248 249 enum class ScrimViewAlpha { 250 MenuProgress, 251 TintAmount, 252 } 253 } 254 } 255