Merge "[Media TTT] Don't use an animated-vector for the loading spinner." into tm-qpr-dev

This commit is contained in:
Caitlin Shkuratov
2023-02-13 17:53:44 +00:00
committed by Android (Google) Code Review
4 changed files with 187 additions and 9 deletions

View File

@@ -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>

View File

@@ -60,14 +60,13 @@
/> />
<!-- 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 <ImageView
android:id="@+id/loading" android:id="@+id/loading"
android:indeterminate="true"
android:layout_width="@dimen/media_ttt_status_icon_size" android:layout_width="@dimen/media_ttt_status_icon_size"
android:layout_height="@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:layout_marginStart="@dimen/media_ttt_last_item_start_margin"
android:indeterminateTint="?androidprv:attr/colorAccentPrimaryVariant" android:src="@drawable/ic_progress_activity"
style="?android:attr/progressBarStyleSmall" android:tint="?androidprv:attr/colorAccentPrimaryVariant"
android:alpha="0.0" android:alpha="0.0"
/> />

View File

@@ -16,6 +16,8 @@
package com.android.systemui.temporarydisplay.chipbar package com.android.systemui.temporarydisplay.chipbar
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import android.os.PowerManager import android.os.PowerManager
@@ -27,11 +29,14 @@ import android.view.View.ACCESSIBILITY_LIVE_REGION_NONE
import android.view.ViewGroup import android.view.ViewGroup
import android.view.WindowManager import android.view.WindowManager
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.VisibleForTesting
import com.android.internal.widget.CachingIconView import com.android.internal.widget.CachingIconView
import com.android.systemui.Gefingerpoken import com.android.systemui.Gefingerpoken
import com.android.systemui.R import com.android.systemui.R
import com.android.systemui.animation.Interpolators
import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription
import com.android.systemui.common.shared.model.Text.Companion.loadText import com.android.systemui.common.shared.model.Text.Companion.loadText
@@ -101,6 +106,15 @@ constructor(
private lateinit var parent: ChipbarRootView 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 = override val windowLayoutParams =
commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) } commonWindowLayoutParams.apply { gravity = Gravity.TOP.or(Gravity.CENTER_HORIZONTAL) }
@@ -143,8 +157,22 @@ constructor(
// ---- End item ---- // ---- End item ----
// Loading // Loading
currentView.requireViewById<View>(R.id.loading).visibility = val isLoading = newInfo.endItem == ChipbarEndItem.Loading
(newInfo.endItem == ChipbarEndItem.Loading).visibleIfTrue() 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 // Error
currentView.requireViewById<View>(R.id.error).visibility = currentView.requireViewById<View>(R.id.error).visibility =
@@ -223,12 +251,17 @@ constructor(
override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) { override fun animateViewOut(view: ViewGroup, removalReason: String?, onAnimationEnd: Runnable) {
val innerView = view.getInnerView() val innerView = view.getInnerView()
innerView.accessibilityLiveRegion = ACCESSIBILITY_LIVE_REGION_NONE 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 // If the view doesn't get animated, the [onAnimationEnd] runnable won't get run. So, just
// run it immediately. // run it immediately.
if (!removed) { if (!removed) {
logger.logAnimateOutFailure() logger.logAnimateOutFailure()
onAnimationEnd.run() fullEndRunnable.run()
} }
updateGestureListening() updateGestureListening()
@@ -269,7 +302,7 @@ constructor(
} }
private fun ViewGroup.getInnerView(): ViewGroup { 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) { override fun getTouchableRegion(view: View, outRect: Rect) {
@@ -283,8 +316,28 @@ constructor(
View.GONE 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 @IdRes private val INFO_TAG = R.id.tag_chipbar_info
private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED" private const val SWIPE_UP_GESTURE_REASON = "SWIPE_UP_GESTURE_DETECTED"
private const val TAG = "ChipbarCoordinator" private const val TAG = "ChipbarCoordinator"
private const val LOADING_ANIMATION_DURATION_MS = 1000L

View File

@@ -27,6 +27,7 @@ import android.view.WindowManager
import android.view.accessibility.AccessibilityManager import android.view.accessibility.AccessibilityManager
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.animation.doOnCancel
import androidx.test.filters.SmallTest import androidx.test.filters.SmallTest
import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.logging.testing.UiEventLoggerFake
import com.android.systemui.R import com.android.systemui.R
@@ -360,6 +361,105 @@ class ChipbarCoordinatorTest : SysuiTestCase() {
assertThat(isClicked).isTrue() assertThat(isClicked).isTrue()
} }
@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 @Test
fun displayView_vibrationEffect_doubleClickEffect() { fun displayView_vibrationEffect_doubleClickEffect() {
underTest.displayView( underTest.displayView(