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 
17 package com.android.systemui.biometrics.ui.binder
18 
19 import android.animation.Animator
20 import android.animation.AnimatorSet
21 import android.animation.ValueAnimator
22 import android.graphics.Outline
23 import android.graphics.Rect
24 import android.transition.AutoTransition
25 import android.transition.TransitionManager
26 import android.util.TypedValue
27 import android.view.Surface
28 import android.view.View
29 import android.view.ViewGroup
30 import android.view.ViewOutlineProvider
31 import android.view.WindowInsets
32 import android.view.WindowManager
33 import android.view.accessibility.AccessibilityManager
34 import android.widget.ImageView
35 import android.widget.TextView
36 import androidx.constraintlayout.widget.ConstraintLayout
37 import androidx.constraintlayout.widget.ConstraintSet
38 import androidx.constraintlayout.widget.Guideline
39 import androidx.core.animation.addListener
40 import androidx.core.view.doOnLayout
41 import androidx.core.view.isGone
42 import androidx.lifecycle.lifecycleScope
43 import com.android.systemui.Flags.constraintBp
44 import com.android.systemui.biometrics.AuthPanelController
45 import com.android.systemui.biometrics.Utils
46 import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
47 import com.android.systemui.biometrics.ui.viewmodel.PromptSize
48 import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
49 import com.android.systemui.biometrics.ui.viewmodel.isLarge
50 import com.android.systemui.biometrics.ui.viewmodel.isLeft
51 import com.android.systemui.biometrics.ui.viewmodel.isMedium
52 import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
53 import com.android.systemui.biometrics.ui.viewmodel.isSmall
54 import com.android.systemui.biometrics.ui.viewmodel.isTop
55 import com.android.systemui.lifecycle.repeatWhenAttached
56 import com.android.systemui.res.R
57 import kotlin.math.abs
58 import kotlinx.coroutines.flow.combine
59 import kotlinx.coroutines.flow.first
60 import kotlinx.coroutines.launch
61 
62 /** Helper for [BiometricViewBinder] to handle resize transitions. */
63 object BiometricViewSizeBinder {
64 
65     private const val ANIMATE_SMALL_TO_MEDIUM_DURATION_MS = 150
66     // TODO(b/201510778): make private when related misuse is fixed
67     const val ANIMATE_MEDIUM_TO_LARGE_DURATION_MS = 450
68 
69     /** Resizes [BiometricPromptLayout] and the [panelViewController] via the [PromptViewModel]. */
70     fun bind(
71         view: View,
72         viewModel: PromptViewModel,
73         viewsToHideWhenSmall: List<View>,
74         viewsToFadeInOnSizeChange: List<View>,
75         panelViewController: AuthPanelController?,
76         jankListener: BiometricJankListener,
77     ) {
78         val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java))
79         val accessibilityManager =
80             requireNotNull(view.context.getSystemService(AccessibilityManager::class.java))
81 
82         fun notifyAccessibilityChanged() {
83             Utils.notifyAccessibilityContentChanged(accessibilityManager, view as ViewGroup)
84         }
85 
86         fun startMonitoredAnimation(animators: List<Animator>) {
87             with(AnimatorSet()) {
88                 addListener(jankListener)
89                 addListener(onEnd = { notifyAccessibilityChanged() })
90                 play(animators.first()).apply { animators.drop(1).forEach { next -> with(next) } }
91                 start()
92             }
93         }
94 
95         if (constraintBp()) {
96             val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
97             val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline)
98             val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
99             val midGuideline = view.findViewById<Guideline>(R.id.midGuideline)
100 
101             val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
102             val panelView = view.requireViewById<View>(R.id.panel)
103             val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
104             val pxToDp =
105                 TypedValue.applyDimension(
106                     TypedValue.COMPLEX_UNIT_DIP,
107                     1f,
108                     view.resources.displayMetrics
109                 )
110             val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
111 
112             var currentSize: PromptSize? = null
113             var currentPosition: PromptPosition = PromptPosition.Bottom
114             panelView.outlineProvider =
115                 object : ViewOutlineProvider() {
116                     override fun getOutline(view: View, outline: Outline) {
117                         when (currentPosition) {
118                             PromptPosition.Right -> {
119                                 outline.setRoundRect(
120                                     0,
121                                     0,
122                                     view.width + cornerRadiusPx,
123                                     view.height,
124                                     cornerRadiusPx.toFloat()
125                                 )
126                             }
127                             PromptPosition.Left -> {
128                                 outline.setRoundRect(
129                                     -cornerRadiusPx,
130                                     0,
131                                     view.width,
132                                     view.height,
133                                     cornerRadiusPx.toFloat()
134                                 )
135                             }
136                             PromptPosition.Bottom,
137                             PromptPosition.Top -> {
138                                 outline.setRoundRect(
139                                     0,
140                                     0,
141                                     view.width,
142                                     view.height + cornerRadiusPx,
143                                     cornerRadiusPx.toFloat()
144                                 )
145                             }
146                         }
147                     }
148                 }
149 
150             // ConstraintSets for animating between prompt sizes
151             val mediumConstraintSet = ConstraintSet()
152             mediumConstraintSet.clone(view as ConstraintLayout)
153 
154             val smallConstraintSet = ConstraintSet()
155             smallConstraintSet.clone(mediumConstraintSet)
156 
157             val largeConstraintSet = ConstraintSet()
158             largeConstraintSet.clone(mediumConstraintSet)
159             largeConstraintSet.constrainMaxWidth(R.id.panel, 0)
160             largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0)
161             largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0)
162 
163             // TODO: Investigate better way to handle 180 rotations
164             val flipConstraintSet = ConstraintSet()
165 
166             view.doOnLayout {
167                 fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
168                     viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
169                     largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
170                     largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
171                     largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
172                     largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
173 
174                     if (hideSensorIcon) {
175                         smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
176                         smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
177                         smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
178                         mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
179                         mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
180                         mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
181                     }
182                 }
183 
184                 view.repeatWhenAttached {
185                     lifecycleScope.launch {
186                         viewModel.iconPosition.collect { position ->
187                             if (position != Rect()) {
188                                 val iconParams =
189                                     iconHolderView.layoutParams as ConstraintLayout.LayoutParams
190 
191                                 if (position.left != 0) {
192                                     iconParams.endToEnd = ConstraintSet.UNSET
193                                     iconParams.leftMargin = position.left
194                                     mediumConstraintSet.clear(
195                                         R.id.biometric_icon,
196                                         ConstraintSet.RIGHT
197                                     )
198                                     mediumConstraintSet.connect(
199                                         R.id.biometric_icon,
200                                         ConstraintSet.LEFT,
201                                         ConstraintSet.PARENT_ID,
202                                         ConstraintSet.LEFT
203                                     )
204                                     mediumConstraintSet.setMargin(
205                                         R.id.biometric_icon,
206                                         ConstraintSet.LEFT,
207                                         position.left
208                                     )
209                                     smallConstraintSet.clear(
210                                         R.id.biometric_icon,
211                                         ConstraintSet.RIGHT
212                                     )
213                                     smallConstraintSet.connect(
214                                         R.id.biometric_icon,
215                                         ConstraintSet.LEFT,
216                                         ConstraintSet.PARENT_ID,
217                                         ConstraintSet.LEFT
218                                     )
219                                     smallConstraintSet.setMargin(
220                                         R.id.biometric_icon,
221                                         ConstraintSet.LEFT,
222                                         position.left
223                                     )
224                                 }
225                                 if (position.top != 0) {
226                                     iconParams.bottomToBottom = ConstraintSet.UNSET
227                                     iconParams.topMargin = position.top
228                                     mediumConstraintSet.clear(
229                                         R.id.biometric_icon,
230                                         ConstraintSet.BOTTOM
231                                     )
232                                     mediumConstraintSet.setMargin(
233                                         R.id.biometric_icon,
234                                         ConstraintSet.TOP,
235                                         position.top
236                                     )
237                                     smallConstraintSet.clear(
238                                         R.id.biometric_icon,
239                                         ConstraintSet.BOTTOM
240                                     )
241                                     smallConstraintSet.setMargin(
242                                         R.id.biometric_icon,
243                                         ConstraintSet.TOP,
244                                         position.top
245                                     )
246                                 }
247                                 if (position.right != 0) {
248                                     iconParams.startToStart = ConstraintSet.UNSET
249                                     iconParams.rightMargin = position.right
250                                     mediumConstraintSet.clear(
251                                         R.id.biometric_icon,
252                                         ConstraintSet.LEFT
253                                     )
254                                     mediumConstraintSet.connect(
255                                         R.id.biometric_icon,
256                                         ConstraintSet.RIGHT,
257                                         ConstraintSet.PARENT_ID,
258                                         ConstraintSet.RIGHT
259                                     )
260                                     mediumConstraintSet.setMargin(
261                                         R.id.biometric_icon,
262                                         ConstraintSet.RIGHT,
263                                         position.right
264                                     )
265                                     smallConstraintSet.clear(
266                                         R.id.biometric_icon,
267                                         ConstraintSet.LEFT
268                                     )
269                                     smallConstraintSet.connect(
270                                         R.id.biometric_icon,
271                                         ConstraintSet.RIGHT,
272                                         ConstraintSet.PARENT_ID,
273                                         ConstraintSet.RIGHT
274                                     )
275                                     smallConstraintSet.setMargin(
276                                         R.id.biometric_icon,
277                                         ConstraintSet.RIGHT,
278                                         position.right
279                                     )
280                                 }
281                                 if (position.bottom != 0) {
282                                     iconParams.topToTop = ConstraintSet.UNSET
283                                     iconParams.bottomMargin = position.bottom
284                                     mediumConstraintSet.clear(
285                                         R.id.biometric_icon,
286                                         ConstraintSet.TOP
287                                     )
288                                     mediumConstraintSet.setMargin(
289                                         R.id.biometric_icon,
290                                         ConstraintSet.BOTTOM,
291                                         position.bottom
292                                     )
293                                     smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
294                                     smallConstraintSet.setMargin(
295                                         R.id.biometric_icon,
296                                         ConstraintSet.BOTTOM,
297                                         position.bottom
298                                     )
299                                 }
300                                 iconHolderView.layoutParams = iconParams
301                             }
302                         }
303                     }
304 
305                     lifecycleScope.launch {
306                         viewModel.iconSize.collect { iconSize ->
307                             iconHolderView.layoutParams.width = iconSize.first
308                             iconHolderView.layoutParams.height = iconSize.second
309                             mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first)
310                             mediumConstraintSet.constrainHeight(
311                                 R.id.biometric_icon,
312                                 iconSize.second
313                             )
314                         }
315                     }
316 
317                     lifecycleScope.launch {
318                         viewModel.guidelineBounds.collect { bounds ->
319                             val bottomInset =
320                                 windowManager.maximumWindowMetrics.windowInsets
321                                     .getInsets(WindowInsets.Type.navigationBars())
322                                     .bottom
323                             mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset)
324 
325                             if (bounds.left >= 0) {
326                                 mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
327                                 smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
328                             } else if (bounds.left < 0) {
329                                 mediumConstraintSet.setGuidelineEnd(
330                                     leftGuideline.id,
331                                     abs(bounds.left)
332                                 )
333                                 smallConstraintSet.setGuidelineEnd(
334                                     leftGuideline.id,
335                                     abs(bounds.left)
336                                 )
337                             }
338 
339                             if (bounds.right >= 0) {
340                                 mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
341                                 smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
342                             } else if (bounds.right < 0) {
343                                 mediumConstraintSet.setGuidelineBegin(
344                                     rightGuideline.id,
345                                     abs(bounds.right)
346                                 )
347                                 smallConstraintSet.setGuidelineBegin(
348                                     rightGuideline.id,
349                                     abs(bounds.right)
350                                 )
351                             }
352 
353                             if (bounds.top >= 0) {
354                                 mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
355                                 smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
356                             } else if (bounds.top < 0) {
357                                 mediumConstraintSet.setGuidelineEnd(
358                                     topGuideline.id,
359                                     abs(bounds.top)
360                                 )
361                                 smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
362                             }
363 
364                             if (midGuideline != null) {
365                                 val left =
366                                     if (bounds.left >= 0) {
367                                         abs(bounds.left)
368                                     } else {
369                                         view.width - abs(bounds.left)
370                                     }
371                                 val right =
372                                     if (bounds.right >= 0) {
373                                         view.width - abs(bounds.right)
374                                     } else {
375                                         abs(bounds.right)
376                                     }
377                                 val mid = (left + right) / 2
378                                 mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid)
379                             }
380                         }
381                     }
382 
383                     lifecycleScope.launch {
384                         combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect {
385                             (hideSensorIcon, size) ->
386                             setVisibilities(hideSensorIcon, size)
387                         }
388                     }
389 
390                     lifecycleScope.launch {
391                         combine(viewModel.position, viewModel.size, ::Pair).collect {
392                             (position, size) ->
393                             if (position.isLeft) {
394                                 if (size.isSmall) {
395                                     flipConstraintSet.clone(smallConstraintSet)
396                                 } else {
397                                     flipConstraintSet.clone(mediumConstraintSet)
398                                 }
399 
400                                 // Move all content to other panel
401                                 flipConstraintSet.connect(
402                                     R.id.scrollView,
403                                     ConstraintSet.LEFT,
404                                     R.id.midGuideline,
405                                     ConstraintSet.LEFT
406                                 )
407                                 flipConstraintSet.connect(
408                                     R.id.scrollView,
409                                     ConstraintSet.RIGHT,
410                                     R.id.rightGuideline,
411                                     ConstraintSet.RIGHT
412                                 )
413                             } else if (position.isTop) {
414                                 // Top position is only used for 180 rotation Udfps
415                                 // Requires repositioning due to sensor location at top of screen
416                                 mediumConstraintSet.connect(
417                                     R.id.scrollView,
418                                     ConstraintSet.TOP,
419                                     R.id.indicator,
420                                     ConstraintSet.BOTTOM
421                                 )
422                                 mediumConstraintSet.connect(
423                                     R.id.scrollView,
424                                     ConstraintSet.BOTTOM,
425                                     R.id.button_bar,
426                                     ConstraintSet.TOP
427                                 )
428                                 mediumConstraintSet.connect(
429                                     R.id.panel,
430                                     ConstraintSet.TOP,
431                                     R.id.biometric_icon,
432                                     ConstraintSet.TOP
433                                 )
434                                 mediumConstraintSet.setMargin(
435                                     R.id.panel,
436                                     ConstraintSet.TOP,
437                                     (-24 * pxToDp).toInt()
438                                 )
439                                 mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
440                             }
441 
442                             when {
443                                 size.isSmall -> {
444                                     if (position.isLeft) {
445                                         flipConstraintSet.applyTo(view)
446                                     } else {
447                                         smallConstraintSet.applyTo(view)
448                                     }
449                                 }
450                                 size.isMedium && currentSize.isSmall -> {
451                                     val autoTransition = AutoTransition()
452                                     autoTransition.setDuration(
453                                         ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
454                                     )
455 
456                                     TransitionManager.beginDelayedTransition(view, autoTransition)
457 
458                                     if (position.isLeft) {
459                                         flipConstraintSet.applyTo(view)
460                                     } else {
461                                         mediumConstraintSet.applyTo(view)
462                                     }
463                                 }
464                                 size.isMedium -> {
465                                     if (position.isLeft) {
466                                         flipConstraintSet.applyTo(view)
467                                     } else {
468                                         mediumConstraintSet.applyTo(view)
469                                     }
470                                 }
471                                 size.isLarge && currentSize.isMedium -> {
472                                     val autoTransition = AutoTransition()
473                                     autoTransition.setDuration(
474                                         ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
475                                     )
476 
477                                     TransitionManager.beginDelayedTransition(view, autoTransition)
478                                     largeConstraintSet.applyTo(view)
479                                 }
480                             }
481 
482                             currentSize = size
483                             currentPosition = position
484                             notifyAccessibilityChanged()
485 
486                             panelView.invalidateOutline()
487                             view.invalidate()
488                             view.requestLayout()
489                         }
490                     }
491                 }
492             }
493         } else if (panelViewController != null) {
494             val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
495             val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
496             val fullSizeYOffset =
497                 view.resources.getDimension(
498                     R.dimen.biometric_dialog_medium_to_large_translation_offset
499                 )
500 
501             // cache the original position of the icon view (as done in legacy view)
502             // this must happen before any size changes can be made
503             view.doOnLayout {
504                 // TODO(b/251476085): this old way of positioning has proven itself unreliable
505                 // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
506                 // pin to the physical sensor
507                 val iconHolderOriginalY = iconHolderView.y
508 
509                 // bind to prompt
510                 // TODO(b/251476085): migrate the legacy panel controller and simplify this
511                 view.repeatWhenAttached {
512                     var currentSize: PromptSize? = null
513                     lifecycleScope.launch {
514                         /**
515                          * View is only set visible in BiometricViewSizeBinder once PromptSize is
516                          * determined that accounts for iconView size, to prevent prompt resizing
517                          * being visible to the user.
518                          *
519                          * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
520                          *   layout is implemented
521                          */
522                         combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
523                             (isIconViewLoaded, size) ->
524                             if (!isIconViewLoaded) {
525                                 return@collect
526                             }
527 
528                             // prepare for animated size transitions
529                             for (v in viewsToHideWhenSmall) {
530                                 v.showContentOrHide(forceHide = size.isSmall)
531                             }
532 
533                             if (viewModel.hideSensorIcon.first()) {
534                                 iconHolderView.visibility = View.GONE
535                             }
536 
537                             if (currentSize == null && size.isSmall) {
538                                 iconHolderView.alpha = 0f
539                             }
540                             if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
541                                 viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
542                             }
543 
544                             // propagate size changes to legacy panel controller and animate
545                             // transitions
546                             view.doOnLayout {
547                                 val width = view.measuredWidth
548                                 val height = view.measuredHeight
549 
550                                 when {
551                                     size.isSmall -> {
552                                         iconHolderView.alpha = 1f
553                                         val bottomInset =
554                                             windowManager.maximumWindowMetrics.windowInsets
555                                                 .getInsets(WindowInsets.Type.navigationBars())
556                                                 .bottom
557                                         iconHolderView.y =
558                                             if (view.isLandscape()) {
559                                                 (view.height -
560                                                     iconHolderView.height -
561                                                     bottomInset) / 2f
562                                             } else {
563                                                 view.height -
564                                                     iconHolderView.height -
565                                                     iconPadding -
566                                                     bottomInset
567                                             }
568                                         val newHeight =
569                                             iconHolderView.height + (2 * iconPadding.toInt()) -
570                                                 iconHolderView.paddingTop -
571                                                 iconHolderView.paddingBottom
572                                         panelViewController.updateForContentDimensions(
573                                             width,
574                                             newHeight + bottomInset,
575                                             0, /* animateDurationMs */
576                                         )
577                                     }
578                                     size.isMedium && currentSize.isSmall -> {
579                                         val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
580                                         panelViewController.updateForContentDimensions(
581                                             width,
582                                             height,
583                                             duration,
584                                         )
585                                         startMonitoredAnimation(
586                                             listOf(
587                                                 iconHolderView.asVerticalAnimator(
588                                                     duration = duration.toLong(),
589                                                     toY =
590                                                         iconHolderOriginalY -
591                                                             viewsToHideWhenSmall
592                                                                 .filter { it.isGone }
593                                                                 .sumOf { it.height },
594                                                 ),
595                                                 viewsToFadeInOnSizeChange.asFadeInAnimator(
596                                                     duration = duration.toLong(),
597                                                     delay = duration.toLong(),
598                                                 ),
599                                             )
600                                         )
601                                     }
602                                     size.isMedium && currentSize.isNullOrNotSmall -> {
603                                         panelViewController.updateForContentDimensions(
604                                             width,
605                                             height,
606                                             0, /* animateDurationMs */
607                                         )
608                                     }
609                                     size.isLarge -> {
610                                         val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
611                                         panelViewController.setUseFullScreen(true)
612                                         panelViewController.updateForContentDimensions(
613                                             panelViewController.containerWidth,
614                                             panelViewController.containerHeight,
615                                             duration,
616                                         )
617 
618                                         startMonitoredAnimation(
619                                             listOf(
620                                                 view.asVerticalAnimator(
621                                                     duration.toLong() * 2 / 3,
622                                                     toY = view.y - fullSizeYOffset
623                                                 ),
624                                                 listOf(view)
625                                                     .asFadeInAnimator(
626                                                         duration = duration.toLong() / 2,
627                                                         delay = duration.toLong(),
628                                                     ),
629                                             )
630                                         )
631                                         // TODO(b/251476085): clean up (copied from legacy)
632                                         if (view.isAttachedToWindow) {
633                                             val parent = view.parent as? ViewGroup
634                                             parent?.removeView(view)
635                                         }
636                                     }
637                                 }
638 
639                                 currentSize = size
640                                 view.visibility = View.VISIBLE
641                                 viewModel.setIsIconViewLoaded(false)
642                                 notifyAccessibilityChanged()
643                             }
644                         }
645                     }
646                 }
647             }
648         }
649     }
650 }
651 
isLandscapenull652 private fun View.isLandscape(): Boolean {
653     val r = context.display.rotation
654     return if (
655         context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
656     ) {
657         r == Surface.ROTATION_0 || r == Surface.ROTATION_180
658     } else {
659         r == Surface.ROTATION_90 || r == Surface.ROTATION_270
660     }
661 }
662 
showContentOrHidenull663 private fun View.showContentOrHide(forceHide: Boolean = false) {
664     val isTextViewWithBlankText = this is TextView && this.text.isBlank()
665     val isImageViewWithoutImage = this is ImageView && this.drawable == null
666     visibility =
667         if (forceHide || isTextViewWithBlankText || isImageViewWithoutImage) {
668             View.GONE
669         } else {
670             View.VISIBLE
671         }
672 }
673 
asVerticalAnimatornull674 private fun View.asVerticalAnimator(
675     duration: Long,
676     toY: Float,
677     fromY: Float = this.y
678 ): ValueAnimator {
679     val animator = ValueAnimator.ofFloat(fromY, toY)
680     animator.duration = duration
681     animator.addUpdateListener { y = it.animatedValue as Float }
682     return animator
683 }
684 
asFadeInAnimatornull685 private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
686     forEach { it.alpha = 0f }
687     val animator = ValueAnimator.ofFloat(0f, 1f)
688     animator.duration = duration
689     animator.startDelay = delay
690     animator.addUpdateListener {
691         val alpha = it.animatedValue as Float
692         forEach { view -> view.alpha = alpha }
693     }
694     return animator
695 }
696