1 /* <lambda>null2 * Copyright (C) 2024 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.media.controls.ui.binder 18 19 import android.content.Context 20 import android.graphics.BlendMode 21 import android.graphics.Color 22 import android.graphics.ColorMatrix 23 import android.graphics.ColorMatrixColorFilter 24 import android.graphics.drawable.Animatable 25 import android.graphics.drawable.ColorDrawable 26 import android.graphics.drawable.GradientDrawable 27 import android.graphics.drawable.LayerDrawable 28 import android.graphics.drawable.TransitionDrawable 29 import android.os.Trace 30 import android.util.Pair 31 import android.view.Gravity 32 import android.view.View 33 import android.widget.ImageButton 34 import androidx.constraintlayout.widget.ConstraintSet 35 import androidx.lifecycle.Lifecycle 36 import androidx.lifecycle.repeatOnLifecycle 37 import com.android.app.tracing.coroutines.launchTraced as launch 38 import com.android.settingslib.widget.AdaptiveIcon 39 import com.android.systemui.Flags 40 import com.android.systemui.animation.Expandable 41 import com.android.systemui.common.shared.model.Icon 42 import com.android.systemui.dagger.qualifiers.Background 43 import com.android.systemui.dagger.qualifiers.Main 44 import com.android.systemui.lifecycle.repeatWhenAttached 45 import com.android.systemui.media.controls.ui.animation.AnimationBindHandler 46 import com.android.systemui.media.controls.ui.animation.ColorSchemeTransition 47 import com.android.systemui.media.controls.ui.controller.MediaViewController 48 import com.android.systemui.media.controls.ui.util.MediaArtworkHelper 49 import com.android.systemui.media.controls.ui.view.MediaViewHolder 50 import com.android.systemui.media.controls.ui.viewmodel.MediaActionViewModel 51 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel 52 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA 53 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY 54 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA 55 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY 56 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_ALL 57 import com.android.systemui.media.controls.ui.viewmodel.MediaControlViewModel.Companion.SEMANTIC_ACTIONS_COMPACT 58 import com.android.systemui.media.controls.ui.viewmodel.MediaOutputSwitcherViewModel 59 import com.android.systemui.media.controls.ui.viewmodel.MediaPlayerViewModel 60 import com.android.systemui.media.controls.util.MediaDataUtils 61 import com.android.systemui.monet.ColorScheme 62 import com.android.systemui.monet.Style 63 import com.android.systemui.plugins.FalsingManager 64 import com.android.systemui.res.R 65 import com.android.systemui.surfaceeffects.ripple.MultiRippleView 66 import com.android.systemui.surfaceeffects.ripple.RippleAnimation 67 import com.android.systemui.surfaceeffects.ripple.RippleAnimationConfig 68 import com.android.systemui.surfaceeffects.ripple.RippleShader 69 import kotlinx.coroutines.CoroutineDispatcher 70 import kotlinx.coroutines.flow.collectLatest 71 import kotlinx.coroutines.withContext 72 73 private const val TAG = "MediaControlViewBinder" 74 75 object MediaControlViewBinder { 76 77 fun bind( 78 viewHolder: MediaViewHolder, 79 viewModel: MediaControlViewModel, 80 viewController: MediaViewController, 81 falsingManager: FalsingManager, 82 @Background backgroundDispatcher: CoroutineDispatcher, 83 @Main mainDispatcher: CoroutineDispatcher, 84 ) { 85 val mediaCard = viewHolder.player 86 mediaCard.repeatWhenAttached { 87 repeatOnLifecycle(Lifecycle.State.STARTED) { 88 launch { 89 viewModel.player.collectLatest { player -> 90 player?.let { 91 if (viewModel.setPlayer(it)) { 92 bindMediaCard( 93 viewHolder, 94 viewController, 95 it, 96 falsingManager, 97 backgroundDispatcher, 98 mainDispatcher, 99 ) 100 viewModel.onMediaControlIsBound(it.artistName, it.titleName) 101 } 102 } 103 } 104 } 105 } 106 } 107 } 108 109 suspend fun bindMediaCard( 110 viewHolder: MediaViewHolder, 111 viewController: MediaViewController, 112 viewModel: MediaPlayerViewModel, 113 falsingManager: FalsingManager, 114 backgroundDispatcher: CoroutineDispatcher, 115 mainDispatcher: CoroutineDispatcher, 116 ) { 117 // Set up media control location and its listener. 118 viewModel.onLocationChanged(viewController.currentEndLocation) 119 viewController.locationChangeListener = viewModel.onLocationChanged 120 121 with(viewHolder) { 122 // AlbumView uses a hardware layer so that clipping of the foreground is handled with 123 // clipping the album art. Otherwise album art shows through at the edges. 124 albumView.setLayerType(View.LAYER_TYPE_HARDWARE, null) 125 turbulenceNoiseView.setBlendMode(BlendMode.SCREEN) 126 loadingEffectView.setBlendMode(BlendMode.SCREEN) 127 loadingEffectView.visibility = View.INVISIBLE 128 129 player.contentDescription = 130 viewModel.contentDescription.invoke(viewController.isGutsVisible) 131 player.setOnClickListener { 132 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 133 if (!viewController.isGutsVisible) { 134 viewModel.onClicked(Expandable.fromView(player)) 135 } 136 } 137 } 138 player.setOnLongClickListener { 139 if (!falsingManager.isFalseLongTap(FalsingManager.LOW_PENALTY)) { 140 if (!viewController.isGutsVisible) { 141 openGuts(viewHolder, viewController, viewModel) 142 } else { 143 closeGuts(viewHolder, viewController, viewModel) 144 } 145 } 146 return@setOnLongClickListener true 147 } 148 } 149 150 viewController.bindSeekBar(viewModel.onSeek, viewModel.onBindSeekbar) 151 bindOutputSwitcherModel( 152 viewHolder, 153 viewModel.outputSwitcher, 154 viewController, 155 falsingManager, 156 ) 157 bindGutsViewModel(viewHolder, viewModel, viewController, falsingManager) 158 bindActionButtons(viewHolder, viewModel, viewController, falsingManager) 159 bindScrubbingTime(viewHolder, viewModel, viewController) 160 161 val isSongUpdated = bindSongMetadata(viewHolder, viewModel, viewController) 162 163 bindArtworkAndColor( 164 viewHolder, 165 viewModel, 166 viewController, 167 backgroundDispatcher, 168 mainDispatcher, 169 isSongUpdated, 170 ) 171 172 if (viewModel.playTurbulenceNoise) { 173 viewController.setUpTurbulenceNoise() 174 } 175 176 // TODO: We don't need to refresh this state constantly, only if the state actually changed 177 // to something which might impact the measurement 178 // State refresh interferes with the translation animation, only run it if it's not running. 179 if (!viewController.metadataAnimationHandler.isRunning) { 180 viewController.refreshState() 181 } 182 } 183 184 private fun bindOutputSwitcherModel( 185 viewHolder: MediaViewHolder, 186 viewModel: MediaOutputSwitcherViewModel, 187 viewController: MediaViewController, 188 falsingManager: FalsingManager, 189 ) { 190 with(viewHolder.seamless) { 191 visibility = View.VISIBLE 192 isEnabled = viewModel.isTapEnabled 193 contentDescription = viewModel.deviceString 194 setOnClickListener { 195 if (!falsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) { 196 viewModel.onClicked.invoke(Expandable.fromView(viewHolder.seamlessButton)) 197 } 198 } 199 } 200 when (viewModel.deviceIcon) { 201 is Icon.Loaded -> { 202 val icon = viewModel.deviceIcon.drawable 203 if (icon is AdaptiveIcon) { 204 icon.setBackgroundColor( 205 viewController.colorSchemeTransition.getDeviceIconColor() 206 ) 207 } 208 viewHolder.seamlessIcon.setImageDrawable(icon) 209 } 210 is Icon.Resource -> viewHolder.seamlessIcon.setImageResource(viewModel.deviceIcon.res) 211 } 212 viewHolder.seamlessButton.alpha = viewModel.alpha 213 viewHolder.seamlessText.text = viewModel.deviceString 214 } 215 216 private fun bindGutsViewModel( 217 viewHolder: MediaViewHolder, 218 viewModel: MediaPlayerViewModel, 219 viewController: MediaViewController, 220 falsingManager: FalsingManager, 221 ) { 222 val gutsViewHolder = viewHolder.gutsViewHolder 223 val model = viewModel.gutsMenu 224 with(gutsViewHolder) { 225 gutsText.text = model.gutsText 226 dismissText.visibility = if (model.isDismissEnabled) View.VISIBLE else View.GONE 227 dismiss.isEnabled = model.isDismissEnabled 228 dismiss.setOnClickListener { 229 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 230 model.onDismissClicked() 231 } 232 } 233 cancelText.background = model.cancelTextBackground 234 cancel.setOnClickListener { 235 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 236 closeGuts(viewHolder, viewController, viewModel) 237 } 238 } 239 settings.setOnClickListener { 240 if (!falsingManager.isFalseTap(FalsingManager.LOW_PENALTY)) { 241 model.onSettingsClicked.invoke() 242 } 243 } 244 setDismissible(model.isDismissEnabled) 245 } 246 } 247 248 private fun bindActionButtons( 249 viewHolder: MediaViewHolder, 250 viewModel: MediaPlayerViewModel, 251 viewController: MediaViewController, 252 falsingManager: FalsingManager, 253 ) { 254 val genericButtons = MediaViewHolder.genericButtonIds.map { viewHolder.getAction(it) } 255 val expandedSet = viewController.expandedLayout 256 val collapsedSet = viewController.collapsedLayout 257 if (viewModel.useSemanticActions) { 258 // Hide all generic buttons 259 genericButtons.forEach { 260 setVisibleAndAlpha(expandedSet, it.id, false) 261 setVisibleAndAlpha(collapsedSet, it.id, false) 262 } 263 264 SEMANTIC_ACTIONS_ALL.forEachIndexed { index, id -> 265 val buttonView = viewHolder.getAction(id) 266 val buttonModel = viewModel.actionButtons[index] 267 if (buttonView.id == R.id.actionPrev) { 268 viewController.setUpPrevButtonInfo( 269 buttonModel.isEnabled, 270 buttonModel.notVisibleValue, 271 ) 272 } else if (buttonView.id == R.id.actionNext) { 273 viewController.setUpNextButtonInfo( 274 buttonModel.isEnabled, 275 buttonModel.notVisibleValue, 276 ) 277 } 278 val animHandler = (buttonView.tag ?: AnimationBindHandler()) as AnimationBindHandler 279 animHandler.tryExecute { 280 if (buttonModel.isEnabled) { 281 if (animHandler.updateRebindId(buttonModel.rebindId)) { 282 animHandler.unregisterAll() 283 animHandler.tryRegister(buttonModel.icon) 284 animHandler.tryRegister(buttonModel.background) 285 bindButtonCommon( 286 buttonView, 287 viewHolder.multiRippleView, 288 buttonModel, 289 viewController, 290 falsingManager, 291 ) 292 } 293 } else { 294 animHandler.unregisterAll() 295 clearButton(buttonView) 296 } 297 val visible = 298 buttonModel.isEnabled && 299 (buttonModel.isVisibleWhenScrubbing || !viewController.isScrubbing) 300 setSemanticButtonVisibleAndAlpha( 301 viewHolder.getAction(id), 302 viewController.expandedLayout, 303 viewController.collapsedLayout, 304 visible, 305 buttonModel.notVisibleValue, 306 buttonModel.showInCollapsed, 307 ) 308 } 309 } 310 } else { 311 // Hide buttons that only appear for semantic actions 312 SEMANTIC_ACTIONS_COMPACT.forEach { buttonId -> 313 setVisibleAndAlpha(expandedSet, buttonId, visible = false) 314 setVisibleAndAlpha(expandedSet, buttonId, visible = false) 315 } 316 317 // Set all generic buttons 318 genericButtons.forEachIndexed { index, button -> 319 if (index < viewModel.actionButtons.size) { 320 val action = viewModel.actionButtons[index] 321 bindButtonCommon( 322 button, 323 viewHolder.multiRippleView, 324 action, 325 viewController, 326 falsingManager, 327 ) 328 setVisibleAndAlpha(expandedSet, button.id, visible = true) 329 setVisibleAndAlpha(collapsedSet, button.id, visible = action.showInCollapsed) 330 } else { 331 // Hide any unused buttons 332 clearButton(button) 333 setVisibleAndAlpha(expandedSet, button.id, visible = false) 334 setVisibleAndAlpha(collapsedSet, button.id, visible = false) 335 } 336 } 337 } 338 updateSeekBarVisibility(viewController.expandedLayout, viewController.isSeekBarEnabled) 339 } 340 341 private fun bindButtonCommon( 342 button: ImageButton, 343 multiRippleView: MultiRippleView, 344 actionViewModel: MediaActionViewModel, 345 viewController: MediaViewController, 346 falsingManager: FalsingManager, 347 ) { 348 button.setImageDrawable(actionViewModel.icon) 349 button.background = actionViewModel.background 350 button.contentDescription = actionViewModel.contentDescription 351 button.isEnabled = actionViewModel.isEnabled 352 if (actionViewModel.isEnabled) { 353 button.setOnClickListener { 354 if (!falsingManager.isFalseTap(FalsingManager.MODERATE_PENALTY)) { 355 actionViewModel.onClicked(it.id) 356 357 viewController.multiRippleController.play( 358 createTouchRippleAnimation( 359 button, 360 viewController.colorSchemeTransition, 361 multiRippleView, 362 ) 363 ) 364 365 if (actionViewModel.icon is Animatable) { 366 actionViewModel.icon.start() 367 } 368 369 if (actionViewModel.background is Animatable) { 370 actionViewModel.background.start() 371 } 372 } 373 } 374 } 375 } 376 377 private fun bindSongMetadata( 378 viewHolder: MediaViewHolder, 379 viewModel: MediaPlayerViewModel, 380 viewController: MediaViewController, 381 ): Boolean { 382 val expandedSet = viewController.expandedLayout 383 val collapsedSet = viewController.collapsedLayout 384 385 return viewController.metadataAnimationHandler.setNext( 386 Triple(viewModel.titleName, viewModel.artistName, viewModel.isExplicitVisible), 387 { 388 viewHolder.titleText.text = viewModel.titleName 389 viewHolder.artistText.text = viewModel.artistName 390 setVisibleAndAlpha( 391 expandedSet, 392 R.id.media_explicit_indicator, 393 viewModel.isExplicitVisible, 394 ) 395 setVisibleAndAlpha( 396 collapsedSet, 397 R.id.media_explicit_indicator, 398 viewModel.isExplicitVisible, 399 ) 400 401 // refreshState is required here to resize the text views (and prevent ellipsis) 402 viewController.refreshState() 403 }, 404 { 405 // After finishing the enter animation, we refresh state. This could pop if 406 // something is incorrectly bound, but needs to be run if other elements were 407 // updated while the enter animation was running 408 viewController.refreshState() 409 }, 410 ) 411 } 412 413 private suspend fun bindArtworkAndColor( 414 viewHolder: MediaViewHolder, 415 viewModel: MediaPlayerViewModel, 416 viewController: MediaViewController, 417 backgroundDispatcher: CoroutineDispatcher, 418 mainDispatcher: CoroutineDispatcher, 419 updateBackground: Boolean, 420 ) { 421 val traceCookie = viewHolder.hashCode() 422 val traceName = "MediaControlViewBinder#bindArtworkAndColor" 423 Trace.beginAsyncSection(traceName, traceCookie) 424 if (updateBackground) { 425 viewController.isArtworkBound = false 426 } 427 // Capture width & height from views in foreground for artwork scaling in background 428 val width = viewController.widthInSceneContainerPx 429 val height = viewController.heightInSceneContainerPx 430 withContext(backgroundDispatcher) { 431 val wallpaperColors = 432 MediaArtworkHelper.getWallpaperColor( 433 viewHolder.albumView.context, 434 backgroundDispatcher, 435 viewModel.backgroundCover, 436 TAG, 437 ) 438 val isArtworkBound = wallpaperColors != null 439 val darkTheme = !Flags.mediaControlsA11yColors() 440 val scheme = 441 wallpaperColors?.let { ColorScheme(it, darkTheme, Style.CONTENT) } 442 ?: let { 443 if (viewModel.launcherIcon is Icon.Loaded) { 444 MediaArtworkHelper.getColorScheme( 445 viewModel.launcherIcon.drawable, 446 TAG, 447 darkTheme, 448 ) 449 } else { 450 null 451 } 452 } 453 val artwork = 454 wallpaperColors?.let { 455 addGradientToPlayerAlbum( 456 viewHolder.albumView.context, 457 viewModel.backgroundCover!!, 458 scheme!!, 459 width, 460 height, 461 ) 462 } ?: ColorDrawable(Color.TRANSPARENT) 463 withContext(mainDispatcher) { 464 // Transition Colors to current color scheme 465 val colorSchemeChanged = 466 viewController.colorSchemeTransition.updateColorScheme(scheme) 467 val albumView = viewHolder.albumView 468 469 // Set up width of album view constraint. 470 viewController.expandedLayout.getConstraint(albumView.id).layout.mWidth = width 471 viewController.collapsedLayout.getConstraint(albumView.id).layout.mWidth = width 472 473 albumView.setPadding(0, 0, 0, 0) 474 if ( 475 updateBackground || 476 colorSchemeChanged || 477 (!viewController.isArtworkBound && isArtworkBound) 478 ) { 479 viewController.prevArtwork?.let { 480 // Since we throw away the last transition, this will pop if your 481 // backgrounds are cycled too fast (or the correct background arrives very 482 // soon after the metadata changes). 483 val transitionDrawable = TransitionDrawable(arrayOf(it, artwork)) 484 485 scaleTransitionDrawableLayer(transitionDrawable, 0, width, height) 486 scaleTransitionDrawableLayer(transitionDrawable, 1, width, height) 487 transitionDrawable.setLayerGravity(0, Gravity.CENTER) 488 transitionDrawable.setLayerGravity(1, Gravity.CENTER) 489 transitionDrawable.isCrossFadeEnabled = true 490 491 albumView.setImageDrawable(transitionDrawable) 492 transitionDrawable.startTransition(if (isArtworkBound) 333 else 80) 493 } ?: albumView.setImageDrawable(artwork) 494 } 495 viewController.isArtworkBound = isArtworkBound 496 viewController.prevArtwork = artwork 497 498 if (viewModel.useGrayColorFilter) { 499 // Used for resume players to use launcher icon 500 viewHolder.appIcon.colorFilter = getGrayscaleFilter() 501 when (viewModel.launcherIcon) { 502 is Icon.Loaded -> 503 viewHolder.appIcon.setImageDrawable(viewModel.launcherIcon.drawable) 504 is Icon.Resource -> 505 viewHolder.appIcon.setImageResource(viewModel.launcherIcon.res) 506 } 507 } else { 508 viewHolder.appIcon.setColorFilter( 509 viewController.colorSchemeTransition.getAppIconColor() 510 ) 511 viewHolder.appIcon.setImageIcon(viewModel.appIcon) 512 } 513 Trace.endAsyncSection(traceName, traceCookie) 514 } 515 } 516 } 517 518 private fun scaleTransitionDrawableLayer( 519 transitionDrawable: TransitionDrawable, 520 layer: Int, 521 targetWidth: Int, 522 targetHeight: Int, 523 ) { 524 val drawable = transitionDrawable.getDrawable(layer) ?: return 525 val width = drawable.intrinsicWidth 526 val height = drawable.intrinsicHeight 527 val scale = 528 MediaDataUtils.getScaleFactor(Pair(width, height), Pair(targetWidth, targetHeight)) 529 if (scale == 0f) return 530 transitionDrawable.setLayerSize(layer, (scale * width).toInt(), (scale * height).toInt()) 531 } 532 533 private fun addGradientToPlayerAlbum( 534 context: Context, 535 artworkIcon: android.graphics.drawable.Icon, 536 mutableColorScheme: ColorScheme, 537 width: Int, 538 height: Int, 539 ): LayerDrawable { 540 val albumArt = MediaArtworkHelper.getScaledBackground(context, artworkIcon, width, height) 541 val startAlpha = 542 if (Flags.mediaControlsA11yColors()) { 543 MEDIA_PLAYER_SCRIM_START_ALPHA 544 } else { 545 MEDIA_PLAYER_SCRIM_START_ALPHA_LEGACY 546 } 547 val endAlpha = 548 if (Flags.mediaControlsA11yColors()) { 549 MEDIA_PLAYER_SCRIM_END_ALPHA 550 } else { 551 MEDIA_PLAYER_SCRIM_END_ALPHA_LEGACY 552 } 553 return MediaArtworkHelper.setUpGradientColorOnDrawable( 554 albumArt, 555 context.getDrawable(R.drawable.qs_media_scrim)?.mutate() as GradientDrawable, 556 mutableColorScheme, 557 startAlpha, 558 endAlpha, 559 ) 560 } 561 562 private fun clearButton(button: ImageButton) { 563 button.setImageDrawable(null) 564 button.contentDescription = null 565 button.isEnabled = false 566 button.background = null 567 } 568 569 private fun bindScrubbingTime( 570 viewHolder: MediaViewHolder, 571 viewModel: MediaPlayerViewModel, 572 viewController: MediaViewController, 573 ) { 574 val expandedSet = viewController.expandedLayout 575 val visible = viewModel.canShowTime && viewController.isScrubbing 576 viewController.canShowScrubbingTime = viewModel.canShowTime 577 setVisibleAndAlpha(expandedSet, viewHolder.scrubbingElapsedTimeView.id, visible) 578 setVisibleAndAlpha(expandedSet, viewHolder.scrubbingTotalTimeView.id, visible) 579 // Collapsed view is always GONE as set in XML, so doesn't need to be updated dynamically. 580 } 581 582 private fun createTouchRippleAnimation( 583 button: ImageButton, 584 colorSchemeTransition: ColorSchemeTransition, 585 multiRippleView: MultiRippleView, 586 ): RippleAnimation { 587 val maxSize = (multiRippleView.width * 2).toFloat() 588 return RippleAnimation( 589 RippleAnimationConfig( 590 RippleShader.RippleShape.CIRCLE, 591 duration = 1500L, 592 centerX = button.x + button.width * 0.5f, 593 centerY = button.y + button.height * 0.5f, 594 maxSize, 595 maxSize, 596 button.context.resources.displayMetrics.density, 597 colorSchemeTransition.getSurfaceEffectColor(), 598 opacity = 100, 599 sparkleStrength = 0f, 600 baseRingFadeParams = null, 601 sparkleRingFadeParams = null, 602 centerFillFadeParams = null, 603 shouldDistort = false, 604 ) 605 ) 606 } 607 608 private fun openGuts( 609 viewHolder: MediaViewHolder, 610 viewController: MediaViewController, 611 viewModel: MediaPlayerViewModel, 612 ) { 613 viewHolder.marquee(true, MediaViewController.GUTS_ANIMATION_DURATION) 614 viewController.openGuts() 615 viewHolder.player.contentDescription = viewModel.contentDescription.invoke(true) 616 viewModel.onLongClicked.invoke() 617 } 618 619 private fun closeGuts( 620 viewHolder: MediaViewHolder, 621 viewController: MediaViewController, 622 viewModel: MediaPlayerViewModel, 623 ) { 624 viewHolder.marquee(false, MediaViewController.GUTS_ANIMATION_DURATION) 625 viewController.closeGuts(false) 626 viewHolder.player.contentDescription = viewModel.contentDescription.invoke(false) 627 } 628 629 fun setVisibleAndAlpha(set: ConstraintSet, resId: Int, visible: Boolean) { 630 setVisibleAndAlpha(set, resId, visible, ConstraintSet.GONE) 631 } 632 633 private fun setVisibleAndAlpha( 634 set: ConstraintSet, 635 resId: Int, 636 visible: Boolean, 637 notVisibleValue: Int, 638 ) { 639 set.setVisibility(resId, if (visible) ConstraintSet.VISIBLE else notVisibleValue) 640 set.setAlpha(resId, if (visible) 1.0f else 0.0f) 641 } 642 643 fun updateSeekBarVisibility(constraintSet: ConstraintSet, isSeekBarEnabled: Boolean) { 644 if (isSeekBarEnabled) { 645 constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.VISIBLE) 646 constraintSet.setAlpha(R.id.media_progress_bar, 1.0f) 647 } else { 648 constraintSet.setVisibility(R.id.media_progress_bar, ConstraintSet.INVISIBLE) 649 constraintSet.setAlpha(R.id.media_progress_bar, 0.0f) 650 } 651 } 652 653 fun setSemanticButtonVisibleAndAlpha( 654 button: ImageButton, 655 expandedSet: ConstraintSet, 656 collapsedSet: ConstraintSet, 657 visible: Boolean, 658 notVisibleValue: Int, 659 showInCollapsed: Boolean, 660 ) { 661 if (notVisibleValue == ConstraintSet.INVISIBLE) { 662 // Since time views should appear instead of buttons. 663 button.isFocusable = visible 664 button.isClickable = visible 665 } 666 setVisibleAndAlpha(expandedSet, button.id, visible, notVisibleValue) 667 setVisibleAndAlpha(collapsedSet, button.id, visible = visible && showInCollapsed) 668 } 669 670 private fun getGrayscaleFilter(): ColorMatrixColorFilter { 671 val matrix = ColorMatrix() 672 matrix.setSaturation(0f) 673 return ColorMatrixColorFilter(matrix) 674 } 675 } 676