• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2025 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.wm.shell.desktopmode
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.animation.AnimatorSet
22 import android.animation.RectEvaluator
23 import android.animation.ValueAnimator
24 import android.app.ActivityManager
25 import android.content.Context
26 import android.graphics.PixelFormat
27 import android.graphics.Rect
28 import android.graphics.drawable.LayerDrawable
29 import android.view.Display
30 import android.view.SurfaceControl
31 import android.view.SurfaceControlViewHost
32 import android.view.View
33 import android.view.WindowManager
34 import android.view.WindowlessWindowManager
35 import android.view.animation.DecelerateInterpolator
36 import android.widget.FrameLayout
37 import androidx.core.animation.doOnEnd
38 import com.android.internal.annotations.VisibleForTesting
39 import com.android.window.flags.Flags
40 import com.android.wm.shell.R
41 import com.android.wm.shell.common.DisplayController
42 import com.android.wm.shell.common.DisplayLayout
43 import com.android.wm.shell.common.ShellExecutor
44 import com.android.wm.shell.common.SyncTransactionQueue
45 import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType
46 import com.android.wm.shell.shared.annotations.ShellDesktopThread
47 import com.android.wm.shell.shared.annotations.ShellMainThread
48 import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
49 import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider
50 import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory
51 import com.android.wm.shell.windowdecor.tiling.SnapEventHandler
52 
53 /**
54  * Container for the view / viewhost of the indicator, ensuring it is created / animated off the
55  * main thread.
56  */
57 @VisibleForTesting
58 class VisualIndicatorViewContainer
59 @JvmOverloads
60 constructor(
61     @ShellDesktopThread private val desktopExecutor: ShellExecutor,
62     @ShellMainThread private val mainExecutor: ShellExecutor,
63     private val indicatorBuilder: SurfaceControl.Builder,
64     private val syncQueue: SyncTransactionQueue,
65     private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory =
66         object : SurfaceControlViewHostFactory {},
67     private val bubbleBoundsProvider: BubbleDropTargetBoundsProvider?,
68     private val snapEventHandler: SnapEventHandler,
69 ) {
70     @VisibleForTesting var indicatorView: View? = null
71     // Optional extra indicator showing the outline of the bubble bar
72     private var barIndicatorView: View? = null
73     private var indicatorViewHost: SurfaceControlViewHost? = null
74     // Below variables and the SyncTransactionQueue are the only variables that should
75     // be accessed from shell main thread. Everything else should be used exclusively
76     // from the desktop thread.
77     private var indicatorLeash: SurfaceControl? = null
78     private var isReleased = false
79 
80     /** Create a fullscreen indicator with no animation */
81     @ShellMainThread
createViewnull82     fun createView(
83         context: Context,
84         display: Display,
85         layout: DisplayLayout,
86         taskInfo: ActivityManager.RunningTaskInfo,
87         taskSurface: SurfaceControl,
88     ) {
89         if (isReleased) return
90         desktopExecutor.execute {
91             val resources = context.resources
92             val metrics = resources.displayMetrics
93             val screenWidth: Int
94             val screenHeight: Int
95             if (Flags.enableBugFixesForSecondaryDisplay()) {
96                 screenWidth = layout.width()
97                 screenHeight = layout.height()
98             } else {
99                 screenWidth = metrics.widthPixels
100                 screenHeight = metrics.heightPixels
101             }
102             indicatorView =
103                 if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
104                     FrameLayout(context)
105                 } else {
106                     View(context)
107                 }
108             val leash =
109                 indicatorBuilder
110                     .setName("Desktop Mode Visual Indicator")
111                     .setContainerLayer()
112                     .setCallsite("DesktopModeVisualIndicator.createView")
113                     .build()
114             val lp =
115                 WindowManager.LayoutParams(
116                     screenWidth,
117                     screenHeight,
118                     WindowManager.LayoutParams.TYPE_APPLICATION,
119                     WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE,
120                     PixelFormat.TRANSPARENT,
121                 )
122             lp.title = "Desktop Mode Visual Indicator"
123             lp.setTrustedOverlay()
124             lp.inputFeatures =
125                 lp.inputFeatures or WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL
126             val windowManager =
127                 WindowlessWindowManager(
128                     taskInfo.configuration,
129                     leash,
130                     /* hostInputTransferToken= */ null,
131                 )
132             indicatorViewHost =
133                 surfaceControlViewHostFactory.create(
134                     context,
135                     display,
136                     windowManager,
137                     "VisualIndicatorViewContainer",
138                 )
139             indicatorView?.let { indicatorViewHost?.setView(it, lp) }
140             showIndicator(taskSurface, leash)
141         }
142     }
143 
144     /** Reparent the indicator to {@code newParent}. */
reparentLeashnull145     fun reparentLeash(t: SurfaceControl.Transaction, newParent: SurfaceControl) {
146         val leash = indicatorLeash ?: return
147         t.reparent(leash, newParent)
148     }
149 
showIndicatornull150     private fun showIndicator(taskSurface: SurfaceControl, leash: SurfaceControl) {
151         mainExecutor.execute {
152             indicatorLeash = leash
153             val t = SurfaceControl.Transaction()
154             t.show(indicatorLeash)
155             // We want this indicator to be behind the dragged task, but in front of all others.
156             t.setRelativeLayer(indicatorLeash, taskSurface, -1)
157             syncQueue.runInSync { transaction: SurfaceControl.Transaction ->
158                 transaction.merge(t)
159                 t.close()
160             }
161         }
162     }
163 
164     @VisibleForTesting
getIndicatorBoundsnull165     fun getIndicatorBounds(): Rect {
166         return indicatorView?.background?.getBounds() ?: Rect()
167     }
168 
169     /**
170      * Takes existing indicator and animates it to bounds reflecting a new indicator type. Should
171      * only be called from the main thread.
172      */
173     @ShellMainThread
transitionIndicatornull174     fun transitionIndicator(
175         taskInfo: ActivityManager.RunningTaskInfo,
176         displayController: DisplayController,
177         currentType: IndicatorType,
178         newType: IndicatorType,
179     ) {
180         if (currentType == newType || isReleased) return
181         desktopExecutor.execute {
182             val layout =
183                 displayController.getDisplayLayout(taskInfo.displayId)
184                     ?: error("Expected to find DisplayLayout for taskId${taskInfo.taskId}.")
185             if (currentType == IndicatorType.NO_INDICATOR) {
186                 fadeInIndicatorInternal(layout, newType, taskInfo.displayId, snapEventHandler)
187             } else if (newType == IndicatorType.NO_INDICATOR) {
188                 fadeOutIndicator(
189                     layout,
190                     currentType,
191                     /* finishCallback= */ null,
192                     taskInfo.displayId,
193                     snapEventHandler,
194                 )
195             } else {
196                 val animStartType = IndicatorType.valueOf(currentType.name)
197                 val indicator = indicatorView ?: return@execute
198                 var animator: Animator =
199                     VisualIndicatorAnimator.animateIndicatorType(
200                         indicator,
201                         layout,
202                         animStartType,
203                         newType,
204                         bubbleBoundsProvider,
205                         taskInfo.displayId,
206                         snapEventHandler,
207                     )
208                 if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
209                     if (currentType.isBubbleType() || newType.isBubbleType()) {
210                         animator = addBarIndicatorAnimation(animator, currentType, newType)
211                     }
212                 }
213                 animator.start()
214             }
215         }
216     }
217 
addBarIndicatorAnimationnull218     private fun addBarIndicatorAnimation(
219         visualIndicatorAnimator: Animator,
220         currentType: IndicatorType,
221         newType: IndicatorType,
222     ): Animator {
223         if (newType.isBubbleType()) {
224             getOrCreateBubbleBarIndicator(newType)?.let { bar ->
225                 return AnimatorSet().apply {
226                     playTogether(visualIndicatorAnimator, fadeBarIndicatorIn(bar))
227                 }
228             }
229         }
230         if (currentType.isBubbleType()) {
231             barIndicatorView?.let { bar ->
232                 barIndicatorView = null
233                 return AnimatorSet().apply {
234                     playTogether(visualIndicatorAnimator, fadeBarIndicatorOut(bar))
235                 }
236             }
237         }
238         return visualIndicatorAnimator
239     }
240 
241     /**
242      * Fade indicator in as provided type.
243      *
244      * Animator fades the indicator in while expanding the bounds outwards.
245      */
fadeInIndicatornull246     fun fadeInIndicator(layout: DisplayLayout, type: IndicatorType, displayId: Int) {
247         if (isReleased) return
248         desktopExecutor.execute {
249             fadeInIndicatorInternal(layout, type, displayId, snapEventHandler)
250         }
251     }
252 
253     /**
254      * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards.
255      */
256     @VisibleForTesting
fadeInIndicatorInternalnull257     fun fadeInIndicatorInternal(
258         layout: DisplayLayout,
259         type: IndicatorType,
260         displayId: Int,
261         snapEventHandler: SnapEventHandler,
262     ) {
263         desktopExecutor.assertCurrentThread()
264         indicatorView?.let { indicator ->
265             indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background)
266             var animator: Animator =
267                 VisualIndicatorAnimator.fadeBoundsIn(
268                     indicator,
269                     type,
270                     layout,
271                     bubbleBoundsProvider,
272                     displayId,
273                     snapEventHandler,
274                 )
275             if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
276                 animator = addBarIndicatorAnimation(animator, IndicatorType.NO_INDICATOR, type)
277             }
278             animator.start()
279         }
280     }
281 
282     /**
283      * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds.
284      *
285      * @param finishCallback called when animation ends or gets cancelled
286      */
fadeOutIndicatornull287     fun fadeOutIndicator(
288         layout: DisplayLayout,
289         currentType: IndicatorType,
290         finishCallback: Runnable?,
291         displayId: Int,
292         snapEventHandler: SnapEventHandler,
293     ) {
294         if (currentType == IndicatorType.NO_INDICATOR) {
295             // In rare cases, fade out can be requested before the indicator has determined its
296             // initial type and started animating in. In this case, no animator is needed.
297             finishCallback?.run()
298             return
299         }
300         desktopExecutor.execute {
301             indicatorView?.let {
302                 val animStartType = IndicatorType.valueOf(currentType.name)
303                 var animator: Animator =
304                     VisualIndicatorAnimator.fadeBoundsOut(
305                         it,
306                         animStartType,
307                         layout,
308                         bubbleBoundsProvider,
309                         displayId,
310                         snapEventHandler,
311                     )
312                 if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) {
313                     animator =
314                         addBarIndicatorAnimation(animator, currentType, IndicatorType.NO_INDICATOR)
315                 }
316                 animator.addListener(
317                     object : AnimatorListenerAdapter() {
318                         override fun onAnimationEnd(animation: Animator) {
319                             if (finishCallback != null) {
320                                 mainExecutor.execute(finishCallback)
321                             }
322                         }
323                     }
324                 )
325                 animator.start()
326             }
327         }
328     }
329 
330     /** Release the indicator and its components when it is no longer needed. */
331     @ShellMainThread
releaseVisualIndicatornull332     fun releaseVisualIndicator() {
333         if (isReleased) return
334         desktopExecutor.execute {
335             indicatorViewHost?.release()
336             indicatorViewHost = null
337         }
338         indicatorLeash?.let {
339             val tx = SurfaceControl.Transaction()
340             tx.remove(it)
341             indicatorLeash = null
342             syncQueue.runInSync { transaction: SurfaceControl.Transaction ->
343                 transaction.merge(tx)
344                 tx.close()
345             }
346         }
347         isReleased = true
348     }
349 
getOrCreateBubbleBarIndicatornull350     private fun getOrCreateBubbleBarIndicator(type: IndicatorType): View? {
351         val container = indicatorView as? FrameLayout ?: return null
352         val onLeft = type == IndicatorType.TO_BUBBLE_LEFT_INDICATOR
353         val bounds = bubbleBoundsProvider?.getBarDropTargetBounds(onLeft) ?: return null
354         val lp = FrameLayout.LayoutParams(bounds.width(), bounds.height())
355         lp.leftMargin = bounds.left
356         lp.topMargin = bounds.top
357         if (barIndicatorView == null) {
358             val indicator = View(container.context)
359             indicator.setBackgroundResource(R.drawable.desktop_windowing_transition_background)
360             container.addView(indicator, lp)
361             barIndicatorView = indicator
362         } else {
363             barIndicatorView?.layoutParams = lp
364         }
365         return barIndicatorView
366     }
367 
fadeBarIndicatorInnull368     private fun fadeBarIndicatorIn(barIndicator: View): Animator {
369         // Use layout bounds as the end bounds in case the view has not been laid out yet
370         val lp = barIndicator.layoutParams
371         val endBounds = Rect(0, 0, lp.width, lp.height)
372         return VisualIndicatorAnimator.fadeBoundsIn(barIndicator, endBounds)
373     }
374 
fadeBarIndicatorOutnull375     private fun fadeBarIndicatorOut(barIndicator: View): Animator {
376         val startBounds = Rect(0, 0, barIndicator.width, barIndicator.height)
377         val barAnimator = VisualIndicatorAnimator.fadeBoundsOut(barIndicator, startBounds)
378         barAnimator.doOnEnd { (indicatorView as? FrameLayout)?.removeView(barIndicator) }
379         return barAnimator
380     }
381 
382     /**
383      * Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions
384      * should only be called from the desktop executor.
385      */
386     @VisibleForTesting
387     class VisualIndicatorAnimator(view: View, startBounds: Rect, endBounds: Rect) :
388         ValueAnimator() {
389         /**
390          * Determines how this animator will interact with the view's alpha: Fade in, fade out, or
391          * no change to alpha
392          */
393         private enum class AlphaAnimType {
394             ALPHA_FADE_IN_ANIM,
395             ALPHA_FADE_OUT_ANIM,
396             ALPHA_NO_CHANGE_ANIM,
397         }
398 
399         private val indicatorView: View = view
400         @VisibleForTesting val indicatorStartBounds = Rect(startBounds)
401         @VisibleForTesting val indicatorEndBounds = endBounds
402         private val mRectEvaluator: RectEvaluator
403 
404         init {
405             setFloatValues(0f, 1f)
406             mRectEvaluator = RectEvaluator(Rect())
407         }
408 
409         /**
410          * Update bounds of view based on current animation fraction. Use of delta is to animate
411          * bounds independently, in case we need to run multiple animations simultaneously.
412          *
413          * @param fraction fraction to use, compared against previous fraction
414          * @param view the view to update
415          */
416         @ShellDesktopThread
updateBoundsnull417         private fun updateBounds(fraction: Float, view: View?) {
418             if (indicatorStartBounds == indicatorEndBounds) {
419                 return
420             }
421             val currentBounds =
422                 mRectEvaluator.evaluate(fraction, indicatorStartBounds, indicatorEndBounds)
423             view?.background?.bounds = currentBounds
424         }
425 
426         /**
427          * Fade in the fullscreen indicator
428          *
429          * @param fraction current animation fraction
430          */
431         @ShellDesktopThread
updateIndicatorAlphanull432         private fun updateIndicatorAlpha(fraction: Float, view: View?) {
433             val drawable = view?.background as LayerDrawable
434             drawable.findDrawableByLayerId(R.id.indicator_stroke).alpha =
435                 (MAXIMUM_OPACITY * fraction).toInt()
436             drawable.findDrawableByLayerId(R.id.indicator_solid).alpha =
437                 (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY).toInt()
438         }
439 
440         companion object {
441             private const val FULLSCREEN_INDICATOR_DURATION = 200
442             private const val FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f
443             private const val INDICATOR_FINAL_OPACITY = 0.35f
444             private const val MAXIMUM_OPACITY = 255
445 
446             @ShellDesktopThread
fadeBoundsInnull447             fun fadeBoundsIn(
448                 view: View,
449                 type: IndicatorType,
450                 displayLayout: DisplayLayout,
451                 bubbleBoundsProvider: BubbleDropTargetBoundsProvider?,
452                 displayId: Int,
453                 snapEventHandler: SnapEventHandler,
454             ): VisualIndicatorAnimator {
455                 val endBounds =
456                     getIndicatorBounds(
457                         displayLayout,
458                         type,
459                         bubbleBoundsProvider,
460                         displayId,
461                         snapEventHandler,
462                     )
463                 return fadeBoundsIn(view, endBounds)
464             }
465 
466             @ShellDesktopThread
fadeBoundsInnull467             fun fadeBoundsIn(view: View, endBounds: Rect): VisualIndicatorAnimator {
468                 val startBounds = getMinBounds(endBounds)
469                 view.background.bounds = startBounds
470                 val animator = VisualIndicatorAnimator(view, startBounds, endBounds)
471                 animator.interpolator = DecelerateInterpolator()
472                 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM)
473                 return animator
474             }
475 
476             @ShellDesktopThread
fadeBoundsOutnull477             fun fadeBoundsOut(
478                 view: View,
479                 type: IndicatorType,
480                 displayLayout: DisplayLayout,
481                 bubbleBoundsProvider: BubbleDropTargetBoundsProvider?,
482                 displayId: Int,
483                 snapEventHandler: SnapEventHandler,
484             ): VisualIndicatorAnimator {
485                 val startBounds =
486                     getIndicatorBounds(
487                         displayLayout,
488                         type,
489                         bubbleBoundsProvider,
490                         displayId,
491                         snapEventHandler,
492                     )
493                 return fadeBoundsOut(view, startBounds)
494             }
495 
496             @ShellDesktopThread
fadeBoundsOutnull497             fun fadeBoundsOut(view: View, startBounds: Rect): VisualIndicatorAnimator {
498                 val endBounds = getMinBounds(startBounds)
499                 view.background.bounds = startBounds
500                 val animator = VisualIndicatorAnimator(view, startBounds, endBounds)
501                 animator.interpolator = DecelerateInterpolator()
502                 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM)
503                 return animator
504             }
505 
506             /**
507              * Create animator for visual indicator changing type (i.e., fullscreen to freeform,
508              * freeform to split, etc.)
509              *
510              * @param view the view for this indicator
511              * @param displayLayout information about the display the transitioning task is
512              *   currently on
513              * @param origType the original indicator type
514              * @param newType the new indicator type
515              * @param desktopExecutor: the executor for the ShellDesktopThread; should be the only
516              *   thread this function runs on
517              */
518             @ShellDesktopThread
animateIndicatorTypenull519             fun animateIndicatorType(
520                 view: View,
521                 displayLayout: DisplayLayout,
522                 origType: IndicatorType,
523                 newType: IndicatorType,
524                 bubbleBoundsProvider: BubbleDropTargetBoundsProvider?,
525                 displayId: Int,
526                 snapEventHandler: SnapEventHandler,
527             ): VisualIndicatorAnimator {
528                 val startBounds =
529                     getIndicatorBounds(
530                         displayLayout,
531                         origType,
532                         bubbleBoundsProvider,
533                         displayId,
534                         snapEventHandler,
535                     )
536                 val endBounds =
537                     getIndicatorBounds(
538                         displayLayout,
539                         newType,
540                         bubbleBoundsProvider,
541                         displayId,
542                         snapEventHandler,
543                     )
544                 val animator = VisualIndicatorAnimator(view, startBounds, endBounds)
545                 animator.interpolator = DecelerateInterpolator()
546                 setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM)
547                 return animator
548             }
549 
550             /** Calculates the bounds the indicator should have when fully faded in. */
getIndicatorBoundsnull551             private fun getIndicatorBounds(
552                 layout: DisplayLayout,
553                 type: IndicatorType,
554                 bubbleBoundsProvider: BubbleDropTargetBoundsProvider?,
555                 displayId: Int,
556                 snapEventHandler: SnapEventHandler,
557             ): Rect {
558                 val desktopStableBounds = Rect()
559                 layout.getStableBounds(desktopStableBounds)
560                 val padding = desktopStableBounds.top
561                 when (type) {
562                     IndicatorType.TO_FULLSCREEN_INDICATOR -> {
563                         desktopStableBounds.top += padding
564                         desktopStableBounds.bottom -= padding
565                         desktopStableBounds.left += padding
566                         desktopStableBounds.right -= padding
567                         return desktopStableBounds
568                     }
569 
570                     IndicatorType.TO_DESKTOP_INDICATOR -> {
571                         val adjustmentPercentage =
572                             (1f - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE)
573                         return Rect(
574                             (adjustmentPercentage * desktopStableBounds.width() / 2).toInt(),
575                             (adjustmentPercentage * desktopStableBounds.height() / 2).toInt(),
576                             (desktopStableBounds.width() -
577                                     (adjustmentPercentage * desktopStableBounds.width() / 2))
578                                 .toInt(),
579                             (desktopStableBounds.height() -
580                                     (adjustmentPercentage * desktopStableBounds.height() / 2))
581                                 .toInt(),
582                         )
583                     }
584 
585                     IndicatorType.TO_SPLIT_LEFT_INDICATOR -> {
586                         val currentLeftBounds = snapEventHandler.getLeftSnapBoundsIfTiled(displayId)
587                         return Rect(
588                             padding,
589                             padding,
590                             currentLeftBounds.right - padding,
591                             desktopStableBounds.height(),
592                         )
593                     }
594                     IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> {
595                         val currentRightBounds =
596                             snapEventHandler.getRightSnapBoundsIfTiled(displayId)
597                         return Rect(
598                             currentRightBounds.left + padding,
599                             padding,
600                             desktopStableBounds.width() - padding,
601                             desktopStableBounds.height(),
602                         )
603                     }
604                     IndicatorType.TO_BUBBLE_LEFT_INDICATOR ->
605                         return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds(
606                             /* onLeft= */ true
607                         ) ?: Rect()
608                     IndicatorType.TO_BUBBLE_RIGHT_INDICATOR ->
609                         return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds(
610                             /* onLeft= */ false
611                         ) ?: Rect()
612                     else -> throw IllegalArgumentException("Invalid indicator type provided.")
613                 }
614             }
615 
616             /** Add necessary listener for animation of indicator */
setupIndicatorAnimationnull617             private fun setupIndicatorAnimation(
618                 animator: VisualIndicatorAnimator,
619                 animType: AlphaAnimType,
620             ) {
621                 animator.addUpdateListener { a: ValueAnimator ->
622                     animator.updateBounds(a.animatedFraction, animator.indicatorView)
623                     if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) {
624                         animator.updateIndicatorAlpha(a.animatedFraction, animator.indicatorView)
625                     } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) {
626                         animator.updateIndicatorAlpha(
627                             1 - a.animatedFraction,
628                             animator.indicatorView,
629                         )
630                     }
631                 }
632                 animator.addListener(
633                     object : AnimatorListenerAdapter() {
634                         override fun onAnimationEnd(animation: Animator) {
635                             animator.indicatorView.background.bounds = animator.indicatorEndBounds
636                         }
637                     }
638                 )
639                 animator.setDuration(FULLSCREEN_INDICATOR_DURATION.toLong())
640             }
641 
642             /**
643              * Return the minimum bounds of a visual indicator, to be used at the end of fading out
644              * and the start of fading in.
645              */
getMinBoundsnull646             private fun getMinBounds(maxBounds: Rect): Rect {
647                 return Rect(
648                     (maxBounds.left + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width()))
649                         .toInt(),
650                     (maxBounds.top + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height()))
651                         .toInt(),
652                     (maxBounds.right - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width()))
653                         .toInt(),
654                     (maxBounds.bottom - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height()))
655                         .toInt(),
656                 )
657             }
658         }
659     }
660 
IndicatorTypenull661     private fun IndicatorType.isBubbleType(): Boolean {
662         return this == IndicatorType.TO_BUBBLE_LEFT_INDICATOR ||
663             this == IndicatorType.TO_BUBBLE_RIGHT_INDICATOR
664     }
665 }
666