diff options
| author | 2023-02-09 21:53:28 +0000 | |
|---|---|---|
| committer | 2023-02-10 19:00:16 +0000 | |
| commit | 287842965a2c05a261bce332d975737df16aaab4 (patch) | |
| tree | 1279c1c26f07a2c5b71cead8e4858a9a8071e6ea | |
| parent | d504771ed34d19ec671ee39cfeee0df527bfe6d4 (diff) | |
[Media TTT] Don't use an animated-vector for the loading spinner.
See bug for more context. tl;dr: AnimatedVectorDrawables will pause
their animation when opening the shade (or in other scenarios), so we
need to use a different kind of drawable as a workaround. This CL just
uses a static drawable and animates its rotation using an
`ObjectAnimator`.
Bug: 243983980
Test: `adb shell cmd statusbar media-ttt-chip-sender MyTablet
TRANSFER_TO_RECEIVER_TRIGGERED` -> see new loading spinner. Pull down
the shade and verify the loading spinner keeps spinning. Plug the device
in to charge to see the charging animation and verify the loading
spinner keeps spinning.
Test: _TRIGGERD -> _SUCCEEDED -> Undo ==> verify that the new loading
spinner for the new TRIGGERED state is spinning
Test: atest ChipbarCoordinatorTest
Change-Id: Ic5701bbb241b919211463df6eb576f810ec2ca22
4 files changed, 187 insertions, 9 deletions
diff --git a/packages/SystemUI/res/drawable/ic_progress_activity.xml b/packages/SystemUI/res/drawable/ic_progress_activity.xml new file mode 100644 index 000000000000..abf0625d40d5 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_progress_activity.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M24,44Q19.8,44 16.15,42.45Q12.5,40.9 9.8,38.2Q7.1,35.5 5.55,31.85Q4,28.2 4,24Q4,19.8 5.55,16.15Q7.1,12.5 9.8,9.8Q12.5,7.1 16.15,5.55Q19.8,4 24,4Q24.6,4 25.05,4.45Q25.5,4.9 25.5,5.5Q25.5,6.1 25.05,6.55Q24.6,7 24,7Q16.95,7 11.975,11.975Q7,16.95 7,24Q7,31.05 11.975,36.025Q16.95,41 24,41Q31.05,41 36.025,36.025Q41,31.05 41,24Q41,23.4 41.45,22.95Q41.9,22.5 42.5,22.5Q43.1,22.5 43.55,22.95Q44,23.4 44,24Q44,28.2 42.45,31.85Q40.9,35.5 38.2,38.2Q35.5,40.9 31.85,42.45Q28.2,44 24,44Z"/> +</vector> diff --git a/packages/SystemUI/res/layout/chipbar.xml b/packages/SystemUI/res/layout/chipbar.xml index 8cf4f4de27da..0ff944c2becf 100644 --- a/packages/SystemUI/res/layout/chipbar.xml +++ b/packages/SystemUI/res/layout/chipbar.xml @@ -60,14 +60,13 @@ /> <!-- At most one of [loading, failure_icon, undo] will be visible at a time. --> - <ProgressBar + <ImageView android:id="@+id/loading" - android:indeterminate="true" android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_height="@dimen/media_ttt_status_icon_size" android:layout_marginStart="@dimen/media_ttt_last_item_start_margin" - android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant" - style="?android:attr/progressBarStyleSmall" + android:src="@drawable/ic_progress_activity" + android:tint="?androidprv:attr/colorAccentPrimaryVariant" android:alpha="0.0" /> diff --git a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt index 696134cde3c9..a20a5b2fdbbc 100644 --- a/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinator.kt @@ -16,6 +16,8 @@ package com.android.systemui.temporarydisplay.chipbar +import android.animation.ObjectAnimator +import android.animation.ValueAnimator import android.content.Context import android.graphics.Rect import android.os.PowerManager @@ -27,11 +29,14 @@ import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE import android.view.ViewGroup import android.view.WindowManager import android.view.accessibility.AccessibilityManager +import android.widget.ImageView import android.widget.TextView import androidx.annotation.IdRes +import androidx.annotation.VisibleForTesting import com.android.internal.widget.CachingIconView import com.android.systemui.Gefingerpoken import com.android.systemui.R +import com.android.systemui.animation.Interpolators import com.android.systemui.classifier.FalsingCollector import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Text.Companion.loadText @@ -101,6 +106,15 @@ constructor( private lateinit var parent: ChipbarRootView + /** The current loading information, or null we're not currently loading. */ + @VisibleForTesting + internal var loadingDetails: LoadingDetails? = null + private set(value) { + // Always cancel the old one before updating + field?.animator?.cancel() + field = value + } + override val windowLayoutParams = commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } @@ -143,8 +157,22 @@ constructor( // ---- End item ---- // Loading - currentView.requireViewById<View>(R.id.loading).visibility = - (newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() + val isLoading = newInfo.endItem == ChipbarEndItem.Loading + val loadingView = currentView.requireViewById<ImageView>(R.id.loading) + loadingView.visibility = isLoading.visibleIfTrue() + + if (isLoading) { + val currentLoadingDetails = loadingDetails + // Since there can be multiple chipbars, we need to check if the loading view is the + // same and possibly re-start the loading animation on the new view. + if (currentLoadingDetails == null || currentLoadingDetails.loadingView != loadingView) { + val newDetails = createLoadingDetails(loadingView) + newDetails.animator.start() + loadingDetails = newDetails + } + } else { + loadingDetails = null + } // Error currentView.requireViewById<View>(R.id.error).visibility = @@ -223,12 +251,17 @@ constructor( override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) { val innerView = view.getInnerView() innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE - val removed = chipbarAnimator.animateViewOut(innerView, onAnimationEnd) + + val fullEndRunnable = Runnable { + loadingDetails = null + onAnimationEnd.run() + } + val removed = chipbarAnimator.animateViewOut(innerView, fullEndRunnable) // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just // run it immediately. if (!removed) { logger.logAnimateOutFailure() - onAnimationEnd.run() + fullEndRunnable.run() } updateGestureListening() @@ -269,7 +302,7 @@ constructor( } private fun ViewGroup.getInnerView(): ViewGroup { - return requireViewById(R.id.chipbar_inner) + return this.requireViewById(R.id.chipbar_inner) } override fun getTouchableRegion(view: View, outRect: Rect) { @@ -283,8 +316,28 @@ constructor( View.GONE } } + + private fun createLoadingDetails(loadingView: View): LoadingDetails { + // Ideally, we would use a <ProgressBar> view, which would automatically handle the loading + // spinner rotation for us. However, due to b/243983980, the ProgressBar animation + // unexpectedly pauses when SysUI starts another window. ObjectAnimator is a workaround that + // won't pause. + val animator = + ObjectAnimator.ofFloat(loadingView, View.ROTATION, 0f, 360f).apply { + duration = LOADING_ANIMATION_DURATION_MS + repeatCount = ValueAnimator.INFINITE + interpolator = Interpolators.LINEAR + } + return LoadingDetails(loadingView, animator) + } + + internal data class LoadingDetails( + val loadingView: View, + val animator: ObjectAnimator, + ) } @IdRes private val INFO_TAG = R.id.tag_chipbar_info private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED" private const val TAG = "ChipbarCoordinator" +private const val LOADING_ANIMATION_DURATION_MS = 1000L diff --git a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt index fc7436a6b273..586bdc6c8215 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/temporarydisplay/chipbar/ChipbarCoordinatorTest.kt @@ -27,6 +27,7 @@ import android.view.WindowManager import android.view.accessibility.AccessibilityManager import android.widget.ImageView import android.widget.TextView +import androidx.core.animation.doOnCancel import androidx.test.filters.SmallTest import com.android.internal.logging.testing.UiEventLoggerFake import com.android.systemui.R @@ -361,6 +362,105 @@ class ChipbarCoordinatorTest : SysuiTestCase() { } @Test + fun displayView_loading_animationStarted() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue() + } + + @Test + fun displayView_notLoading_noAnimation() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Error, + ) + ) + + assertThat(underTest.loadingDetails).isNull() + } + + @Test + fun displayView_loadingThenNotLoading_animationStopped() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val animator = underTest.loadingDetails!!.animator + var cancelled = false + animator.doOnCancel { cancelled = true } + + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Button(Text.Loaded("button")) {}, + ) + ) + + assertThat(cancelled).isTrue() + assertThat(underTest.loadingDetails).isNull() + } + + @Test + fun displayView_loadingThenHideView_animationStopped() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val animator = underTest.loadingDetails!!.animator + var cancelled = false + animator.doOnCancel { cancelled = true } + + underTest.removeView(DEVICE_ID, "TestReason") + + assertThat(cancelled).isTrue() + assertThat(underTest.loadingDetails).isNull() + } + + @Test + fun displayView_loadingThenNewLoading_animationStaysTheSame() { + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + val animator = underTest.loadingDetails!!.animator + var cancelled = false + animator.doOnCancel { cancelled = true } + + underTest.displayView( + createChipbarInfo( + Icon.Resource(R.id.check_box, null), + Text.Loaded("new text"), + endItem = ChipbarEndItem.Loading, + ) + ) + + assertThat(underTest.loadingDetails!!.animator).isEqualTo(animator) + assertThat(underTest.loadingDetails!!.animator.isStarted).isTrue() + assertThat(cancelled).isFalse() + } + + @Test fun displayView_vibrationEffect_doubleClickEffect() { underTest.displayView( createChipbarInfo( |