1 /*
<lambda>null2  * Copyright (C) 2021 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.views
18 
19 import android.animation.AnimatorSet
20 import android.animation.ObjectAnimator
21 import android.content.Context
22 import android.graphics.Rect
23 import android.graphics.drawable.ShapeDrawable
24 import android.graphics.drawable.shapes.RectShape
25 import android.util.AttributeSet
26 import android.view.Gravity
27 import android.view.MotionEvent
28 import android.view.View
29 import android.view.ViewGroup
30 import android.widget.FrameLayout
31 import android.widget.LinearLayout
32 import com.android.launcher3.DeviceProfile
33 import com.android.launcher3.InsettableFrameLayout
34 import com.android.launcher3.R
35 import com.android.launcher3.popup.ArrowPopup
36 import com.android.launcher3.popup.RoundedArrowDrawable
37 import com.android.launcher3.popup.SystemShortcut
38 import com.android.launcher3.util.Themes
39 import com.android.quickstep.TaskOverlayFactory
40 import com.android.quickstep.views.TaskView.TaskContainer
41 
42 class TaskMenuViewWithArrow<T> : ArrowPopup<T> where T : RecentsViewContainer, T : Context {
43     companion object {
44         const val TAG = "TaskMenuViewWithArrow"
45 
46         fun showForTask(taskContainer: TaskContainer, alignedOptionIndex: Int = 0): Boolean {
47             val container: RecentsViewContainer =
48                 RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
49             val taskMenuViewWithArrow =
50                 container.layoutInflater.inflate(
51                     R.layout.task_menu_with_arrow,
52                     container.dragLayer,
53                     false
54                 ) as TaskMenuViewWithArrow<*>
55 
56             return taskMenuViewWithArrow.populateAndShowForTask(taskContainer, alignedOptionIndex)
57         }
58     }
59 
60     constructor(context: Context) : super(context)
61     constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
62     constructor(
63         context: Context,
64         attrs: AttributeSet,
65         defStyleAttr: Int
66     ) : super(context, attrs, defStyleAttr)
67 
68     init {
69         clipToOutline = true
70 
71         shouldScaleArrow = true
72         mIsArrowRotated = true
73         // This synchronizes the arrow and menu to open at the same time
74         mOpenChildFadeStartDelay = mOpenFadeStartDelay
75         mOpenChildFadeDuration = mOpenFadeDuration
76         mCloseFadeStartDelay = mCloseChildFadeStartDelay
77         mCloseFadeDuration = mCloseChildFadeDuration
78     }
79 
80     private var alignedOptionIndex: Int = 0
81     private val extraSpaceForRowAlignment: Int
82         get() = optionMeasuredHeight * alignedOptionIndex
83     private val menuPaddingEnd = context.resources.getDimensionPixelSize(R.dimen.task_card_margin)
84 
85     private lateinit var taskView: TaskView
86     private lateinit var optionLayout: LinearLayout
87     private lateinit var taskContainer: TaskContainer
88 
89     private var optionMeasuredHeight = 0
90     private val arrowHorizontalPadding: Int
91         get() =
92             if (taskView.isFocusedTask)
93                 resources.getDimensionPixelSize(R.dimen.task_menu_horizontal_padding)
94             else 0
95 
96     private var iconView: IconView? = null
97     private var scrim: View? = null
98     private val scrimAlpha = 0.8f
99 
100     override fun isOfType(type: Int): Boolean = type and TYPE_TASK_MENU != 0
101 
102     override fun getTargetObjectLocation(outPos: Rect?) {
103         popupContainer.getDescendantRectRelativeToSelf(taskContainer.iconView.asView(), outPos)
104     }
105 
106     override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean {
107         if (ev?.action == MotionEvent.ACTION_DOWN) {
108             if (!popupContainer.isEventOverView(this, ev)) {
109                 close(true)
110                 return true
111             }
112         }
113         return false
114     }
115 
116     override fun onFinishInflate() {
117         super.onFinishInflate()
118         optionLayout = requireViewById(R.id.menu_option_layout)
119     }
120 
121     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
122         val maxMenuHeight: Int = calculateMaxHeight()
123         val newHeightMeasureSpec =
124             if (MeasureSpec.getSize(heightMeasureSpec) > maxMenuHeight) {
125                 MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST)
126             } else heightMeasureSpec
127         super.onMeasure(widthMeasureSpec, newHeightMeasureSpec)
128     }
129 
130     private fun calculateMaxHeight(): Int {
131         val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin)
132         return taskView.pagedOrientationHandler.getTaskMenuHeight(
133             taskInsetMargin,
134             mActivityContext.deviceProfile,
135             translationX,
136             translationY
137         )
138     }
139 
140     private fun populateAndShowForTask(
141         taskContainer: TaskContainer,
142         alignedOptionIndex: Int
143     ): Boolean {
144         if (isAttachedToWindow) {
145             return false
146         }
147 
148         taskView = taskContainer.taskView
149         this.taskContainer = taskContainer
150         this.alignedOptionIndex = alignedOptionIndex
151         if (!populateMenu()) return false
152         addScrim()
153         show()
154         return true
155     }
156 
157     private fun addScrim() {
158         scrim =
159             View(context).apply {
160                 layoutParams =
161                     FrameLayout.LayoutParams(
162                         FrameLayout.LayoutParams.MATCH_PARENT,
163                         FrameLayout.LayoutParams.MATCH_PARENT
164                     )
165                 setBackgroundColor(Themes.getAttrColor(context, R.attr.overviewScrimColor))
166                 alpha = 0f
167             }
168         popupContainer.addView(scrim)
169     }
170 
171     /** @return true if successfully able to populate task view menu, false otherwise */
172     private fun populateMenu(): Boolean {
173         // Icon may not be loaded
174         if (taskContainer.iconView.drawable == null) return false
175 
176         addMenuOptions()
177         return optionLayout.childCount > 0
178     }
179 
180     private fun addMenuOptions() {
181         // Add the options
182         TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer).forEach {
183             this.addMenuOption(it)
184         }
185 
186         // Add the spaces between items
187         val divider = ShapeDrawable(RectShape())
188         divider.paint.color = resources.getColor(android.R.color.transparent)
189         val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt()
190         optionLayout.showDividers = SHOW_DIVIDER_MIDDLE
191 
192         // Set the orientation, which makes the menu show
193         val recentsView: RecentsView<*, *> = mActivityContext.getOverviewPanel()
194         val orientationHandler = recentsView.pagedOrientationHandler
195         val deviceProfile: DeviceProfile = mActivityContext.deviceProfile
196         orientationHandler.setTaskOptionsMenuLayoutOrientation(
197             deviceProfile,
198             optionLayout,
199             dividerSpacing,
200             divider
201         )
202     }
203 
204     private fun addMenuOption(menuOption: SystemShortcut<*>) {
205         val menuOptionView =
206             mActivityContext.layoutInflater.inflate(R.layout.task_view_menu_option, this, false)
207                 as LinearLayout
208         menuOption.setIconAndLabelFor(
209             menuOptionView.requireViewById(R.id.icon),
210             menuOptionView.requireViewById(R.id.text)
211         )
212         val lp = menuOptionView.layoutParams as LayoutParams
213         lp.width = LayoutParams.MATCH_PARENT
214         menuOptionView.setPaddingRelative(
215             menuOptionView.paddingStart,
216             menuOptionView.paddingTop,
217             menuPaddingEnd,
218             menuOptionView.paddingBottom
219         )
220         menuOptionView.setOnClickListener { view: View? -> menuOption.onClick(view) }
221         optionLayout.addView(menuOptionView)
222     }
223 
224     override fun assignMarginsAndBackgrounds(viewGroup: ViewGroup) {
225         assignMarginsAndBackgrounds(
226             this,
227             Themes.getAttrColor(context, com.android.internal.R.attr.colorSurface)
228         )
229     }
230 
231     override fun onCreateOpenAnimation(anim: AnimatorSet) {
232         scrim?.let {
233             anim.play(
234                 ObjectAnimator.ofFloat(it, View.ALPHA, 0f, scrimAlpha)
235                     .setDuration(mOpenDuration.toLong())
236             )
237         }
238     }
239 
240     override fun onCreateCloseAnimation(anim: AnimatorSet) {
241         scrim?.let {
242             anim.play(
243                 ObjectAnimator.ofFloat(it, View.ALPHA, scrimAlpha, 0f)
244                     .setDuration(mCloseDuration.toLong())
245             )
246         }
247     }
248 
249     override fun closeComplete() {
250         super.closeComplete()
251         popupContainer.removeView(scrim)
252         popupContainer.removeView(iconView)
253     }
254 
255     /**
256      * Copy the iconView from taskView to dragLayer so it can stay on top of the scrim. It needs to
257      * be called after [getTargetObjectLocation] because [mTempRect] needs to be populated.
258      */
259     private fun copyIconToDragLayer(insets: Rect) {
260         iconView =
261             IconView(context).apply {
262                 layoutParams =
263                     FrameLayout.LayoutParams(
264                         taskContainer.iconView.width,
265                         taskContainer.iconView.height
266                     )
267                 x = mTempRect.left.toFloat() - insets.left
268                 y = mTempRect.top.toFloat() - insets.top
269                 drawable = taskContainer.iconView.drawable
270                 setDrawableSize(
271                     taskContainer.iconView.drawableWidth,
272                     taskContainer.iconView.drawableHeight
273                 )
274             }
275 
276         popupContainer.addView(iconView)
277     }
278 
279     /**
280      * Orients this container to the left or right of the given icon, aligning with the desired row.
281      *
282      * These are the preferred orientations, in order (RTL prefers right-aligned over left):
283      * - Right and first option aligned
284      * - Right and second option aligned
285      * - Left and first option aligned
286      * - Left and second option aligned
287      *
288      * So we always align right if there is enough horizontal space
289      */
290     override fun orientAboutObject() {
291         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
292         // Needed for offsets later
293         optionMeasuredHeight = optionLayout.getChildAt(0).measuredHeight
294         val extraHorizontalSpace = (mArrowHeight + mArrowOffsetVertical + arrowHorizontalPadding)
295 
296         val widthWithArrow = measuredWidth + paddingLeft + paddingRight + extraHorizontalSpace
297         getTargetObjectLocation(mTempRect)
298         val dragLayer: InsettableFrameLayout = popupContainer
299         val insets = dragLayer.insets
300 
301         copyIconToDragLayer(insets)
302 
303         // Put this menu to the right of the icon if there is space,
304         // which means the arrow is left aligned with the menu
305         val rightAlignedMenuStartX = mTempRect.left - widthWithArrow
306         val leftAlignedMenuStartX = mTempRect.right + extraHorizontalSpace
307         mIsLeftAligned =
308             if (mIsRtl) {
309                 rightAlignedMenuStartX + insets.left < 0
310             } else {
311                 leftAlignedMenuStartX + (widthWithArrow - extraHorizontalSpace) + insets.left <
312                     dragLayer.width - insets.right
313             }
314 
315         var menuStartX = if (mIsLeftAligned) leftAlignedMenuStartX else rightAlignedMenuStartX
316 
317         // Offset y so that the arrow and row are center-aligned with the original icon.
318         val iconHeight = mTempRect.height()
319         val yOffset = (optionMeasuredHeight - iconHeight) / 2
320         var menuStartY = mTempRect.top - yOffset - extraSpaceForRowAlignment
321 
322         // Insets are added later, so subtract them now.
323         menuStartX -= insets.left
324         menuStartY -= insets.top
325 
326         x = menuStartX.toFloat()
327         y = menuStartY.toFloat()
328 
329         val lp = layoutParams as FrameLayout.LayoutParams
330         val arrowLp = mArrow.layoutParams as FrameLayout.LayoutParams
331         lp.gravity = Gravity.TOP
332         arrowLp.gravity = lp.gravity
333     }
334 
335     override fun addArrow() {
336         popupContainer.addView(mArrow)
337         mArrow.x = getArrowX()
338         mArrow.y = y + (optionMeasuredHeight / 2) - (mArrowHeight / 2) + extraSpaceForRowAlignment
339 
340         updateArrowColor()
341 
342         // This is inverted (x = height, y = width) because the arrow is rotated
343         mArrow.pivotX = if (mIsLeftAligned) 0f else mArrowHeight.toFloat()
344         mArrow.pivotY = 0f
345     }
346 
347     private fun getArrowX(): Float {
348         return if (mIsLeftAligned) x - mArrowHeight else x + measuredWidth + mArrowOffsetVertical
349     }
350 
351     override fun updateArrowColor() {
352         mArrow.background =
353             RoundedArrowDrawable.createHorizontalRoundedArrow(
354                 mArrowWidth.toFloat(),
355                 mArrowHeight.toFloat(),
356                 mArrowPointRadius.toFloat(),
357                 mIsLeftAligned,
358                 mArrowColor
359             )
360         elevation = mElevation
361         mArrow.elevation = mElevation
362     }
363 }
364