Merge "[Media TTT] Animate the chip in." into tm-d1-dev

This commit is contained in:
Caitlin Cassidy
2022-06-14 18:37:54 +00:00
committed by Android (Google) Code Review
7 changed files with 323 additions and 66 deletions

View File

@@ -21,6 +21,7 @@
<!-- ViewBoundsAnimator --> <!-- ViewBoundsAnimator -->
<item type="id" name="tag_animator"/> <item type="id" name="tag_animator"/>
<item type="id" name="tag_alpha_animator"/>
<item type="id" name="tag_layout_listener"/> <item type="id" name="tag_layout_listener"/>
<item type="id" name="tag_override_bottom"/> <item type="id" name="tag_override_bottom"/>
<item type="id" name="tag_override_left"/> <item type="id" name="tag_override_left"/>

View File

@@ -39,6 +39,7 @@ class ViewHierarchyAnimator {
private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD private val DEFAULT_INTERPOLATOR = Interpolators.STANDARD
private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE private val DEFAULT_ADDITION_INTERPOLATOR = Interpolators.STANDARD_DECELERATE
private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE private val DEFAULT_REMOVAL_INTERPOLATOR = Interpolators.STANDARD_ACCELERATE
private val DEFAULT_FADE_IN_INTERPOLATOR = Interpolators.ALPHA_IN
/** The properties used to animate the view bounds. */ /** The properties used to animate the view bounds. */
private val PROPERTIES = mapOf( private val PROPERTIES = mapOf(
@@ -162,6 +163,10 @@ class ViewHierarchyAnimator {
* animate an already visible view, see [animate] and [animateNextUpdate]. * animate an already visible view, see [animate] and [animateNextUpdate].
* *
* Then animator unregisters itself once the first addition animation is complete. * Then animator unregisters itself once the first addition animation is complete.
*
* @param includeFadeIn true if the animator should also fade in the view and child views.
* @param fadeInInterpolator the interpolator to use when fading in the view. Unused if
* [includeFadeIn] is false.
*/ */
@JvmOverloads @JvmOverloads
fun animateAddition( fun animateAddition(
@@ -169,7 +174,9 @@ class ViewHierarchyAnimator {
origin: Hotspot = Hotspot.CENTER, origin: Hotspot = Hotspot.CENTER,
interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR, interpolator: Interpolator = DEFAULT_ADDITION_INTERPOLATOR,
duration: Long = DEFAULT_DURATION, duration: Long = DEFAULT_DURATION,
includeMargins: Boolean = false includeMargins: Boolean = false,
includeFadeIn: Boolean = false,
fadeInInterpolator: Interpolator = DEFAULT_FADE_IN_INTERPOLATOR
): Boolean { ): Boolean {
if (isVisible( if (isVisible(
rootView.visibility, rootView.visibility,
@@ -186,6 +193,42 @@ class ViewHierarchyAnimator {
origin, interpolator, duration, ignorePreviousValues = !includeMargins origin, interpolator, duration, ignorePreviousValues = !includeMargins
) )
addListener(rootView, listener, recursive = true) addListener(rootView, listener, recursive = true)
if (!includeFadeIn) {
return true
}
if (rootView is ViewGroup) {
// First, fade in the container view
val containerDuration = duration / 6
createAndStartFadeInAnimator(
rootView, containerDuration, startDelay = 0, interpolator = fadeInInterpolator
)
// Then, fade in the child views
val childDuration = duration / 3
for (i in 0 until rootView.childCount) {
val view = rootView.getChildAt(i)
createAndStartFadeInAnimator(
view,
childDuration,
// Wait until the container fades in before fading in the children
startDelay = containerDuration,
interpolator = fadeInInterpolator
)
}
// For now, we don't recursively fade in additional sub views (e.g. grandchild
// views) since it hasn't been necessary, but we could add that functionality.
} else {
// Fade in the view during the first half of the addition
createAndStartFadeInAnimator(
rootView,
duration / 2,
startDelay = 0,
interpolator = fadeInInterpolator
)
}
return true return true
} }
@@ -834,6 +877,27 @@ class ViewHierarchyAnimator {
view.setTag(R.id.tag_animator, animator) view.setTag(R.id.tag_animator, animator)
animator.start() animator.start()
} }
private fun createAndStartFadeInAnimator(
view: View,
duration: Long,
startDelay: Long,
interpolator: Interpolator
) {
val animator = ObjectAnimator.ofFloat(view, "alpha", 1f)
animator.startDelay = startDelay
animator.duration = duration
animator.interpolator = interpolator
animator.addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
view.setTag(R.id.tag_alpha_animator, null /* tag */)
}
})
(view.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.cancel()
view.setTag(R.id.tag_alpha_animator, animator)
animator.start()
}
} }
/** An enum used to determine the origin of addition animations. */ /** An enum used to determine the origin of addition animations. */

View File

@@ -13,18 +13,27 @@
~ See the License for the specific language governing permissions and ~ See the License for the specific language governing permissions and
~ limitations under the License. ~ limitations under the License.
--> -->
<LinearLayout <!-- Wrap in a frame layout so that we can update the margins on the inner layout. (Since this view
is the root view of a window, we cannot change the root view's margins.) -->
<!-- Alphas start as 0 because the view will be animated in. -->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/media_ttt_sender_chip" android:id="@+id/media_ttt_sender_chip"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/media_ttt_sender_chip_inner"
android:orientation="horizontal" android:orientation="horizontal"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="@dimen/media_ttt_chip_outer_padding" android:padding="@dimen/media_ttt_chip_outer_padding"
android:background="@drawable/media_ttt_chip_background" android:background="@drawable/media_ttt_chip_background"
android:layout_marginTop="50dp" android:layout_marginTop="20dp"
android:clipToPadding="false" android:clipToPadding="false"
android:gravity="center_vertical" android:gravity="center_vertical"
android:alpha="0.0"
> >
<com.android.internal.widget.CachingIconView <com.android.internal.widget.CachingIconView
@@ -32,6 +41,7 @@
android:layout_width="@dimen/media_ttt_app_icon_size" android:layout_width="@dimen/media_ttt_app_icon_size"
android:layout_height="@dimen/media_ttt_app_icon_size" android:layout_height="@dimen/media_ttt_app_icon_size"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:alpha="0.0"
/> />
<TextView <TextView
@@ -40,10 +50,10 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="@dimen/media_ttt_text_size" android:textSize="@dimen/media_ttt_text_size"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
android:alpha="0.0"
/> />
<!-- At most one of [loading, failure_icon, undo] will be visible at a time. --> <!-- At most one of [loading, failure_icon, undo] will be visible at a time. -->
<ProgressBar <ProgressBar
android:id="@+id/loading" android:id="@+id/loading"
android:indeterminate="true" android:indeterminate="true"
@@ -52,6 +62,7 @@
android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant" android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant"
style="?android:attr/progressBarStyleSmall" style="?android:attr/progressBarStyleSmall"
android:alpha="0.0"
/> />
<ImageView <ImageView
@@ -61,6 +72,7 @@
android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:src="@drawable/ic_warning" android:src="@drawable/ic_warning"
android:tint="@color/GM2_red_500" android:tint="@color/GM2_red_500"
android:alpha="0.0"
/> />
<TextView <TextView
@@ -78,6 +90,8 @@
android:layout_marginTop="@dimen/media_ttt_undo_button_vertical_negative_margin" android:layout_marginTop="@dimen/media_ttt_undo_button_vertical_negative_margin"
android:layout_marginBottom="@dimen/media_ttt_undo_button_vertical_negative_margin" android:layout_marginBottom="@dimen/media_ttt_undo_button_vertical_negative_margin"
android:background="@drawable/media_ttt_undo_background" android:background="@drawable/media_ttt_undo_background"
android:alpha="0.0"
/> />
</LinearLayout> </LinearLayout>
</FrameLayout>

View File

@@ -106,6 +106,7 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>(
PowerManager.WAKE_REASON_APPLICATION, PowerManager.WAKE_REASON_APPLICATION,
"com.android.systemui:media_tap_to_transfer_activated" "com.android.systemui:media_tap_to_transfer_activated"
) )
animateChipIn(currentChipView)
} }
// Cancel and re-set the chip timeout each time we get a new state. // Cancel and re-set the chip timeout each time we get a new state.
@@ -137,6 +138,12 @@ abstract class MediaTttChipControllerCommon<T : ChipInfoCommon>(
*/ */
abstract fun updateChipView(chipInfo: T, currentChipView: ViewGroup) abstract fun updateChipView(chipInfo: T, currentChipView: ViewGroup)
/**
* A method that can be implemented by subclcasses to do custom animations for when the chip
* appears.
*/
open fun animateChipIn(chipView: ViewGroup) {}
/** /**
* Returns the size that the icon should be, or null if no size override is needed. * Returns the size that the icon should be, or null if no size override is needed.
*/ */

View File

@@ -27,6 +27,8 @@ import android.view.WindowManager
import android.widget.TextView import android.widget.TextView
import com.android.internal.statusbar.IUndoMediaTransferCallback import com.android.internal.statusbar.IUndoMediaTransferCallback
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.animation.ViewHierarchyAnimator
import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.media.taptotransfer.common.ChipInfoCommon import com.android.systemui.media.taptotransfer.common.ChipInfoCommon
@@ -124,7 +126,6 @@ class MediaTttChipControllerSender @Inject constructor(
currentChipView.requireViewById<View>(R.id.loading).visibility = currentChipView.requireViewById<View>(R.id.loading).visibility =
chipState.isMidTransfer.visibleIfTrue() chipState.isMidTransfer.visibleIfTrue()
// Undo // Undo
val undoView = currentChipView.requireViewById<View>(R.id.undo) val undoView = currentChipView.requireViewById<View>(R.id.undo)
val undoClickListener = chipState.undoClickListener( val undoClickListener = chipState.undoClickListener(
@@ -138,6 +139,17 @@ class MediaTttChipControllerSender @Inject constructor(
chipState.isTransferFailure.visibleIfTrue() chipState.isTransferFailure.visibleIfTrue()
} }
override fun animateChipIn(chipView: ViewGroup) {
ViewHierarchyAnimator.animateAddition(
chipView.requireViewById<ViewGroup>(R.id.media_ttt_sender_chip_inner),
ViewHierarchyAnimator.Hotspot.TOP,
Interpolators.EMPHASIZED_DECELERATE,
duration = 500L,
includeMargins = true,
includeFadeIn = true,
)
}
override fun removeChip(removalReason: String) { override fun removeChip(removalReason: String) {
// Don't remove the chip if we're mid-transfer since the user should still be able to // Don't remove the chip if we're mid-transfer since the user should still be able to
// see the status of the transfer. (But do remove it if it's finally timed out.) // see the status of the transfer. (But do remove it if it's finally timed out.)

View File

@@ -520,6 +520,145 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
endAnimation(rootView) endAnimation(rootView)
} }
@Test
fun animatesAppearingViewsFadeIn_alphaStartsAtZero_endsAtOne() {
rootView.alpha = 0f
ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 1f)
endFadeInAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_alpha_animator))
assertEquals(1f, rootView.alpha)
}
@Test
fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_endsAtOne() {
rootView.alpha = 0.2f
ViewHierarchyAnimator.animateAddition(rootView, includeFadeIn = true)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 1f)
endFadeInAnimation(rootView)
assertNull(rootView.getTag(R.id.tag_alpha_animator))
assertEquals(1f, rootView.alpha)
}
@Test
fun animatesAppearingViewsFadeIn_alphaStartsAsZero_alphaUpdatedMidAnimation() {
rootView.alpha = 0f
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = true,
fadeInInterpolator = Interpolators.LINEAR
)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 0.42f)
assertEquals(0.42f, rootView.alpha)
}
@Test
fun animatesAppearingViewsFadeIn_alphaStartsAboveZero_alphaUpdatedMidAnimation() {
rootView.alpha = 0.6f
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = true,
fadeInInterpolator = Interpolators.LINEAR
)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 0.5f)
assertEquals(0.8f, rootView.alpha)
}
@Test
fun animatesAppearingViewsFadeIn_childViewAlphasAlsoAnimated() {
rootView.alpha = 0f
val firstChild = View(context)
firstChild.alpha = 0f
val secondChild = View(context)
secondChild.alpha = 0f
rootView.addView(firstChild)
rootView.addView(secondChild)
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = true,
fadeInInterpolator = Interpolators.LINEAR
)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 0.5f)
assertEquals(0.5f, rootView.alpha)
assertEquals(0.5f, firstChild.alpha)
assertEquals(0.5f, secondChild.alpha)
}
@Test
fun animatesAppearingViewsFadeIn_animatesFromPreviousAnimationProgress() {
rootView.alpha = 0f
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = true,
fadeInInterpolator = Interpolators.LINEAR
)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 0.5f)
assertEquals(0.5f, rootView.alpha)
assertNotNull(rootView.getTag(R.id.tag_alpha_animator))
// IF we request animation again
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = true,
fadeInInterpolator = Interpolators.LINEAR
)
// THEN the alpha remains at its current value (it doesn't get reset to 0)
assertNotNull(rootView.getTag(R.id.tag_alpha_animator))
assertEquals(0.5f, rootView.alpha)
// IF we advance the new animation to the end
advanceFadeInAnimation(rootView, fraction = 1f)
endFadeInAnimation(rootView)
// THEN we still end at the correct value
assertNull(rootView.getTag(R.id.tag_alpha_animator))
assertEquals(1f, rootView.alpha)
}
@Test
fun animatesAppearingViews_fadeInFalse_alphasNotUpdated() {
rootView.alpha = 0.3f
val firstChild = View(context)
firstChild.alpha = 0.4f
val secondChild = View(context)
secondChild.alpha = 0.5f
rootView.addView(firstChild)
rootView.addView(secondChild)
ViewHierarchyAnimator.animateAddition(
rootView,
includeFadeIn = false,
fadeInInterpolator = Interpolators.LINEAR
)
rootView.layout(50 /* l */, 50 /* t */, 100 /* r */, 100 /* b */)
advanceFadeInAnimation(rootView, fraction = 1f)
assertEquals(0.3f, rootView.alpha)
assertEquals(0.4f, firstChild.alpha)
assertEquals(0.5f, secondChild.alpha)
}
@Test
fun animatesViewRemovalFromStartToEnd() { fun animatesViewRemovalFromStartToEnd() {
setUpRootWithChildren() setUpRootWithChildren()
@@ -1003,6 +1142,16 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
} }
} }
private fun advanceFadeInAnimation(rootView: View, fraction: Float) {
(rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.setCurrentFraction(fraction)
if (rootView is ViewGroup) {
for (i in 0 until rootView.childCount) {
advanceFadeInAnimation(rootView.getChildAt(i), fraction)
}
}
}
private fun endAnimation(rootView: View) { private fun endAnimation(rootView: View) {
(rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.end() (rootView.getTag(R.id.tag_animator) as? ObjectAnimator)?.end()
@@ -1012,4 +1161,14 @@ ViewHierarchyAnimatorTest : SysuiTestCase() {
} }
} }
} }
private fun endFadeInAnimation(rootView: View) {
(rootView.getTag(R.id.tag_alpha_animator) as? ObjectAnimator)?.end()
if (rootView is ViewGroup) {
for (i in 0 until rootView.childCount) {
endFadeInAnimation(rootView.getChildAt(i))
}
}
}
} }

View File

@@ -25,9 +25,9 @@ import android.os.PowerManager
import android.testing.AndroidTestingRunner import android.testing.AndroidTestingRunner
import android.testing.TestableLooper import android.testing.TestableLooper
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.logging.testing.UiEventLoggerFake
@@ -620,22 +620,22 @@ class MediaTttChipControllerSenderTest : SysuiTestCase() {
verify(windowManager).removeView(any()) verify(windowManager).removeView(any())
} }
private fun LinearLayout.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon) private fun ViewGroup.getAppIconView() = this.requireViewById<ImageView>(R.id.app_icon)
private fun LinearLayout.getChipText(): String = private fun ViewGroup.getChipText(): String =
(this.requireViewById<TextView>(R.id.text)).text as String (this.requireViewById<TextView>(R.id.text)).text as String
private fun LinearLayout.getLoadingIconVisibility(): Int = private fun ViewGroup.getLoadingIconVisibility(): Int =
this.requireViewById<View>(R.id.loading).visibility this.requireViewById<View>(R.id.loading).visibility
private fun LinearLayout.getUndoButton(): View = this.requireViewById(R.id.undo) private fun ViewGroup.getUndoButton(): View = this.requireViewById(R.id.undo)
private fun LinearLayout.getFailureIcon(): View = this.requireViewById(R.id.failure_icon) private fun ViewGroup.getFailureIcon(): View = this.requireViewById(R.id.failure_icon)
private fun getChipView(): LinearLayout { private fun getChipView(): ViewGroup {
val viewCaptor = ArgumentCaptor.forClass(View::class.java) val viewCaptor = ArgumentCaptor.forClass(View::class.java)
verify(windowManager).addView(viewCaptor.capture(), any()) verify(windowManager).addView(viewCaptor.capture(), any())
return viewCaptor.value as LinearLayout return viewCaptor.value as ViewGroup
} }
/** Helper method providing default parameters to not clutter up the tests. */ /** Helper method providing default parameters to not clutter up the tests. */