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