Commit b9a6206a authored by Eric Seckler's avatar Eric Seckler Committed by Commit Bot

Reland "feed: Don't spin the spinner when its ancestors are hidden"

This is a reland of 46c618aa with an
added null check in MaterialSpinnerView.updateAnimationState (see diff
from patch set 1).

Original change's description:
> feed: Don't spin the spinner when its ancestors are hidden
>
> Currently, the spinner used in feed will only stop() its animation if
> the spinner view itself is hidden through setVisibility(). However, in
> reality, one of its ancestor views is hidden instead.
>
> This causes a continuous invisible animation to tick at the display's
> refresh rate (usually 60fps) if the spinner was ever shown, until it is
> destroyed (when the NTP views are eventually evicted).
>
> Instead, stop the animation also when any of the spinner's ancestors
> become hidden by listening for visibility changes and window attachment
> state changes.
>
> Bug: 1151391
> Change-Id: Ib7b7a8e56f083619420fd58f4340991253402535
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2552781
> Reviewed-by: Sami Kyöstilä <skyostil@chromium.org>
> Reviewed-by: Jian Li <jianli@chromium.org>
> Commit-Queue: Eric Seckler <eseckler@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#830957}

Bug: 1151391, 1152817
Change-Id: I306b623fc5d7243cf96998367f01380a80833aa7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2560108Reviewed-by: default avatarSami Kyöstilä <skyostil@chromium.org>
Reviewed-by: default avatarJian Li <jianli@chromium.org>
Auto-Submit: Eric Seckler <eseckler@chromium.org>
Commit-Queue: Jian Li <jianli@chromium.org>
Cr-Commit-Position: refs/heads/master@{#831188}
parent b42b6094
...@@ -14,11 +14,15 @@ import androidx.annotation.Nullable; ...@@ -14,11 +14,15 @@ import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView; import androidx.appcompat.widget.AppCompatImageView;
import androidx.swiperefreshlayout.widget.CircularProgressDrawable; import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
import org.chromium.base.FeatureList;
import org.chromium.base.TraceEvent;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.chrome.browser.flags.ChromeFeatureList;
/** View that shows a Material themed spinner. */ /** View that shows a Material themed spinner. */
public class MaterialSpinnerView extends AppCompatImageView { public class MaterialSpinnerView extends AppCompatImageView {
private final CircularProgressDrawable mSpinner; private final CircularProgressDrawable mSpinner;
private final boolean mAlwaysAnimate;
public MaterialSpinnerView(Context context) { public MaterialSpinnerView(Context context) {
this(context, null); this(context, null);
...@@ -33,6 +37,7 @@ public class MaterialSpinnerView extends AppCompatImageView { ...@@ -33,6 +37,7 @@ public class MaterialSpinnerView extends AppCompatImageView {
@SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"}) @SuppressWarnings({"nullness:argument.type.incompatible", "nullness:method.invocation.invalid"})
public MaterialSpinnerView(Context context, @Nullable AttributeSet attrs, int defStyle) { public MaterialSpinnerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle); super(context, attrs, defStyle);
TraceEvent.begin("MaterialSpinnerView");
mSpinner = new CircularProgressDrawable(context); mSpinner = new CircularProgressDrawable(context);
mSpinner.setStyle(CircularProgressDrawable.DEFAULT); mSpinner.setStyle(CircularProgressDrawable.DEFAULT);
setImageDrawable(mSpinner); setImageDrawable(mSpinner);
...@@ -40,19 +45,49 @@ public class MaterialSpinnerView extends AppCompatImageView { ...@@ -40,19 +45,49 @@ public class MaterialSpinnerView extends AppCompatImageView {
Theme theme = context.getTheme(); Theme theme = context.getTheme();
theme.resolveAttribute(R.attr.feedSpinnerColor, typedValue, true); theme.resolveAttribute(R.attr.feedSpinnerColor, typedValue, true);
mSpinner.setColorSchemeColors(typedValue.data); mSpinner.setColorSchemeColors(typedValue.data);
mAlwaysAnimate = FeatureList.isInitialized() ? ChromeFeatureList.isEnabled(
ChromeFeatureList.INTEREST_FEED_SPINNER_ALWAYS_ANIMATE)
: false;
updateAnimationState(isAttachedToWindow());
TraceEvent.end("MaterialSpinnerView");
}
if (getVisibility() == View.VISIBLE) { @Override
mSpinner.start(); protected void onVisibilityChanged(View changedView, int visibility) {
} super.onVisibilityChanged(changedView, visibility);
updateAnimationState(isAttachedToWindow());
} }
@Override @Override
public void setVisibility(int visibility) { protected void onAttachedToWindow() {
super.setVisibility(visibility); super.onAttachedToWindow();
updateAnimationState(/*isAttached=*/true);
}
@Override
protected void onDetachedFromWindow() {
// isAttachedToWindow() doesn't turn false during onDetachedFromWindow(), so we pass the new
// attachment state into updateAnimationState() here explicitly.
updateAnimationState(/*isAttached=*/false);
super.onDetachedFromWindow();
}
private void updateAnimationState(boolean isAttached) {
// Some Android versions call onVisibilityChanged() during the View's constructor before the
// spinner is created.
if (mSpinner == null) return;
// TODO(crbug.com/1151391): This feature is used for A:B testing to determine the impact of
// a bug fix. Remove after experiment is complete.
if (mAlwaysAnimate) {
if (!mSpinner.isRunning()) mSpinner.start();
return;
}
if (mSpinner.isRunning() && getVisibility() != View.VISIBLE) { boolean visible = isShown() && isAttached;
if (mSpinner.isRunning() && !visible) {
mSpinner.stop(); mSpinner.stop();
} else if (!mSpinner.isRunning() && getVisibility() == View.VISIBLE) { } else if (!mSpinner.isRunning() && visible) {
mSpinner.start(); mSpinner.start();
} }
} }
......
...@@ -8,6 +8,7 @@ import static com.google.common.truth.Truth.assertThat; ...@@ -8,6 +8,7 @@ import static com.google.common.truth.Truth.assertThat;
import android.app.Activity; import android.app.Activity;
import android.view.View; import android.view.View;
import android.widget.FrameLayout;
import androidx.swiperefreshlayout.widget.CircularProgressDrawable; import androidx.swiperefreshlayout.widget.CircularProgressDrawable;
...@@ -16,6 +17,7 @@ import org.junit.Test; ...@@ -16,6 +17,7 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.robolectric.Robolectric; import org.robolectric.Robolectric;
import org.robolectric.annotation.Config; import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.chromium.chrome.R; import org.chromium.chrome.R;
import org.chromium.testing.local.LocalRobolectricTestRunner; import org.chromium.testing.local.LocalRobolectricTestRunner;
...@@ -23,38 +25,52 @@ import org.chromium.testing.local.LocalRobolectricTestRunner; ...@@ -23,38 +25,52 @@ import org.chromium.testing.local.LocalRobolectricTestRunner;
/** Tests for {@link MaterialSpinnerView}. */ /** Tests for {@link MaterialSpinnerView}. */
@RunWith(LocalRobolectricTestRunner.class) @RunWith(LocalRobolectricTestRunner.class)
@Config(manifest = Config.NONE) @Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.PAUSED)
public class MaterialSpinnerViewTest { public class MaterialSpinnerViewTest {
private FrameLayout mLayout;
private MaterialSpinnerView mMaterialSpinnerView; private MaterialSpinnerView mMaterialSpinnerView;
private CircularProgressDrawable mAnimationDrawable;
@Before @Before
public void setUp() { public void setUp() {
Activity context = Robolectric.buildActivity(Activity.class).get(); Activity activity = Robolectric.setupActivity(Activity.class);
context.setTheme(R.style.Light); activity.setTheme(R.style.Light);
mMaterialSpinnerView = new MaterialSpinnerView(context);
// Attach the spinner inside a layout, so we can hide either the spinner
// or the parent view (ie. the layout) in the tests. Note that we
// require the looper to stay paused (LooperMode.Mode.PAUSED) for the
// duration of the tests. Otherwise, Robolectric will run through the
// animation and stop it before the tests get run. Because
// Robolectric.setupActivity() will run the looper until idle, we call
// setContentView() only after launching the activity above.
mMaterialSpinnerView = new MaterialSpinnerView(activity);
mAnimationDrawable = (CircularProgressDrawable) mMaterialSpinnerView.getDrawable();
mLayout = new FrameLayout(activity);
mLayout.addView(mMaterialSpinnerView);
activity.setContentView(mLayout);
} }
@Test @Test
public void testInit_isVisible_spinnerStarted() { public void testInit_isVisible_spinnerStarted() {
assertThat(mMaterialSpinnerView.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mMaterialSpinnerView.getVisibility()).isEqualTo(View.VISIBLE);
assertThat(mMaterialSpinnerView.isShown()).isTrue();
assertThat(((CircularProgressDrawable) mMaterialSpinnerView.getDrawable()).isRunning()) assertThat(mAnimationDrawable.isRunning()).isTrue();
.isTrue();
} }
@Test @Test
public void testSetVisibility_gone_stopsSpinner() { public void testSetVisibility_gone_stopsSpinner() {
mMaterialSpinnerView.setVisibility(View.GONE); mMaterialSpinnerView.setVisibility(View.GONE);
assertThat(((CircularProgressDrawable) mMaterialSpinnerView.getDrawable()).isRunning()) assertThat(mAnimationDrawable.isRunning()).isFalse();
.isFalse();
} }
@Test @Test
public void testSetVisibility_invisible_stopsSpinner() { public void testSetVisibility_invisible_stopsSpinner() {
mMaterialSpinnerView.setVisibility(View.INVISIBLE); mMaterialSpinnerView.setVisibility(View.INVISIBLE);
assertThat(((CircularProgressDrawable) mMaterialSpinnerView.getDrawable()).isRunning()) assertThat(mAnimationDrawable.isRunning()).isFalse();
.isFalse();
} }
@Test @Test
...@@ -62,7 +78,43 @@ public class MaterialSpinnerViewTest { ...@@ -62,7 +78,43 @@ public class MaterialSpinnerViewTest {
mMaterialSpinnerView.setVisibility(View.GONE); mMaterialSpinnerView.setVisibility(View.GONE);
mMaterialSpinnerView.setVisibility(View.VISIBLE); mMaterialSpinnerView.setVisibility(View.VISIBLE);
assertThat(((CircularProgressDrawable) mMaterialSpinnerView.getDrawable()).isRunning()) assertThat(mAnimationDrawable.isRunning()).isTrue();
.isTrue(); }
@Test
public void testContainerSetVisibility_gone_stopsSpinner() {
mLayout.setVisibility(View.GONE);
assertThat(mAnimationDrawable.isRunning()).isFalse();
}
@Test
public void testContainerSetVisibility_invisible_stopsSpinner() {
mLayout.setVisibility(View.INVISIBLE);
assertThat(mAnimationDrawable.isRunning()).isFalse();
}
@Test
public void testContainerSetVisibility_toTrue_startsSpinner() {
mLayout.setVisibility(View.GONE);
mLayout.setVisibility(View.VISIBLE);
assertThat(mAnimationDrawable.isRunning()).isTrue();
}
@Test
public void testDetachFromWindow_stopsSpinner() {
mLayout.removeView(mMaterialSpinnerView);
assertThat(mAnimationDrawable.isRunning()).isFalse();
}
@Test
public void testAttachToWindow_startsSpinner() {
mLayout.removeView(mMaterialSpinnerView);
mLayout.addView(mMaterialSpinnerView);
assertThat(mAnimationDrawable.isRunning()).isTrue();
} }
} }
...@@ -106,6 +106,7 @@ const base::Feature* kFeaturesExposedToJava[] = { ...@@ -106,6 +106,7 @@ const base::Feature* kFeaturesExposedToJava[] = {
&feature_engagement::kIPHTabSwitcherButtonFeature, &feature_engagement::kIPHTabSwitcherButtonFeature,
&feed::kInterestFeedContentSuggestions, &feed::kInterestFeedContentSuggestions,
&feed::kInterestFeedNoticeCardAutoDismiss, &feed::kInterestFeedNoticeCardAutoDismiss,
&feed::kInterestFeedSpinnerAlwaysAnimate,
&feed::kInterestFeedV1ClicksAndViewsConditionalUpload, &feed::kInterestFeedV1ClicksAndViewsConditionalUpload,
&feed::kInterestFeedV2, &feed::kInterestFeedV2,
&feed::kReportFeedUserActions, &feed::kReportFeedUserActions,
......
...@@ -322,6 +322,8 @@ public abstract class ChromeFeatureList { ...@@ -322,6 +322,8 @@ public abstract class ChromeFeatureList {
public static final String INTEREST_FEED_CONTENT_SUGGESTIONS = "InterestFeedContentSuggestions"; public static final String INTEREST_FEED_CONTENT_SUGGESTIONS = "InterestFeedContentSuggestions";
public static final String INTEREST_FEED_NOTICE_CARD_AUTO_DISMISS = public static final String INTEREST_FEED_NOTICE_CARD_AUTO_DISMISS =
"InterestFeedNoticeCardAutoDismiss"; "InterestFeedNoticeCardAutoDismiss";
public static final String INTEREST_FEED_SPINNER_ALWAYS_ANIMATE =
"InterestFeedSpinnerAlwaysAnimate";
public static final String INTEREST_FEED_V2 = "InterestFeedV2"; public static final String INTEREST_FEED_V2 = "InterestFeedV2";
public static final String KITKAT_SUPPORTED = "KitKatSupported"; public static final String KITKAT_SUPPORTED = "KitKatSupported";
public static final String LOOKALIKE_NAVIGATION_URL_SUGGESTIONS_UI = public static final String LOOKALIKE_NAVIGATION_URL_SUGGESTIONS_UI =
......
...@@ -49,6 +49,10 @@ const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload{ ...@@ -49,6 +49,10 @@ const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload{
const base::Feature kInterestFeedNoticeCardAutoDismiss{ const base::Feature kInterestFeedNoticeCardAutoDismiss{
"InterestFeedNoticeCardAutoDismiss", base::FEATURE_DISABLED_BY_DEFAULT}; "InterestFeedNoticeCardAutoDismiss", base::FEATURE_DISABLED_BY_DEFAULT};
// Used for A:B testing of a bug fix (crbug.com/1151391).
const base::Feature kInterestFeedSpinnerAlwaysAnimate{
"InterestFeedSpinnerAlwaysAnimate", base::FEATURE_DISABLED_BY_DEFAULT};
const char kDefaultReferrerUrl[] = const char kDefaultReferrerUrl[] =
"https://www.googleapis.com/auth/chrome-content-suggestions"; "https://www.googleapis.com/auth/chrome-content-suggestions";
......
...@@ -33,6 +33,8 @@ extern const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload; ...@@ -33,6 +33,8 @@ extern const base::Feature kInterestFeedV2ClicksAndViewsConditionalUpload;
extern const base::Feature kInterestFeedNoticeCardAutoDismiss; extern const base::Feature kInterestFeedNoticeCardAutoDismiss;
extern const base::Feature kInterestFeedSpinnerAlwaysAnimate;
std::string GetFeedReferrerUrl(); std::string GetFeedReferrerUrl();
} // namespace feed } // namespace feed
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment