blob: 1113579e417c20d13509836deab816fd32182197 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.biometrics
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.PointF
import android.util.AttributeSet
import android.view.View
import android.view.animation.PathInterpolator
import com.android.internal.R.attr.interpolator
import com.android.internal.graphics.ColorUtils
import com.android.systemui.animation.Interpolators
import com.android.systemui.statusbar.LightRevealScrim
import com.android.systemui.statusbar.charging.RippleShader
private const val RIPPLE_ANIMATION_DURATION: Long = 1533
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
/**
* Expanding ripple effect
* - startUnlockedRipple for the transition from biometric authentication success to showing
* launcher.
* - startDwellRipple for the ripple expansion out when the user has their finger down on the UDFPS
* sensor area
* - retractRipple for the ripple animation inwards to signal a failure
*/
class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f)
private val dwellPulseDuration = 50L
private val dwellAlphaDuration = dwellPulseDuration
private val dwellAlpha: Float = 1f
private val dwellExpandDuration = 1200L - dwellPulseDuration
private val aodDwellPulseDuration = 50L
private var aodDwellAlphaDuration = aodDwellPulseDuration
private var aodDwellAlpha: Float = .8f
private var aodDwellExpandDuration = 1200L - aodDwellPulseDuration
private val retractDuration = 400L
private var alphaInDuration: Long = 0
private var unlockedRippleInProgress: Boolean = false
private val rippleShader = RippleShader()
private val ripplePaint = Paint()
private var retractAnimator: Animator? = null
private var dwellPulseOutAnimator: Animator? = null
private var radius: Float = 0f
set(value) {
rippleShader.radius = value
field = value
}
private var origin: PointF = PointF()
set(value) {
rippleShader.origin = value
field = value
}
init {
rippleShader.color = 0xffffffff.toInt() // default color
rippleShader.progress = 0f
rippleShader.sparkleStrength = RIPPLE_SPARKLE_STRENGTH
ripplePaint.shader = rippleShader
visibility = GONE
}
fun setSensorLocation(location: PointF) {
origin = location
radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
}
fun setAlphaInDuration(duration: Long) {
alphaInDuration = duration
}
/**
* Animate ripple inwards back to radius 0
*/
fun retractRipple() {
if (retractAnimator?.isRunning == true) {
return // let the animation finish
}
if (dwellPulseOutAnimator?.isRunning == true) {
val retractRippleAnimator = ValueAnimator.ofFloat(rippleShader.progress, 0f)
.apply {
interpolator = retractInterpolator
duration = retractDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val retractAlphaAnimator = ValueAnimator.ofInt(255, 0).apply {
interpolator = Interpolators.LINEAR
duration = retractDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
retractAnimator = AnimatorSet().apply {
playTogether(retractRippleAnimator, retractAlphaAnimator)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
dwellPulseOutAnimator?.cancel()
rippleShader.shouldFadeOutRipple = false
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
visibility = GONE
resetRippleAlpha()
}
})
start()
}
}
}
/**
* Ripple that moves animates from an outer ripple ring of
* startRadius => endRadius => expandedRadius
*/
fun startDwellRipple(
startRadius: Float,
endRadius: Float,
expandedRadius: Float,
isDozing: Boolean
) {
if (unlockedRippleInProgress || dwellPulseOutAnimator?.isRunning == true) {
return
}
// we divide by 4 because the desired startRadius and endRadius is for the ripple's outer
// ring see RippleShader
val startDwellProgress = startRadius / radius / 4f
val endInitialDwellProgress = endRadius / radius / 4f
val endExpandDwellProgress = expandedRadius / radius / 4f
val alpha = if (isDozing) aodDwellAlpha else dwellAlpha
val pulseOutEndAlpha = (255 * alpha).toInt()
val expandDwellEndAlpha = kotlin.math.min((255 * (alpha + .25f)).toInt(), 255)
val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(startDwellProgress,
endInitialDwellProgress).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = if (isDozing) aodDwellPulseDuration else dwellPulseDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val dwellPulseOutAlphaAnimator = ValueAnimator.ofInt(0, pulseOutEndAlpha).apply {
interpolator = Interpolators.LINEAR
duration = if (isDozing) aodDwellAlphaDuration else dwellAlphaDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
// slowly animate outwards until we receive a call to retractRipple or startUnlockedRipple
val expandDwellRippleAnimator = ValueAnimator.ofFloat(endInitialDwellProgress,
endExpandDwellProgress).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = if (isDozing) aodDwellExpandDuration else dwellExpandDuration
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val expandDwellAlphaAnimator = ValueAnimator.ofInt(pulseOutEndAlpha, expandDwellEndAlpha)
.apply {
interpolator = Interpolators.LINEAR
duration = if (isDozing) aodDwellExpandDuration else dwellExpandDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
val initialDwellPulseOutAnimator = AnimatorSet().apply {
playTogether(dwellPulseOutRippleAnimator, dwellPulseOutAlphaAnimator)
}
val expandDwellAnimator = AnimatorSet().apply {
playTogether(expandDwellRippleAnimator, expandDwellAlphaAnimator)
}
dwellPulseOutAnimator = AnimatorSet().apply {
playSequentially(
initialDwellPulseOutAnimator,
expandDwellAnimator
)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
retractAnimator?.cancel()
rippleShader.shouldFadeOutRipple = false
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
visibility = GONE
resetRippleAlpha()
}
})
start()
}
}
/**
* Ripple that bursts outwards from the position of the sensor to the edges of the screen
*/
fun startUnlockedRipple(onAnimationEnd: Runnable?, lightReveal: LightRevealScrim?) {
if (unlockedRippleInProgress) {
return // Ignore if ripple effect is already playing
}
var rippleStart = 0f
var alphaDuration = alphaInDuration
if (dwellPulseOutAnimator?.isRunning == true || retractAnimator?.isRunning == true) {
rippleStart = rippleShader.progress
alphaDuration = 0
dwellPulseOutAnimator?.cancel()
retractAnimator?.cancel()
}
val rippleAnimator = ValueAnimator.ofFloat(rippleStart, 1f).apply {
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
duration = RIPPLE_ANIMATION_DURATION
addUpdateListener { animator ->
val now = animator.currentPlayTime
rippleShader.progress = animator.animatedValue as Float
rippleShader.time = now.toFloat()
invalidate()
}
}
val revealAnimator = ValueAnimator.ofFloat(.1f, 1f).apply {
interpolator = rippleAnimator.interpolator
duration = rippleAnimator.duration
addUpdateListener { animator ->
lightReveal?.revealAmount = animator.animatedValue as Float
}
}
val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply {
duration = alphaDuration
addUpdateListener { animator ->
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
animator.animatedValue as Int
)
invalidate()
}
}
val animatorSet = AnimatorSet().apply {
playTogether(
rippleAnimator,
revealAnimator,
alphaInAnimator
)
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationStart(animation: Animator?) {
unlockedRippleInProgress = true
rippleShader.shouldFadeOutRipple = true
visibility = VISIBLE
}
override fun onAnimationEnd(animation: Animator?) {
onAnimationEnd?.run()
unlockedRippleInProgress = false
visibility = GONE
}
})
}
animatorSet.start()
}
fun resetRippleAlpha() {
rippleShader.color = ColorUtils.setAlphaComponent(
rippleShader.color,
255
)
}
fun setColor(color: Int) {
rippleShader.color = color
resetRippleAlpha()
}
override fun onDraw(canvas: Canvas?) {
// To reduce overdraw, we mask the effect to a circle whose radius is big enough to cover
// the active effect area. Values here should be kept in sync with the
// animation implementation in the ripple shader.
val maskRadius = (1 - (1 - rippleShader.progress) * (1 - rippleShader.progress) *
(1 - rippleShader.progress)) * radius * 2f
canvas?.drawCircle(origin.x, origin.y, maskRadius, ripplePaint)
}
}