From 4b40d32ae4556fc1b9103a3c405f1b96e50fe993 Mon Sep 17 00:00:00 2001 From: Nicolo' Mazzucato Date: Mon, 31 Jan 2022 16:22:02 +0100 Subject: [PATCH] Animate notification shade during unfold Adds some constant translation to elements of the notification shade during fold/unfold. This is only applied when the device is in natural rotation. KeyguardUnfoldTransition was doing the same: the logic has been extracted in UnfoldConstantTranslateAnimator, that is now used by both classes. KeyguardUnfoldTransition's tests related to the animator have been moved to UnfoldConstantTranslateAnimatorTest.kt. + Minor formatting issues fixed (using ktfmt) Bug: 201411030 Test: atest UnfoldConstantTranslateAnimatorTest KeyguardUnfoldTransitionTest + Tested manually on lockscreen and notification shade Change-Id: Ic8d4cf5d7b2a9f8fc366842623c87c15cf79766f --- .../UnfoldConstantTranslateAnimator.kt | 96 ++++++++++++++ .../keyguard/KeyguardUnfoldTransition.kt | 99 ++++----------- ...ificationPanelUnfoldAnimationController.kt | 52 ++++++++ .../NotificationPanelViewController.java | 7 ++ .../systemui/unfold/SysUIUnfoldModule.kt | 3 + .../keyguard/KeyguardUnfoldTransitionTest.kt | 93 +++----------- .../UnfoldConstantTranslateAnimatorTest.kt | 117 ++++++++++++++++++ 7 files changed, 320 insertions(+), 147 deletions(-) create mode 100644 packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelUnfoldAnimationController.kt create mode 100644 packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt new file mode 100644 index 0000000000000..ffab3cd79d7fa --- /dev/null +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimator.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 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.systemui.shared.animation + +import android.view.View +import android.view.ViewGroup +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import java.lang.ref.WeakReference + +/** + * Translates items away/towards the hinge when the device is opened/closed, according to the + * direction specified in [ViewIdToTranslate.direction], for a maximum of [translationMax] when + * progresses are 0. + */ +class UnfoldConstantTranslateAnimator( + private val viewsIdToTranslate: Set, + private val progressProvider: UnfoldTransitionProgressProvider +) : TransitionProgressListener { + + private var viewsToTranslate = listOf() + private lateinit var rootView: ViewGroup + private var translationMax = 0f + + fun init(rootView: ViewGroup, translationMax: Float) { + this.rootView = rootView + this.translationMax = translationMax + progressProvider.addCallback(this) + } + + override fun onTransitionStarted() { + registerViewsForAnimation(rootView, viewsIdToTranslate) + } + + override fun onTransitionProgress(progress: Float) { + translateViews(progress) + } + + override fun onTransitionFinished() { + translateViews(progress = 1f) + } + + private fun translateViews(progress: Float) { + // progress == 0 -> -translationMax + // progress == 1 -> 0 + val xTrans = (progress - 1f) * translationMax + viewsToTranslate.forEach { (view, direction, shouldBeAnimated) -> + if (shouldBeAnimated()) { + view.get()?.translationX = xTrans * direction.multiplier + } + } + } + + /** Finds in [parent] all views specified by [ids] and register them for the animation. */ + private fun registerViewsForAnimation(parent: ViewGroup, ids: Set) { + viewsToTranslate = + ids.mapNotNull { (id, dir, pred) -> + parent.findViewById(id)?.let { view -> + ViewToTranslate(WeakReference(view), dir, pred) + } + } + } + + /** Represents a view to animate. [rootView] should contain a view with [viewId] inside. */ + data class ViewIdToTranslate( + val viewId: Int, + val direction: Direction, + val shouldBeAnimated: () -> Boolean = { true } + ) + + private data class ViewToTranslate( + val view: WeakReference, + val direction: Direction, + val shouldBeAnimated: () -> Boolean + ) + + /** Direction of the animation. */ + enum class Direction(val multiplier: Float) { + LEFT(-1f), + RIGHT(1f), + } +} diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt index cb25e1a2a40ec..89d6fb5f062f0 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUnfoldTransition.kt @@ -17,11 +17,13 @@ package com.android.keyguard import android.content.Context -import android.view.View import android.view.ViewGroup import com.android.systemui.R +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.LEFT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.RIGHT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate import com.android.systemui.unfold.SysUIUnfoldScope -import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import javax.inject.Inject @@ -30,84 +32,37 @@ import javax.inject.Inject * the set of ids, which also dictact which direction to move and when, via a filter function. */ @SysUIUnfoldScope -class KeyguardUnfoldTransition @Inject constructor( - val context: Context, - val unfoldProgressProvider: NaturalRotationUnfoldProgressProvider +class KeyguardUnfoldTransition +@Inject +constructor( + private val context: Context, + unfoldProgressProvider: NaturalRotationUnfoldProgressProvider ) { - companion object { - final val LEFT = -1 - final val RIGHT = 1 - } + /** Certain views only need to move if they are not currently centered */ + var statusViewCentered = false private val filterSplitShadeOnly = { !statusViewCentered } private val filterNever = { true } - private val ids = setOf( - Triple(R.id.keyguard_status_area, LEFT, filterNever), - Triple(R.id.controls_button, LEFT, filterNever), - Triple(R.id.lockscreen_clock_view_large, LEFT, filterSplitShadeOnly), - Triple(R.id.lockscreen_clock_view, LEFT, filterNever), - Triple(R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly), - Triple(R.id.wallet_button, RIGHT, filterNever) - ) - private var parent: ViewGroup? = null - private var views = listOf Boolean>>() - private var xTranslationMax = 0f - - /** - * Certain views only need to move if they are not currently centered - */ - var statusViewCentered = false - - init { - unfoldProgressProvider.addCallback( - object : TransitionProgressListener { - override fun onTransitionStarted() { - findViews() - } - - override fun onTransitionProgress(progress: Float) { - translateViews(progress) - } - - override fun onTransitionFinished() { - translateViews(1f) - } - } - ) + private val translateAnimator by lazy { + UnfoldConstantTranslateAnimator( + viewsIdToTranslate = + setOf( + ViewIdToTranslate(R.id.keyguard_status_area, LEFT, filterNever), + ViewIdToTranslate(R.id.controls_button, LEFT, filterNever), + ViewIdToTranslate(R.id.lockscreen_clock_view_large, LEFT, filterSplitShadeOnly), + ViewIdToTranslate(R.id.lockscreen_clock_view, LEFT, filterNever), + ViewIdToTranslate( + R.id.notification_stack_scroller, RIGHT, filterSplitShadeOnly), + ViewIdToTranslate(R.id.wallet_button, RIGHT, filterNever)), + progressProvider = unfoldProgressProvider) } - /** - * Relies on the [parent] to locate views to translate - */ + /** Relies on the [parent] to locate views to translate. */ fun setup(parent: ViewGroup) { - this.parent = parent - xTranslationMax = context.resources.getDimensionPixelSize( - R.dimen.keyguard_unfold_translation_x).toFloat() - } - - /** - * Manually translate views based on set direction. At the moment - * [UnfoldMoveFromCenterAnimator] exists but moves all views a dynamic distance - * from their mid-point. This code instead will only ever translate by a fixed amount. - */ - private fun translateViews(progress: Float) { - val xTrans = progress * xTranslationMax - xTranslationMax - views.forEach { - (view, direction, pred) -> if (pred()) { - view.setTranslationX(xTrans * direction) - } - } - } - - private fun findViews() { - parent?.let { p -> - views = ids.mapNotNull { - (id, direction, pred) -> p.findViewById(id)?.let { - Triple(it, direction, pred) - } - } - } + val translationMax = + context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat() + translateAnimator.init(parent, translationMax) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelUnfoldAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelUnfoldAnimationController.kt new file mode 100644 index 0000000000000..ff48755f750a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelUnfoldAnimationController.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2022 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.systemui.statusbar.phone + +import android.content.Context +import android.view.ViewGroup +import com.android.systemui.R +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.LEFT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction.RIGHT +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate +import com.android.systemui.unfold.SysUIUnfoldScope +import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider +import javax.inject.Inject + +@SysUIUnfoldScope +class NotificationPanelUnfoldAnimationController +@Inject +constructor(private val context: Context, progressProvider: NaturalRotationUnfoldProgressProvider) { + + private val translateAnimator by lazy { + UnfoldConstantTranslateAnimator( + viewsIdToTranslate = + setOf( + ViewIdToTranslate(R.id.quick_settings_panel, LEFT), + ViewIdToTranslate(R.id.notification_stack_scroller, RIGHT), + ViewIdToTranslate(R.id.rightLayout, RIGHT), + ViewIdToTranslate(R.id.clock, LEFT), + ViewIdToTranslate(R.id.date, LEFT)), + progressProvider = progressProvider) + } + + fun setup(root: ViewGroup) { + val translationMax = + context.resources.getDimensionPixelSize(R.dimen.notification_side_paddings).toFloat() + translateAnimator.init(root, translationMax) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index bb0ed95cc6ca7..1870588f34aef 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -669,6 +669,8 @@ public class NotificationPanelViewController extends PanelViewController private boolean mStatusViewCentered = true; private Optional mKeyguardUnfoldTransition; + private Optional + mNotificationPanelUnfoldAnimationController; private final ListenerSet mNotifEventSourceCallbacks = new ListenerSet<>(); @@ -929,6 +931,8 @@ public class NotificationPanelViewController extends PanelViewController mMaxKeyguardNotifications = resources.getInteger(R.integer.keyguard_max_notification_count); mKeyguardUnfoldTransition = unfoldComponent.map(c -> c.getKeyguardUnfoldTransition()); + mNotificationPanelUnfoldAnimationController = unfoldComponent.map( + SysUIUnfoldComponent::getNotificationPanelUnfoldAnimationController); mCommunalSourceMonitorCallback = (source) -> { mUiExecutor.execute(() -> setCommunalSource(source)); @@ -1064,6 +1068,8 @@ public class NotificationPanelViewController extends PanelViewController mTapAgainViewController.init(); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); + mNotificationPanelUnfoldAnimationController.ifPresent(controller -> + controller.setup(mNotificationContainerParent)); } @Override @@ -1319,6 +1325,7 @@ public class NotificationPanelViewController extends PanelViewController setKeyguardBottomAreaVisibility(mBarState, false); mKeyguardUnfoldTransition.ifPresent(u -> u.setup(mView)); + mNotificationPanelUnfoldAnimationController.ifPresent(u -> u.setup(mView)); } private void attachSplitShadeMediaPlayerContainer(FrameLayout container) { diff --git a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt index 07f9c5487c411..7350b37e4b664 100644 --- a/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt +++ b/packages/SystemUI/src/com/android/systemui/unfold/SysUIUnfoldModule.kt @@ -18,6 +18,7 @@ package com.android.systemui.unfold import com.android.keyguard.KeyguardUnfoldTransition import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.phone.NotificationPanelUnfoldAnimationController import com.android.systemui.statusbar.phone.StatusBarMoveFromCenterAnimationController import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.unfold.util.ScopedUnfoldTransitionProgressProvider @@ -85,6 +86,8 @@ interface SysUIUnfoldComponent { fun getStatusBarMoveFromCenterAnimationController(): StatusBarMoveFromCenterAnimationController + fun getNotificationPanelUnfoldAnimationController(): NotificationPanelUnfoldAnimationController + fun getFoldAodAnimationController(): FoldAodAnimationController fun getUnfoldTransitionWallpaperController(): UnfoldTransitionWallpaperController diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt index 164f83dda9b7e..6c1f008e9337a 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUnfoldTransitionTest.kt @@ -20,23 +20,21 @@ import android.testing.AndroidTestingRunner import android.view.View import android.view.ViewGroup import androidx.test.filters.SmallTest -import com.android.keyguard.KeyguardUnfoldTransition.Companion.LEFT -import com.android.keyguard.KeyguardUnfoldTransition.Companion.RIGHT import com.android.systemui.R import com.android.systemui.SysuiTestCase import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener import com.android.systemui.unfold.util.NaturalRotationUnfoldProgressProvider import com.android.systemui.util.mockito.capture -import org.junit.Assert.assertEquals +import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor import org.mockito.Captor import org.mockito.Mock -import org.mockito.MockitoAnnotations -import org.mockito.Mockito.`when` import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations /** * Translates items away/towards the hinge when the device is opened/closed. This is controlled by @@ -46,14 +44,11 @@ import org.mockito.Mockito.verify @RunWith(AndroidTestingRunner::class) class KeyguardUnfoldTransitionTest : SysuiTestCase() { - @Mock - private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider + @Mock private lateinit var progressProvider: NaturalRotationUnfoldProgressProvider - @Captor - private lateinit var progressListenerCaptor: ArgumentCaptor + @Captor private lateinit var progressListenerCaptor: ArgumentCaptor - @Mock - private lateinit var parent: ViewGroup + @Mock private lateinit var parent: ViewGroup private lateinit var keyguardUnfoldTransition: KeyguardUnfoldTransition private lateinit var progressListener: TransitionProgressListener @@ -63,87 +58,35 @@ class KeyguardUnfoldTransitionTest : SysuiTestCase() { fun setup() { MockitoAnnotations.initMocks(this) - xTranslationMax = context.resources.getDimensionPixelSize( - R.dimen.keyguard_unfold_translation_x).toFloat() + xTranslationMax = + context.resources.getDimensionPixelSize(R.dimen.keyguard_unfold_translation_x).toFloat() - keyguardUnfoldTransition = KeyguardUnfoldTransition( - getContext(), - progressProvider - ) - - verify(progressProvider).addCallback(capture(progressListenerCaptor)) - progressListener = progressListenerCaptor.value + keyguardUnfoldTransition = KeyguardUnfoldTransition(context, progressProvider) keyguardUnfoldTransition.setup(parent) keyguardUnfoldTransition.statusViewCentered = false - } - @Test - fun onTransition_noMatchingIds() { - // GIVEN no views matching any ids - // WHEN the transition starts - progressListener.onTransitionStarted() - progressListener.onTransitionProgress(.1f) - - // THEN nothing... no exceptions - } - - @Test - fun onTransition_oneMovesLeft() { - // GIVEN one view with a matching id - val view = View(getContext()) - `when`(parent.findViewById(R.id.keyguard_status_area)).thenReturn(view) - - moveAndValidate(listOf(view to LEFT)) - } - - @Test - fun onTransition_oneMovesLeftAndOneMovesRightMultipleTimes() { - // GIVEN two views with a matching id - val leftView = View(getContext()) - val rightView = View(getContext()) - `when`(parent.findViewById(R.id.keyguard_status_area)).thenReturn(leftView) - `when`(parent.findViewById(R.id.notification_stack_scroller)).thenReturn(rightView) - - moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) - moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) + verify(progressProvider).addCallback(capture(progressListenerCaptor)) + progressListener = progressListenerCaptor.value } @Test fun onTransition_centeredViewDoesNotMove() { keyguardUnfoldTransition.statusViewCentered = true - val view = View(getContext()) + val view = View(context) `when`(parent.findViewById(R.id.lockscreen_clock_view_large)).thenReturn(view) - moveAndValidate(listOf(view to 0)) - } - - private fun moveAndValidate(list: List>) { - // Compare values as ints because -0f != 0f - - // WHEN the transition starts progressListener.onTransitionStarted() + assertThat(view.translationX).isZero() + progressListener.onTransitionProgress(0f) + assertThat(view.translationX).isZero() - list.forEach { (view, direction) -> - assertEquals((-xTranslationMax * direction).toInt(), view.getTranslationX().toInt()) - } + progressListener.onTransitionProgress(0.5f) + assertThat(view.translationX).isZero() - // WHEN the transition progresses, translation is updated - progressListener.onTransitionProgress(.5f) - list.forEach { (view, direction) -> - assertEquals( - (-xTranslationMax / 2f * direction).toInt(), - view.getTranslationX().toInt() - ) - } - - // WHEN the transition ends, translation is completed - progressListener.onTransitionProgress(1f) progressListener.onTransitionFinished() - list.forEach { (view, _) -> - assertEquals(0, view.getTranslationX().toInt()) - } + assertThat(view.translationX).isZero() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt new file mode 100644 index 0000000000000..32314159f865a --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/shared/animation/UnfoldConstantTranslateAnimatorTest.kt @@ -0,0 +1,117 @@ +package com.android.systemui.shared.animation + +import android.testing.AndroidTestingRunner +import android.view.View +import android.view.ViewGroup +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.Direction +import com.android.systemui.shared.animation.UnfoldConstantTranslateAnimator.ViewIdToTranslate +import com.android.systemui.unfold.UnfoldTransitionProgressProvider +import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +class UnfoldConstantTranslateAnimatorTest : SysuiTestCase() { + + @Mock private lateinit var progressProvider: UnfoldTransitionProgressProvider + + @Mock private lateinit var parent: ViewGroup + + @Captor private lateinit var progressListenerCaptor: ArgumentCaptor + + private lateinit var animator: UnfoldConstantTranslateAnimator + private lateinit var progressListener: TransitionProgressListener + + private val viewsIdToRegister = + setOf( + ViewIdToTranslate(LEFT_VIEW_ID, Direction.LEFT), + ViewIdToTranslate(RIGHT_VIEW_ID, Direction.RIGHT)) + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + animator = + UnfoldConstantTranslateAnimator(viewsIdToRegister, progressProvider) + + animator.init(parent, MAX_TRANSLATION) + + verify(progressProvider).addCallback(progressListenerCaptor.capture()) + progressListener = progressListenerCaptor.value + } + + @Test + fun onTransition_noMatchingIds() { + // GIVEN no views matching any ids + // WHEN the transition starts + progressListener.onTransitionStarted() + progressListener.onTransitionProgress(.1f) + + // THEN nothing... no exceptions + } + + @Test + fun onTransition_oneMovesLeft() { + // GIVEN one view with a matching id + val view = View(context) + whenever(parent.findViewById(LEFT_VIEW_ID)).thenReturn(view) + + moveAndValidate(listOf(view to LEFT)) + } + + @Test + fun onTransition_oneMovesLeftAndOneMovesRightMultipleTimes() { + // GIVEN two views with a matching id + val leftView = View(context) + val rightView = View(context) + whenever(parent.findViewById(LEFT_VIEW_ID)).thenReturn(leftView) + whenever(parent.findViewById(RIGHT_VIEW_ID)).thenReturn(rightView) + + moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) + moveAndValidate(listOf(leftView to LEFT, rightView to RIGHT)) + } + + private fun moveAndValidate(list: List>) { + // Compare values as ints because -0f != 0f + + // WHEN the transition starts + progressListener.onTransitionStarted() + progressListener.onTransitionProgress(0f) + + list.forEach { (view, direction) -> + assertEquals((-MAX_TRANSLATION * direction).toInt(), view.translationX.toInt()) + } + + // WHEN the transition progresses, translation is updated + progressListener.onTransitionProgress(.5f) + list.forEach { (view, direction) -> + assertEquals((-MAX_TRANSLATION / 2f * direction).toInt(), view.translationX.toInt()) + } + + // WHEN the transition ends, translation is completed + progressListener.onTransitionProgress(1f) + progressListener.onTransitionFinished() + list.forEach { (view, _) -> assertEquals(0, view.translationX.toInt()) } + } + + companion object { + private val LEFT = Direction.LEFT.multiplier.toInt() + private val RIGHT = Direction.RIGHT.multiplier.toInt() + + private const val MAX_TRANSLATION = 42f + + private const val LEFT_VIEW_ID = 1 + private const val RIGHT_VIEW_ID = 2 + } +}