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