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