• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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