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