1 /* <lambda>null2 * 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.wm.shell.shared.multiinstance 18 import android.animation.Animator 19 import android.animation.AnimatorListenerAdapter 20 import android.animation.AnimatorSet 21 import android.animation.ObjectAnimator 22 import android.annotation.ColorInt 23 import android.content.Context 24 import android.graphics.Bitmap 25 import android.graphics.drawable.ShapeDrawable 26 import android.graphics.drawable.shapes.RoundRectShape 27 import android.util.TypedValue 28 import android.view.MotionEvent.ACTION_OUTSIDE 29 import android.view.SurfaceView 30 import android.view.View 31 import android.view.View.ALPHA 32 import android.view.View.SCALE_X 33 import android.view.View.SCALE_Y 34 import android.view.ViewGroup.MarginLayoutParams 35 import android.widget.LinearLayout 36 import android.window.TaskSnapshot 37 import com.android.wm.shell.shared.R 38 39 /** 40 * View for the All Windows menu option, used by both Desktop Windowing and Taskbar. 41 * The menu displays icons of all open instances of an app. Clicking the icon should launch 42 * the instance, which will be performed by the child class. 43 */ 44 abstract class ManageWindowsViewContainer( 45 val context: Context, 46 @ColorInt private val menuBackgroundColor: Int 47 ) { 48 lateinit var menuView: ManageWindowsView 49 50 /** Creates the base menu view and fills it with icon views. */ 51 fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot?>>, 52 onIconClickListener: ((Int) -> Unit), 53 onOutsideClickListener: (() -> Unit)): ManageWindowsView { 54 val bitmapList = snapshotList 55 .filter { it.second != null } 56 .map { (index, snapshot) -> 57 index to Bitmap.wrapHardwareBuffer(snapshot!!.hardwareBuffer, snapshot.colorSpace) 58 } 59 return createAndShowMenuView( 60 bitmapList, 61 onIconClickListener, 62 onOutsideClickListener 63 ) 64 } 65 66 /** Creates the menu view with the given bitmaps, and displays it. */ 67 fun createAndShowMenuView( 68 snapshotList: List<Pair<Int, Bitmap?>>, 69 onIconClickListener: ((Int) -> Unit), 70 onOutsideClickListener: (() -> Unit) 71 ): ManageWindowsView { 72 menuView = ManageWindowsView(context, menuBackgroundColor).apply { 73 this.onOutsideClickListener = onOutsideClickListener 74 this.onIconClickListener = onIconClickListener 75 this.generateIconViews(snapshotList) 76 } 77 addToContainer(menuView) 78 return menuView 79 } 80 81 /** Play the animation for opening the menu. */ 82 fun animateOpen() { 83 menuView.animateOpen() 84 } 85 86 /** 87 * Play the animation for closing the menu. On finish, will run the provided callback, 88 * which will be responsible for removing the view from the container used in [addToContainer]. 89 */ 90 fun animateClose() { 91 menuView.animateClose { removeFromContainer() } 92 } 93 94 /** Adds the menu view to the container responsible for displaying it. */ 95 abstract fun addToContainer(menuView: ManageWindowsView) 96 97 /** Removes the menu view from the container used in the method above */ 98 abstract fun removeFromContainer() 99 100 companion object { 101 const val MANAGE_WINDOWS_MINIMUM_INSTANCES = 2 102 } 103 104 class ManageWindowsView( 105 private val context: Context, 106 menuBackgroundColor: Int 107 ) { 108 private val animators = mutableListOf<Animator>() 109 private val iconViews = mutableListOf<SurfaceView>() 110 val rootView: LinearLayout = LinearLayout(context) 111 var menuHeight = 0 112 var menuWidth = 0 113 var onIconClickListener: ((Int) -> Unit)? = null 114 var onOutsideClickListener: (() -> Unit)? = null 115 116 init { 117 rootView.orientation = LinearLayout.VERTICAL 118 val menuBackground = ShapeDrawable() 119 val menuRadius = getDimensionPixelSize(MENU_RADIUS_DP) 120 menuBackground.shape = RoundRectShape( 121 FloatArray(8) { menuRadius }, 122 null, 123 null 124 ) 125 menuBackground.paint.color = menuBackgroundColor 126 rootView.background = menuBackground 127 rootView.elevation = getDimensionPixelSize(MENU_ELEVATION_DP) 128 rootView.setOnTouchListener { _, event -> 129 if (event.actionMasked == ACTION_OUTSIDE) { 130 onOutsideClickListener?.invoke() 131 } 132 return@setOnTouchListener true 133 } 134 } 135 136 private fun getDimensionPixelSize(sizeDp: Float): Float { 137 return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 138 sizeDp, context.resources.displayMetrics) 139 } 140 141 fun generateIconViews( 142 snapshotList: List<Pair<Int, Bitmap?>> 143 ) { 144 menuWidth = 0 145 menuHeight = 0 146 rootView.removeAllViews() 147 val instanceIconHeight = getDimensionPixelSize(ICON_HEIGHT_DP) 148 val instanceIconWidth = getDimensionPixelSize(ICON_WIDTH_DP) 149 val iconRadius = getDimensionPixelSize(ICON_RADIUS_DP) 150 val iconMargin = getDimensionPixelSize(ICON_MARGIN_DP) 151 var rowLayout: LinearLayout? = null 152 // Add each icon to the menu, adding a new row when needed. 153 for ((iconCount, taskInfoSnapshotPair) in snapshotList.withIndex()) { 154 val taskId = taskInfoSnapshotPair.first 155 val snapshotBitmap = taskInfoSnapshotPair.second 156 // Once a row is filled, make a new row and increase the menu height. 157 if (iconCount % MENU_MAX_ICONS_PER_ROW == 0) { 158 rowLayout = LinearLayout(context) 159 rowLayout.orientation = LinearLayout.HORIZONTAL 160 rootView.addView(rowLayout) 161 menuHeight += (instanceIconHeight + iconMargin).toInt() 162 } 163 164 val croppedBitmap = snapshotBitmap?.let { cropBitmap(it) } 165 val scaledSnapshotBitmap = croppedBitmap?.let { 166 Bitmap.createScaledBitmap( 167 it, instanceIconWidth.toInt(), instanceIconHeight.toInt(), true /* filter */ 168 ) 169 } 170 val appSnapshotButton = SurfaceView(context) 171 appSnapshotButton.cornerRadius = iconRadius 172 appSnapshotButton.setZOrderOnTop(true) 173 appSnapshotButton.contentDescription = context.resources.getString( 174 R.string.manage_windows_icon_text, iconCount + 1 175 ) 176 appSnapshotButton.setOnClickListener { 177 onIconClickListener?.invoke(taskId) 178 } 179 val lp = MarginLayoutParams( 180 instanceIconWidth.toInt(), instanceIconHeight.toInt() 181 ) 182 lp.apply { 183 marginStart = iconMargin.toInt() 184 topMargin = iconMargin.toInt() 185 } 186 appSnapshotButton.layoutParams = lp 187 // If we haven't already reached one full row, increment width. 188 if (iconCount < MENU_MAX_ICONS_PER_ROW) { 189 menuWidth += (instanceIconWidth + iconMargin).toInt() 190 } 191 rowLayout?.addView(appSnapshotButton) 192 iconViews += appSnapshotButton 193 appSnapshotButton.requestLayout() 194 rowLayout?.post { 195 appSnapshotButton.holder.surface 196 .attachAndQueueBufferWithColorSpace( 197 scaledSnapshotBitmap?.hardwareBuffer, 198 scaledSnapshotBitmap?.colorSpace 199 ) 200 } 201 } 202 // Add margin again for the right/bottom of the menu. 203 menuWidth += iconMargin.toInt() 204 menuHeight += iconMargin.toInt() 205 } 206 207 private fun cropBitmap( 208 bitmapToCrop: Bitmap 209 ): Bitmap { 210 val ratioToMatch = ICON_WIDTH_DP / ICON_HEIGHT_DP 211 val bitmapWidth = bitmapToCrop.width 212 val bitmapHeight = bitmapToCrop.height 213 if (bitmapWidth > bitmapHeight * ratioToMatch) { 214 // Crop based on height 215 val newWidth = bitmapHeight * ratioToMatch 216 return Bitmap.createBitmap( 217 bitmapToCrop, 218 ((bitmapWidth - newWidth) / 2).toInt(), 219 0, 220 newWidth.toInt(), 221 bitmapHeight 222 ) 223 } else { 224 // Crop based on width 225 val newHeight = bitmapWidth / ratioToMatch 226 return Bitmap.createBitmap( 227 bitmapToCrop, 228 0, 229 ((bitmapHeight - newHeight) / 2).toInt(), 230 bitmapWidth, 231 newHeight.toInt() 232 ) 233 } 234 } 235 236 /** Play the animation for opening the menu. */ 237 fun animateOpen() { 238 animateView(rootView, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE, 239 MENU_START_ALPHA, MENU_FULL_ALPHA 240 ) 241 for (view in iconViews) { 242 animateView(view, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE, 243 MENU_START_ALPHA, MENU_FULL_ALPHA 244 ) 245 } 246 createAnimatorSet().start() 247 } 248 249 /** Play the animation for closing the menu. */ 250 fun animateClose(callback: () -> Unit) { 251 animateView(rootView, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE, 252 MENU_FULL_ALPHA, MENU_START_ALPHA 253 ) 254 for (view in iconViews) { 255 animateView(view, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE, 256 MENU_FULL_ALPHA, MENU_START_ALPHA 257 ) 258 } 259 createAnimatorSet().apply { 260 addListener( 261 object : AnimatorListenerAdapter() { 262 override fun onAnimationEnd(animation: Animator) { 263 callback.invoke() 264 } 265 } 266 ) 267 start() 268 } 269 } 270 271 private fun animateView( 272 view: View, 273 startBoundsScale: Float, 274 endBoundsScale: Float, 275 startAlpha: Float, 276 endAlpha: Float) { 277 animators += ObjectAnimator.ofFloat( 278 view, 279 SCALE_X, 280 startBoundsScale, 281 endBoundsScale 282 ).apply { 283 duration = MENU_BOUNDS_ANIM_DURATION 284 } 285 animators += ObjectAnimator.ofFloat( 286 view, 287 SCALE_Y, 288 startBoundsScale, 289 endBoundsScale 290 ).apply { 291 duration = MENU_BOUNDS_ANIM_DURATION 292 } 293 animators += ObjectAnimator.ofFloat( 294 view, 295 ALPHA, 296 startAlpha, 297 endAlpha 298 ).apply { 299 duration = MENU_ALPHA_ANIM_DURATION 300 startDelay = MENU_ALPHA_ANIM_DELAY 301 } 302 } 303 304 private fun createAnimatorSet(): AnimatorSet { 305 val animatorSet = AnimatorSet().apply { 306 playTogether(animators) 307 } 308 animators.clear() 309 return animatorSet 310 } 311 312 companion object { 313 private const val MENU_RADIUS_DP = 26f 314 private const val ICON_WIDTH_DP = 204f 315 private const val ICON_HEIGHT_DP = 127.5f 316 private const val ICON_RADIUS_DP = 16f 317 private const val ICON_MARGIN_DP = 16f 318 private const val MENU_ELEVATION_DP = 1f 319 private const val MENU_MAX_ICONS_PER_ROW = 3 320 private const val MENU_BOUNDS_ANIM_DURATION = 200L 321 private const val MENU_BOUNDS_SHRUNK_SCALE = 0.8f 322 private const val MENU_BOUNDS_FULL_SCALE = 1f 323 private const val MENU_ALPHA_ANIM_DURATION = 100L 324 private const val MENU_ALPHA_ANIM_DELAY = 50L 325 private const val MENU_START_ALPHA = 0f 326 private const val MENU_FULL_ALPHA = 1f 327 } 328 } 329 } 330