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() {
|
default void init() {
|
||||||
// Initialize components that have no direct tie to the dagger dependency graph,
|
// Initialize components that have no direct tie to the dagger dependency graph,
|
||||||
// but are critical to this component's operation
|
// but are critical to this component's operation
|
||||||
// TODO(b/205034537): I think this is a good idea?
|
|
||||||
getSysUIUnfoldComponent().ifPresent(c -> {
|
getSysUIUnfoldComponent().ifPresent(c -> {
|
||||||
c.getUnfoldLightRevealOverlayAnimation().init();
|
c.getUnfoldLightRevealOverlayAnimation().init();
|
||||||
c.getUnfoldTransitionWallpaperController().init();
|
c.getUnfoldTransitionWallpaperController().init();
|
||||||
|
c.getUnfoldHapticsPlayer();
|
||||||
});
|
});
|
||||||
getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init());
|
getNaturalRotationUnfoldProgressProvider().ifPresent(o -> o.init());
|
||||||
// No init method needed, just needs to be gotten so that it's created.
|
// No init method needed, just needs to be gotten so that it's created.
|
||||||
|
|||||||
@@ -92,5 +92,7 @@ interface SysUIUnfoldComponent {
|
|||||||
|
|
||||||
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
|
fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController
|
||||||
|
|
||||||
|
fun getUnfoldHapticsPlayer(): UnfoldHapticsPlayer
|
||||||
|
|
||||||
fun getUnfoldLightRevealOverlayAnimation(): UnfoldLightRevealOverlayAnimation
|
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
|
@Test
|
||||||
fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() {
|
fun testUnfold_screenAvailableOnlyAfterFullUnfold_emitsIncreasingTransitionEvents() {
|
||||||
runOnMainThreadWithInterval(
|
runOnMainThreadWithInterval(
|
||||||
@@ -157,6 +173,12 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
|
|||||||
currentRecording!!.addProgress(progress)
|
currentRecording!!.addProgress(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTransitionFinishing() {
|
||||||
|
assertWithMessage("Received transition finishing event when it's not started")
|
||||||
|
.that(currentRecording).isNotNull()
|
||||||
|
currentRecording!!.onFinishing()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTransitionFinished() {
|
override fun onTransitionFinished() {
|
||||||
assertWithMessage("Received transition finish event when it's not started")
|
assertWithMessage("Received transition finish event when it's not started")
|
||||||
.that(currentRecording).isNotNull()
|
.that(currentRecording).isNotNull()
|
||||||
@@ -171,6 +193,7 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
|
|||||||
|
|
||||||
class UnfoldTransitionRecording {
|
class UnfoldTransitionRecording {
|
||||||
private val progressHistory: MutableList<Float> = arrayListOf()
|
private val progressHistory: MutableList<Float> = arrayListOf()
|
||||||
|
private var finishingInvocations: Int = 0
|
||||||
|
|
||||||
fun addProgress(progress: Float) {
|
fun addProgress(progress: Float) {
|
||||||
assertThat(progress).isAtMost(1.0f)
|
assertThat(progress).isAtMost(1.0f)
|
||||||
@@ -179,6 +202,10 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
|
|||||||
progressHistory += progress
|
progressHistory += progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onFinishing() {
|
||||||
|
finishingInvocations++
|
||||||
|
}
|
||||||
|
|
||||||
fun assertIncreasingProgress() {
|
fun assertIncreasingProgress() {
|
||||||
assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
|
assertThat(progressHistory.size).isGreaterThan(MIN_ANIMATION_EVENTS)
|
||||||
assertThat(progressHistory).isInOrder()
|
assertThat(progressHistory).isInOrder()
|
||||||
@@ -206,6 +233,11 @@ class PhysicsBasedUnfoldTransitionProgressProviderTest : SysuiTestCase() {
|
|||||||
.isInOrder(Comparator.reverseOrder<Float>())
|
.isInOrder(Comparator.reverseOrder<Float>())
|
||||||
assertThat(progressHistory.last()).isEqualTo(0.0f)
|
assertThat(progressHistory.last()).isEqualTo(0.0f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun assertHasSingleFinishingEvent() {
|
||||||
|
assertWithMessage("onTransitionFinishing callback should be invoked exactly " +
|
||||||
|
"one time").that(finishingInvocations).isEqualTo(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ android_library {
|
|||||||
"dagger2",
|
"dagger2",
|
||||||
"jsr330",
|
"jsr330",
|
||||||
],
|
],
|
||||||
|
kotlincflags: ["-Xjvm-default=enable"],
|
||||||
java_version: "1.8",
|
java_version: "1.8",
|
||||||
min_sdk_version: "current",
|
min_sdk_version: "current",
|
||||||
plugins: ["dagger2-compiler"],
|
plugins: ["dagger2-compiler"],
|
||||||
|
|||||||
@@ -34,8 +34,28 @@ interface UnfoldTransitionProgressProvider : CallbackController<TransitionProgre
|
|||||||
fun destroy()
|
fun destroy()
|
||||||
|
|
||||||
interface TransitionProgressListener {
|
interface TransitionProgressListener {
|
||||||
|
/** Called when transition is started */
|
||||||
|
@JvmDefault
|
||||||
fun onTransitionStarted() {}
|
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) {}
|
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) {
|
override fun onAnimationStart(animator: Animator) {
|
||||||
listeners.forEach { it.onTransitionStarted() }
|
listeners.forEach { it.onTransitionStarted() }
|
||||||
|
listeners.forEach { it.onTransitionFinishing() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAnimationEnd(animator: Animator) {
|
override fun onAnimationEnd(animator: Animator) {
|
||||||
|
|||||||
@@ -125,6 +125,10 @@ class PhysicsBasedUnfoldTransitionProgressProvider(
|
|||||||
|
|
||||||
private fun cancelTransition(endValue: Float, animate: Boolean) {
|
private fun cancelTransition(endValue: Float, animate: Boolean) {
|
||||||
if (isTransitionRunning && animate) {
|
if (isTransitionRunning && animate) {
|
||||||
|
if (endValue == 1.0f && !isAnimatedCancelRunning) {
|
||||||
|
listeners.forEach { it.onTransitionFinishing() }
|
||||||
|
}
|
||||||
|
|
||||||
isAnimatedCancelRunning = true
|
isAnimatedCancelRunning = true
|
||||||
springAnimation.animateToFinalPosition(endValue)
|
springAnimation.animateToFinalPosition(endValue)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -110,6 +110,12 @@ constructor(source: UnfoldTransitionProgressProvider? = null) :
|
|||||||
lastTransitionProgress = progress
|
lastTransitionProgress = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTransitionFinishing() {
|
||||||
|
if (isReadyToHandleTransition) {
|
||||||
|
listeners.forEach { it.onTransitionFinishing() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onTransitionFinished() {
|
override fun onTransitionFinished() {
|
||||||
if (isReadyToHandleTransition) {
|
if (isReadyToHandleTransition) {
|
||||||
listeners.forEach { it.onTransitionFinished() }
|
listeners.forEach { it.onTransitionFinished() }
|
||||||
|
|||||||
Reference in New Issue
Block a user