1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.systemui.media.taptotransfer.receiver 17 18 import android.content.Context 19 import android.content.res.ColorStateList 20 import android.view.View 21 import android.view.WindowManager 22 import com.android.settingslib.Utils 23 import com.android.systemui.res.R 24 import javax.inject.Inject 25 26 /** 27 * A controller responsible for the animation of the ripples shown in media tap-to-transfer on the 28 * receiving device. 29 */ 30 class MediaTttReceiverRippleController 31 @Inject 32 constructor( 33 private val context: Context, 34 private val windowManager: WindowManager, 35 ) { 36 37 private var maxRippleWidth: Float = 0f 38 private var maxRippleHeight: Float = 0f 39 40 /** Expands the icon and main ripple to in-progress state */ 41 fun expandToInProgressState( 42 mainRippleView: ReceiverChipRippleView, 43 iconRippleView: ReceiverChipRippleView, 44 ) { 45 expandRipple(mainRippleView, isIconRipple = false) 46 expandRipple(iconRippleView, isIconRipple = true) 47 } 48 49 private fun expandRipple(rippleView: ReceiverChipRippleView, isIconRipple: Boolean) { 50 if (rippleView.rippleInProgress()) { 51 // Skip if ripple is still playing 52 return 53 } 54 55 // In case the device orientation changes, we need to reset the layout. 56 rippleView.addOnLayoutChangeListener( 57 View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> 58 if (v == null) return@OnLayoutChangeListener 59 60 val layoutChangedRippleView = v as ReceiverChipRippleView 61 if (isIconRipple) { 62 layoutIconRipple(layoutChangedRippleView) 63 } else { 64 layoutRipple(layoutChangedRippleView) 65 } 66 layoutChangedRippleView.invalidate() 67 } 68 ) 69 rippleView.addOnAttachStateChangeListener( 70 object : View.OnAttachStateChangeListener { 71 override fun onViewDetachedFromWindow(view: View) {} 72 73 override fun onViewAttachedToWindow(view: View) { 74 if (view == null) { 75 return 76 } 77 val attachedRippleView = view as ReceiverChipRippleView 78 if (isIconRipple) { 79 layoutIconRipple(attachedRippleView) 80 } else { 81 layoutRipple(attachedRippleView) 82 } 83 attachedRippleView.expandRipple() 84 attachedRippleView.removeOnAttachStateChangeListener(this) 85 } 86 } 87 ) 88 } 89 90 /** Expands the ripple to cover the screen. */ 91 fun expandToSuccessState(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable?) { 92 layoutRipple(rippleView, isFullScreen = true) 93 rippleView.expandToFull(maxRippleHeight, onAnimationEnd) 94 } 95 96 /** Collapses the ripple. */ 97 fun collapseRipple(rippleView: ReceiverChipRippleView, onAnimationEnd: Runnable? = null) { 98 rippleView.collapseRipple(onAnimationEnd) 99 } 100 101 private fun layoutRipple(rippleView: ReceiverChipRippleView, isFullScreen: Boolean = false) { 102 val windowBounds = windowManager.currentWindowMetrics.bounds 103 val height = windowBounds.height().toFloat() 104 val width = windowBounds.width().toFloat() 105 106 if (isFullScreen) { 107 maxRippleHeight = height * 2f 108 maxRippleWidth = width * 2f 109 } else { 110 maxRippleHeight = getRippleSize() 111 maxRippleWidth = getRippleSize() 112 } 113 rippleView.setMaxSize(maxRippleWidth, maxRippleHeight) 114 // Center the ripple on the bottom of the screen in the middle. 115 rippleView.setCenter(width * 0.5f, height) 116 rippleView.setColor(getRippleColor(), RIPPLE_OPACITY) 117 } 118 119 private fun layoutIconRipple(iconRippleView: ReceiverChipRippleView) { 120 val windowBounds = windowManager.currentWindowMetrics.bounds 121 val height = windowBounds.height().toFloat() 122 val width = windowBounds.width().toFloat() 123 val radius = getReceiverIconSize().toFloat() 124 125 iconRippleView.setMaxSize(radius * 0.8f, radius * 0.8f) 126 iconRippleView.setCenter( 127 width * 0.5f, 128 height - getReceiverIconSize() * 0.5f - getReceiverIconBottomMargin() 129 ) 130 iconRippleView.setColor(getRippleColor(), RIPPLE_OPACITY) 131 } 132 133 private fun getRippleColor(): Int { 134 var colorStateList = 135 ColorStateList.valueOf( 136 Utils.getColorAttrDefaultColor(context, R.attr.wallpaperTextColorAccent) 137 ) 138 return colorStateList.withLStar(TONE_PERCENT).defaultColor 139 } 140 141 /** Returns the size of the ripple. */ 142 internal fun getRippleSize(): Float { 143 return getReceiverIconSize() * 4f 144 } 145 146 /** Returns the size of the icon of the receiver. */ 147 internal fun getReceiverIconSize(): Int { 148 return context.resources.getDimensionPixelSize(R.dimen.media_ttt_icon_size_receiver) 149 } 150 151 /** Return the bottom margin of the icon of the receiver. */ 152 internal fun getReceiverIconBottomMargin(): Int { 153 // Adding a margin to make sure ripple behind the icon is not cut by the screen bounds. 154 return context.resources.getDimensionPixelSize( 155 R.dimen.media_ttt_receiver_icon_bottom_margin 156 ) 157 } 158 159 companion object { 160 const val RIPPLE_OPACITY = 70 161 const val TONE_PERCENT = 95f 162 } 163 } 164