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