Fix one-icon shelf flicker on lockscreen swipe-up
The flicker happens because floating point comparison
for viewStart and shelfStart is off by ~0.001f sometimes,
such that the notification is thought to be above the shelf,
causing the shelf to hide since it no longer has a full view.
Bug: 222123657
Test: NotificationShelfTest
Test: on lockscreen, swipe up with one icon in shelf
=> no flickering
Change-Id: Iad2bb24d4a51d5a831aa3193de10f768fa7c41be
This commit is contained in:
@@ -326,9 +326,9 @@ public class NotificationShelf extends ActivatableNotificationView implements
|
||||
|| child.isPinned();
|
||||
boolean isLastChild = child == lastChild;
|
||||
final float viewStart = child.getTranslationY();
|
||||
|
||||
final float inShelfAmount = updateShelfTransformation(i, child, scrollingFast,
|
||||
expandingAnimated, isLastChild);
|
||||
final float shelfClipStart = getTranslationY() - mPaddingBetweenElements;
|
||||
final float inShelfAmount = getAmountInShelf(i, child, scrollingFast,
|
||||
expandingAnimated, isLastChild, shelfClipStart);
|
||||
|
||||
// TODO(b/172289889) scale mPaddingBetweenElements with expansion amount
|
||||
if ((isLastChild && !child.isInShelf()) || aboveShelf || backgroundForceHidden) {
|
||||
@@ -609,10 +609,18 @@ public class NotificationShelf extends ActivatableNotificationView implements
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the amount how much this notification is in the shelf
|
||||
* @param i Index of the view in the host layout.
|
||||
* @param view The current ExpandableView.
|
||||
* @param scrollingFast Whether we are scrolling fast.
|
||||
* @param expandingAnimated Whether we are expanding a notification.
|
||||
* @param isLastChild Whether this is the last view.
|
||||
* @param shelfClipStart The point at which notifications start getting clipped by the shelf.
|
||||
* @return The amount how much this notification is in the shelf.
|
||||
* 0f is not in shelf. 1f is completely in shelf.
|
||||
*/
|
||||
private float updateShelfTransformation(int i, ExpandableView view, boolean scrollingFast,
|
||||
boolean expandingAnimated, boolean isLastChild) {
|
||||
@VisibleForTesting
|
||||
public float getAmountInShelf(int i, ExpandableView view, boolean scrollingFast,
|
||||
boolean expandingAnimated, boolean isLastChild, float shelfClipStart) {
|
||||
|
||||
// Let's calculate how much the view is in the shelf
|
||||
float viewStart = view.getTranslationY();
|
||||
@@ -635,29 +643,33 @@ public class NotificationShelf extends ActivatableNotificationView implements
|
||||
float viewEnd = viewStart + fullHeight;
|
||||
float fullTransitionAmount = 0.0f;
|
||||
float iconTransitionAmount = 0.0f;
|
||||
float shelfStart = getTranslationY() - mPaddingBetweenElements;
|
||||
|
||||
// Don't animate shelf icons during shade expansion.
|
||||
if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) {
|
||||
// TODO(b/172289889) handle icon placement for notification that is clipped by the shelf
|
||||
if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) {
|
||||
fullTransitionAmount = 1f;
|
||||
iconTransitionAmount = 1f;
|
||||
}
|
||||
} else if (viewEnd >= shelfStart
|
||||
|
||||
} else if (viewEnd >= shelfClipStart
|
||||
&& (!mAmbientState.isUnlockHintRunning() || view.isInShelf())
|
||||
&& (mAmbientState.isShadeExpanded()
|
||||
|| (!view.isPinned() && !view.isHeadsUpAnimatingAway()))) {
|
||||
|
||||
if (viewStart < shelfStart) {
|
||||
float fullAmount = (shelfStart - viewStart) / fullHeight;
|
||||
if (viewStart < shelfClipStart && Math.abs(viewStart - shelfClipStart) > 0.001f) {
|
||||
// Partially clipped by shelf.
|
||||
float fullAmount = (shelfClipStart - viewStart) / fullHeight;
|
||||
fullAmount = Math.min(1.0f, fullAmount);
|
||||
fullTransitionAmount = 1.0f - fullAmount;
|
||||
if (isLastChild) {
|
||||
// Reduce icon transform distance to completely fade in shelf icon
|
||||
// by the time the notification icon fades out, and vice versa
|
||||
iconTransitionAmount = (shelfStart - viewStart)
|
||||
iconTransitionAmount = (shelfClipStart - viewStart)
|
||||
/ (iconTransformStart - viewStart);
|
||||
} else {
|
||||
iconTransitionAmount = (shelfStart - iconTransformStart) / transformDistance;
|
||||
iconTransitionAmount = (shelfClipStart - iconTransformStart)
|
||||
/ transformDistance;
|
||||
}
|
||||
iconTransitionAmount = MathUtils.constrain(iconTransitionAmount, 0.0f, 1.0f);
|
||||
iconTransitionAmount = 1.0f - iconTransitionAmount;
|
||||
@@ -772,6 +784,9 @@ public class NotificationShelf extends ActivatableNotificationView implements
|
||||
}
|
||||
|
||||
private NotificationIconContainer.IconState getIconState(StatusBarIconView icon) {
|
||||
if (mShelfIcons == null) {
|
||||
return null;
|
||||
}
|
||||
return mShelfIcons.getIconState(icon);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@ import android.testing.TestableLooper.RunWithLooper
|
||||
import androidx.test.filters.SmallTest
|
||||
import com.android.systemui.SysuiTestCase
|
||||
import com.android.systemui.statusbar.NotificationShelf
|
||||
import junit.framework.Assert.assertFalse
|
||||
import junit.framework.Assert.assertTrue
|
||||
import com.android.systemui.statusbar.StatusBarIconView
|
||||
import com.android.systemui.statusbar.notification.row.ExpandableView
|
||||
import junit.framework.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@@ -143,6 +144,113 @@ class NotificationShelfTest : SysuiTestCase() {
|
||||
assertFalse(isYBelowShelfInView)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAmountInShelf_lastViewBelowShelf_completelyInShelf() {
|
||||
val shelfClipStart = 0f
|
||||
val viewStart = 1f
|
||||
|
||||
val expandableView = mock(ExpandableView::class.java)
|
||||
whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
|
||||
whenever(expandableView.translationY).thenReturn(viewStart)
|
||||
whenever(expandableView.actualHeight).thenReturn(20)
|
||||
|
||||
whenever(expandableView.minHeight).thenReturn(20)
|
||||
whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
|
||||
whenever(expandableView.isInShelf).thenReturn(true)
|
||||
|
||||
whenever(ambientState.isOnKeyguard).thenReturn(true)
|
||||
whenever(ambientState.isExpansionChanging).thenReturn(false)
|
||||
whenever(ambientState.isShadeExpanded).thenReturn(true)
|
||||
|
||||
val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
|
||||
/* view= */ expandableView,
|
||||
/* scrollingFast= */ false,
|
||||
/* expandingAnimated= */ false,
|
||||
/* isLastChild= */ true,
|
||||
shelfClipStart)
|
||||
assertEquals(1f, amountInShelf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAmountInShelf_lastViewAlmostBelowShelf_completelyInShelf() {
|
||||
val viewStart = 0f
|
||||
val shelfClipStart = 0.001f
|
||||
|
||||
val expandableView = mock(ExpandableView::class.java)
|
||||
whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
|
||||
whenever(expandableView.translationY).thenReturn(viewStart)
|
||||
whenever(expandableView.actualHeight).thenReturn(20)
|
||||
|
||||
whenever(expandableView.minHeight).thenReturn(20)
|
||||
whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
|
||||
whenever(expandableView.isInShelf).thenReturn(true)
|
||||
|
||||
whenever(ambientState.isOnKeyguard).thenReturn(true)
|
||||
whenever(ambientState.isExpansionChanging).thenReturn(false)
|
||||
whenever(ambientState.isShadeExpanded).thenReturn(true)
|
||||
|
||||
val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
|
||||
/* view= */ expandableView,
|
||||
/* scrollingFast= */ false,
|
||||
/* expandingAnimated= */ false,
|
||||
/* isLastChild= */ true,
|
||||
shelfClipStart)
|
||||
assertEquals(1f, amountInShelf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAmountInShelf_lastViewHalfClippedByShelf_halfInShelf() {
|
||||
val viewStart = 0f
|
||||
val shelfClipStart = 10f
|
||||
|
||||
val expandableView = mock(ExpandableView::class.java)
|
||||
whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
|
||||
whenever(expandableView.translationY).thenReturn(viewStart)
|
||||
whenever(expandableView.actualHeight).thenReturn(25)
|
||||
|
||||
whenever(expandableView.minHeight).thenReturn(25)
|
||||
whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
|
||||
whenever(expandableView.isInShelf).thenReturn(true)
|
||||
|
||||
whenever(ambientState.isOnKeyguard).thenReturn(true)
|
||||
whenever(ambientState.isExpansionChanging).thenReturn(false)
|
||||
whenever(ambientState.isShadeExpanded).thenReturn(true)
|
||||
|
||||
val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
|
||||
/* view= */ expandableView,
|
||||
/* scrollingFast= */ false,
|
||||
/* expandingAnimated= */ false,
|
||||
/* isLastChild= */ true,
|
||||
shelfClipStart)
|
||||
assertEquals(0.5f, amountInShelf)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getAmountInShelf_lastViewAboveShelf_notInShelf() {
|
||||
val viewStart = 0f
|
||||
val shelfClipStart = 15f
|
||||
|
||||
val expandableView = mock(ExpandableView::class.java)
|
||||
whenever(expandableView.shelfIcon).thenReturn(mock(StatusBarIconView::class.java))
|
||||
whenever(expandableView.translationY).thenReturn(viewStart)
|
||||
whenever(expandableView.actualHeight).thenReturn(10)
|
||||
|
||||
whenever(expandableView.minHeight).thenReturn(10)
|
||||
whenever(expandableView.shelfTransformationTarget).thenReturn(null) // use translationY
|
||||
whenever(expandableView.isInShelf).thenReturn(false)
|
||||
|
||||
whenever(ambientState.isExpansionChanging).thenReturn(false)
|
||||
whenever(ambientState.isOnKeyguard).thenReturn(true)
|
||||
|
||||
val amountInShelf = shelf.getAmountInShelf(/* i= */ 0,
|
||||
/* view= */ expandableView,
|
||||
/* scrollingFast= */ false,
|
||||
/* expandingAnimated= */ false,
|
||||
/* isLastChild= */ true,
|
||||
shelfClipStart)
|
||||
assertEquals(0f, amountInShelf)
|
||||
}
|
||||
|
||||
private fun setFractionToShade(fraction: Float) {
|
||||
whenever(ambientState.fractionToShade).thenReturn(fraction)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user