1 /*
<lambda>null2  * Copyright (C) 2022 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.taptotransfer.receiver
18 
19 import android.animation.Animator
20 import android.animation.AnimatorListenerAdapter
21 import android.content.Context
22 import android.util.AttributeSet
23 import com.android.systemui.surfaceeffects.ripple.RippleShader
24 import com.android.systemui.surfaceeffects.ripple.RippleView
25 import kotlin.math.pow
26 
27 /**
28  * An expanding ripple effect for the media tap-to-transfer receiver chip.
29  */
30 class ReceiverChipRippleView(context: Context?, attrs: AttributeSet?) : RippleView(context, attrs) {
31 
32     // Indicates whether the ripple started expanding.
33     private var isStarted: Boolean
34 
35     init {
36         setupShader(RippleShader.RippleShape.CIRCLE)
37         setupRippleFadeParams()
38         setSparkleStrength(0f)
39         isStarted = false
40     }
41 
42     fun expandRipple(onAnimationEnd: Runnable? = null) {
43         duration = DEFAULT_DURATION
44         isStarted = true
45         super.startRipple(onAnimationEnd)
46     }
47 
48     /** Used to animate out the ripple. No-op if the ripple was never started via [startRipple]. */
49     fun collapseRipple(onAnimationEnd: Runnable? = null) {
50         if (!isStarted) {
51             return // Ignore if ripple is not started yet.
52         }
53         duration = DEFAULT_DURATION
54         // Reset all listeners to animator.
55         animator.removeAllListeners()
56         animator.addListener(object : AnimatorListenerAdapter() {
57             override fun onAnimationEnd(animation: Animator) {
58                 onAnimationEnd?.run()
59                 isStarted = false
60             }
61         })
62         animator.reverse()
63     }
64 
65     // Expands the ripple to cover full screen.
66     fun expandToFull(newHeight: Float, onAnimationEnd: Runnable? = null) {
67         if (!isStarted) {
68             return
69         }
70         // Reset all listeners to animator.
71         animator.removeAllListeners()
72         animator.removeAllUpdateListeners()
73 
74         // Only show the outline as ripple expands and disappears when animation ends.
75         removeRippleFill()
76 
77         val startingPercentage = calculateStartingPercentage(newHeight)
78         animator.duration = EXPAND_TO_FULL_DURATION
79         animator.addUpdateListener { updateListener ->
80             val now = updateListener.currentPlayTime
81             val progress = updateListener.animatedValue as Float
82             rippleShader.rawProgress = startingPercentage + (progress * (1 - startingPercentage))
83             rippleShader.distortionStrength = 1 - rippleShader.rawProgress
84             rippleShader.pixelDensity = 1 - rippleShader.rawProgress
85             rippleShader.time = now.toFloat()
86             invalidate()
87         }
88         animator.addListener(object : AnimatorListenerAdapter() {
89             override fun onAnimationEnd(animation: Animator) {
90                 animation?.let { visibility = GONE }
91                 onAnimationEnd?.run()
92                 isStarted = false
93             }
94         })
95         animator.start()
96     }
97 
98     // Calculates the actual starting percentage according to ripple shader progress set method.
99     // Check calculations in [RippleShader.progress]
100     fun calculateStartingPercentage(newHeight: Float): Float {
101         val ratio = rippleShader.rippleSize.currentHeight / newHeight
102         val remainingPercentage = (1 - ratio).toDouble().pow(1 / 3.toDouble()).toFloat()
103         return 1 - remainingPercentage
104     }
105 
106     private fun setupRippleFadeParams() {
107         with(rippleShader) {
108             // No fade out for the base ring.
109             baseRingFadeParams.fadeOutStart = 1f
110             baseRingFadeParams.fadeOutEnd = 1f
111 
112             // No fade in and outs for the center fill, as we always draw it.
113             centerFillFadeParams.fadeInStart = 0f
114             centerFillFadeParams.fadeInEnd = 0f
115             centerFillFadeParams.fadeOutStart = 1f
116             centerFillFadeParams.fadeOutEnd = 1f
117         }
118     }
119 
120     private fun removeRippleFill() {
121         with(rippleShader) {
122             // Set back to default because we modified them in [setupRippleFadeParams].
123             baseRingFadeParams.fadeOutStart = RippleShader.DEFAULT_BASE_RING_FADE_OUT_START
124             baseRingFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
125 
126             centerFillFadeParams.fadeInStart = RippleShader.DEFAULT_FADE_IN_START
127             centerFillFadeParams.fadeInEnd = RippleShader.DEFAULT_CENTER_FILL_FADE_IN_END
128 
129             // To avoid a seam showing up, we should match either:
130             // 1. baseRingFadeParams#fadeInEnd and centerFillFadeParams#fadeOutStart
131             // 2. baseRingFadeParams#fadeOutStart and centerFillFadeOutStart
132             // Here we go with 1 to fade in the centerFill faster.
133             centerFillFadeParams.fadeOutStart = baseRingFadeParams.fadeInEnd
134             centerFillFadeParams.fadeOutEnd = RippleShader.DEFAULT_FADE_OUT_END
135         }
136     }
137 
138     companion object {
139         const val DEFAULT_DURATION = 333L
140         const val EXPAND_TO_FULL_DURATION = 1000L
141     }
142 }
143