Merge "Remove unnecessary animation cancellations when exiting low light" into udc-qpr-dev
This commit is contained in:
@@ -23,6 +23,7 @@ import android.content.ComponentName
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import com.android.dream.lowlight.dagger.LowLightDreamModule
|
import com.android.dream.lowlight.dagger.LowLightDreamModule
|
||||||
import com.android.dream.lowlight.dagger.qualifiers.Application
|
import com.android.dream.lowlight.dagger.qualifiers.Application
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.TimeoutCancellationException
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
@@ -103,6 +104,11 @@ class LowLightDreamManager @Inject constructor(
|
|||||||
)
|
)
|
||||||
} catch (ex: TimeoutCancellationException) {
|
} catch (ex: TimeoutCancellationException) {
|
||||||
Log.e(TAG, "timed out while waiting for low light animation", ex)
|
Log.e(TAG, "timed out while waiting for low light animation", ex)
|
||||||
|
} catch (ex: CancellationException) {
|
||||||
|
Log.w(TAG, "low light transition animation cancelled")
|
||||||
|
// Catch the cancellation so that we still set the system dream component if the
|
||||||
|
// animation is cancelled, such as by a user tapping to wake as the transition to
|
||||||
|
// low light happens.
|
||||||
}
|
}
|
||||||
dreamManager.setSystemDreamComponent(
|
dreamManager.setSystemDreamComponent(
|
||||||
if (shouldEnterLowLight) lowLightDreamComponent else null
|
if (shouldEnterLowLight) lowLightDreamComponent else null
|
||||||
|
|||||||
@@ -110,15 +110,5 @@ class LowLightTransitionCoordinator @Inject constructor() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
animator.addListener(listener)
|
animator.addListener(listener)
|
||||||
continuation.invokeOnCancellation {
|
|
||||||
try {
|
|
||||||
animator.removeListener(listener)
|
|
||||||
animator.cancel()
|
|
||||||
} catch (exception: IndexOutOfBoundsException) {
|
|
||||||
// TODO(b/285666217): remove this try/catch once a proper fix is implemented.
|
|
||||||
// Cancelling the animator can cause an exception since we may be removing a
|
|
||||||
// listener during the cancellation. See b/285666217 for more details.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.dream.lowlight.util
|
||||||
|
|
||||||
|
import android.view.animation.Interpolator
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interpolator wrapper that shortens another interpolator from its original duration to a portion
|
||||||
|
* of that duration.
|
||||||
|
*
|
||||||
|
* For example, an `originalDuration` of 1000 and a `newDuration` of 200 results in an animation
|
||||||
|
* that when played for 200ms is the exact same as the first 200ms of a 1000ms animation if using
|
||||||
|
* the original interpolator.
|
||||||
|
*
|
||||||
|
* This is useful for the transition between the user dream and the low light clock as some
|
||||||
|
* animations are defined in the spec to be longer than the total duration of the animation. For
|
||||||
|
* example, the low light clock exit translation animation is defined to last >1s while the actual
|
||||||
|
* fade out of the low light clock is only 250ms, meaning the clock isn't visible anymore after
|
||||||
|
* 250ms.
|
||||||
|
*
|
||||||
|
* Since the dream framework currently only allows one dream to be visible and running, we use this
|
||||||
|
* interpolator to play just the first 250ms of the translation animation. Simply reducing the
|
||||||
|
* duration of the animation would result in the text exiting much faster than intended, so a custom
|
||||||
|
* interpolator is needed.
|
||||||
|
*/
|
||||||
|
class TruncatedInterpolator(
|
||||||
|
private val baseInterpolator: Interpolator,
|
||||||
|
originalDuration: Float,
|
||||||
|
newDuration: Float
|
||||||
|
) : Interpolator {
|
||||||
|
private val scaleFactor: Float
|
||||||
|
|
||||||
|
init {
|
||||||
|
scaleFactor = newDuration / originalDuration
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getInterpolation(input: Float): Float {
|
||||||
|
return baseInterpolator.getInterpolation(input * scaleFactor)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,7 @@ android_test {
|
|||||||
"androidx.test.runner",
|
"androidx.test.runner",
|
||||||
"androidx.test.rules",
|
"androidx.test.rules",
|
||||||
"androidx.test.ext.junit",
|
"androidx.test.ext.junit",
|
||||||
|
"animationlib",
|
||||||
"frameworks-base-testutils",
|
"frameworks-base-testutils",
|
||||||
"junit",
|
"junit",
|
||||||
"kotlinx_coroutines_test",
|
"kotlinx_coroutines_test",
|
||||||
|
|||||||
@@ -152,6 +152,21 @@ class LowLightDreamManagerTest {
|
|||||||
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setAmbientLightMode_animationCancelled_SetsSystemDream() = testScope.runTest {
|
||||||
|
mLowLightDreamManager.setAmbientLightMode(LowLightDreamManager.AMBIENT_LIGHT_MODE_LOW_LIGHT)
|
||||||
|
runCurrent()
|
||||||
|
cancelEnterAnimations()
|
||||||
|
runCurrent()
|
||||||
|
// Animation never finishes, but we should still set the system dream
|
||||||
|
verify(mDreamManager).setSystemDreamComponent(DREAM_COMPONENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelEnterAnimations() {
|
||||||
|
val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
|
||||||
|
listener.onAnimationCancel(mEnterAnimator)
|
||||||
|
}
|
||||||
|
|
||||||
private fun completeEnterAnimations() {
|
private fun completeEnterAnimations() {
|
||||||
val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
|
val listener = withArgCaptor { verify(mEnterAnimator).addListener(capture()) }
|
||||||
listener.onAnimationEnd(mEnterAnimator)
|
listener.onAnimationEnd(mEnterAnimator)
|
||||||
|
|||||||
@@ -158,26 +158,6 @@ class LowLightTransitionCoordinatorTest {
|
|||||||
assertThat(job.isCancelled).isTrue()
|
assertThat(job.isCancelled).isTrue()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
fun shouldCancelAnimatorWhenJobCancelled() = testScope.runTest {
|
|
||||||
whenever(mEnterListener.onBeforeEnterLowLight()).thenReturn(mAnimator)
|
|
||||||
val coordinator = LowLightTransitionCoordinator()
|
|
||||||
coordinator.setLowLightEnterListener(mEnterListener)
|
|
||||||
val job = launch {
|
|
||||||
coordinator.waitForLowLightTransitionAnimation(timeout = TIMEOUT, entering = true)
|
|
||||||
}
|
|
||||||
runCurrent()
|
|
||||||
// Animator listener is added and the runnable is not run yet.
|
|
||||||
verify(mAnimator).addListener(mAnimatorListenerCaptor.capture())
|
|
||||||
verify(mAnimator, never()).cancel()
|
|
||||||
assertThat(job.isCompleted).isFalse()
|
|
||||||
|
|
||||||
job.cancel()
|
|
||||||
// We should have removed the listener and cancelled the animator
|
|
||||||
verify(mAnimator).removeListener(mAnimatorListenerCaptor.value)
|
|
||||||
verify(mAnimator).cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
|
private val TIMEOUT = 1.toDuration(DurationUnit.SECONDS)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2023 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.dream.lowlight.util
|
||||||
|
|
||||||
|
import android.testing.AndroidTestingRunner
|
||||||
|
import androidx.test.filters.SmallTest
|
||||||
|
import com.android.app.animation.Interpolators
|
||||||
|
import com.google.common.truth.Truth
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@SmallTest
|
||||||
|
@RunWith(AndroidTestingRunner::class)
|
||||||
|
class TruncatedInterpolatorTest {
|
||||||
|
@Test
|
||||||
|
fun truncatedInterpolator_matchesRegularInterpolator() {
|
||||||
|
val originalInterpolator = Interpolators.EMPHASIZED
|
||||||
|
val truncatedInterpolator =
|
||||||
|
TruncatedInterpolator(originalInterpolator, ORIGINAL_DURATION_MS, NEW_DURATION_MS)
|
||||||
|
|
||||||
|
// Both interpolators should start at the same value.
|
||||||
|
var animationPercent = 0f
|
||||||
|
Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
|
||||||
|
.isEqualTo(originalInterpolator.getInterpolation(animationPercent))
|
||||||
|
|
||||||
|
animationPercent = 1f
|
||||||
|
Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
|
||||||
|
.isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
|
||||||
|
|
||||||
|
animationPercent = 0.25f
|
||||||
|
Truth.assertThat(truncatedInterpolator.getInterpolation(animationPercent))
|
||||||
|
.isEqualTo(originalInterpolator.getInterpolation(animationPercent * DURATION_RATIO))
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val ORIGINAL_DURATION_MS: Float = 1000f
|
||||||
|
private const val NEW_DURATION_MS: Float = 200f
|
||||||
|
private const val DURATION_RATIO: Float = NEW_DURATION_MS / ORIGINAL_DURATION_MS
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import androidx.core.animation.doOnEnd
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import com.android.app.animation.Interpolators
|
import com.android.app.animation.Interpolators
|
||||||
|
import com.android.dream.lowlight.util.TruncatedInterpolator
|
||||||
import com.android.systemui.R
|
import com.android.systemui.R
|
||||||
import com.android.systemui.complication.ComplicationHostViewController
|
import com.android.systemui.complication.ComplicationHostViewController
|
||||||
import com.android.systemui.complication.ComplicationLayoutParams
|
import com.android.systemui.complication.ComplicationLayoutParams
|
||||||
@@ -204,31 +205,28 @@ constructor(
|
|||||||
translationYAnimator(
|
translationYAnimator(
|
||||||
from = 0f,
|
from = 0f,
|
||||||
to = -mDreamInTranslationYDistance.toFloat(),
|
to = -mDreamInTranslationYDistance.toFloat(),
|
||||||
durationMs = mDreamInTranslationYDurationMs,
|
durationMs = mDreamInComplicationsAnimDurationMs,
|
||||||
delayMs = 0,
|
delayMs = 0,
|
||||||
interpolator = Interpolators.EMPHASIZED
|
// Truncate the animation from the full duration to match the alpha
|
||||||
|
// animation so that the whole animation ends at the same time.
|
||||||
|
interpolator =
|
||||||
|
TruncatedInterpolator(
|
||||||
|
Interpolators.EMPHASIZED,
|
||||||
|
/*originalDuration=*/ mDreamInTranslationYDurationMs.toFloat(),
|
||||||
|
/*newDuration=*/ mDreamInComplicationsAnimDurationMs.toFloat()
|
||||||
|
)
|
||||||
),
|
),
|
||||||
alphaAnimator(
|
alphaAnimator(
|
||||||
from =
|
from =
|
||||||
mCurrentAlphaAtPosition.getOrDefault(
|
mCurrentAlphaAtPosition.getOrDefault(
|
||||||
key = POSITION_BOTTOM,
|
key = POSITION_BOTTOM,
|
||||||
defaultValue = 1f
|
defaultValue = 1f
|
||||||
),
|
),
|
||||||
to = 0f,
|
to = 0f,
|
||||||
durationMs = mDreamInComplicationsAnimDurationMs,
|
durationMs = mDreamInComplicationsAnimDurationMs,
|
||||||
delayMs = 0,
|
delayMs = 0,
|
||||||
positions = POSITION_BOTTOM
|
positions = POSITION_BOTTOM
|
||||||
)
|
),
|
||||||
.apply {
|
|
||||||
doOnEnd {
|
|
||||||
// The logical end of the animation is once the alpha and blur
|
|
||||||
// animations finish, end the animation so that any listeners are
|
|
||||||
// notified. The Y translation animation is much longer than all of
|
|
||||||
// the other animations due to how the spec is defined, but is not
|
|
||||||
// expected to run to completion.
|
|
||||||
mAnimator?.end()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
alphaAnimator(
|
alphaAnimator(
|
||||||
from =
|
from =
|
||||||
mCurrentAlphaAtPosition.getOrDefault(
|
mCurrentAlphaAtPosition.getOrDefault(
|
||||||
|
|||||||
Reference in New Issue
Block a user