| /* |
| * 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.graphics.ColorUtils |
| 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 on the transition from biometric authentication success to showing |
| * launcher. |
| */ |
| class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) { |
| private var rippleInProgress: Boolean = false |
| private val rippleShader = RippleShader() |
| private val ripplePaint = Paint() |
| private var radius: Float = 0.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 startRipple(onAnimationEnd: Runnable?, lightReveal: LightRevealScrim?) { |
| if (rippleInProgress) { |
| return // Ignore if ripple effect is already playing |
| } |
| |
| val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply { |
| interpolator = PathInterpolator(0.4f, 0f, 0f, 1f) |
| duration = RIPPLE_ANIMATION_DURATION |
| addUpdateListener { animator -> |
| val now = animator.currentPlayTime |
| rippleShader.progress = animator.animatedValue as Float |
| rippleShader.time = now.toFloat() |
| |
| lightReveal?.revealAmount = animator.animatedValue as Float |
| invalidate() |
| } |
| } |
| |
| val revealAnimator = ValueAnimator.ofFloat(0f, 1f).apply { |
| interpolator = rippleAnimator.interpolator |
| startDelay = 10 |
| duration = rippleAnimator.duration |
| addUpdateListener { animator -> |
| lightReveal?.revealAmount = animator.animatedValue as Float |
| } |
| } |
| |
| val alphaInAnimator = ValueAnimator.ofInt(0, 127).apply { |
| duration = 167 |
| addUpdateListener { animator -> |
| rippleShader.color = ColorUtils.setAlphaComponent( |
| rippleShader.color, |
| animator.animatedValue as Int |
| ) |
| invalidate() |
| } |
| } |
| |
| val alphaOutAnimator = ValueAnimator.ofInt(127, 0).apply { |
| startDelay = 417 |
| duration = 1116 |
| addUpdateListener { animator -> |
| rippleShader.color = ColorUtils.setAlphaComponent( |
| rippleShader.color, |
| animator.animatedValue as Int |
| ) |
| invalidate() |
| } |
| } |
| |
| val animatorSet = AnimatorSet().apply { |
| playTogether( |
| rippleAnimator, |
| revealAnimator, |
| alphaInAnimator, |
| alphaOutAnimator |
| ) |
| addListener(object : AnimatorListenerAdapter() { |
| override fun onAnimationStart(animation: Animator?) { |
| rippleInProgress = true |
| visibility = VISIBLE |
| } |
| |
| override fun onAnimationEnd(animation: Animator?) { |
| onAnimationEnd?.run() |
| rippleInProgress = false |
| visibility = GONE |
| } |
| }) |
| } |
| animatorSet.start() |
| } |
| |
| fun setColor(color: Int) { |
| rippleShader.color = color |
| } |
| |
| 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 * 1.5f |
| canvas?.drawCircle(origin.x, origin.y, maskRadius, ripplePaint) |
| } |
| } |