Disabled SUBPIXEL_TEXT_FLAG for notification shade while fold unfold

Disabled the SUBPIXEL_TEXT_FLAG for all TextViews of the Notification Shade while folding and unfolding is in progress.

The SUBPIXEL_TEXT_FLAG is responsible for the default Android behaviour that tries to align the font to the nearest pixel, causing the text to jump as the nearest pixel keeps changing during folding and unfolding.

To solve this issue, we have disabled the SUBPIXEL_TEXT_FLAG for the TextViews of the Notification Shade during folding and unfolding, resulting in smoother text transitions.

Test: atest com.android.systemui.shared.animation.DisableSubpixelTextTransitionListenerTest.kt
Fixes: 227427854
Flag: SPLIT_SHADE_SUBPIXEL_OPTIMIZATION
Change-Id: I6ada51f912ee1c086674b19ea39d4cab3ad161a8
This commit is contained in:
dshivangi
2023-04-03 17:46:16 +01:00
committed by Shivangi Dubey
parent 9e30a4e0f2
commit 32de1c9474
7 changed files with 242 additions and 1 deletions

View File

@@ -0,0 +1,84 @@
/*
* 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.
*/
package com.android.systemui.shared.animation
import android.graphics.Paint
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.forEach
import com.android.systemui.unfold.UnfoldTransitionProgressProvider.TransitionProgressListener
import com.android.systemui.util.traceSection
import java.lang.ref.WeakReference
/**
* A listener which disables subpixel flag for all TextView children of a given parent ViewGroup
* during fold/unfold transitions.
*/
class DisableSubpixelTextTransitionListener(private val rootView: ViewGroup?) :
TransitionProgressListener {
private val childrenTextViews: MutableList<WeakReference<TextView>> = mutableListOf()
private var isTransitionInProgress: Boolean = false
override fun onTransitionStarted() {
isTransitionInProgress = true
traceSection("subpixelFlagSetForTextView") {
traceSection("subpixelFlagTraverseHierarchy") {
getAllChildTextView(rootView, childrenTextViews)
}
traceSection("subpixelFlagDisableForTextView") {
childrenTextViews.forEach { child ->
val childTextView = child.get() ?: return@forEach
childTextView.paintFlags = childTextView.paintFlags or Paint.SUBPIXEL_TEXT_FLAG
}
}
}
}
override fun onTransitionFinished() {
if (!isTransitionInProgress) return
isTransitionInProgress = false
traceSection("subpixelFlagEnableForTextView") {
childrenTextViews.forEach { child ->
val childTextView = child.get() ?: return@forEach
childTextView.paintFlags =
childTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG.inv()
}
childrenTextViews.clear()
}
}
/**
* Populates a list of all TextView children of a given parent ViewGroup
*
* @param parent the root ViewGroup for which to retrieve TextView children
* @param childrenTextViews the list to store the retrieved TextView children
*/
private fun getAllChildTextView(
parent: ViewGroup?,
childrenTextViews: MutableList<WeakReference<TextView>>
) {
parent?.forEach { child ->
when (child) {
is ViewGroup -> getAllChildTextView(child, childrenTextViews)
is TextView -> {
if ((child.paintFlags and Paint.SUBPIXEL_TEXT_FLAG) <= 0) {
childrenTextViews.add(WeakReference(child))
}
}
}
}
}
}

View File

@@ -689,6 +689,11 @@ object Flags {
val ADVANCED_VPN_ENABLED = releasedFlag(2800, name = "AdvancedVpn__enable_feature",
namespace = "vpn")
// TODO(b/277201412): Tracking Bug
@JvmField
val SPLIT_SHADE_SUBPIXEL_OPTIMIZATION =
unreleasedFlag(2805, "split_shade_subpixel_optimization", teamfood = true)
// TODO(b/278761837): Tracking Bug
@JvmField
val USE_NEW_ACTIVITY_STARTER = unreleasedFlag(2801, name = "use_new_activity_starter",

View File

@@ -54,6 +54,7 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition
import com.android.systemui.multishade.domain.interactor.MultiShadeInteractor;
import com.android.systemui.multishade.domain.interactor.MultiShadeMotionEventInteractor;
import com.android.systemui.multishade.ui.view.MultiShadeView;
import com.android.systemui.shared.animation.DisableSubpixelTextTransitionListener;
import com.android.systemui.statusbar.DragDownHelper;
import com.android.systemui.statusbar.LockscreenShadeTransitionController;
import com.android.systemui.statusbar.NotificationInsetsController;
@@ -68,9 +69,11 @@ import com.android.systemui.statusbar.phone.PhoneStatusBarViewController;
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.unfold.UnfoldTransitionProgressProvider;
import com.android.systemui.util.time.SystemClock;
import java.io.PrintWriter;
import java.util.Optional;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -114,7 +117,7 @@ public class NotificationShadeWindowViewController {
private boolean mIsTrackingBarGesture = false;
private boolean mIsOcclusionTransitionRunning = false;
private DisableSubpixelTextTransitionListener mDisableSubpixelTextTransitionListener;
private final Consumer<TransitionStep> mLockscreenToDreamingTransition =
(TransitionStep step) -> {
mIsOcclusionTransitionRunning =
@@ -139,6 +142,7 @@ public class NotificationShadeWindowViewController {
LockIconViewController lockIconViewController,
CentralSurfaces centralSurfaces,
NotificationShadeWindowController controller,
Optional<UnfoldTransitionProgressProvider> unfoldTransitionProgressProvider,
KeyguardUnlockAnimationController keyguardUnlockAnimationController,
NotificationInsetsController notificationInsetsController,
AmbientState ambientState,
@@ -174,6 +178,7 @@ public class NotificationShadeWindowViewController {
// This view is not part of the newly inflated expanded status bar.
mBrightnessMirror = mView.findViewById(R.id.brightness_mirror_container);
mDisableSubpixelTextTransitionListener = new DisableSubpixelTextTransitionListener(mView);
KeyguardBouncerViewBinder.bind(
mView.findViewById(R.id.keyguard_bouncer_container),
keyguardBouncerViewModel,
@@ -184,6 +189,11 @@ public class NotificationShadeWindowViewController {
mLockscreenToDreamingTransition);
mClock = clock;
if (featureFlags.isEnabled(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION)) {
unfoldTransitionProgressProvider.ifPresent(
progressProvider -> progressProvider.addCallback(
mDisableSubpixelTextTransitionListener));
}
if (ComposeFacade.INSTANCE.isComposeAvailable()
&& featureFlags.isEnabled(Flags.DUAL_SHADE)) {
mMultiShadeMotionEventInteractor = multiShadeMotionEventInteractorProvider.get();

View File

@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.PhoneStatusBarViewController
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.any
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
@@ -71,6 +72,7 @@ import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
import org.mockito.MockitoAnnotations
import java.util.Optional
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@@ -101,6 +103,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
@Mock lateinit var keyguardBouncerComponentFactory: KeyguardBouncerComponent.Factory
@Mock lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@Mock
private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
@Mock lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
lateinit var primaryBouncerToGoneTransitionViewModel: PrimaryBouncerToGoneTransitionViewModel
@@ -129,6 +133,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
@@ -158,6 +163,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() {
lockIconViewController,
centralSurfaces,
notificationShadeWindowController,
unfoldTransitionProgressProvider,
keyguardUnlockAnimationController,
notificationInsetsController,
ambientState,

View File

@@ -53,6 +53,7 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll
import com.android.systemui.statusbar.phone.CentralSurfaces
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.android.systemui.statusbar.window.StatusBarWindowStateController
import com.android.systemui.unfold.UnfoldTransitionProgressProvider
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
@@ -71,6 +72,7 @@ import org.mockito.Mock
import org.mockito.Mockito.spy
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
import java.util.Optional
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidTestingRunner::class)
@@ -103,6 +105,8 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
@Mock private lateinit var keyguardBouncerComponent: KeyguardBouncerComponent
@Mock
private lateinit var keyguardSecurityContainerController: KeyguardSecurityContainerController
@Mock
private lateinit var unfoldTransitionProgressProvider: Optional<UnfoldTransitionProgressProvider>
@Mock private lateinit var notificationInsetsController: NotificationInsetsController
@Mock private lateinit var keyguardTransitionInteractor: KeyguardTransitionInteractor
@Mock
@@ -141,6 +145,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
featureFlags.set(Flags.TRACKPAD_GESTURE_COMMON, true)
featureFlags.set(Flags.TRACKPAD_GESTURE_FEATURES, false)
featureFlags.set(Flags.DUAL_SHADE, false)
featureFlags.set(Flags.SPLIT_SHADE_SUBPIXEL_OPTIMIZATION, true)
val inputProxy = MultiShadeInputProxy()
testScope = TestScope()
val multiShadeInteractor =
@@ -169,6 +174,7 @@ class NotificationShadeWindowViewTest : SysuiTestCase() {
lockIconViewController,
centralSurfaces,
notificationShadeWindowController,
unfoldTransitionProgressProvider,
keyguardUnlockAnimationController,
notificationInsetsController,
ambientState,

View File

@@ -0,0 +1,130 @@
/*
* 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.
*/
package com.android.systemui.shared.animation
import android.graphics.Paint
import android.testing.AndroidTestingRunner
import android.widget.FrameLayout
import android.widget.TextView
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@SmallTest
@RunWith(AndroidTestingRunner::class)
class DisableSubpixelTextTransitionListenerTest : SysuiTestCase() {
private lateinit var disableSubpixelTextTransitionListener:
DisableSubpixelTextTransitionListener
private var rootViewWithNoTextView = FrameLayout(context)
private var rootView = FrameLayout(context)
private var childView = FrameLayout(context)
private var childViewWithNoTextView = FrameLayout(context)
private var childTextView = TextView(context)
private var childOfChildTextView = TextView(context)
@Before
fun setup() {
childView.addView(childOfChildTextView)
rootView.addView(childTextView)
rootView.addView(childView)
}
@Test
fun onTransitionStarted_addsSubpixelFlagToChildTextView() {
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
assertThat(childTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isGreaterThan(0)
}
@Test
fun onTransitionStarted_addsSupbixelFlagToChildOfChildTextView() {
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
assertThat(childOfChildTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isGreaterThan(0)
}
@Test
fun onTransitionFinished_removeSupbixelFlagFromChildTextView() {
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
disableSubpixelTextTransitionListener.onTransitionFinished()
assertThat(childTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isEqualTo(0)
}
@Test
fun onTransitionFinished_removeSupbixelFlagFromChildOfChildTextView() {
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
disableSubpixelTextTransitionListener.onTransitionFinished()
assertThat(childOfChildTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isEqualTo(0)
}
@Test
fun whenRootViewIsNull_runWithoutExceptions() {
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(null)
disableSubpixelTextTransitionListener.onTransitionStarted()
disableSubpixelTextTransitionListener.onTransitionFinished()
}
@Test
fun whenFlagAlreadyPresent_flagNotRemovedOnTransitionFinished() {
childTextView.paintFlags = childTextView.paintFlags or Paint.SUBPIXEL_TEXT_FLAG
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
disableSubpixelTextTransitionListener.onTransitionFinished()
assertThat(childTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isGreaterThan(0)
}
@Test
fun whenFlagNotPresent_flagRemovedOnTransitionFinished() {
childTextView.paintFlags = childTextView.paintFlags or Paint.SUBPIXEL_TEXT_FLAG
disableSubpixelTextTransitionListener = DisableSubpixelTextTransitionListener(rootView)
disableSubpixelTextTransitionListener.onTransitionStarted()
disableSubpixelTextTransitionListener.onTransitionFinished()
assertThat(childOfChildTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isEqualTo(0)
}
@Test
fun whenRootViewHasNoChildTextView_flagNotAddedToRelatedTextviews() {
rootViewWithNoTextView.addView(childViewWithNoTextView)
rootView.addView(rootViewWithNoTextView)
disableSubpixelTextTransitionListener =
DisableSubpixelTextTransitionListener(rootViewWithNoTextView)
disableSubpixelTextTransitionListener.onTransitionStarted()
assertThat(childTextView.paintFlags and Paint.SUBPIXEL_TEXT_FLAG).isEqualTo(0)
}
}