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