ActionBar tab mode using fragments; needs styles.
Change-Id: I8f931dae447e7b64de827d3c17766e5f8ad794e2
diff --git a/core/java/android/app/ActionBar.java b/core/java/android/app/ActionBar.java
index 67133e0..d0b3ac4 100644
--- a/core/java/android/app/ActionBar.java
+++ b/core/java/android/app/ActionBar.java
@@ -206,9 +206,11 @@
*
* @return The current navigation mode.
*
+ * @see #setStandardNavigationMode()
* @see #setStandardNavigationMode(CharSequence)
* @see #setStandardNavigationMode(CharSequence, CharSequence)
* @see #setDropdownNavigationMode(SpinnerAdapter)
+ * @see #setTabNavigationMode()
* @see #setCustomNavigationMode(View)
*/
public abstract int getNavigationMode();
@@ -217,10 +219,98 @@
* @return The current set of display options.
*/
public abstract int getDisplayOptions();
-
+
+ /**
+ * Start a context mode controlled by <code>callback</code>.
+ * The {@link ContextModeCallback} will receive lifecycle events for the duration
+ * of the context mode.
+ *
+ * @param callback Callback handler that will manage this context mode.
+ */
public abstract void startContextMode(ContextModeCallback callback);
+
+ /**
+ * Finish the current context mode.
+ */
public abstract void finishContextMode();
-
+
+ /**
+ * Set the action bar into tabbed navigation mode.
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ * @see #removeTab(Tab)
+ * @see #removeTabAt(int)
+ */
+ public abstract void setTabNavigationMode();
+
+ /**
+ * Set the action bar into tabbed navigation mode.
+ *
+ * @param containerViewId Id of the container view where tab content fragments should appear.
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ * @see #removeTab(Tab)
+ * @see #removeTabAt(int)
+ */
+ public abstract void setTabNavigationMode(int containerViewId);
+
+ /**
+ * Create and return a new {@link Tab}.
+ * This tab will not be included in the action bar until it is added.
+ *
+ * @return A new Tab
+ *
+ * @see #addTab(Tab)
+ * @see #insertTab(Tab, int)
+ */
+ public abstract Tab newTab();
+
+ /**
+ * Add a tab for use in tabbed navigation mode. The tab will be added at the end of the list.
+ *
+ * @param tab Tab to add
+ */
+ public abstract void addTab(Tab tab);
+
+ /**
+ * Insert a tab for use in tabbed navigation mode. The tab will be inserted at
+ * <code>position</code>.
+ *
+ * @param tab The tab to add
+ * @param position The new position of the tab
+ */
+ public abstract void insertTab(Tab tab, int position);
+
+ /**
+ * Remove a tab from the action bar.
+ *
+ * @param tab The tab to remove
+ */
+ public abstract void removeTab(Tab tab);
+
+ /**
+ * Remove a tab from the action bar.
+ *
+ * @param position Position of the tab to remove
+ */
+ public abstract void removeTabAt(int position);
+
+ /**
+ * Select the specified tab. If it is not a child of this action bar it will be added.
+ *
+ * @param tab Tab to select
+ */
+ public abstract void selectTab(Tab tab);
+
+ /**
+ * Select the tab at <code>position</code>
+ *
+ * @param position Position of the tab to select
+ */
+ public abstract void selectTabAt(int position);
+
/**
* Represents a contextual mode of the Action Bar. Context modes can be used for
* modal interactions with activity content and replace the normal Action Bar until finished.
@@ -350,4 +440,74 @@
*/
public boolean onNavigationItemSelected(int itemPosition, long itemId);
}
+
+ /**
+ * A tab in the action bar.
+ *
+ * <p>Tabs manage the hiding and showing of {@link Fragment}s.
+ */
+ public static abstract class Tab {
+ /**
+ * An invalid position for a tab.
+ *
+ * @see #getPosition()
+ */
+ public static final int INVALID_POSITION = -1;
+
+ /**
+ * Return the current position of this tab in the action bar.
+ *
+ * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
+ * the action bar.
+ */
+ public abstract int getPosition();
+
+ /**
+ * Return the icon associated with this tab.
+ *
+ * @return The tab's icon
+ */
+ public abstract Drawable getIcon();
+
+ /**
+ * Return the text of this tab.
+ *
+ * @return The tab's text
+ */
+ public abstract CharSequence getText();
+
+ /**
+ * Set the icon displayed on this tab.
+ *
+ * @param icon The drawable to use as an icon
+ */
+ public abstract void setIcon(Drawable icon);
+
+ /**
+ * Set the text displayed on this tab. Text may be truncated if there is not
+ * room to display the entire string.
+ *
+ * @param text The text to display
+ */
+ public abstract void setText(CharSequence text);
+
+ /**
+ * Returns the fragment that will be shown when this tab is selected.
+ *
+ * @return Fragment associated with this tab
+ */
+ public abstract Fragment getFragment();
+
+ /**
+ * Set the fragment that will be shown when this tab is selected.
+ *
+ * @param fragment Fragment to associate with this tab
+ */
+ public abstract void setFragment(Fragment fragment);
+
+ /**
+ * Select this tab. Only valid if the tab has been added to the action bar.
+ */
+ public abstract void select();
+ }
}
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 8825b8c..4166b89 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -1676,11 +1676,11 @@
*/
private void initActionBar() {
Window window = getWindow();
- if (!window.hasFeature(Window.FEATURE_ACTION_BAR)) {
+ if (!window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) {
return;
}
- mActionBar = new ActionBarImpl(getWindow().getDecorView());
+ mActionBar = new ActionBarImpl(this);
}
/**
diff --git a/core/java/com/android/internal/app/ActionBarImpl.java b/core/java/com/android/internal/app/ActionBarImpl.java
index 6ac68aa..6cf455c 100644
--- a/core/java/com/android/internal/app/ActionBarImpl.java
+++ b/core/java/com/android/internal/app/ActionBarImpl.java
@@ -22,6 +22,9 @@
import com.android.internal.widget.ActionBarView;
import android.app.ActionBar;
+import android.app.Activity;
+import android.app.Fragment;
+import android.app.FragmentTransaction;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.Menu;
@@ -31,6 +34,8 @@
import android.widget.SpinnerAdapter;
import android.widget.ViewAnimator;
+import java.util.ArrayList;
+
/**
* ActionBarImpl is the ActionBar implementation used
* by devices of all screen sizes. If it detects a compatible decor,
@@ -42,10 +47,21 @@
private static final int NORMAL_VIEW = 0;
private static final int CONTEXT_VIEW = 1;
+ private static final int TAB_SWITCH_SHOW_HIDE = 0;
+ private static final int TAB_SWITCH_ADD_REMOVE = 1;
+
+ private Activity mActivity;
+
private ViewAnimator mAnimatorView;
private ActionBarView mActionView;
private ActionBarContextView mUpperContextView;
private LinearLayout mLowerContextView;
+
+ private ArrayList<TabImpl> mTabs = new ArrayList<TabImpl>();
+
+ private int mTabContainerViewId = android.R.id.content;
+ private TabImpl mSelectedTab;
+ private int mTabSwitchMode = TAB_SWITCH_ADD_REMOVE;
private ContextMode mContextMode;
@@ -64,7 +80,9 @@
}
};
- public ActionBarImpl(View decor) {
+ public ActionBarImpl(Activity activity) {
+ final View decor = activity.getWindow().getDecorView();
+ mActivity = activity;
mActionView = (ActionBarView) decor.findViewById(com.android.internal.R.id.action_bar);
mUpperContextView = (ActionBarContextView) decor.findViewById(
com.android.internal.R.id.action_context_bar);
@@ -83,32 +101,54 @@
}
public void setCustomNavigationMode(View view) {
+ cleanupTabs();
mActionView.setCustomNavigationView(view);
mActionView.setCallback(null);
}
public void setDropdownNavigationMode(SpinnerAdapter adapter, NavigationCallback callback) {
+ cleanupTabs();
mActionView.setCallback(callback);
mActionView.setNavigationMode(NAVIGATION_MODE_DROPDOWN_LIST);
mActionView.setDropdownAdapter(adapter);
}
public void setStandardNavigationMode() {
+ cleanupTabs();
mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD);
mActionView.setCallback(null);
}
public void setStandardNavigationMode(CharSequence title) {
+ cleanupTabs();
setStandardNavigationMode(title, null);
}
public void setStandardNavigationMode(CharSequence title, CharSequence subtitle) {
+ cleanupTabs();
mActionView.setNavigationMode(NAVIGATION_MODE_STANDARD);
mActionView.setTitle(title);
mActionView.setSubtitle(subtitle);
mActionView.setCallback(null);
}
+ private void cleanupTabs() {
+ if (mSelectedTab != null) {
+ selectTab(null);
+ }
+ if (!mTabs.isEmpty()) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ final int tabCount = mTabs.size();
+ for (int i = 0; i < tabCount; i++) {
+ trans.remove(mTabs.get(i).getFragment());
+ }
+ trans.commit();
+ }
+ mTabs.clear();
+ }
+ }
+
public void setTitle(CharSequence title) {
mActionView.setTitle(title);
}
@@ -174,6 +214,113 @@
}
}
+ private void configureTab(Tab tab, int position) {
+ final TabImpl tabi = (TabImpl) tab;
+ final boolean isFirstTab = mTabs.isEmpty();
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ final Fragment frag = tabi.getFragment();
+
+ tabi.setPosition(position);
+ mTabs.add(position, tabi);
+
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ if (!frag.isAdded()) {
+ trans.add(mTabContainerViewId, frag);
+ }
+ }
+
+ if (isFirstTab) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.show(frag);
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.add(mTabContainerViewId, frag);
+ }
+ mSelectedTab = tabi;
+ } else {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.hide(frag);
+ }
+ }
+ trans.commit();
+ }
+
+ @Override
+ public void addTab(Tab tab) {
+ mActionView.addTab(tab);
+ configureTab(tab, mTabs.size());
+ }
+
+ @Override
+ public void insertTab(Tab tab, int position) {
+ mActionView.insertTab(tab, position);
+ configureTab(tab, position);
+ }
+
+ @Override
+ public Tab newTab() {
+ return new TabImpl();
+ }
+
+ @Override
+ public void removeTab(Tab tab) {
+ removeTabAt(tab.getPosition());
+ }
+
+ @Override
+ public void removeTabAt(int position) {
+ mActionView.removeTabAt(position);
+ mTabs.remove(position);
+
+ final int newTabCount = mTabs.size();
+ for (int i = position; i < newTabCount; i++) {
+ mTabs.get(i).setPosition(i);
+ }
+
+ selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
+ }
+
+ @Override
+ public void setTabNavigationMode() {
+ mActionView.setNavigationMode(NAVIGATION_MODE_TABS);
+ }
+
+ @Override
+ public void setTabNavigationMode(int containerViewId) {
+ mTabContainerViewId = containerViewId;
+ setTabNavigationMode();
+ }
+
+ @Override
+ public void selectTab(Tab tab) {
+ if (mSelectedTab == tab) {
+ return;
+ }
+
+ mActionView.setTabSelected(tab != null ? tab.getPosition() : Tab.INVALID_POSITION);
+ final FragmentTransaction trans = mActivity.openFragmentTransaction();
+ if (mSelectedTab != null) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.hide(mSelectedTab.getFragment());
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.remove(mSelectedTab.getFragment());
+ }
+ }
+ if (tab != null) {
+ if (mTabSwitchMode == TAB_SWITCH_SHOW_HIDE) {
+ trans.show(tab.getFragment());
+ } else if (mTabSwitchMode == TAB_SWITCH_ADD_REMOVE) {
+ trans.add(mTabContainerViewId, tab.getFragment());
+ }
+ }
+ mSelectedTab = (TabImpl) tab;
+ trans.commit();
+ }
+
+ @Override
+ public void selectTabAt(int position) {
+ selectTab(mTabs.get(position));
+ }
+
/**
* @hide
*/
@@ -233,6 +380,60 @@
if (!actionItem.invoke()) {
mCallback.onContextItemClicked(this, item);
}
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public class TabImpl extends ActionBar.Tab {
+ private Fragment mFragment;
+ private Drawable mIcon;
+ private CharSequence mText;
+ private int mPosition;
+
+ @Override
+ public Fragment getFragment() {
+ return mFragment;
+ }
+
+ @Override
+ public Drawable getIcon() {
+ return mIcon;
+ }
+
+ @Override
+ public int getPosition() {
+ return mPosition;
+ }
+
+ public void setPosition(int position) {
+ mPosition = position;
+ }
+
+ @Override
+ public CharSequence getText() {
+ return mText;
+ }
+
+ @Override
+ public void setFragment(Fragment fragment) {
+ mFragment = fragment;
+ }
+
+ @Override
+ public void setIcon(Drawable icon) {
+ mIcon = icon;
+ }
+
+ @Override
+ public void setText(CharSequence text) {
+ mText = text;
+ }
+
+ @Override
+ public void select() {
+ selectTab(this);
}
}
}
diff --git a/core/java/com/android/internal/widget/ActionBarView.java b/core/java/com/android/internal/widget/ActionBarView.java
index e919f1b..48707b9 100644
--- a/core/java/com/android/internal/widget/ActionBarView.java
+++ b/core/java/com/android/internal/widget/ActionBarView.java
@@ -31,8 +31,10 @@
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
+import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
+import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
@@ -85,6 +87,7 @@
private TextView mTitleView;
private TextView mSubtitleView;
private Spinner mSpinner;
+ private LinearLayout mTabLayout;
private View mCustomNavView;
private boolean mShowMenu;
@@ -128,7 +131,8 @@
ApplicationInfo info = context.getApplicationInfo();
PackageManager pm = context.getPackageManager();
- mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode, ActionBar.NAVIGATION_MODE_STANDARD);
+ mNavigationMode = a.getInt(R.styleable.ActionBar_navigationMode,
+ ActionBar.NAVIGATION_MODE_STANDARD);
mTitle = a.getText(R.styleable.ActionBar_title);
mSubtitle = a.getText(R.styleable.ActionBar_subtitle);
mDisplayOptions = a.getInt(R.styleable.ActionBar_displayOptions, DISPLAY_DEFAULT);
@@ -271,6 +275,11 @@
mCustomNavView = null;
}
break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ removeView(mTabLayout);
+ mTabLayout = null;
+ }
}
switch (mode) {
@@ -286,6 +295,10 @@
case ActionBar.NAVIGATION_MODE_CUSTOM:
addView(mCustomNavView);
break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ mTabLayout = new LinearLayout(getContext());
+ addView(mTabLayout);
+ break;
}
mNavigationMode = mode;
requestLayout();
@@ -308,6 +321,35 @@
return mDisplayOptions;
}
+ private TabView createTabView(ActionBar.Tab tab) {
+ final TabView tabView = new TabView(getContext(), tab);
+ tabView.setFocusable(true);
+ tabView.setOnClickListener(new TabClickListener());
+ return tabView;
+ }
+
+ public void addTab(ActionBar.Tab tab) {
+ final boolean isFirst = mTabLayout.getChildCount() == 0;
+ final TabView tabView = createTabView(tab);
+ mTabLayout.addView(tabView);
+ if (isFirst) {
+ tabView.setSelected(true);
+ }
+ }
+
+ public void insertTab(ActionBar.Tab tab, int position) {
+ final boolean isFirst = mTabLayout.getChildCount() == 0;
+ final TabView tabView = createTabView(tab);
+ mTabLayout.addView(tabView, position);
+ if (isFirst) {
+ tabView.setSelected(true);
+ }
+ }
+
+ public void removeTabAt(int position) {
+ mTabLayout.removeViewAt(position);
+ }
+
@Override
protected LayoutParams generateDefaultLayoutParams() {
// Used by custom nav views if they don't supply layout params. Everything else
@@ -356,7 +398,7 @@
case ActionBar.NAVIGATION_MODE_TABS:
throw new UnsupportedOperationException(
- "Tab navigation isn't supported yet!");
+ "Inflating tab navigation isn't supported yet!");
case ActionBar.NAVIGATION_MODE_CUSTOM:
if (mCustomNavView != null) {
@@ -381,6 +423,14 @@
addView(mTitleLayout);
}
+ public void setTabSelected(int position) {
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ child.setSelected(i == position);
+ }
+ }
+
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
@@ -442,6 +492,13 @@
MeasureSpec.makeMeasureSpec(customNavHeight, customNavHeightMode));
}
break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ mTabLayout.measure(
+ MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.AT_MOST),
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+ }
+ break;
}
setMeasuredDimension(contentWidth, mContentHeight);
@@ -486,6 +543,10 @@
x += positionChild(mCustomNavView, x, y, contentHeight) + mSpacing;
}
break;
+ case ActionBar.NAVIGATION_MODE_TABS:
+ if (mTabLayout != null) {
+ x += positionChild(mTabLayout, x, y, contentHeight) + mSpacing;
+ }
}
x = r - l - getPaddingRight();
@@ -515,4 +576,59 @@
return childWidth;
}
+
+ private static class TabView extends LinearLayout {
+ private ActionBar.Tab mTab;
+
+ public TabView(Context context, ActionBar.Tab tab) {
+ super(context);
+ mTab = tab;
+
+ // TODO Style tabs based on the theme
+
+ final Drawable icon = tab.getIcon();
+ final CharSequence text = tab.getText();
+
+ if (icon != null) {
+ ImageView iconView = new ImageView(context);
+ iconView.setImageDrawable(icon);
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ iconView.setLayoutParams(lp);
+ addView(iconView);
+ }
+
+ if (text != null) {
+ TextView textView = new TextView(context);
+ textView.setText(text);
+ textView.setSingleLine();
+ textView.setEllipsize(TruncateAt.END);
+ LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT);
+ lp.gravity = Gravity.CENTER_VERTICAL;
+ textView.setLayoutParams(lp);
+ addView(textView);
+ }
+
+ setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT,
+ LayoutParams.MATCH_PARENT, 1));
+ }
+
+ public ActionBar.Tab getTab() {
+ return mTab;
+ }
+ }
+
+ private class TabClickListener implements OnClickListener {
+ public void onClick(View view) {
+ TabView tabView = (TabView) view;
+ tabView.getTab().select();
+ final int tabCount = mTabLayout.getChildCount();
+ for (int i = 0; i < tabCount; i++) {
+ final View child = mTabLayout.getChildAt(i);
+ child.setSelected(child == view);
+ }
+ }
+ }
}