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