• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 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.AnimatorSet
19 import android.animation.ObjectAnimator
20 import android.animation.RectEvaluator
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.Drawable
26 import android.util.AttributeSet
27 import android.view.View
28 import android.view.ViewAnimationUtils
29 import android.view.ViewOutlineProvider
30 import android.widget.FrameLayout
31 import android.widget.ImageView
32 import android.widget.TextView
33 import com.android.app.animation.Interpolators
34 import com.android.launcher3.LauncherAnimUtils.SCALE_PROPERTY
35 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_X
36 import com.android.launcher3.LauncherAnimUtils.VIEW_TRANSLATE_Y
37 import com.android.launcher3.R
38 import com.android.launcher3.Utilities
39 import com.android.launcher3.util.MultiPropertyFactory
40 import com.android.launcher3.util.MultiPropertyFactory.FloatBiFunction
41 import com.android.launcher3.util.MultiValueAlpha
42 import com.android.quickstep.util.RecentsOrientedState
43 import kotlin.math.max
44 import kotlin.math.min
45 
46 /** An icon app menu view which can be used in place of an IconView in overview TaskViews. */
47 class IconAppChipView
48 @JvmOverloads
49 constructor(
50     context: Context,
51     attrs: AttributeSet? = null,
52     defStyleAttr: Int = 0,
53     defStyleRes: Int = 0,
54 ) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), TaskViewIcon {
55 
56     private var iconView: IconView? = null
57     private var iconArrowView: ImageView? = null
58     private var menuAnchorView: View? = null
59     // Two textview so we can ellipsize the collapsed view and crossfade on expand to the full name.
60     private var iconTextCollapsedView: TextView? = null
61     private var iconTextExpandedView: TextView? = null
62 
63     private val backgroundRelativeLtrLocation = Rect()
64     private val backgroundAnimationRectEvaluator = RectEvaluator(backgroundRelativeLtrLocation)
65 
66     // Menu dimensions
67     private val collapsedMenuDefaultWidth: Int =
68         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_width)
69     private val expandedMenuDefaultWidth: Int =
70         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_width)
71     private val collapsedMenuDefaultHeight =
72         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_collapsed_height)
73     private val expandedMenuDefaultHeight =
74         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_height)
75     private val iconMenuMarginTopStart =
76         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_top_start_margin)
77     private val menuToChipGap: Int =
78         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_expanded_gap)
79 
80     // Background dimensions
81     private val backgroundMarginTopStart: Int =
82         resources.getDimensionPixelSize(
83             R.dimen.task_thumbnail_icon_menu_background_margin_top_start
84         )
85 
86     // Contents dimensions
87     private val appNameHorizontalMargin =
88         resources.getDimensionPixelSize(
89             R.dimen.task_thumbnail_icon_menu_app_name_margin_horizontal_collapsed
90         )
91     private val arrowMarginEnd =
92         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_margin)
93     private val iconViewMarginStart =
94         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_view_start_margin)
95     private val appIconSize =
96         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_collapsed_size)
97     private val arrowSize =
98         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_arrow_size)
99     private val iconViewDrawableExpandedSize =
100         resources.getDimensionPixelSize(R.dimen.task_thumbnail_icon_menu_app_icon_expanded_size)
101 
102     private var animator: AnimatorSet? = null
103 
104     private val multiValueAlpha: MultiValueAlpha =
105         MultiValueAlpha(this, NUM_ALPHA_CHANNELS).apply { setUpdateVisibility(true) }
106 
107     private val viewTranslationX: MultiPropertyFactory<View> =
108         MultiPropertyFactory(this, VIEW_TRANSLATE_X, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
109 
110     private val viewTranslationY: MultiPropertyFactory<View> =
111         MultiPropertyFactory(this, VIEW_TRANSLATE_Y, INDEX_COUNT_TRANSLATION, SUM_AGGREGATOR)
112 
113     var maxWidth = Int.MAX_VALUE
114         /**
115          * Sets the maximum width of this Icon Menu. This is usually used when space is limited for
116          * split screen.
117          */
118         set(value) {
119             // Width showing only the app icon and arrow. Max width should not be set to less than
120             // this.
121             val minMaxWidth = iconViewMarginStart + appIconSize + arrowSize + arrowMarginEnd
122             field = max(value, minMaxWidth)
123         }
124 
125     var status: AppChipStatus = AppChipStatus.Collapsed
126         private set
127 
128     override fun onFinishInflate() {
129         super.onFinishInflate()
130         iconView = findViewById(R.id.icon_view)
131         iconTextCollapsedView = findViewById(R.id.icon_text_collapsed)
132         iconTextExpandedView = findViewById(R.id.icon_text_expanded)
133         iconArrowView = findViewById(R.id.icon_arrow)
134         menuAnchorView = findViewById(R.id.icon_view_menu_anchor)
135     }
136 
137     override fun setText(text: CharSequence?) {
138         iconTextCollapsedView?.text = text
139         iconTextExpandedView?.text = text
140     }
141 
142     override fun getDrawable(): Drawable? = iconView?.drawable
143 
144     override fun setDrawable(icon: Drawable?) {
145         iconView?.drawable = icon
146     }
147 
148     override fun setDrawableSize(iconWidth: Int, iconHeight: Int) {
149         iconView?.setDrawableSize(iconWidth, iconHeight)
150     }
151 
152     override fun setIconOrientation(orientationState: RecentsOrientedState, isGridTask: Boolean) {
153         val orientationHandler = orientationState.orientationHandler
154         // Layout params for anchor view
155         val anchorLayoutParams = menuAnchorView!!.layoutParams as LayoutParams
156         anchorLayoutParams.topMargin = expandedMenuDefaultHeight + menuToChipGap
157         menuAnchorView!!.layoutParams = anchorLayoutParams
158 
159         // Layout Params for the Menu View (this)
160         val iconMenuParams = layoutParams as LayoutParams
161         iconMenuParams.width = expandedMenuDefaultWidth
162         iconMenuParams.height = expandedMenuDefaultHeight
163         orientationHandler.setIconAppChipMenuParams(
164             this,
165             iconMenuParams,
166             iconMenuMarginTopStart,
167             iconMenuMarginTopStart,
168         )
169         layoutParams = iconMenuParams
170 
171         // Layout params for the background
172         val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
173         backgroundRelativeLtrLocation.set(collapsedBackgroundBounds)
174         outlineProvider =
175             object : ViewOutlineProvider() {
176                 val mRtlAppliedOutlineBounds: Rect = Rect()
177 
178                 override fun getOutline(view: View, outline: Outline) {
179                     mRtlAppliedOutlineBounds.set(backgroundRelativeLtrLocation)
180                     if (isLayoutRtl) {
181                         val width = width
182                         mRtlAppliedOutlineBounds.left = width - backgroundRelativeLtrLocation.right
183                         mRtlAppliedOutlineBounds.right = width - backgroundRelativeLtrLocation.left
184                     }
185                     outline.setRoundRect(
186                         mRtlAppliedOutlineBounds,
187                         mRtlAppliedOutlineBounds.height() / 2f,
188                     )
189                 }
190             }
191 
192         // Layout Params for the Icon View
193         val iconParams = iconView!!.layoutParams as LayoutParams
194         val iconMarginStartRelativeToParent = iconViewMarginStart + backgroundMarginTopStart
195         orientationHandler.setIconAppChipChildrenParams(iconParams, iconMarginStartRelativeToParent)
196 
197         iconView!!.layoutParams = iconParams
198         iconView!!.setDrawableSize(appIconSize, appIconSize)
199 
200         // Layout Params for the collapsed Icon Text View
201         val textMarginStart =
202             iconMarginStartRelativeToParent + appIconSize + appNameHorizontalMargin
203         val iconTextCollapsedParams = iconTextCollapsedView!!.layoutParams as LayoutParams
204         orientationHandler.setIconAppChipChildrenParams(iconTextCollapsedParams, textMarginStart)
205         val collapsedTextWidth =
206             (collapsedBackgroundBounds.width() -
207                 iconViewMarginStart -
208                 appIconSize -
209                 arrowSize -
210                 appNameHorizontalMargin -
211                 arrowMarginEnd)
212         iconTextCollapsedParams.width = collapsedTextWidth
213         iconTextCollapsedView!!.layoutParams = iconTextCollapsedParams
214         iconTextCollapsedView!!.alpha = 1f
215 
216         // Layout Params for the expanded Icon Text View
217         val iconTextExpandedParams = iconTextExpandedView!!.layoutParams as LayoutParams
218         orientationHandler.setIconAppChipChildrenParams(iconTextExpandedParams, textMarginStart)
219         iconTextExpandedView!!.layoutParams = iconTextExpandedParams
220         iconTextExpandedView!!.alpha = 0f
221         iconTextExpandedView!!.setRevealClip(
222             true,
223             0f,
224             appIconSize / 2f,
225             collapsedTextWidth.toFloat(),
226         )
227 
228         // Layout Params for the Icon Arrow View
229         val iconArrowParams = iconArrowView!!.layoutParams as LayoutParams
230         val arrowMarginStart = collapsedBackgroundBounds.right - arrowMarginEnd - arrowSize
231         orientationHandler.setIconAppChipChildrenParams(iconArrowParams, arrowMarginStart)
232         iconArrowView!!.pivotY = iconArrowParams.height / 2f
233         iconArrowView!!.layoutParams = iconArrowParams
234 
235         // This method is called twice sometimes (like when rotating split tasks). It is called
236         // once before onMeasure and onLayout, and again after onMeasure but before onLayout with
237         // a new width. This happens because we update widths on rotation and on measure of
238         // grouped task views. Calling requestLayout() does not guarantee a call to onMeasure if
239         // it has just measured, so we explicitly call it here.
240         measure(
241             MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY),
242             MeasureSpec.makeMeasureSpec(layoutParams.height, MeasureSpec.EXACTLY),
243         )
244     }
245 
246     override fun setIconColorTint(color: Int, amount: Float) {
247         // RecentsView's COLOR_TINT animates between 0 and 0.5f, we want to hide the app chip menu.
248         val colorTintAlpha = Utilities.mapToRange(amount, 0f, 0.5f, 1f, 0f, Interpolators.LINEAR)
249         multiValueAlpha[INDEX_COLOR_FILTER_ALPHA].value = colorTintAlpha
250     }
251 
252     override fun setContentAlpha(alpha: Float) {
253         multiValueAlpha[INDEX_CONTENT_ALPHA].value = alpha
254     }
255 
256     override fun setModalAlpha(alpha: Float) {
257         multiValueAlpha[INDEX_MODAL_ALPHA].value = alpha
258     }
259 
260     override fun setFlexSplitAlpha(alpha: Float) {
261         multiValueAlpha[INDEX_MINIMUM_RATIO_ALPHA].value = alpha
262     }
263 
264     override fun getDrawableWidth(): Int = iconView?.drawableWidth ?: 0
265 
266     override fun getDrawableHeight(): Int = iconView?.drawableHeight ?: 0
267 
268     /** Gets the view split x-axis translation */
269     fun getSplitTranslationX(): MultiPropertyFactory<View>.MultiProperty =
270         viewTranslationX.get(INDEX_SPLIT_TRANSLATION)
271 
272     /**
273      * Sets the view split x-axis translation
274      *
275      * @param value x-axis translation
276      */
277     fun setSplitTranslationX(value: Float) {
278         getSplitTranslationX().value = value
279     }
280 
281     /** Gets the view split y-axis translation */
282     fun getSplitTranslationY(): MultiPropertyFactory<View>.MultiProperty =
283         viewTranslationY[INDEX_SPLIT_TRANSLATION]
284 
285     /**
286      * Sets the view split y-axis translation
287      *
288      * @param value y-axis translation
289      */
290     fun setSplitTranslationY(value: Float) {
291         getSplitTranslationY().value = value
292     }
293 
294     /** Gets the menu x-axis translation for split task */
295     fun getMenuTranslationX(): MultiPropertyFactory<View>.MultiProperty =
296         viewTranslationX[INDEX_MENU_TRANSLATION]
297 
298     /** Gets the menu y-axis translation for split task */
299     fun getMenuTranslationY(): MultiPropertyFactory<View>.MultiProperty =
300         viewTranslationY[INDEX_MENU_TRANSLATION]
301 
302     internal fun revealAnim(isRevealing: Boolean, animated: Boolean = true) {
303         cancelInProgressAnimations()
304         val collapsedBackgroundBounds = getCollapsedBackgroundLtrBounds()
305         val expandedBackgroundBounds = getExpandedBackgroundLtrBounds()
306         val initialBackground = Rect(backgroundRelativeLtrLocation)
307         animator = AnimatorSet()
308 
309         if (isRevealing) {
310             val isRtl = isLayoutRtl
311             bringToFront()
312             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
313             val expandedTextRevealAnim =
314                 ViewAnimationUtils.createCircularReveal(
315                     iconTextExpandedView,
316                     0,
317                     iconTextExpandedView!!.height / 2,
318                     iconTextCollapsedView!!.width.toFloat(),
319                     iconTextExpandedView!!.width.toFloat(),
320                 )
321             // Animate background clipping
322             val backgroundAnimator =
323                 ValueAnimator.ofObject(
324                     backgroundAnimationRectEvaluator,
325                     initialBackground,
326                     expandedBackgroundBounds,
327                 )
328             backgroundAnimator.addUpdateListener { invalidateOutline() }
329 
330             val iconViewScaling = iconViewDrawableExpandedSize / appIconSize.toFloat()
331             val arrowTranslationX =
332                 (expandedBackgroundBounds.right - collapsedBackgroundBounds.right).toFloat()
333             val iconCenterToTextCollapsed = appIconSize / 2f + appNameHorizontalMargin
334             val iconCenterToTextExpanded =
335                 iconViewDrawableExpandedSize / 2f + appNameHorizontalMargin
336             val textTranslationX = iconCenterToTextExpanded - iconCenterToTextCollapsed
337 
338             val textTranslationXWithRtl = if (isRtl) -textTranslationX else textTranslationX
339             val arrowTranslationWithRtl = if (isRtl) -arrowTranslationX else arrowTranslationX
340 
341             animator!!.playTogether(
342                 expandedTextRevealAnim,
343                 backgroundAnimator,
344                 ObjectAnimator.ofFloat(iconView, SCALE_X, iconViewScaling),
345                 ObjectAnimator.ofFloat(iconView, SCALE_Y, iconViewScaling),
346                 ObjectAnimator.ofFloat(
347                     iconTextCollapsedView,
348                     TRANSLATION_X,
349                     textTranslationXWithRtl,
350                 ),
351                 ObjectAnimator.ofFloat(
352                     iconTextExpandedView,
353                     TRANSLATION_X,
354                     textTranslationXWithRtl,
355                 ),
356                 ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 0f),
357                 ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 1f),
358                 ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, arrowTranslationWithRtl),
359                 ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, -1f),
360             )
361             animator!!.duration = MENU_BACKGROUND_REVEAL_DURATION.toLong()
362             status = AppChipStatus.Expanded
363         } else {
364             // Clip expanded text with reveal animation so it doesn't go beyond the edge of the menu
365             val expandedTextClipAnim =
366                 ViewAnimationUtils.createCircularReveal(
367                     iconTextExpandedView,
368                     0,
369                     iconTextExpandedView!!.height / 2,
370                     iconTextExpandedView!!.width.toFloat(),
371                     iconTextCollapsedView!!.width.toFloat(),
372                 )
373 
374             // Animate background clipping
375             val backgroundAnimator =
376                 ValueAnimator.ofObject(
377                     backgroundAnimationRectEvaluator,
378                     initialBackground,
379                     collapsedBackgroundBounds,
380                 )
381             backgroundAnimator.addUpdateListener { valueAnimator: ValueAnimator? ->
382                 invalidateOutline()
383             }
384 
385             animator!!.playTogether(
386                 expandedTextClipAnim,
387                 backgroundAnimator,
388                 ObjectAnimator.ofFloat(iconView, SCALE_PROPERTY, 1f),
389                 ObjectAnimator.ofFloat(iconTextCollapsedView, TRANSLATION_X, 0f),
390                 ObjectAnimator.ofFloat(iconTextExpandedView, TRANSLATION_X, 0f),
391                 ObjectAnimator.ofFloat(iconTextCollapsedView, ALPHA, 1f),
392                 ObjectAnimator.ofFloat(iconTextExpandedView, ALPHA, 0f),
393                 ObjectAnimator.ofFloat(iconArrowView, TRANSLATION_X, 0f),
394                 ObjectAnimator.ofFloat(iconArrowView, SCALE_Y, 1f),
395             )
396             animator!!.duration = MENU_BACKGROUND_HIDE_DURATION.toLong()
397             status = AppChipStatus.Collapsed
398         }
399 
400         if (!animated) animator!!.duration = 0
401         animator!!.interpolator = Interpolators.EMPHASIZED
402         animator!!.start()
403     }
404 
405     private fun getCollapsedBackgroundLtrBounds(): Rect {
406         val bounds =
407             Rect(0, 0, min(maxWidth, collapsedMenuDefaultWidth), collapsedMenuDefaultHeight)
408         bounds.offset(backgroundMarginTopStart, backgroundMarginTopStart)
409         return bounds
410     }
411 
412     private fun getExpandedBackgroundLtrBounds() =
413         Rect(0, 0, expandedMenuDefaultWidth, expandedMenuDefaultHeight)
414 
415     private fun cancelInProgressAnimations() {
416         // We null the `AnimatorSet` because it holds references to the `Animators` which aren't
417         // expecting to be mutable and will cause a crash if they are re-used.
418         if (animator != null && animator!!.isStarted) {
419             animator!!.cancel()
420             animator = null
421         }
422     }
423 
424     override fun focusSearch(direction: Int): View? {
425         if (mParent == null) return null
426         return when (direction) {
427             FOCUS_RIGHT,
428             FOCUS_DOWN -> mParent.focusSearch(this, View.FOCUS_FORWARD)
429             FOCUS_UP,
430             FOCUS_LEFT -> mParent.focusSearch(this, View.FOCUS_BACKWARD)
431             else -> super.focusSearch(direction)
432         }
433     }
434 
435     fun reset() {
436         setText(null)
437         setDrawable(null)
438     }
439 
440     override fun asView(): View = this
441 
442     enum class AppChipStatus {
443         Expanded,
444         Collapsed,
445     }
446 
447     private companion object {
448         private val SUM_AGGREGATOR = FloatBiFunction { a: Float, b: Float -> a + b }
449 
450         private const val MENU_BACKGROUND_REVEAL_DURATION = 417
451         private const val MENU_BACKGROUND_HIDE_DURATION = 333
452 
453         private const val NUM_ALPHA_CHANNELS = 4
454         private const val INDEX_CONTENT_ALPHA = 0
455         private const val INDEX_COLOR_FILTER_ALPHA = 1
456         private const val INDEX_MODAL_ALPHA = 2
457         /** Used to hide the app chip for 90:10 flex split. */
458         private const val INDEX_MINIMUM_RATIO_ALPHA = 3
459 
460         private const val INDEX_SPLIT_TRANSLATION = 0
461         private const val INDEX_MENU_TRANSLATION = 1
462         private const val INDEX_COUNT_TRANSLATION = 2
463     }
464 }
465