People: Action bar tab refactoring
- Now ActionBarAdapter manages the action bar tabs.
- Now ActionBarAdapter.setCurrentTab() should always be used to select
a tab programmatically, rather than directly calling
ActionBar.setSelectedNavigationItem().
- Do not re-create/re-initialize ActionBarAdapter/ActionBar for new
intents.
- Simplify fragment visibility update logic in PeopleActivity.
- Do not clear ActionBarAdapter.Listener in PeopleActivity.onPause;
do this only in onDestroy.
Activity is paused when we're processing onNewIntent(), but we still want to
get callbacks during this.
Change-Id: I93ec35e569e1854923503734693b6404cff92f50
diff --git a/src/com/android/contacts/ContactsActivity.java b/src/com/android/contacts/ContactsActivity.java
index 1414f80..020d135 100644
--- a/src/com/android/contacts/ContactsActivity.java
+++ b/src/com/android/contacts/ContactsActivity.java
@@ -21,6 +21,7 @@
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
+import android.app.FragmentTransaction;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -117,4 +118,12 @@
}
return result;
}
+
+ protected static void showFragment(FragmentTransaction ft, Fragment f) {
+ if ((f != null) && f.isHidden()) ft.show(f);
+ }
+
+ protected static void hideFragment(FragmentTransaction ft, Fragment f) {
+ if ((f != null) && !f.isHidden()) ft.hide(f);
+ }
}
diff --git a/src/com/android/contacts/activities/ActionBarAdapter.java b/src/com/android/contacts/activities/ActionBarAdapter.java
index 3a183d2..dc37d3e 100644
--- a/src/com/android/contacts/activities/ActionBarAdapter.java
+++ b/src/com/android/contacts/activities/ActionBarAdapter.java
@@ -22,7 +22,10 @@
import android.app.ActionBar;
import android.app.ActionBar.LayoutParams;
+import android.app.ActionBar.Tab;
+import android.app.FragmentTransaction;
import android.content.Context;
+import android.content.res.TypedArray;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
@@ -30,6 +33,7 @@
import android.widget.SearchView;
import android.widget.SearchView.OnCloseListener;
import android.widget.SearchView.OnQueryTextListener;
+import android.widget.TabHost.OnTabChangeListener;
/**
* Adapter for the action bar at the top of the Contacts activity.
@@ -42,6 +46,12 @@
}
void onAction(Action action);
+
+ /**
+ * Called when the user selects a tab. The new tab can be obtained using
+ * {@link #getCurrentTab}.
+ */
+ void onSelectedTabChanged();
}
private static final String EXTRA_KEY_SEARCH_MODE = "navBar.searchMode";
@@ -59,27 +69,33 @@
private Listener mListener;
- private ActionBar mActionBar;
+ private final ActionBar mActionBar;
+ private final MyTabListener mTabListener = new MyTabListener();
+ public enum TabState {
+ FAVORITES, ALL, GROUPS;
- public ActionBarAdapter(Context context, Listener listener) {
- mContext = context;
- mListener = listener;
- mSearchLabelText = mContext.getString(R.string.search_label);
- mAlwaysShowSearchView = mContext.getResources().getBoolean(R.bool.always_show_search_view);
+ public static TabState fromInt(int value) {
+ switch (value) {
+ case 0:
+ return FAVORITES;
+ case 1:
+ return ALL;
+ case 2:
+ return GROUPS;
+ }
+ throw new IllegalArgumentException("Invalid value: " + value);
+ }
}
- public void onCreate(Bundle savedState, ContactsRequest request, ActionBar actionBar) {
- mActionBar = actionBar;
- mQueryString = null;
+ private TabState mCurrentTab = TabState.FAVORITES;
- if (savedState != null) {
- mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
- mQueryString = savedState.getString(EXTRA_KEY_QUERY);
- } else {
- mSearchMode = request.isSearchMode();
- mQueryString = request.getQueryString();
- }
+ public ActionBarAdapter(Context context, Listener listener, ActionBar actionBar) {
+ mContext = context;
+ mListener = listener;
+ mActionBar = actionBar;
+ mSearchLabelText = mContext.getString(R.string.search_label);
+ mAlwaysShowSearchView = mContext.getResources().getBoolean(R.bool.always_show_search_view);
// Set up search view.
View customSearchView = LayoutInflater.from(mContext).inflate(R.layout.custom_action_bar,
@@ -97,6 +113,30 @@
mSearchView.setQuery(mQueryString, false);
mActionBar.setCustomView(customSearchView, layoutParams);
+ mActionBar.setDisplayShowTitleEnabled(true);
+
+ // TODO Just use a boolean resource instead of styles.
+ TypedArray array = mContext.obtainStyledAttributes(null, R.styleable.ActionBarHomeIcon);
+ boolean showHomeIcon = array.getBoolean(R.styleable.ActionBarHomeIcon_show_home_icon, true);
+ array.recycle();
+ mActionBar.setDisplayShowHomeEnabled(showHomeIcon);
+
+ addTab(TabState.FAVORITES, mContext.getString(R.string.contactsFavoritesLabel));
+ addTab(TabState.ALL, mContext.getString(R.string.contactsAllLabel));
+ addTab(TabState.GROUPS, mContext.getString(R.string.contactsGroupsLabel));
+ }
+
+ public void initialize(Bundle savedState, ContactsRequest request) {
+ if (savedState == null) {
+ mSearchMode = request.isSearchMode();
+ mQueryString = request.getQueryString();
+ } else {
+ mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
+ mQueryString = savedState.getString(EXTRA_KEY_QUERY);
+
+ // Just set to the field here. The listener will be notified by update().
+ mCurrentTab = TabState.fromInt(savedState.getInt(EXTRA_KEY_SELECTED_TAB));
+ }
update();
}
@@ -104,6 +144,55 @@
mListener = listener;
}
+ private void addTab(TabState tabState, String text) {
+ final Tab tab = mActionBar.newTab();
+ tab.setTag(tabState);
+ tab.setText(text);
+ tab.setTabListener(mTabListener);
+ mActionBar.addTab(tab);
+ }
+
+ private class MyTabListener implements ActionBar.TabListener {
+ /**
+ * If true, it won't call {@link #setCurrentTab} in {@link #onTabSelected}.
+ * This flag is used when we want to programmatically update the current tab without
+ * {@link #onTabSelected} getting called.
+ */
+ public boolean mIgnoreTabSelected;
+
+ @Override public void onTabReselected(Tab tab, FragmentTransaction ft) { }
+ @Override public void onTabUnselected(Tab tab, FragmentTransaction ft) { }
+
+ @Override public void onTabSelected(Tab tab, FragmentTransaction ft) {
+ if (!mIgnoreTabSelected) {
+ setCurrentTab((TabState)tab.getTag());
+ }
+ }
+ }
+
+ /**
+ * Change the current tab, and notify the listener.
+ */
+ public void setCurrentTab(TabState tab) {
+ if (tab == null) throw new NullPointerException();
+ if (tab == mCurrentTab) {
+ return;
+ }
+ mCurrentTab = tab;
+
+ int index = mCurrentTab.ordinal();
+ if ((mActionBar.getNavigationMode() == ActionBar.NAVIGATION_MODE_TABS)
+ && (index != mActionBar.getSelectedNavigationIndex())) {
+ mActionBar.setSelectedNavigationItem(index);
+ }
+
+ if (mListener != null) mListener.onSelectedTabChanged();
+ }
+
+ public TabState getCurrentTab() {
+ return mCurrentTab;
+ }
+
public boolean isSearchMode() {
return mSearchMode;
}
@@ -134,7 +223,7 @@
}
}
- public void update() {
+ private void update() {
if (mSearchMode) {
mActionBar.setDisplayShowCustomEnabled(true);
if (mAlwaysShowSearchView) {
@@ -144,16 +233,32 @@
// Phone -- search view gets focus
setFocusOnSearchView();
}
- mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_STANDARD) {
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ }
if (mListener != null) {
mListener.onAction(Action.START_SEARCH_MODE);
}
} else {
mActionBar.setDisplayShowCustomEnabled(mAlwaysShowSearchView);
- mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ if (mActionBar.getNavigationMode() != ActionBar.NAVIGATION_MODE_TABS) {
+ // setNavigationMode will trigger onTabSelected() with the tab which was previously
+ // selected.
+ // The issue is that when we're first switching to the tab navigation mode after
+ // screen orientation changes, onTabSelected() will get called with the first tab
+ // (i.e. favorite), which would results in mCurrentTab getting set to FAVORITES and
+ // we'd lose restored tab.
+ // So let's just disable the callback here temporarily. We'll notify the listener
+ // after this anyway.
+ mTabListener.mIgnoreTabSelected = true;
+ mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
+ mActionBar.setSelectedNavigationItem(mCurrentTab.ordinal());
+ mTabListener.mIgnoreTabSelected = false;
+ }
mActionBar.setTitle(null);
if (mListener != null) {
mListener.onAction(Action.STOP_SEARCH_MODE);
+ mListener.onSelectedTabChanged();
}
}
}
@@ -192,16 +297,7 @@
public void onSaveInstanceState(Bundle outState) {
outState.putBoolean(EXTRA_KEY_SEARCH_MODE, mSearchMode);
outState.putString(EXTRA_KEY_QUERY, mQueryString);
- outState.putInt(EXTRA_KEY_SELECTED_TAB, mActionBar.getSelectedNavigationIndex());
- }
-
- public void onRestoreInstanceState(Bundle savedState) {
- mSearchMode = savedState.getBoolean(EXTRA_KEY_SEARCH_MODE);
- mQueryString = savedState.getString(EXTRA_KEY_QUERY);
- int selectedTab = savedState.getInt(EXTRA_KEY_SELECTED_TAB);
- if (selectedTab >= 0) {
- mActionBar.setSelectedNavigationItem(selectedTab);
- }
+ outState.putInt(EXTRA_KEY_SELECTED_TAB, mCurrentTab.ordinal());
}
private void setFocusOnSearchView() {
diff --git a/src/com/android/contacts/activities/PeopleActivity.java b/src/com/android/contacts/activities/PeopleActivity.java
index 2445a1f..5c610ef 100644
--- a/src/com/android/contacts/activities/PeopleActivity.java
+++ b/src/com/android/contacts/activities/PeopleActivity.java
@@ -20,6 +20,7 @@
import com.android.contacts.ContactSaveService;
import com.android.contacts.ContactsActivity;
import com.android.contacts.R;
+import com.android.contacts.activities.ActionBarAdapter.TabState;
import com.android.contacts.detail.ContactDetailFragment;
import com.android.contacts.detail.ContactDetailLayoutController;
import com.android.contacts.detail.ContactDetailTabCarousel;
@@ -60,8 +61,6 @@
import android.accounts.Account;
import android.app.ActionBar;
-import android.app.ActionBar.Tab;
-import android.app.ActionBar.TabListener;
import android.app.Activity;
import android.app.Fragment;
import android.app.FragmentManager;
@@ -69,7 +68,6 @@
import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.Intent;
-import android.content.res.TypedArray;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -167,35 +165,6 @@
private final Handler mHandler = new Handler();
- /**
- * TODO: Use ViewPager so that tabs can be swiped left and right. Figure out how to use the
- * support library in our app.
- */
- private final TabListener mTabListener = new TabListener() {
- @Override
- public void onTabUnselected(Tab tab, FragmentTransaction ft) {
- hideFragmentOnTabUnselect((TabState) tab.getTag(), ft);
- }
-
- @Override
- public void onTabSelected(Tab tab, FragmentTransaction ft) {
- final TabState tabState = (TabState) tab.getTag();
- setSelectedTab(tabState);
- showFragmentOnTabSelect(tabState, ft);
- invalidateOptionsMenu();
- }
-
- @Override
- public void onTabReselected(Tab tab, FragmentTransaction ft) {
- }
- };
-
- private enum TabState {
- FAVORITES, ALL, GROUPS
- }
-
- private TabState mSelectedTab;
-
public PeopleActivity() {
mIntentResolver = new ContactsIntentResolver(this);
mContactListFilterController = new ContactListFilterController(this);
@@ -330,9 +299,11 @@
}
setTitle(mRequest.getActivityTitle());
- ActionBar actionBar = getActionBar();
- mActionBarAdapter = new ActionBarAdapter(this, this);
- mActionBarAdapter.onCreate(savedState, mRequest, getActionBar());
+ if (createContentView) {
+ mActionBarAdapter = new ActionBarAdapter(this, this, getActionBar());
+ }
+ mActionBarAdapter.initialize(savedState, mRequest);
+
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
ContactDetailTabCarousel tabCarousel = (ContactDetailTabCarousel)
@@ -342,120 +313,15 @@
mContactDetailFragmentListener);
if (createContentView) {
- actionBar.removeAllTabs();
- Tab favoritesTab = actionBar.newTab();
- favoritesTab.setTag(TabState.FAVORITES);
- favoritesTab.setText(getString(R.string.contactsFavoritesLabel));
- favoritesTab.setTabListener(mTabListener);
- actionBar.addTab(favoritesTab);
-
- Tab allTab = actionBar.newTab();
- allTab.setTag(TabState.ALL);
- allTab.setText(getString(R.string.contactsAllLabel));
- allTab.setTabListener(mTabListener);
- actionBar.addTab(allTab);
-
- Tab groupsTab = actionBar.newTab();
- groupsTab.setTag(TabState.GROUPS);
- groupsTab.setText(getString(R.string.contactsGroupsLabel));
- groupsTab.setTabListener(mTabListener);
- actionBar.addTab(groupsTab);
- actionBar.setDisplayShowTitleEnabled(true);
-
- TypedArray a = obtainStyledAttributes(null, R.styleable.ActionBarHomeIcon);
- boolean showHomeIcon = a.getBoolean(R.styleable.ActionBarHomeIcon_show_home_icon, true);
- actionBar.setDisplayShowHomeEnabled(showHomeIcon);
-
+ // TODO Is the createContentView test really necessary?
invalidateOptionsMenuIfNeeded();
}
configureFragments(savedState == null);
}
- private void hideFragmentOnTabUnselect(TabState newTabState, FragmentTransaction ft) {
- switch (newTabState) {
- case FAVORITES: {
- ft.hide(mFavoritesFragment);
- if (mFrequentFragment != null) {
- ft.hide(mFrequentFragment);
- }
- break;
- }
- case ALL: {
- ft.hide(mAllFragment);
- if (mContactDetailFragment != null) {
- ft.hide(mContactDetailFragment);
- }
- break;
- }
- case GROUPS: {
- ft.hide(mGroupsFragment);
- if (mGroupDetailFragment != null) {
- ft.hide(mGroupDetailFragment);
- }
- break;
- }
- default: {
- throw new IllegalStateException("Unexpected tab state: " + newTabState);
- }
- }
- }
-
- private void showFragmentOnTabSelect(TabState newTabState, FragmentTransaction ft) {
- switch (newTabState) {
- case FAVORITES: {
- ft.show(mFavoritesFragment);
- if (mFrequentFragment != null) {
- ft.show(mFrequentFragment);
- }
- break;
- }
- case ALL: {
- ft.show(mAllFragment);
- if (mContactDetailFragment != null) {
- ft.show(mContactDetailFragment);
- }
- break;
- }
- case GROUPS: {
- ft.show(mGroupsFragment);
- if (mGroupDetailFragment != null) {
- ft.show(mGroupDetailFragment);
- }
- break;
- }
- default: {
- throw new IllegalStateException("Unexpected tab state: " + newTabState);
- }
- }
- }
-
- private void setSelectedTab(TabState tab) {
- mSelectedTab = tab;
-
- if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
- switch (mSelectedTab) {
- case FAVORITES:
- mFavoritesView.setVisibility(View.VISIBLE);
- mBrowserView.setVisibility(View.GONE);
- mDetailsView.setVisibility(View.GONE);
- break;
- case GROUPS:
- case ALL:
- mFavoritesView.setVisibility(View.GONE);
- mBrowserView.setVisibility(View.VISIBLE);
- mDetailsView.setVisibility(View.VISIBLE);
- break;
- }
- }
- }
-
@Override
protected void onPause() {
- if (mActionBarAdapter != null) {
- mActionBarAdapter.setListener(null);
- }
-
mOptionsMenuContactsAvailable = false;
mProviderStatus = -1;
@@ -466,11 +332,12 @@
@Override
protected void onResume() {
super.onResume();
- if (mActionBarAdapter != null) {
- mActionBarAdapter.setListener(this);
- }
mProviderStatusLoader.setProviderStatusListener(this);
updateFragmentVisibility();
+
+ // Re-register the listener, which may have been cleared when onSaveInstanceState was
+ // called. See also: onSaveInstanceState
+ mActionBarAdapter.setListener(this);
}
@Override
@@ -479,6 +346,12 @@
super.onStart();
}
+ @Override
+ protected void onDestroy() {
+ mActionBarAdapter.setListener(null);
+ super.onDestroy();
+ }
+
private void configureFragments(boolean fromRequest) {
if (fromRequest) {
ContactListFilter filter = null;
@@ -503,7 +376,7 @@
break;
case ContactsRequest.ACTION_VIEW_CONTACT:
if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
- getActionBar().setSelectedNavigationItem(TabState.ALL.ordinal());
+ mActionBarAdapter.setCurrentTab(TabState.ALL);
}
}
@@ -562,33 +435,12 @@
public void onAction(Action action) {
switch (action) {
case START_SEARCH_MODE:
- // Checking if multi fragments are being displayed
- if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
- mFavoritesView.setVisibility(View.GONE);
- mBrowserView.setVisibility(View.VISIBLE);
- mDetailsView.setVisibility(View.VISIBLE);
- }
- // Bring the contact list fragment (and detail fragment if applicable) to the front
- FragmentTransaction ft = getFragmentManager().beginTransaction();
- ft.show(mAllFragment);
- if (mContactDetailFragment != null) ft.show(mContactDetailFragment);
- ft.commit();
clearSearch();
+ updateFragmentsVisibility();
break;
case STOP_SEARCH_MODE:
- // Refresh the fragments because search mode was using them to display search
- // results.
clearSearch();
-
- // If the last selected tab was not the "All contacts" tab, then hide these
- // fragments because we need to show favorites or groups.
- if (mSelectedTab != null && !mSelectedTab.equals(TabState.ALL)) {
- FragmentTransaction transaction = getFragmentManager().beginTransaction();
- transaction.hide(mAllFragment);
- if (mContactDetailFragment != null) transaction.hide(mContactDetailFragment);
- transaction.commit();
- }
- if (mSelectedTab != null) setSelectedTab(mSelectedTab);
+ updateFragmentsVisibility();
break;
case CHANGE_SEARCH_QUERY:
loadSearch(mActionBarAdapter.getQueryString());
@@ -598,6 +450,72 @@
}
}
+ @Override
+ public void onSelectedTabChanged() {
+ updateFragmentsVisibility();
+ }
+
+ /**
+ * Updates the fragment/view visibility according to the current mode, such as
+ * {@link ActionBarAdapter#isSearchMode()} and {@link ActionBarAdapter#getCurrentTab()}.
+ */
+ private void updateFragmentsVisibility() {
+ TabState tab = mActionBarAdapter.getCurrentTab();
+
+ // If in search mode, we use the all list + contact details to show the result.
+ if (mActionBarAdapter.isSearchMode()) {
+ tab = TabState.ALL;
+ }
+ if (PhoneCapabilityTester.isUsingTwoPanes(this)) {
+ switch (tab) {
+ case FAVORITES:
+ mFavoritesView.setVisibility(View.VISIBLE);
+ mBrowserView.setVisibility(View.GONE);
+ mDetailsView.setVisibility(View.GONE);
+ break;
+ case GROUPS:
+ case ALL:
+ mFavoritesView.setVisibility(View.GONE);
+ mBrowserView.setVisibility(View.VISIBLE);
+ mDetailsView.setVisibility(View.VISIBLE);
+ break;
+ }
+ }
+ FragmentManager fragmentManager = getFragmentManager();
+ FragmentTransaction ft = fragmentManager.beginTransaction();
+
+ switch (tab) {
+ case FAVORITES:
+ showFragment(ft, mFavoritesFragment);
+ showFragment(ft, mFrequentFragment);
+ hideFragment(ft, mAllFragment);
+ hideFragment(ft, mContactDetailFragment);
+ hideFragment(ft, mGroupsFragment);
+ hideFragment(ft, mGroupDetailFragment);
+ break;
+ case ALL:
+ hideFragment(ft, mFavoritesFragment);
+ hideFragment(ft, mFrequentFragment);
+ showFragment(ft, mAllFragment);
+ showFragment(ft, mContactDetailFragment);
+ hideFragment(ft, mGroupsFragment);
+ hideFragment(ft, mGroupDetailFragment);
+ break;
+ case GROUPS:
+ hideFragment(ft, mFavoritesFragment);
+ hideFragment(ft, mFrequentFragment);
+ hideFragment(ft, mAllFragment);
+ hideFragment(ft, mContactDetailFragment);
+ showFragment(ft, mGroupsFragment);
+ showFragment(ft, mGroupDetailFragment);
+ break;
+ }
+ if (!ft.isEmpty()) {
+ ft.commit();
+ fragmentManager.executePendingTransactions();
+ }
+ }
+
private void clearSearch() {
loadSearch("");
}
@@ -1034,7 +952,7 @@
searchMenu.setVisible(false); // Don't show the search menu in search mode.
}
} else {
- switch (mSelectedTab) {
+ switch (mActionBarAdapter.getCurrentTab()) {
case FAVORITES:
// TODO: Fall through until we determine what the menu items should be for
// this tab
@@ -1283,21 +1201,21 @@
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
- if (mActionBarAdapter != null) {
- mActionBarAdapter.onSaveInstanceState(outState);
- }
+ mActionBarAdapter.onSaveInstanceState(outState);
if (mContactDetailLayoutController != null) {
mContactDetailLayoutController.onSaveInstanceState(outState);
}
+
+ // Clear the listener to make sure we don't get callbacks after onSaveInstanceState,
+ // in order to avoid doing fragment transactions after it.
+ // TODO Figure out a better way to deal with the issue.
+ mActionBarAdapter.setListener(null);
}
@Override
protected void onRestoreInstanceState(Bundle inState) {
super.onRestoreInstanceState(inState);
mSearchMode = inState.getBoolean(KEY_SEARCH_MODE);
- if (mActionBarAdapter != null) {
- mActionBarAdapter.onRestoreInstanceState(inState);
- }
if (mContactDetailLayoutController != null) {
mContactDetailLayoutController.onRestoreInstanceState(inState);
}