Add dwell-ripple am: 3ad5b8bb1e am: f26e310e0d
Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/15779694 Change-Id: I5124667fefea9efa5458f5edd1d5447608342ef2
This commit is contained in:
@@ -20,6 +20,7 @@ import android.content.Context
|
|||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.graphics.PointF
|
import android.graphics.PointF
|
||||||
import android.hardware.biometrics.BiometricSourceType
|
import android.hardware.biometrics.BiometricSourceType
|
||||||
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import com.android.keyguard.KeyguardUpdateMonitor
|
import com.android.keyguard.KeyguardUpdateMonitor
|
||||||
import com.android.keyguard.KeyguardUpdateMonitorCallback
|
import com.android.keyguard.KeyguardUpdateMonitorCallback
|
||||||
@@ -38,6 +39,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController
|
|||||||
import com.android.systemui.util.ViewController
|
import com.android.systemui.util.ViewController
|
||||||
import java.io.PrintWriter
|
import java.io.PrintWriter
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Controls the ripple effect that shows when authentication is successful.
|
* Controls the ripple effect that shows when authentication is successful.
|
||||||
@@ -54,12 +56,18 @@ class AuthRippleController @Inject constructor(
|
|||||||
private val notificationShadeWindowController: NotificationShadeWindowController,
|
private val notificationShadeWindowController: NotificationShadeWindowController,
|
||||||
private val bypassController: KeyguardBypassController,
|
private val bypassController: KeyguardBypassController,
|
||||||
private val biometricUnlockController: BiometricUnlockController,
|
private val biometricUnlockController: BiometricUnlockController,
|
||||||
|
private val udfpsControllerProvider: Provider<UdfpsController>,
|
||||||
rippleView: AuthRippleView?
|
rippleView: AuthRippleView?
|
||||||
) : ViewController<AuthRippleView>(rippleView) {
|
) : ViewController<AuthRippleView>(rippleView) {
|
||||||
var fingerprintSensorLocation: PointF? = null
|
var fingerprintSensorLocation: PointF? = null
|
||||||
private var faceSensorLocation: PointF? = null
|
private var faceSensorLocation: PointF? = null
|
||||||
private var circleReveal: LightRevealEffect? = null
|
private var circleReveal: LightRevealEffect? = null
|
||||||
|
|
||||||
|
private var udfpsController: UdfpsController? = null
|
||||||
|
private var dwellScale = 2f
|
||||||
|
private var expandedDwellScale = 2.5f
|
||||||
|
private var udfpsRadius: Float = -1f
|
||||||
|
|
||||||
override fun onInit() {
|
override fun onInit() {
|
||||||
mView.setAlphaInDuration(sysuiContext.resources.getInteger(
|
mView.setAlphaInDuration(sysuiContext.resources.getInteger(
|
||||||
R.integer.auth_ripple_alpha_in_duration).toLong())
|
R.integer.auth_ripple_alpha_in_duration).toLong())
|
||||||
@@ -67,9 +75,11 @@ class AuthRippleController @Inject constructor(
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public override fun onViewAttached() {
|
public override fun onViewAttached() {
|
||||||
|
authController.addCallback(authControllerCallback)
|
||||||
updateRippleColor()
|
updateRippleColor()
|
||||||
updateSensorLocation()
|
updateSensorLocation()
|
||||||
authController.addCallback(authControllerCallback)
|
updateUdfpsDependentParams()
|
||||||
|
udfpsController?.addCallback(udfpsControllerCallback)
|
||||||
configurationController.addCallback(configurationChangedListener)
|
configurationController.addCallback(configurationChangedListener)
|
||||||
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
|
keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback)
|
||||||
commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
|
commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() }
|
||||||
@@ -77,6 +87,7 @@ class AuthRippleController @Inject constructor(
|
|||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public override fun onViewDetached() {
|
public override fun onViewDetached() {
|
||||||
|
udfpsController?.removeCallback(udfpsControllerCallback)
|
||||||
authController.removeCallback(authControllerCallback)
|
authController.removeCallback(authControllerCallback)
|
||||||
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
|
keyguardUpdateMonitor.removeCallback(keyguardUpdateMonitorCallback)
|
||||||
configurationController.removeCallback(configurationChangedListener)
|
configurationController.removeCallback(configurationChangedListener)
|
||||||
@@ -94,18 +105,18 @@ class AuthRippleController @Inject constructor(
|
|||||||
if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
|
if (biometricSourceType == BiometricSourceType.FINGERPRINT &&
|
||||||
fingerprintSensorLocation != null) {
|
fingerprintSensorLocation != null) {
|
||||||
mView.setSensorLocation(fingerprintSensorLocation!!)
|
mView.setSensorLocation(fingerprintSensorLocation!!)
|
||||||
showRipple()
|
showUnlockedRipple()
|
||||||
} else if (biometricSourceType == BiometricSourceType.FACE &&
|
} else if (biometricSourceType == BiometricSourceType.FACE &&
|
||||||
faceSensorLocation != null) {
|
faceSensorLocation != null) {
|
||||||
if (!bypassController.canBypass()) {
|
if (!bypassController.canBypass()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mView.setSensorLocation(faceSensorLocation!!)
|
mView.setSensorLocation(faceSensorLocation!!)
|
||||||
showRipple()
|
showUnlockedRipple()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showRipple() {
|
private fun showUnlockedRipple() {
|
||||||
notificationShadeWindowController.setForcePluginOpen(true, this)
|
notificationShadeWindowController.setForcePluginOpen(true, this)
|
||||||
val biometricUnlockMode = biometricUnlockController.mode
|
val biometricUnlockMode = biometricUnlockController.mode
|
||||||
val useCircleReveal = circleReveal != null &&
|
val useCircleReveal = circleReveal != null &&
|
||||||
@@ -117,7 +128,7 @@ class AuthRippleController @Inject constructor(
|
|||||||
lightRevealScrim?.revealEffect = circleReveal!!
|
lightRevealScrim?.revealEffect = circleReveal!!
|
||||||
}
|
}
|
||||||
|
|
||||||
mView.startRipple(
|
mView.startUnlockedRipple(
|
||||||
/* end runnable */
|
/* end runnable */
|
||||||
Runnable {
|
Runnable {
|
||||||
notificationShadeWindowController.setForcePluginOpen(false, this)
|
notificationShadeWindowController.setForcePluginOpen(false, this)
|
||||||
@@ -152,7 +163,7 @@ class AuthRippleController @Inject constructor(
|
|||||||
Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
|
Utils.getColorAttr(sysuiContext, android.R.attr.colorAccent).defaultColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
val keyguardUpdateMonitorCallback =
|
private val keyguardUpdateMonitorCallback =
|
||||||
object : KeyguardUpdateMonitorCallback() {
|
object : KeyguardUpdateMonitorCallback() {
|
||||||
override fun onBiometricAuthenticated(
|
override fun onBiometricAuthenticated(
|
||||||
userId: Int,
|
userId: Int,
|
||||||
@@ -161,9 +172,13 @@ class AuthRippleController @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
showRipple(biometricSourceType)
|
showRipple(biometricSourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onBiometricAuthFailed(biometricSourceType: BiometricSourceType?) {
|
||||||
|
mView.retractRipple()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val configurationChangedListener =
|
private val configurationChangedListener =
|
||||||
object : ConfigurationController.ConfigurationListener {
|
object : ConfigurationController.ConfigurationListener {
|
||||||
override fun onConfigChanged(newConfig: Configuration?) {
|
override fun onConfigChanged(newConfig: Configuration?) {
|
||||||
updateSensorLocation()
|
updateSensorLocation()
|
||||||
@@ -179,14 +194,97 @@ class AuthRippleController @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val authControllerCallback = AuthController.Callback { updateSensorLocation() }
|
private val udfpsControllerCallback =
|
||||||
|
object : UdfpsController.Callback {
|
||||||
|
override fun onFingerDown() {
|
||||||
|
if (fingerprintSensorLocation == null) {
|
||||||
|
Log.e("AuthRipple", "fingerprintSensorLocation=null onFingerDown. " +
|
||||||
|
"Skip showing dwell ripple")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mView.setSensorLocation(fingerprintSensorLocation!!)
|
||||||
|
mView.startDwellRipple(
|
||||||
|
/* startRadius */ udfpsRadius,
|
||||||
|
/* endRadius */ udfpsRadius * dwellScale,
|
||||||
|
/* expandedRadius */ udfpsRadius * expandedDwellScale)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFingerUp() {
|
||||||
|
mView.retractRipple()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val authControllerCallback = AuthController.Callback {
|
||||||
|
updateSensorLocation()
|
||||||
|
updateUdfpsDependentParams()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUdfpsDependentParams() {
|
||||||
|
authController.udfpsProps?.let {
|
||||||
|
if (it.size > 0) {
|
||||||
|
udfpsRadius = it[0].sensorRadius.toFloat()
|
||||||
|
udfpsController = udfpsControllerProvider.get()
|
||||||
|
|
||||||
|
if (mView.isAttachedToWindow) {
|
||||||
|
udfpsController?.addCallback(udfpsControllerCallback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
inner class AuthRippleCommand : Command {
|
inner class AuthRippleCommand : Command {
|
||||||
|
fun printDwellInfo(pw: PrintWriter) {
|
||||||
|
pw.println("dwell ripple: " +
|
||||||
|
"\n\tsensorLocation=$fingerprintSensorLocation" +
|
||||||
|
"\n\tdwellScale=$dwellScale" +
|
||||||
|
"\n\tdwellAlpha=${mView.dwellAlpha}, " +
|
||||||
|
"duration=${mView.dwellAlphaDuration}" +
|
||||||
|
"\n\tdwellExpand=$expandedDwellScale" +
|
||||||
|
"\n\t(crash systemui to reset to default)")
|
||||||
|
}
|
||||||
|
|
||||||
override fun execute(pw: PrintWriter, args: List<String>) {
|
override fun execute(pw: PrintWriter, args: List<String>) {
|
||||||
if (args.isEmpty()) {
|
if (args.isEmpty()) {
|
||||||
invalidCommand(pw)
|
invalidCommand(pw)
|
||||||
} else {
|
} else {
|
||||||
when (args[0]) {
|
when (args[0]) {
|
||||||
|
"dwellScale" -> {
|
||||||
|
if (args.size > 1 && args[1].toFloatOrNull() != null) {
|
||||||
|
dwellScale = args[1].toFloat()
|
||||||
|
printDwellInfo(pw)
|
||||||
|
} else {
|
||||||
|
pw.println("expected float argument <dwellScale>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"dwellAlpha" -> {
|
||||||
|
if (args.size > 2 && args[1].toFloatOrNull() != null &&
|
||||||
|
args[2].toLongOrNull() != null) {
|
||||||
|
mView.dwellAlpha = args[1].toFloat()
|
||||||
|
if (args[2].toFloat() > 200L) {
|
||||||
|
pw.println("alpha animation duration must be less than 200ms.")
|
||||||
|
}
|
||||||
|
mView.dwellAlphaDuration = kotlin.math.min(args[2].toLong(), 200L)
|
||||||
|
printDwellInfo(pw)
|
||||||
|
} else {
|
||||||
|
pw.println("expected two float arguments:" +
|
||||||
|
" <dwellAlpha> <dwellAlphaDuration>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"dwellExpand" -> {
|
||||||
|
if (args.size > 1 && args[1].toFloatOrNull() != null) {
|
||||||
|
val expandedScale = args[1].toFloat()
|
||||||
|
if (expandedScale <= dwellScale) {
|
||||||
|
pw.println("invalid expandedScale. must be greater than " +
|
||||||
|
"dwellScale=$dwellScale, but given $expandedScale")
|
||||||
|
} else {
|
||||||
|
expandedDwellScale = expandedScale
|
||||||
|
}
|
||||||
|
printDwellInfo(pw)
|
||||||
|
} else {
|
||||||
|
pw.println("expected float argument <expandedScale>")
|
||||||
|
}
|
||||||
|
}
|
||||||
"fingerprint" -> {
|
"fingerprint" -> {
|
||||||
pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
|
pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation")
|
||||||
showRipple(BiometricSourceType.FINGERPRINT)
|
showRipple(BiometricSourceType.FINGERPRINT)
|
||||||
@@ -205,7 +303,7 @@ class AuthRippleController @Inject constructor(
|
|||||||
pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " +
|
pw.println("custom ripple sensorLocation=" + args[1].toFloat() + ", " +
|
||||||
args[2].toFloat())
|
args[2].toFloat())
|
||||||
mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat()))
|
mView.setSensorLocation(PointF(args[1].toFloat(), args[2].toFloat()))
|
||||||
showRipple()
|
showUnlockedRipple()
|
||||||
}
|
}
|
||||||
else -> invalidCommand(pw)
|
else -> invalidCommand(pw)
|
||||||
}
|
}
|
||||||
@@ -215,6 +313,9 @@ class AuthRippleController @Inject constructor(
|
|||||||
override fun help(pw: PrintWriter) {
|
override fun help(pw: PrintWriter) {
|
||||||
pw.println("Usage: adb shell cmd statusbar auth-ripple <command>")
|
pw.println("Usage: adb shell cmd statusbar auth-ripple <command>")
|
||||||
pw.println("Available commands:")
|
pw.println("Available commands:")
|
||||||
|
pw.println(" dwellScale <200ms_scale: float>")
|
||||||
|
pw.println(" dwellAlpha <alpha: float> <duration : long>")
|
||||||
|
pw.println(" dwellExpand <expanded_scale: float>")
|
||||||
pw.println(" fingerprint")
|
pw.println(" fingerprint")
|
||||||
pw.println(" face")
|
pw.println(" face")
|
||||||
pw.println(" custom <x-location: int> <y-location: int>")
|
pw.println(" custom <x-location: int> <y-location: int>")
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ import android.graphics.PointF
|
|||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.PathInterpolator
|
import android.view.animation.PathInterpolator
|
||||||
|
import com.android.internal.R.attr.interpolator
|
||||||
import com.android.internal.graphics.ColorUtils
|
import com.android.internal.graphics.ColorUtils
|
||||||
|
import com.android.systemui.animation.Interpolators
|
||||||
import com.android.systemui.statusbar.LightRevealScrim
|
import com.android.systemui.statusbar.LightRevealScrim
|
||||||
import com.android.systemui.statusbar.charging.RippleShader
|
import com.android.systemui.statusbar.charging.RippleShader
|
||||||
|
|
||||||
@@ -34,15 +36,28 @@ private const val RIPPLE_ANIMATION_DURATION: Long = 1533
|
|||||||
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
|
private const val RIPPLE_SPARKLE_STRENGTH: Float = 0.4f
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Expanding ripple effect on the transition from biometric authentication success to showing
|
* Expanding ripple effect
|
||||||
|
* - startUnlockedRipple for the transition from biometric authentication success to showing
|
||||||
* launcher.
|
* 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) {
|
class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, attrs) {
|
||||||
|
private val retractInterpolator = PathInterpolator(.05f, .93f, .1f, 1f)
|
||||||
|
private val dwellPulseDuration = 200L
|
||||||
|
var dwellAlphaDuration = dwellPulseDuration
|
||||||
|
private val dwellExpandDuration = 1200L - dwellPulseDuration
|
||||||
|
private val retractDuration = 400L
|
||||||
|
|
||||||
|
var dwellAlpha: Float = .5f
|
||||||
private var alphaInDuration: Long = 0
|
private var alphaInDuration: Long = 0
|
||||||
private var rippleInProgress: Boolean = false
|
private var unlockedRippleInProgress: Boolean = false
|
||||||
private val rippleShader = RippleShader()
|
private val rippleShader = RippleShader()
|
||||||
private val ripplePaint = Paint()
|
private val ripplePaint = Paint()
|
||||||
private var radius: Float = 0.0f
|
private var retractAnimator: Animator? = null
|
||||||
|
private var dwellPulseOutAnimator: Animator? = null
|
||||||
|
private var radius: Float = 0f
|
||||||
set(value) {
|
set(value) {
|
||||||
rippleShader.radius = value
|
rippleShader.radius = value
|
||||||
field = value
|
field = value
|
||||||
@@ -63,21 +78,182 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
|
|||||||
|
|
||||||
fun setSensorLocation(location: PointF) {
|
fun setSensorLocation(location: PointF) {
|
||||||
origin = location
|
origin = location
|
||||||
radius = maxOf(location.x, location.y, width - location.x, height - location.y)
|
radius = maxOf(location.x, location.y, width - location.x, height - location.y).toFloat()
|
||||||
.toFloat()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setAlphaInDuration(duration: Long) {
|
fun setAlphaInDuration(duration: Long) {
|
||||||
alphaInDuration = duration
|
alphaInDuration = duration
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startRipple(onAnimationEnd: Runnable?, lightReveal: LightRevealScrim?) {
|
/**
|
||||||
if (rippleInProgress) {
|
* 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) {
|
||||||
|
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 pulseOutEndAlpha = (255 * dwellAlpha).toInt()
|
||||||
|
val expandDwellEndAlpha = kotlin.math.min((255 * (dwellAlpha + .25f)).toInt(), 255)
|
||||||
|
val dwellPulseOutRippleAnimator = ValueAnimator.ofFloat(startDwellProgress,
|
||||||
|
endInitialDwellProgress).apply {
|
||||||
|
interpolator = Interpolators.LINEAR_OUT_SLOW_IN
|
||||||
|
duration = 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 = 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 = 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 = 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
|
return // Ignore if ripple effect is already playing
|
||||||
}
|
}
|
||||||
|
|
||||||
val rippleAnimator = ValueAnimator.ofFloat(0f, 1f).apply {
|
var rippleStart = 0f
|
||||||
interpolator = PathInterpolator(0f, 0f, .2f, 1f)
|
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
|
duration = RIPPLE_ANIMATION_DURATION
|
||||||
addUpdateListener { animator ->
|
addUpdateListener { animator ->
|
||||||
val now = animator.currentPlayTime
|
val now = animator.currentPlayTime
|
||||||
@@ -97,7 +273,7 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
|
|||||||
}
|
}
|
||||||
|
|
||||||
val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply {
|
val alphaInAnimator = ValueAnimator.ofInt(0, 255).apply {
|
||||||
duration = alphaInDuration
|
duration = alphaDuration
|
||||||
addUpdateListener { animator ->
|
addUpdateListener { animator ->
|
||||||
rippleShader.color = ColorUtils.setAlphaComponent(
|
rippleShader.color = ColorUtils.setAlphaComponent(
|
||||||
rippleShader.color,
|
rippleShader.color,
|
||||||
@@ -115,13 +291,14 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
|
|||||||
)
|
)
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
addListener(object : AnimatorListenerAdapter() {
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
override fun onAnimationStart(animation: Animator?) {
|
||||||
rippleInProgress = true
|
unlockedRippleInProgress = true
|
||||||
|
rippleShader.shouldFadeOutRipple = true
|
||||||
visibility = VISIBLE
|
visibility = VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator?) {
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
onAnimationEnd?.run()
|
onAnimationEnd?.run()
|
||||||
rippleInProgress = false
|
unlockedRippleInProgress = false
|
||||||
visibility = GONE
|
visibility = GONE
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -129,8 +306,16 @@ class AuthRippleView(context: Context?, attrs: AttributeSet?) : View(context, at
|
|||||||
animatorSet.start()
|
animatorSet.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetRippleAlpha() {
|
||||||
|
rippleShader.color = ColorUtils.setAlphaComponent(
|
||||||
|
rippleShader.color,
|
||||||
|
255
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun setColor(color: Int) {
|
fun setColor(color: Int) {
|
||||||
rippleShader.color = color
|
rippleShader.color = color
|
||||||
|
resetRippleAlpha()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDraw(canvas: Canvas?) {
|
override fun onDraw(canvas: Canvas?) {
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ import com.android.systemui.util.concurrency.DelayableExecutor;
|
|||||||
import com.android.systemui.util.concurrency.Execution;
|
import com.android.systemui.util.concurrency.Execution;
|
||||||
import com.android.systemui.util.time.SystemClock;
|
import com.android.systemui.util.time.SystemClock;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@@ -158,6 +160,7 @@ public class UdfpsController implements DozeReceiver {
|
|||||||
private Runnable mAodInterruptRunnable;
|
private Runnable mAodInterruptRunnable;
|
||||||
private boolean mOnFingerDown;
|
private boolean mOnFingerDown;
|
||||||
private boolean mAttemptedToDismissKeyguard;
|
private boolean mAttemptedToDismissKeyguard;
|
||||||
|
private Set<Callback> mCallbacks = new HashSet<>();
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
public static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES =
|
public static final AudioAttributes VIBRATION_SONIFICATION_ATTRIBUTES =
|
||||||
@@ -876,6 +879,20 @@ public class UdfpsController implements DozeReceiver {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a callback for fingerUp and fingerDown events
|
||||||
|
*/
|
||||||
|
public void addCallback(Callback cb) {
|
||||||
|
mCallbacks.add(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove callback
|
||||||
|
*/
|
||||||
|
public void removeCallback(Callback cb) {
|
||||||
|
mCallbacks.remove(cb);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel updfs scan affordances - ability to hide the HbmSurfaceView (white circle) before
|
* Cancel updfs scan affordances - ability to hide the HbmSurfaceView (white circle) before
|
||||||
* user explicitly lifts their finger. Generally, this should be called whenever udfps fails
|
* user explicitly lifts their finger. Generally, this should be called whenever udfps fails
|
||||||
@@ -930,6 +947,10 @@ public class UdfpsController implements DozeReceiver {
|
|||||||
mFingerprintManager.onUiReady(mSensorProps.sensorId);
|
mFingerprintManager.onUiReady(mSensorProps.sensorId);
|
||||||
Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0);
|
Trace.endAsyncSection("UdfpsController.e2e.startIllumination", 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (Callback cb : mCallbacks) {
|
||||||
|
cb.onFingerDown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFingerUp() {
|
private void onFingerUp() {
|
||||||
@@ -942,6 +963,9 @@ public class UdfpsController implements DozeReceiver {
|
|||||||
}
|
}
|
||||||
if (mOnFingerDown) {
|
if (mOnFingerDown) {
|
||||||
mFingerprintManager.onPointerUp(mSensorProps.sensorId);
|
mFingerprintManager.onPointerUp(mSensorProps.sensorId);
|
||||||
|
for (Callback cb : mCallbacks) {
|
||||||
|
cb.onFingerUp();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mOnFingerDown = false;
|
mOnFingerDown = false;
|
||||||
if (mView.isIlluminationRequested()) {
|
if (mView.isIlluminationRequested()) {
|
||||||
@@ -962,4 +986,19 @@ public class UdfpsController implements DozeReceiver {
|
|||||||
mView.setOnTouchListener(mOnTouchListener);
|
mView.setOnTouchListener(mOnTouchListener);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for fingerUp and fingerDown events.
|
||||||
|
*/
|
||||||
|
public interface Callback {
|
||||||
|
/**
|
||||||
|
* Called onFingerUp events. Will only be called if the finger was previously down.
|
||||||
|
*/
|
||||||
|
void onFingerUp();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called onFingerDown events.
|
||||||
|
*/
|
||||||
|
void onFingerDown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,8 +147,12 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) {
|
|||||||
|
|
||||||
val fadeIn = subProgress(0f, 0.1f, value)
|
val fadeIn = subProgress(0f, 0.1f, value)
|
||||||
val fadeOutNoise = subProgress(0.4f, 1f, value)
|
val fadeOutNoise = subProgress(0.4f, 1f, value)
|
||||||
val fadeOutRipple = subProgress(0.3f, 1f, value)
|
var fadeOutRipple = 0f
|
||||||
val fadeCircle = subProgress(0f, 0.2f, value)
|
var fadeCircle = 0f
|
||||||
|
if (shouldFadeOutRipple) {
|
||||||
|
fadeCircle = subProgress(0f, 0.2f, value)
|
||||||
|
fadeOutRipple = subProgress(0.3f, 1f, value)
|
||||||
|
}
|
||||||
setUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
|
setUniform("in_fadeSparkle", Math.min(fadeIn, 1 - fadeOutNoise))
|
||||||
setUniform("in_fadeCircle", 1 - fadeCircle)
|
setUniform("in_fadeCircle", 1 - fadeCircle)
|
||||||
setUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
|
setUniform("in_fadeRing", Math.min(fadeIn, 1 - fadeOutRipple))
|
||||||
@@ -200,4 +204,6 @@ class RippleShader internal constructor() : RuntimeShader(SHADER, false) {
|
|||||||
field = value
|
field = value
|
||||||
setUniform("in_pixelDensity", value)
|
setUniform("in_pixelDensity", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldFadeOutRipple: Boolean = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import org.mockito.Mockito.reset
|
|||||||
import org.mockito.Mockito.verify
|
import org.mockito.Mockito.verify
|
||||||
import org.mockito.MockitoAnnotations
|
import org.mockito.MockitoAnnotations
|
||||||
|
|
||||||
|
import javax.inject.Provider
|
||||||
|
|
||||||
@SmallTest
|
@SmallTest
|
||||||
@RunWith(AndroidTestingRunner::class)
|
@RunWith(AndroidTestingRunner::class)
|
||||||
class AuthRippleControllerTest : SysuiTestCase() {
|
class AuthRippleControllerTest : SysuiTestCase() {
|
||||||
@@ -55,10 +57,14 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
|
@Mock private lateinit var notificationShadeWindowController: NotificationShadeWindowController
|
||||||
@Mock private lateinit var bypassController: KeyguardBypassController
|
@Mock private lateinit var bypassController: KeyguardBypassController
|
||||||
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
|
@Mock private lateinit var biometricUnlockController: BiometricUnlockController
|
||||||
|
@Mock private lateinit var udfpsControllerProvider: Provider<UdfpsController>
|
||||||
|
@Mock private lateinit var udfpsController: UdfpsController
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
MockitoAnnotations.initMocks(this)
|
MockitoAnnotations.initMocks(this)
|
||||||
|
`when`(udfpsControllerProvider.get()).thenReturn(udfpsController)
|
||||||
|
|
||||||
controller = AuthRippleController(
|
controller = AuthRippleController(
|
||||||
statusBar,
|
statusBar,
|
||||||
context,
|
context,
|
||||||
@@ -69,6 +75,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
notificationShadeWindowController,
|
notificationShadeWindowController,
|
||||||
bypassController,
|
bypassController,
|
||||||
biometricUnlockController,
|
biometricUnlockController,
|
||||||
|
udfpsControllerProvider,
|
||||||
rippleView
|
rippleView
|
||||||
)
|
)
|
||||||
controller.init()
|
controller.init()
|
||||||
@@ -93,7 +100,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
// THEN update sensor location and show ripple
|
// THEN update sensor location and show ripple
|
||||||
verify(rippleView).setSensorLocation(fpsLocation)
|
verify(rippleView).setSensorLocation(fpsLocation)
|
||||||
verify(rippleView).startRipple(any(), any())
|
verify(rippleView).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -114,7 +121,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
false /* isStrongBiometric */)
|
false /* isStrongBiometric */)
|
||||||
|
|
||||||
// THEN no ripple
|
// THEN no ripple
|
||||||
verify(rippleView, never()).startRipple(any(), any())
|
verify(rippleView, never()).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -135,7 +142,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
false /* isStrongBiometric */)
|
false /* isStrongBiometric */)
|
||||||
|
|
||||||
// THEN no ripple
|
// THEN no ripple
|
||||||
verify(rippleView, never()).startRipple(any(), any())
|
verify(rippleView, never()).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -159,7 +166,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
|
|
||||||
// THEN show ripple
|
// THEN show ripple
|
||||||
verify(rippleView).setSensorLocation(faceLocation)
|
verify(rippleView).setSensorLocation(faceLocation)
|
||||||
verify(rippleView).startRipple(any(), any())
|
verify(rippleView).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -179,7 +186,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
false /* isStrongBiometric */)
|
false /* isStrongBiometric */)
|
||||||
|
|
||||||
// THEN no ripple
|
// THEN no ripple
|
||||||
verify(rippleView, never()).startRipple(any(), any())
|
verify(rippleView, never()).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -194,7 +201,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
0 /* userId */,
|
0 /* userId */,
|
||||||
BiometricSourceType.FACE /* type */,
|
BiometricSourceType.FACE /* type */,
|
||||||
false /* isStrongBiometric */)
|
false /* isStrongBiometric */)
|
||||||
verify(rippleView, never()).startRipple(any(), any())
|
verify(rippleView, never()).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -209,7 +216,7 @@ class AuthRippleControllerTest : SysuiTestCase() {
|
|||||||
0 /* userId */,
|
0 /* userId */,
|
||||||
BiometricSourceType.FINGERPRINT /* type */,
|
BiometricSourceType.FINGERPRINT /* type */,
|
||||||
false /* isStrongBiometric */)
|
false /* isStrongBiometric */)
|
||||||
verify(rippleView, never()).startRipple(any(), any())
|
verify(rippleView, never()).startUnlockedRipple(any(), any())
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user