• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2018 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 package com.android.quickstep.views
17 
18 import android.animation.Animator
19 import android.animation.AnimatorSet
20 import android.animation.ObjectAnimator
21 import android.animation.ValueAnimator
22 import android.content.Context
23 import android.graphics.Outline
24 import android.graphics.Rect
25 import android.graphics.drawable.ShapeDrawable
26 import android.graphics.drawable.shapes.RectShape
27 import android.util.AttributeSet
28 import android.view.Gravity
29 import android.view.KeyEvent
30 import android.view.MotionEvent
31 import android.view.View
32 import android.view.ViewOutlineProvider
33 import android.widget.LinearLayout
34 import android.widget.TextView
35 import androidx.core.content.res.ResourcesCompat
36 import com.android.app.animation.Interpolators
37 import com.android.launcher3.AbstractFloatingView
38 import com.android.launcher3.Flags.enableOverviewIconMenu
39 import com.android.launcher3.Flags.enableRefactorTaskThumbnail
40 import com.android.launcher3.R
41 import com.android.launcher3.anim.AnimationSuccessListener
42 import com.android.launcher3.anim.RoundedRectRevealOutlineProvider
43 import com.android.launcher3.popup.SystemShortcut
44 import com.android.launcher3.util.MultiPropertyFactory
45 import com.android.launcher3.util.SplitConfigurationOptions
46 import com.android.launcher3.views.BaseDragLayer
47 import com.android.quickstep.TaskOverlayFactory
48 import com.android.quickstep.TaskUtils
49 import com.android.quickstep.util.TaskCornerRadius
50 import java.util.function.Consumer
51 import kotlin.math.max
52 
53 /** Contains options for a recent task when long-pressing its icon. */
54 class TaskMenuView
55 @JvmOverloads
56 constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int = 0) :
57     AbstractFloatingView(context, attrs, defStyleAttr) {
58     private val recentsViewContainer: RecentsViewContainer =
59         RecentsViewContainer.containerFromContext(context)
60     private val tempRect = Rect()
61     private val taskName: TextView by lazy { findViewById(R.id.task_name) }
62     private val optionLayout: LinearLayout by lazy { findViewById(R.id.menu_option_layout) }
63     private var openCloseAnimator: AnimatorSet? = null
64     private var revealAnimator: ValueAnimator? = null
65     private var onClosingStartCallback: Runnable? = null
66     private lateinit var taskView: TaskView
67     private lateinit var taskContainer: TaskContainer
68     private var menuTranslationXBeforeOpen = 0f
69     private var menuTranslationYBeforeOpen = 0f
70 
71     // Spaced claimed below Overview (taskbar and insets)
72     private val taskbarTop by lazy {
73         recentsViewContainer.deviceProfile.heightPx -
74             recentsViewContainer.deviceProfile.overviewActionsClaimedSpaceBelow
75     }
76     private val minMenuTop by lazy { taskContainer.iconView.height.toFloat() }
77     // TODO(b/401476868): Replace overviewRowSpacing with correct margin to the taskbarTop.
78     private val maxMenuBottom by lazy {
79         (taskbarTop - recentsViewContainer.deviceProfile.overviewRowSpacing).toFloat()
80     }
81 
82     init {
83         clipToOutline = true
84     }
85 
86     override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
87         if (ev.action == MotionEvent.ACTION_DOWN) {
88             if (!recentsViewContainer.dragLayer.isEventOverView(this, ev)) {
89                 // TODO: log this once we have a new container type for it?
90                 animateOpenOrClosed(true)
91                 return true
92             }
93         }
94         return false
95     }
96 
97     override fun handleClose(animate: Boolean) {
98         animateOpenOrClosed(true, animated = false)
99     }
100 
101     override fun isOfType(type: Int): Boolean = (type and TYPE_TASK_MENU) != 0
102 
103     override fun getOutlineProvider(): ViewOutlineProvider =
104         object : ViewOutlineProvider() {
105             override fun getOutline(view: View, outline: Outline) {
106                 outline.setRoundRect(
107                     0,
108                     0,
109                     view.width,
110                     view.height,
111                     TaskCornerRadius.get(view.context),
112                 )
113             }
114         }
115 
116     override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
117         var heightMeasure = heightMeasureSpec
118         val maxMenuHeight = calculateMaxHeight()
119         if (MeasureSpec.getSize(heightMeasure) > maxMenuHeight) {
120             heightMeasure = MeasureSpec.makeMeasureSpec(maxMenuHeight, MeasureSpec.AT_MOST)
121         }
122         super.onMeasure(widthMeasureSpec, heightMeasure)
123     }
124 
125     fun onRotationChanged() {
126         openCloseAnimator?.let { if (it.isRunning) it.end() }
127         if (mIsOpen) {
128             optionLayout.removeAllViews()
129             if (enableOverviewIconMenu() || !populateAndLayoutMenu()) {
130                 close(false)
131             }
132         }
133     }
134 
135     private fun populateAndShowForTask(taskContainer: TaskContainer): Boolean {
136         if (isAttachedToWindow) return false
137         recentsViewContainer.dragLayer.addView(this)
138         taskView = taskContainer.taskView
139         this.taskContainer = taskContainer
140         if (!populateAndLayoutMenu()) return false
141         post { this.animateOpen() }
142         return true
143     }
144 
145     /** @return true if successfully able to populate task view menu, false otherwise */
146     private fun populateAndLayoutMenu(): Boolean {
147         addMenuOptions(taskContainer)
148         orientAroundTaskView(taskContainer)
149         return true
150     }
151 
152     private fun addMenuOptions(taskContainer: TaskContainer) {
153         if (enableOverviewIconMenu()) {
154             removeView(taskName)
155         } else {
156             taskName.text = TaskUtils.getTitle(context, taskContainer.task)
157             taskName.setOnClickListener { close(true) }
158         }
159         TaskOverlayFactory.getEnabledShortcuts(taskView, taskContainer)
160             .forEach(Consumer { menuOption: SystemShortcut<*> -> this.addMenuOption(menuOption) })
161     }
162 
163     private fun addMenuOption(menuOption: SystemShortcut<*>) {
164         val menuOptionView =
165             recentsViewContainer.layoutInflater.inflate(R.layout.task_view_menu_option, this, false)
166                 as LinearLayout
167         if (enableOverviewIconMenu()) {
168             menuOptionView.background =
169                 ResourcesCompat.getDrawable(
170                     resources,
171                     R.drawable.app_chip_menu_item_bg,
172                     context.theme,
173                 )
174             menuOptionView.foreground =
175                 ResourcesCompat.getDrawable(
176                     resources,
177                     R.drawable.app_chip_menu_item_fg,
178                     context.theme,
179                 )
180         }
181         menuOption.setIconAndLabelFor(
182             menuOptionView.findViewById(R.id.icon),
183             menuOptionView.findViewById(R.id.text),
184         )
185         val lp = menuOptionView.layoutParams as LayoutParams
186         taskView.pagedOrientationHandler.setLayoutParamsForTaskMenuOptionItem(
187             lp,
188             menuOptionView,
189             recentsViewContainer.deviceProfile,
190         )
191         // Set an onClick listener on each menu option. The onClick method is responsible for
192         // ending LiveTile mode on the thumbnail if needed.
193         menuOptionView.setOnClickListener { v: View? -> menuOption.onClick(v) }
194         optionLayout.addView(menuOptionView)
195     }
196 
197     private fun orientAroundTaskView(taskContainer: TaskContainer) {
198         val recentsView = recentsViewContainer.getOverviewPanel<RecentsView<*, *>>()
199         val orientationHandler = recentsView.pagedOrientationHandler
200         measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED)
201 
202         // Get Position
203         val deviceProfile = recentsViewContainer.deviceProfile
204         recentsViewContainer.dragLayer.getDescendantRectRelativeToSelf(
205             if (enableOverviewIconMenu()) iconView.findViewById(R.id.icon_view_menu_anchor)
206             else taskContainer.snapshotView,
207             tempRect,
208         )
209         val insets = recentsViewContainer.dragLayer.getInsets()
210         val params = layoutParams as BaseDragLayer.LayoutParams
211         params.width =
212             orientationHandler.getTaskMenuWidth(
213                 taskContainer.snapshotView,
214                 deviceProfile,
215                 taskContainer.stagePosition,
216             )
217         // Gravity set to Left instead of Start as sTempRect.left measures Left distance not Start
218         params.gravity = Gravity.LEFT
219         layoutParams = params
220         scaleX = taskView.scaleX
221         scaleY = taskView.scaleY
222 
223         // Set divider spacing
224         val divider = ShapeDrawable(RectShape())
225         divider.paint.color = resources.getColor(android.R.color.transparent)
226         val dividerSpacing = resources.getDimension(R.dimen.task_menu_spacing).toInt()
227         optionLayout.showDividers =
228             if (enableOverviewIconMenu()) SHOW_DIVIDER_NONE else SHOW_DIVIDER_MIDDLE
229 
230         optionLayout.background =
231             if (enableOverviewIconMenu()) {
232                 ResourcesCompat.getDrawable(resources, R.drawable.app_chip_menu_bg, context.theme)
233             } else {
234                 null
235             }
236 
237         orientationHandler.setTaskOptionsMenuLayoutOrientation(
238             deviceProfile,
239             optionLayout,
240             dividerSpacing,
241             divider,
242         )
243         val thumbnailAlignedX = (tempRect.left - insets.left).toFloat()
244         val thumbnailAlignedY = (tempRect.top - insets.top).toFloat()
245 
246         // Changing pivot to make computations easier
247         // NOTE: Changing the pivots means the rotated view gets rotated about the new pivots set,
248         // which would render the X and Y position set here incorrect
249         pivotX = 0f
250         pivotY = 0f
251         rotation = orientationHandler.degreesRotated
252 
253         if (enableOverviewIconMenu()) {
254             elevation = resources.getDimension(R.dimen.task_thumbnail_icon_menu_elevation)
255             translationX = thumbnailAlignedX
256             translationY = thumbnailAlignedY
257         } else {
258             // Margin that insets the menuView inside the taskView
259             val taskInsetMargin = resources.getDimension(R.dimen.task_card_margin)
260             translationX =
261                 orientationHandler.getTaskMenuX(
262                     thumbnailAlignedX,
263                     this.taskContainer.snapshotView,
264                     deviceProfile,
265                     taskInsetMargin,
266                     iconView,
267                 )
268             translationY =
269                 orientationHandler.getTaskMenuY(
270                     thumbnailAlignedY,
271                     this.taskContainer.snapshotView,
272                     this.taskContainer.stagePosition,
273                     this,
274                     taskInsetMargin,
275                     iconView,
276                 )
277         }
278     }
279 
280     private fun animateOpen() {
281         menuTranslationYBeforeOpen = translationY
282         menuTranslationXBeforeOpen = translationX
283         animateOpenOrClosed(false)
284         mIsOpen = true
285     }
286 
287     private val iconView: View
288         get() = taskContainer.iconView.asView()
289 
290     private fun animateOpenOrClosed(closing: Boolean, animated: Boolean = true) {
291         openCloseAnimator?.let { if (it.isRunning) it.cancel() }
292         openCloseAnimator = AnimatorSet()
293         // If we're opening, we just start from the beginning as a new `TaskMenuView` is created
294         // each time we do the open animation so there will never be a partial value here.
295         var revealAnimationStartProgress = 0f
296         if (closing && revealAnimator != null) {
297             revealAnimationStartProgress = 1f - revealAnimator!!.animatedFraction
298         }
299         revealAnimator =
300             createOpenCloseOutlineProvider()
301                 .createRevealAnimator(this, closing, revealAnimationStartProgress)
302         revealAnimator!!.interpolator =
303             if (enableOverviewIconMenu()) Interpolators.EMPHASIZED else Interpolators.DECELERATE
304         val openCloseAnimatorBuilder = openCloseAnimator!!.play(revealAnimator)
305         if (enableOverviewIconMenu()) {
306             animateOpenOrCloseAppChip(closing, openCloseAnimatorBuilder)
307         }
308         openCloseAnimatorBuilder.with(
309             ObjectAnimator.ofFloat(this, ALPHA, (if (closing) 0 else 1).toFloat())
310         )
311         if (enableRefactorTaskThumbnail()) {
312             revealAnimator?.addUpdateListener { animation: ValueAnimator ->
313                 val animatedFraction = animation.animatedFraction
314                 val openProgress = if (closing) (1 - animatedFraction) else animatedFraction
315                 taskContainer.updateMenuOpenProgress(openProgress)
316             }
317         } else {
318             openCloseAnimatorBuilder.with(
319                 ObjectAnimator.ofFloat(
320                     taskContainer.thumbnailViewDeprecated,
321                     TaskThumbnailViewDeprecated.DIM_ALPHA,
322                     if (closing) 0f else TaskView.MAX_PAGE_SCRIM_ALPHA,
323                 )
324             )
325         }
326         openCloseAnimator!!.addListener(
327             object : AnimationSuccessListener() {
328                 override fun onAnimationStart(animation: Animator) {
329                     visibility = VISIBLE
330                     if (closing) onClosingStartCallback?.run()
331                 }
332 
333                 override fun onAnimationSuccess(animator: Animator) {
334                     if (closing) closeComplete()
335                 }
336             }
337         )
338         val animationDuration =
339             when {
340                 animated && closing -> REVEAL_CLOSE_DURATION
341                 animated && !closing -> REVEAL_OPEN_DURATION
342                 else -> 0L
343             }
344         openCloseAnimator!!.setDuration(animationDuration)
345         openCloseAnimator!!.start()
346     }
347 
348     private fun TaskView.isOnGridBottomRow(): Boolean =
349         (recentsViewContainer.getOverviewPanel<View>() as RecentsView<*, *>).isOnGridBottomRow(this)
350 
351     private fun closeComplete() {
352         mIsOpen = false
353         recentsViewContainer.dragLayer.removeView(this)
354         revealAnimator = null
355     }
356 
357     private fun createOpenCloseOutlineProvider(): RoundedRectRevealOutlineProvider {
358         val radius = TaskCornerRadius.get(mContext)
359         val fromRect =
360             Rect(
361                 if (enableOverviewIconMenu() && isLayoutRtl) width else 0,
362                 0,
363                 if (enableOverviewIconMenu() && !isLayoutRtl) 0 else width,
364                 0,
365             )
366         val toRect = Rect(0, 0, width, height)
367         return RoundedRectRevealOutlineProvider(radius, radius, fromRect, toRect)
368     }
369 
370     /**
371      * Calculates max height based on how much space we have available. If not enough space then the
372      * view will scroll. The maximum menu size will sit inside the task with a margin on the top and
373      * bottom.
374      */
375     private fun calculateMaxHeight(): Int =
376         taskView.pagedOrientationHandler.getTaskMenuHeight(
377             taskInsetMargin = resources.getDimension(R.dimen.task_card_margin), // taskInsetMargin
378             deviceProfile = recentsViewContainer.deviceProfile,
379             taskMenuX = translationX,
380             taskMenuY =
381                 // Bottom menu can translate up to show more options. So we use the min
382                 // translation allowed to calculate its max height.
383                 if (enableOverviewIconMenu() && taskView.isOnGridBottomRow()) minMenuTop
384                 else translationY,
385         )
386 
387     private fun setOnClosingStartCallback(onClosingStartCallback: Runnable?) {
388         this.onClosingStartCallback = onClosingStartCallback
389     }
390 
391     private fun animateOpenOrCloseAppChip(closing: Boolean, animatorBuilder: AnimatorSet.Builder) {
392         val iconAppChip = taskContainer.iconView.asView() as IconAppChipView
393 
394         // Animate menu up for enough room to display full menu when task on bottom row.
395         var additionalTranslationY = 0f
396         if (taskView.isOnGridBottomRow()) {
397             val currentMenuBottom: Float = menuTranslationYBeforeOpen + height
398             additionalTranslationY =
399                 if (currentMenuBottom < maxMenuBottom) 0f
400                 // Translate menu up for enough room to display full menu when task on bottom row.
401                 else maxMenuBottom - currentMenuBottom
402 
403             val currentMenuTop = menuTranslationYBeforeOpen + additionalTranslationY
404             // If it translate above the min accepted, it translates to the top of the screen
405             if (currentMenuTop < minMenuTop) {
406                 // It subtracts the menuTranslation to make it 0 (top of the screen) + chip size.
407                 additionalTranslationY = -menuTranslationYBeforeOpen + minMenuTop
408             }
409         }
410 
411         val translationYAnim =
412             ObjectAnimator.ofFloat(
413                 this,
414                 TRANSLATION_Y,
415                 if (closing) menuTranslationYBeforeOpen
416                 else menuTranslationYBeforeOpen + additionalTranslationY,
417             )
418         translationYAnim.interpolator = Interpolators.EMPHASIZED
419         animatorBuilder.with(translationYAnim)
420 
421         val menuTranslationYAnim: ObjectAnimator =
422             ObjectAnimator.ofFloat(
423                 iconAppChip.getMenuTranslationY(),
424                 MultiPropertyFactory.MULTI_PROPERTY_VALUE,
425                 if (closing) 0f else additionalTranslationY,
426             )
427         menuTranslationYAnim.interpolator = Interpolators.EMPHASIZED
428         animatorBuilder.with(menuTranslationYAnim)
429 
430         var additionalTranslationX = 0f
431         if (
432             taskContainer.stagePosition == SplitConfigurationOptions.STAGE_POSITION_BOTTOM_OR_RIGHT
433         ) {
434             // Animate menu and icon when split task would display off the side of the screen.
435             additionalTranslationX =
436                 max(
437                         (translationX + width -
438                                 (recentsViewContainer.deviceProfile.widthPx -
439                                     resources.getDimensionPixelSize(
440                                         R.dimen.task_menu_edge_padding
441                                     ) * 2))
442                             .toDouble(),
443                         0.0,
444                     )
445                     .toFloat()
446         }
447 
448         val translationXAnim =
449             ObjectAnimator.ofFloat(
450                 this,
451                 TRANSLATION_X,
452                 if (closing) menuTranslationXBeforeOpen
453                 else menuTranslationXBeforeOpen - additionalTranslationX,
454             )
455         translationXAnim.interpolator = Interpolators.EMPHASIZED
456         animatorBuilder.with(translationXAnim)
457 
458         val menuTranslationXAnim: ObjectAnimator =
459             ObjectAnimator.ofFloat(
460                 iconAppChip.getMenuTranslationX(),
461                 MultiPropertyFactory.MULTI_PROPERTY_VALUE,
462                 if (closing) 0f else -additionalTranslationX,
463             )
464         menuTranslationXAnim.interpolator = Interpolators.EMPHASIZED
465         animatorBuilder.with(menuTranslationXAnim)
466     }
467 
468     override fun dispatchKeyEvent(event: KeyEvent): Boolean {
469         if (enableOverviewIconMenu()) {
470             if (event.action != KeyEvent.ACTION_DOWN) return super.dispatchKeyEvent(event)
471 
472             val isFirstMenuOptionFocused = optionLayout.indexOfChild(optionLayout.focusedChild) == 0
473             val isLastMenuOptionFocused =
474                 optionLayout.indexOfChild(optionLayout.focusedChild) == optionLayout.childCount - 1
475             if (
476                 (isLastMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_DOWN) ||
477                     (isFirstMenuOptionFocused && event.keyCode == KeyEvent.KEYCODE_DPAD_UP)
478             ) {
479                 iconView.requestFocus()
480                 return true
481             }
482         }
483         return super.dispatchKeyEvent(event)
484     }
485 
486     companion object {
487         private val REVEAL_OPEN_DURATION = if (enableOverviewIconMenu()) 417L else 150L
488         private val REVEAL_CLOSE_DURATION = if (enableOverviewIconMenu()) 333L else 100L
489 
490         /** Show a task menu for the given taskContainer. */
491         /** Show a task menu for the given taskContainer. */
492         @JvmOverloads
493         fun showForTask(
494             taskContainer: TaskContainer,
495             onClosingStartCallback: Runnable? = null,
496         ): Boolean {
497             val container: RecentsViewContainer =
498                 RecentsViewContainer.containerFromContext(taskContainer.taskView.context)
499             val taskMenuView =
500                 container.layoutInflater.inflate(R.layout.task_menu, container.dragLayer, false)
501                     as TaskMenuView
502             taskMenuView.setOnClosingStartCallback(onClosingStartCallback)
503             return taskMenuView.populateAndShowForTask(taskContainer)
504         }
505     }
506 }
507