Merge "[Unfold transition] Add haptics effect" into tm-qpr-dev

This commit is contained in:
Nick Chameyev
2022-12-12 10:17:18 +00:00
committed by Android (Google) Code Review
9 changed files with 161 additions and 2 deletions

View File

@@ -125,10 +125,10 @@ public interface SysUIComponent {
default void init() {
// Initialize components that have no direct tie to the dagger dependency graph,
// but are critical to this component's operation
// TODO(b/205034537): I think this is a good idea?
getSysUIUnfoldComponent().ifPresent(c -> {
c.getUnfoldLightRevealOverlayAnimation().init();
c.getUnfoldTransitionWallpaperController().init();
c.getUnfoldHapticsPlayer();
});
getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init());
// No init method needed, just needs to be gotten so that it's created.

View File

@@ -92,5 +92,7 @@ interface SysUIUnfoldComponent {
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer
fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
}

View File

@@ -0,0 +1,93 @@
package com.android.systemui.unfold
import android.os.SystemProperties
import android.os.VibrationEffect
import android.os.Vibrator
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import javax.inject.Inject
/**
* Class that plays a haptics effect during unfolding a foldable device
*/
@SysUIUnfoldScope
class UnfoldHapticsPlayer
@Inject
constructor(
unfoldTransitionProgressProvider: UnfoldTransitionProgressProvider,
private val vibrator: Vibrator?
) : TransitionProgressListener {
init {
if (vibrator != null) {
// We don't need to remove the callback because we should listen to it
// the whole time when SystemUI process is alive
unfoldTransitionProgressProvider.addCallback(this)
}
}
private var lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN
override fun onTransitionStarted() {
lastTransitionProgress = TRANSITION_PROGRESS_CLOSED
}
override fun onTransitionProgress(progress: Float) {
lastTransitionProgress = progress
}
override fun onTransitionFinishing() {
// Run haptics only if the animation is long enough to notice
if (lastTransitionProgress < TRANSITION_NOTICEABLE_THRESHOLD) {
playHaptics()
}
}
override fun onTransitionFinished() {
lastTransitionProgress = TRANSITION_PROGRESS_FULL_OPEN
}
private fun playHaptics() {
vibrator?.vibrate(effect)
}
private val hapticsScale: Float
get() {
val intensityString = SystemProperties.get("persist.unfold.haptics_scale", "0.1")
return intensityString.toFloatOrNull() ?: 0.1f
}
private val hapticsScaleTick: Float
get() {
val intensityString =
SystemProperties.get("persist.unfold.haptics_scale_end_tick", "0.6")
return intensityString.toFloatOrNull() ?: 0.6f
}
private val primitivesCount: Int
get() {
val count = SystemProperties.get("persist.unfold.primitives_count", "18")
return count.toIntOrNull() ?: 18
}
private val effect: VibrationEffect by lazy {
val composition =
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, 0F, 0)
repeat(primitivesCount) {
composition.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
hapticsScale,
0
)
}
composition
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, hapticsScaleTick)
.compose()
}
}
private const val TRANSITION_PROGRESS_CLOSED = 0f
private const val TRANSITION_PROGRESS_FULL_OPEN = 1f
private const val TRANSITION_NOTICEABLE_THRESHOLD = 0.9f

View File

@@ -68,6 +68,22 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
}
}
@Test
fun testUnfold_emitsFinishingEvent() {
runOnMainThreadWithInterval(
{ foldStateProvider.sendFoldUpdate(FOLD_UPDATE_START_OPENING) },
{ foldStateProvider.sendHingeAngleUpdate(10f) },
{ foldStateProvider.sendFoldUpdate(FOLD_UPDATE_UNFOLDED_SCREEN_AVAILABLE) },
{ foldStateProvider.sendHingeAngleUpdate(90f) },
{ foldStateProvider.sendHingeAngleUpdate(180f) },
{ foldStateProvider.sendFoldUpdate(FOLD_UPDATE_FINISH_FULL_OPEN) },
)
with(listener.ensureTransitionFinished()) {
assertHasSingleFinishingEvent()
}
}
@Test
fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() {
runOnMainThreadWithInterval(
@@ -157,6 +173,12 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
currentRecording!!.addProgress(progress)
}
override fun onTransitionFinishing() {
assertWithMessage("Received transition finishing event when it's not started")
.that(currentRecording).isNotNull()
currentRecording!!.onFinishing()
}
override fun onTransitionFinished() {
assertWithMessage("Received transition finish event when it's not started")
.that(currentRecording).isNotNull()
@@ -171,6 +193,7 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
class UnfoldTransitionRecording {
private val progressHistory: MutableList<Float> = arrayListOf()
private var finishingInvocations: Int = 0
fun addProgress(progress: Float) {
assertThat(progress).isAtMost(1.0f)
@@ -179,6 +202,10 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
progressHistory += progress
}
fun onFinishing() {
finishingInvocations++
}
fun assertIncreasingProgress() {
assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
assertThat(progressHistory).isInOrder()
@@ -206,6 +233,11 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
.isInOrder(Comparator.reverseOrder<Float>())
assertThat(progressHistory.last()).isEqualTo(0.0f)
}
fun assertHasSingleFinishingEvent() {
assertWithMessage("onTransitionFinishing callback should be invoked exactly " +
"one time").that(finishingInvocations).isEqualTo(1)
}
}
private companion object {

View File

@@ -33,6 +33,7 @@ android_library {
"dagger2",
"jsr330",
],
kotlincflags: ["-Xjvm-default=enable"],
java_version: "1.8",
min_sdk_version: "current",
plugins: ["dagger2-compiler"],

View File

@@ -34,8 +34,28 @@ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgre
fun destroy()
interface TransitionProgressListener {
/** Called when transition is started */
@JvmDefault
fun onTransitionStarted() {}
fun onTransitionFinished() {}
/**
* Called whenever transition progress is updated, [progress] is a value of the animation
* where 0 is fully folded, 1 is fully unfolded
*/
@JvmDefault
fun onTransitionProgress(@FloatRange(from = 0.0, to = 1.0) progress: Float) {}
/**
* Called when the progress provider determined that the transition is about to finish soon.
*
* For example, in [PhysicsBasedUnfoldTransitionProgressProvider] this could happen when the
* animation is not tied to the hinge angle anymore and it is about to run fixed animation.
*/
@JvmDefault
fun onTransitionFinishing() {}
/** Called when transition is completely finished */
@JvmDefault
fun onTransitionFinished() {}
}
}

View File

@@ -88,6 +88,7 @@ internal class FixedTimingTransitionProgressProvider(
override fun onAnimationStart(animator: Animator) {
listeners.forEach { it.onTransitionStarted() }
listeners.forEach { it.onTransitionFinishing() }
}
override fun onAnimationEnd(animator: Animator) {

View File

@@ -125,6 +125,10 @@ class PhysicsBasedUnfoldTransitionProgressProvider(
private fun cancelTransition(endValue: Float, animate: Boolean) {
if (isTransitionRunning && animate) {
if (endValue == 1.0f && !isAnimatedCancelRunning) {
listeners.forEach { it.onTransitionFinishing() }
}
isAnimatedCancelRunning = true
springAnimation.animateToFinalPosition(endValue)
} else {

View File

@@ -110,6 +110,12 @@ constructor(source: UnfoldTransitionProgressProvider? = null) :
lastTransitionProgress = progress
}
override fun onTransitionFinishing() {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinishing() }
}
}
override fun onTransitionFinished() {
if (isReadyToHandleTransition) {
listeners.forEach { it.onTransitionFinished() }