Merge "[Unfold transition] Add haptics effect" into tm-qpr-dev
This commit is contained in:
committed by
Android (Google) Code Review
commit
37b5e8b974
@@ -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.
|
||||
|
||||
@@ -92,5 +92,7 @@ interface SysUIUnfoldComponent {
|
||||
|
||||
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
|
||||
|
||||
fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer
|
||||
|
||||
fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -33,6 +33,7 @@ android_library {
|
||||
"dagger2",
|
||||
"jsr330",
|
||||
],
|
||||
kotlincflags: ["-Xjvm-default=enable"],
|
||||
java_version: "1.8",
|
||||
min_sdk_version: "current",
|
||||
plugins: ["dagger2-compiler"],
|
||||
|
||||
@@ -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() {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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() }
|
||||
|
||||
Reference in New Issue
Block a user