Reduce jank around loading view when opening data usage UI

Change-Id: I3d23d8160b046de8fe125ba0697b7b3d7786453c
Fix: 28181319
Test: robotests
This commit is contained in:
Fan Zhang
2017-06-26 14:22:45 -07:00
parent c26ae0677e
commit 896f1b363c
8 changed files with 227 additions and 86 deletions

View File

@@ -49,6 +49,7 @@ import com.android.settings.applications.LayoutPreference;
import com.android.settings.core.InstrumentedPreferenceFragment;
import com.android.settings.core.instrumentation.Instrumentable;
import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.CustomDialogPreference;
import com.android.settingslib.CustomEditTextPreference;
import com.android.settingslib.HelpUtils;
@@ -240,14 +241,11 @@ public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceF
unregisterObserverIfNeeded();
}
public void showLoadingWhenEmpty() {
View loading = getView().findViewById(R.id.loading_container);
setEmptyView(loading);
}
public void setLoading(boolean loading, boolean animate) {
View loading_container = getView().findViewById(R.id.loading_container);
Utils.handleLoadingContainer(loading_container, getListView(), !loading, animate);
View loadingContainer = getView().findViewById(R.id.loading_container);
LoadingViewController.handleLoadingContainer(loadingContainer, getListView(),
!loading /* done */,
animate);
}
public void registerObserverIfNeeded() {

View File

@@ -949,43 +949,6 @@ public final class Utils extends com.android.settingslib.Utils {
return result;
}
// TODO: move this out of Utils to a mixin or a controller or a helper class.
@Deprecated
public static void handleLoadingContainer(View loading, View doneLoading, boolean done,
boolean animate) {
setViewShown(loading, !done, animate);
setViewShown(doneLoading, done, animate);
}
private static void setViewShown(final View view, boolean shown, boolean animate) {
if (animate) {
Animation animation = AnimationUtils.loadAnimation(view.getContext(),
shown ? android.R.anim.fade_in : android.R.anim.fade_out);
if (shown) {
view.setVisibility(View.VISIBLE);
} else {
animation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.INVISIBLE);
}
});
}
view.startAnimation(animation);
} else {
view.clearAnimation();
view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE);
}
}
/**
* Returns the application info of the currently installed MDM package.
*/

View File

@@ -81,6 +81,7 @@ import com.android.settings.notification.AppNotificationSettings;
import com.android.settings.notification.ConfigureNotificationSettings;
import com.android.settings.notification.NotificationBackend;
import com.android.settings.notification.NotificationBackend.AppRow;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.HelpUtils;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.AppEntry;
@@ -848,6 +849,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment
private final AppStateBaseBridge mExtraInfoBridge;
private final Handler mBgHandler;
private final Handler mFgHandler;
private final LoadingViewController mLoadingViewController;
private int mFilterMode;
private ArrayList<ApplicationsState.AppEntry> mBaseEntries;
@@ -893,12 +895,6 @@ public class ManageApplications extends InstrumentedPreferenceFragment
}
};
private Runnable mShowLoadingContainerRunnable = new Runnable() {
public void run() {
Utils.handleLoadingContainer(mManageApplications.mLoadingContainer,
mManageApplications.mListContainer, false /* done */, false /* animate */);
}
};
public ApplicationsAdapter(ApplicationsState state, ManageApplications manageApplications,
int filterMode) {
@@ -907,6 +903,10 @@ public class ManageApplications extends InstrumentedPreferenceFragment
mBgHandler = new Handler(mState.getBackgroundLooper());
mSession = state.newSession(this);
mManageApplications = manageApplications;
mLoadingViewController = new LoadingViewController(
mManageApplications.mLoadingContainer,
mManageApplications.mListContainer
);
mContext = manageApplications.getActivity();
mPm = mContext.getPackageManager();
mFilterMode = filterMode;
@@ -1108,11 +1108,7 @@ public class ManageApplications extends InstrumentedPreferenceFragment
if (mSession.getAllApps().size() != 0
&& mManageApplications.mListContainer.getVisibility() != View.VISIBLE) {
// Cancel any pending task to show the loading animation and show the list of
// apps directly.
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
Utils.handleLoadingContainer(mManageApplications.mLoadingContainer,
mManageApplications.mListContainer, true, true);
mLoadingViewController.showContent(true /* animate */);
}
if (mManageApplications.mListType == LIST_TYPE_USAGE_ACCESS) {
// No enabled or disabled filters for usage access.
@@ -1166,11 +1162,9 @@ public class ManageApplications extends InstrumentedPreferenceFragment
void updateLoading() {
final boolean appLoaded = mHasReceivedLoadEntries && mSession.getAllApps().size() != 0;
if (appLoaded) {
Utils.handleLoadingContainer(mManageApplications.mLoadingContainer,
mManageApplications.mListContainer, true /* done */, false /* animate */);
mLoadingViewController.showContent(false /* animate */);
} else {
mFgHandler.postDelayed(
mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS);
mLoadingViewController.showLoadingViewDelayed();
}
}

View File

@@ -15,7 +15,6 @@
*/
package com.android.settings.applications;
import android.app.Fragment;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -27,7 +26,7 @@ import android.view.ViewGroup;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import com.android.settings.Utils;
import com.android.settings.widget.LoadingViewController;
public class RunningServices extends SettingsPreferenceFragment {
@@ -37,6 +36,7 @@ public class RunningServices extends SettingsPreferenceFragment {
private RunningProcessesView mRunningProcessesView;
private Menu mOptionsMenu;
private View mLoadingContainer;
private LoadingViewController mLoadingViewController;
@Override
public void onCreate(Bundle savedInstanceState) {
@@ -49,10 +49,11 @@ public class RunningServices extends SettingsPreferenceFragment {
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.manage_applications_running, null);
mRunningProcessesView = (RunningProcessesView) rootView.findViewById(
R.id.running_processes);
mRunningProcessesView = rootView.findViewById(R.id.running_processes);
mRunningProcessesView.doCreate();
mLoadingContainer = rootView.findViewById(R.id.loading_container);
mLoadingViewController = new LoadingViewController(
mLoadingContainer, mRunningProcessesView);
return rootView;
}
@@ -71,7 +72,7 @@ public class RunningServices extends SettingsPreferenceFragment {
public void onResume() {
super.onResume();
boolean haveData = mRunningProcessesView.doResume(this, mRunningProcessesAvail);
Utils.handleLoadingContainer(mLoadingContainer, mRunningProcessesView, haveData, false);
mLoadingViewController.handleLoadingContainer(haveData /* done */, false /* animate */);
}
@Override
@@ -115,7 +116,7 @@ public class RunningServices extends SettingsPreferenceFragment {
private final Runnable mRunningProcessesAvail = new Runnable() {
@Override
public void run() {
Utils.handleLoadingContainer(mLoadingContainer, mRunningProcessesView, true, true);
mLoadingViewController.showContent(true /* animate */);
}
};

View File

@@ -50,6 +50,7 @@ import android.widget.Spinner;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settings.R;
import com.android.settings.datausage.CycleAdapter.SpinnerInterface;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.AppItem;
import com.android.settingslib.net.ChartData;
import com.android.settingslib.net.ChartDataLoader;
@@ -96,13 +97,13 @@ public class DataUsageList extends DataUsageBase {
};
private INetworkStatsSession mStatsSession;
private ChartDataUsagePreference mChart;
private NetworkTemplate mTemplate;
private int mSubId;
private ChartData mChartData;
private LoadingViewController mLoadingViewController;
private UidDetailProvider mUidDetailProvider;
private CycleAdapter mCycleAdapter;
private Spinner mCycleSpinner;
@@ -110,6 +111,7 @@ public class DataUsageList extends DataUsageBase {
private PreferenceGroup mApps;
private View mHeader;
@Override
public int getMetricsCategory() {
return MetricsEvent.DATA_USAGE_LIST;
@@ -176,7 +178,10 @@ public class DataUsageList extends DataUsageBase {
mCycleSpinner.setSelection(position);
}
}, mCycleListener, true);
setLoading(true, false);
mLoadingViewController = new LoadingViewController(
getView().findViewById(R.id.loading_container), getListView());
mLoadingViewController.showLoadingViewDelayed();
}
@Override
@@ -523,7 +528,7 @@ public class DataUsageList extends DataUsageBase {
@Override
public void onLoadFinished(Loader<ChartData> loader, ChartData data) {
setLoading(false, true);
mLoadingViewController.showContent(false /* animate */);
mChartData = data;
mChart.setNetworkStats(mChartData.network);

View File

@@ -0,0 +1,106 @@
/*
* Copyright (C) 2017 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.settings.widget;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
/**
* A helper class that manages show/hide loading spinner.
*/
public class LoadingViewController {
private static final long DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS = 100L;
public final Handler mFgHandler;
public final View mLoadingView;
public final View mContentView;
public LoadingViewController(View loadingView, View contentView) {
mLoadingView = loadingView;
mContentView = contentView;
mFgHandler = new Handler(Looper.getMainLooper());
}
private Runnable mShowLoadingContainerRunnable = new Runnable() {
public void run() {
handleLoadingContainer(false /* done */, false /* animate */);
}
};
public void showContent(boolean animate) {
// Cancel any pending task to show the loading animation and show the list of
// apps directly.
mFgHandler.removeCallbacks(mShowLoadingContainerRunnable);
handleLoadingContainer(true /* show */, animate);
}
public void showLoadingViewDelayed() {
mFgHandler.postDelayed(
mShowLoadingContainerRunnable, DELAY_SHOW_LOADING_CONTAINER_THRESHOLD_MS);
}
public void handleLoadingContainer(boolean done, boolean animate) {
handleLoadingContainer(mLoadingView, mContentView, done, animate);
}
/**
* Show/hide loading view and content view.
*
* @param loading The loading spinner view
* @param content The content view
* @param done If true, content is set visible and loading is set invisible.
* @param animate Whether or not content/loading views should animate in/out.
*/
public static void handleLoadingContainer(View loading, View content, boolean done,
boolean animate) {
setViewShown(loading, !done, animate);
setViewShown(content, done, animate);
}
private static void setViewShown(final View view, boolean shown, boolean animate) {
if (animate) {
Animation animation = AnimationUtils.loadAnimation(view.getContext(),
shown ? android.R.anim.fade_in : android.R.anim.fade_out);
if (shown) {
view.setVisibility(View.VISIBLE);
} else {
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationRepeat(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.INVISIBLE);
}
});
}
view.startAnimation(animation);
} else {
view.clearAnimation();
view.setVisibility(shown ? View.VISIBLE : View.INVISIBLE);
}
}
}

View File

@@ -37,12 +37,10 @@ import com.android.settings.testutils.shadow.SettingsShadowResources;
import com.android.settings.testutils.shadow.SettingsShadowResources.SettingsShadowTheme;
import com.android.settings.testutils.shadow.ShadowDynamicIndexableContentMonitor;
import com.android.settings.testutils.shadow.ShadowEventLogWriter;
import com.android.settings.widget.LoadingViewController;
import com.android.settingslib.applications.ApplicationsState;
import com.android.settingslib.applications.ApplicationsState.Callbacks;
import com.android.settingslib.core.lifecycle.Lifecycle;
import java.util.ArrayList;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -53,10 +51,11 @@ import org.robolectric.annotation.Config;
import org.robolectric.fakes.RoboMenuItem;
import org.robolectric.util.ReflectionHelpers;
import java.util.ArrayList;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
@@ -170,12 +169,12 @@ public class ManageApplicationsTest {
ReflectionHelpers.setField(fragment, "mLoadingContainer", mock(View.class));
ReflectionHelpers.setField(fragment, "mListContainer", mock(View.class));
when(fragment.getActivity()).thenReturn(mock(Activity.class));
final Runnable showLoadingContainerRunnable = mock(Runnable.class);
final Handler handler = mock(Handler.class);
final ManageApplications.ApplicationsAdapter adapter =
spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0));
ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable",
showLoadingContainerRunnable);
final LoadingViewController loadingViewController =
mock(LoadingViewController.class);
ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController);
ReflectionHelpers.setField(adapter, "mFgHandler", handler);
// app loading completed
@@ -186,7 +185,7 @@ public class ManageApplicationsTest {
adapter.updateLoading();
verify(handler, never()).postDelayed(eq(showLoadingContainerRunnable), anyLong());
verify(loadingViewController, never()).showLoadingViewDelayed();
}
@Test
@@ -195,12 +194,13 @@ public class ManageApplicationsTest {
ReflectionHelpers.setField(fragment, "mLoadingContainer", mock(View.class));
ReflectionHelpers.setField(fragment, "mListContainer", mock(View.class));
when(fragment.getActivity()).thenReturn(mock(Activity.class));
final Runnable showLoadingContainerRunnable = mock(Runnable.class);
final Handler handler = mock(Handler.class);
final ManageApplications.ApplicationsAdapter adapter =
spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0));
ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable",
showLoadingContainerRunnable);
final LoadingViewController loadingViewController =
mock(LoadingViewController.class);
ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController);
ReflectionHelpers.setField(adapter, "mFgHandler", handler);
// app loading not yet completed
@@ -208,11 +208,11 @@ public class ManageApplicationsTest {
adapter.updateLoading();
verify(handler).postDelayed(eq(showLoadingContainerRunnable), anyLong());
verify(loadingViewController).showLoadingViewDelayed();
}
@Test
public void onRebuildComplete_shouldCancelDelayedRunnable() {
public void onRebuildComplete_shouldHideLoadingView() {
final Context context = RuntimeEnvironment.application;
final ManageApplications fragment = mock(ManageApplications.class);
final View loadingContainer = mock(View.class);
@@ -223,12 +223,12 @@ public class ManageApplicationsTest {
ReflectionHelpers.setField(fragment, "mLoadingContainer", loadingContainer);
ReflectionHelpers.setField(fragment, "mListContainer", listContainer);
when(fragment.getActivity()).thenReturn(mock(Activity.class));
final Runnable showLoadingContainerRunnable = mock(Runnable.class);
final Handler handler = mock(Handler.class);
final ManageApplications.ApplicationsAdapter adapter =
spy(new ManageApplications.ApplicationsAdapter(mState, fragment, 0));
ReflectionHelpers.setField(adapter, "mShowLoadingContainerRunnable",
showLoadingContainerRunnable);
final LoadingViewController loadingViewController =
mock(LoadingViewController.class);
ReflectionHelpers.setField(adapter, "mLoadingViewController", loadingViewController);
ReflectionHelpers.setField(adapter, "mFgHandler", handler);
ReflectionHelpers.setField(adapter, "mFilterMode", -1);
@@ -244,7 +244,7 @@ public class ManageApplicationsTest {
adapter.onRebuildComplete(null);
verify(handler).removeCallbacks(showLoadingContainerRunnable);
verify(loadingViewController).showContent(true /* animate */);
}
private void setUpOptionMenus() {

View File

@@ -0,0 +1,74 @@
/*
* Copyright (C) 2017 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.settings.widget;
import android.content.Context;
import android.os.Handler;
import android.view.View;
import com.android.settings.TestConfig;
import com.android.settings.testutils.SettingsRobolectricTestRunner;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyLong;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
@RunWith(SettingsRobolectricTestRunner.class)
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
public class LoadingViewControllerTest {
private Context mContext;
private View mLoadingView;
private View mContentView;
private LoadingViewController mController;
@Before
public void setUp() {
mContext = RuntimeEnvironment.application;
mLoadingView = new View(mContext);
mContentView = new View(mContext);
mController = new LoadingViewController(mLoadingView, mContentView);
}
@Test
public void showContent_shouldSetContentVisible() {
mController.showContent(false /* animate */);
assertThat(mContentView.getVisibility()).isEqualTo(View.VISIBLE);
}
@Test
public void showLoadingViewDelayed_shouldPostRunnable() {
final Handler handler = mock(Handler.class);
ReflectionHelpers.setField(mController, "mFgHandler", handler);
mController.showLoadingViewDelayed();
verify(handler).postDelayed(any(Runnable.class), anyLong());
}
}