TabHost.java revision 61c9d4b4509aeaab1fcad24fb5a63c874d2fd941
1/*
2 * Copyright (C) 2006 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.widget;
18
19import com.android.internal.R;
20
21import android.app.LocalActivityManager;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.drawable.Drawable;
25import android.os.Build;
26import android.util.AttributeSet;
27import android.view.KeyEvent;
28import android.view.LayoutInflater;
29import android.view.SoundEffectConstants;
30import android.view.View;
31import android.view.ViewGroup;
32import android.view.ViewTreeObserver;
33import android.view.Window;
34
35import java.util.ArrayList;
36import java.util.List;
37
38/**
39 * Container for a tabbed window view. This object holds two children: a set of tab labels that the
40 * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that
41 * page. The individual elements are typically controlled using this container object, rather than
42 * setting values on the child elements themselves.
43 */
44public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener {
45
46    private TabWidget mTabWidget;
47    private FrameLayout mTabContent;
48    private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2);
49    /**
50     * This field should be made private, so it is hidden from the SDK.
51     * {@hide}
52     */
53    protected int mCurrentTab = -1;
54    private View mCurrentView = null;
55    /**
56     * This field should be made private, so it is hidden from the SDK.
57     * {@hide}
58     */
59    protected LocalActivityManager mLocalActivityManager = null;
60    private OnTabChangeListener mOnTabChangeListener;
61    private OnKeyListener mTabKeyListener;
62
63    public TabHost(Context context) {
64        super(context);
65        initTabHost();
66    }
67
68    public TabHost(Context context, AttributeSet attrs) {
69        super(context, attrs);
70        initTabHost();
71    }
72
73    private void initTabHost() {
74        setFocusableInTouchMode(true);
75        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
76
77        mCurrentTab = -1;
78        mCurrentView = null;
79    }
80
81    /**
82     * Get a new {@link TabSpec} associated with this tab host.
83     * @param tag required tag of tab.
84     */
85    public TabSpec newTabSpec(String tag) {
86        return new TabSpec(tag);
87    }
88
89
90
91    /**
92      * <p>Call setup() before adding tabs if loading TabHost using findViewById().
93      * <i><b>However</i></b>: You do not need to call setup() after getTabHost()
94      * in {@link android.app.TabActivity TabActivity}.
95      * Example:</p>
96<pre>mTabHost = (TabHost)findViewById(R.id.tabhost);
97mTabHost.setup();
98mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1");
99      */
100    public void setup() {
101        mTabWidget = (TabWidget) findViewById(com.android.internal.R.id.tabs);
102        if (mTabWidget == null) {
103            throw new RuntimeException(
104                    "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'");
105        }
106
107        // KeyListener to attach to all tabs. Detects non-navigation keys
108        // and relays them to the tab content.
109        mTabKeyListener = new OnKeyListener() {
110            public boolean onKey(View v, int keyCode, KeyEvent event) {
111                switch (keyCode) {
112                    case KeyEvent.KEYCODE_DPAD_CENTER:
113                    case KeyEvent.KEYCODE_DPAD_LEFT:
114                    case KeyEvent.KEYCODE_DPAD_RIGHT:
115                    case KeyEvent.KEYCODE_DPAD_UP:
116                    case KeyEvent.KEYCODE_DPAD_DOWN:
117                    case KeyEvent.KEYCODE_ENTER:
118                        return false;
119
120                }
121                mTabContent.requestFocus(View.FOCUS_FORWARD);
122                return mTabContent.dispatchKeyEvent(event);
123            }
124
125        };
126
127        mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() {
128            public void onTabSelectionChanged(int tabIndex, boolean clicked) {
129                setCurrentTab(tabIndex);
130                if (clicked) {
131                    mTabContent.requestFocus(View.FOCUS_FORWARD);
132                }
133            }
134        });
135
136        mTabContent = (FrameLayout) findViewById(com.android.internal.R.id.tabcontent);
137        if (mTabContent == null) {
138            throw new RuntimeException(
139                    "Your TabHost must have a FrameLayout whose id attribute is "
140                            + "'android.R.id.tabcontent'");
141        }
142    }
143
144    /**
145     * If you are using {@link TabSpec#setContent(android.content.Intent)}, this
146     * must be called since the activityGroup is needed to launch the local activity.
147     *
148     * This is done for you if you extend {@link android.app.TabActivity}.
149     * @param activityGroup Used to launch activities for tab content.
150     */
151    public void setup(LocalActivityManager activityGroup) {
152        setup();
153        mLocalActivityManager = activityGroup;
154    }
155
156
157    @Override
158    protected void onAttachedToWindow() {
159        super.onAttachedToWindow();
160        final ViewTreeObserver treeObserver = getViewTreeObserver();
161        if (treeObserver != null) {
162            treeObserver.addOnTouchModeChangeListener(this);
163        }
164    }
165
166    @Override
167    protected void onDetachedFromWindow() {
168        super.onDetachedFromWindow();
169        final ViewTreeObserver treeObserver = getViewTreeObserver();
170        if (treeObserver != null) {
171            treeObserver.removeOnTouchModeChangeListener(this);
172        }
173    }
174
175    /**
176     * {@inheritDoc}
177     */
178    public void onTouchModeChanged(boolean isInTouchMode) {
179        if (!isInTouchMode) {
180            // leaving touch mode.. if nothing has focus, let's give it to
181            // the indicator of the current tab
182            if (mCurrentView != null && (!mCurrentView.hasFocus() || mCurrentView.isFocused())) {
183                mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus();
184            }
185        }
186    }
187
188    /**
189     * Add a tab.
190     * @param tabSpec Specifies how to create the indicator and content.
191     */
192    public void addTab(TabSpec tabSpec) {
193
194        if (tabSpec.mIndicatorStrategy == null) {
195            throw new IllegalArgumentException("you must specify a way to create the tab indicator.");
196        }
197
198        if (tabSpec.mContentStrategy == null) {
199            throw new IllegalArgumentException("you must specify a way to create the tab content");
200        }
201        View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView();
202        tabIndicator.setOnKeyListener(mTabKeyListener);
203
204        // If this is a custom view, then do not draw the bottom strips for
205        // the tab indicators.
206        if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) {
207            mTabWidget.setStripEnabled(false);
208        }
209        mTabWidget.addView(tabIndicator);
210        mTabSpecs.add(tabSpec);
211
212        if (mCurrentTab == -1) {
213            setCurrentTab(0);
214        }
215    }
216
217
218    /**
219     * Removes all tabs from the tab widget associated with this tab host.
220     */
221    public void clearAllTabs() {
222        mTabWidget.removeAllViews();
223        initTabHost();
224        mTabContent.removeAllViews();
225        mTabSpecs.clear();
226        requestLayout();
227        invalidate();
228    }
229
230    public TabWidget getTabWidget() {
231        return mTabWidget;
232    }
233
234    public int getCurrentTab() {
235        return mCurrentTab;
236    }
237
238    public String getCurrentTabTag() {
239        if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
240            return mTabSpecs.get(mCurrentTab).getTag();
241        }
242        return null;
243    }
244
245    public View getCurrentTabView() {
246        if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
247            return mTabWidget.getChildTabViewAt(mCurrentTab);
248        }
249        return null;
250    }
251
252    public View getCurrentView() {
253        return mCurrentView;
254    }
255
256    public void setCurrentTabByTag(String tag) {
257        int i;
258        for (i = 0; i < mTabSpecs.size(); i++) {
259            if (mTabSpecs.get(i).getTag().equals(tag)) {
260                setCurrentTab(i);
261                break;
262            }
263        }
264    }
265
266    /**
267     * Get the FrameLayout which holds tab content
268     */
269    public FrameLayout getTabContentView() {
270        return mTabContent;
271    }
272
273    @Override
274    public boolean dispatchKeyEvent(KeyEvent event) {
275        final boolean handled = super.dispatchKeyEvent(event);
276
277        // unhandled key ups change focus to tab indicator for embedded activities
278        // when there is nothing that will take focus from default focus searching
279        if (!handled
280                && (event.getAction() == KeyEvent.ACTION_DOWN)
281                && (event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP)
282                && (mCurrentView != null)
283                && (mCurrentView.isRootNamespace())
284                && (mCurrentView.hasFocus())
285                && (mCurrentView.findFocus().focusSearch(View.FOCUS_UP) == null)) {
286            mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus();
287            playSoundEffect(SoundEffectConstants.NAVIGATION_UP);
288            return true;
289        }
290        return handled;
291    }
292
293
294    @Override
295    public void dispatchWindowFocusChanged(boolean hasFocus) {
296        if (mCurrentView != null){
297            mCurrentView.dispatchWindowFocusChanged(hasFocus);
298        }
299    }
300
301    public void setCurrentTab(int index) {
302        if (index < 0 || index >= mTabSpecs.size()) {
303            return;
304        }
305
306        if (index == mCurrentTab) {
307            return;
308        }
309
310        // notify old tab content
311        if (mCurrentTab != -1) {
312            mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed();
313        }
314
315        mCurrentTab = index;
316        final TabHost.TabSpec spec = mTabSpecs.get(index);
317
318        // Call the tab widget's focusCurrentTab(), instead of just
319        // selecting the tab.
320        mTabWidget.focusCurrentTab(mCurrentTab);
321
322        // tab content
323        mCurrentView = spec.mContentStrategy.getContentView();
324
325        if (mCurrentView.getParent() == null) {
326            mTabContent
327                    .addView(
328                            mCurrentView,
329                            new ViewGroup.LayoutParams(
330                                    ViewGroup.LayoutParams.MATCH_PARENT,
331                                    ViewGroup.LayoutParams.MATCH_PARENT));
332        }
333
334        if (!mTabWidget.hasFocus()) {
335            // if the tab widget didn't take focus (likely because we're in touch mode)
336            // give the current tab content view a shot
337            mCurrentView.requestFocus();
338        }
339
340        //mTabContent.requestFocus(View.FOCUS_FORWARD);
341        invokeOnTabChangeListener();
342    }
343
344    /**
345     * Register a callback to be invoked when the selected state of any of the items
346     * in this list changes
347     * @param l
348     * The callback that will run
349     */
350    public void setOnTabChangedListener(OnTabChangeListener l) {
351        mOnTabChangeListener = l;
352    }
353
354    private void invokeOnTabChangeListener() {
355        if (mOnTabChangeListener != null) {
356            mOnTabChangeListener.onTabChanged(getCurrentTabTag());
357        }
358    }
359
360    /**
361     * Interface definition for a callback to be invoked when tab changed
362     */
363    public interface OnTabChangeListener {
364        void onTabChanged(String tabId);
365    }
366
367
368    /**
369     * Makes the content of a tab when it is selected. Use this if your tab
370     * content needs to be created on demand, i.e. you are not showing an
371     * existing view or starting an activity.
372     */
373    public interface TabContentFactory {
374        /**
375         * Callback to make the tab contents
376         *
377         * @param tag
378         *            Which tab was selected.
379         * @return The view to display the contents of the selected tab.
380         */
381        View createTabContent(String tag);
382    }
383
384
385    /**
386     * A tab has a tab indicator, content, and a tag that is used to keep
387     * track of it.  This builder helps choose among these options.
388     *
389     * For the tab indicator, your choices are:
390     * 1) set a label
391     * 2) set a label and an icon
392     *
393     * For the tab content, your choices are:
394     * 1) the id of a {@link View}
395     * 2) a {@link TabContentFactory} that creates the {@link View} content.
396     * 3) an {@link Intent} that launches an {@link android.app.Activity}.
397     */
398    public class TabSpec {
399
400        private String mTag;
401
402        private IndicatorStrategy mIndicatorStrategy;
403        private ContentStrategy mContentStrategy;
404
405        private TabSpec(String tag) {
406            mTag = tag;
407        }
408
409        /**
410         * Specify a label as the tab indicator.
411         */
412        public TabSpec setIndicator(CharSequence label) {
413            mIndicatorStrategy = new LabelIndicatorStrategy(label);
414            return this;
415        }
416
417        /**
418         * Specify a label and icon as the tab indicator.
419         */
420        public TabSpec setIndicator(CharSequence label, Drawable icon) {
421            mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon);
422            return this;
423        }
424
425        /**
426         * Specify a view as the tab indicator.
427         */
428        public TabSpec setIndicator(View view) {
429            mIndicatorStrategy = new ViewIndicatorStrategy(view);
430            return this;
431        }
432
433        /**
434         * Specify the id of the view that should be used as the content
435         * of the tab.
436         */
437        public TabSpec setContent(int viewId) {
438            mContentStrategy = new ViewIdContentStrategy(viewId);
439            return this;
440        }
441
442        /**
443         * Specify a {@link android.widget.TabHost.TabContentFactory} to use to
444         * create the content of the tab.
445         */
446        public TabSpec setContent(TabContentFactory contentFactory) {
447            mContentStrategy = new FactoryContentStrategy(mTag, contentFactory);
448            return this;
449        }
450
451        /**
452         * Specify an intent to use to launch an activity as the tab content.
453         */
454        public TabSpec setContent(Intent intent) {
455            mContentStrategy = new IntentContentStrategy(mTag, intent);
456            return this;
457        }
458
459
460        public String getTag() {
461            return mTag;
462        }
463    }
464
465    /**
466     * Specifies what you do to create a tab indicator.
467     */
468    private static interface IndicatorStrategy {
469
470        /**
471         * Return the view for the indicator.
472         */
473        View createIndicatorView();
474    }
475
476    /**
477     * Specifies what you do to manage the tab content.
478     */
479    private static interface ContentStrategy {
480
481        /**
482         * Return the content view.  The view should may be cached locally.
483         */
484        View getContentView();
485
486        /**
487         * Perhaps do something when the tab associated with this content has
488         * been closed (i.e make it invisible, or remove it).
489         */
490        void tabClosed();
491    }
492
493    /**
494     * How to create a tab indicator that just has a label.
495     */
496    private class LabelIndicatorStrategy implements IndicatorStrategy {
497
498        private final CharSequence mLabel;
499
500        private LabelIndicatorStrategy(CharSequence label) {
501            mLabel = label;
502        }
503
504        public View createIndicatorView() {
505            final Context context = getContext();
506            LayoutInflater inflater =
507                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
508            View tabIndicator = inflater.inflate(R.layout.tab_indicator,
509                    mTabWidget, // tab widget is the parent
510                    false); // no inflate params
511
512            final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
513            tv.setText(mLabel);
514
515            if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
516                // Donut apps get old color scheme
517                tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
518                tv.setTextColor(context.getResources().getColorStateList(R.color.tab_indicator_text_v4));
519            }
520
521            return tabIndicator;
522        }
523    }
524
525    /**
526     * How we create a tab indicator that has a label and an icon
527     */
528    private class LabelAndIconIndicatorStrategy implements IndicatorStrategy {
529
530        private final CharSequence mLabel;
531        private final Drawable mIcon;
532
533        private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) {
534            mLabel = label;
535            mIcon = icon;
536        }
537
538        public View createIndicatorView() {
539            final Context context = getContext();
540            LayoutInflater inflater =
541                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
542            View tabIndicator = inflater.inflate(R.layout.tab_indicator,
543                    mTabWidget, // tab widget is the parent
544                    false); // no inflate params
545
546            final TextView tv = (TextView) tabIndicator.findViewById(R.id.title);
547            tv.setText(mLabel);
548
549            final ImageView iconView = (ImageView) tabIndicator.findViewById(R.id.icon);
550            iconView.setImageDrawable(mIcon);
551
552            if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
553                // Donut apps get old color scheme
554                tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
555                tv.setTextColor(context.getResources().getColorStateList(R.color.tab_indicator_text_v4));
556            }
557
558            return tabIndicator;
559        }
560    }
561
562    /**
563     * How to create a tab indicator by specifying a view.
564     */
565    private class ViewIndicatorStrategy implements IndicatorStrategy {
566
567        private final View mView;
568
569        private ViewIndicatorStrategy(View view) {
570            mView = view;
571        }
572
573        public View createIndicatorView() {
574            return mView;
575        }
576    }
577
578    /**
579     * How to create the tab content via a view id.
580     */
581    private class ViewIdContentStrategy implements ContentStrategy {
582
583        private final View mView;
584
585        private ViewIdContentStrategy(int viewId) {
586            mView = mTabContent.findViewById(viewId);
587            if (mView != null) {
588                mView.setVisibility(View.GONE);
589            } else {
590                throw new RuntimeException("Could not create tab content because " +
591                        "could not find view with id " + viewId);
592            }
593        }
594
595        public View getContentView() {
596            mView.setVisibility(View.VISIBLE);
597            return mView;
598        }
599
600        public void tabClosed() {
601            mView.setVisibility(View.GONE);
602        }
603    }
604
605    /**
606     * How tab content is managed using {@link TabContentFactory}.
607     */
608    private class FactoryContentStrategy implements ContentStrategy {
609        private View mTabContent;
610        private final CharSequence mTag;
611        private TabContentFactory mFactory;
612
613        public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) {
614            mTag = tag;
615            mFactory = factory;
616        }
617
618        public View getContentView() {
619            if (mTabContent == null) {
620                mTabContent = mFactory.createTabContent(mTag.toString());
621            }
622            mTabContent.setVisibility(View.VISIBLE);
623            return mTabContent;
624        }
625
626        public void tabClosed() {
627            mTabContent.setVisibility(View.GONE);
628        }
629    }
630
631    /**
632     * How tab content is managed via an {@link Intent}: the content view is the
633     * decorview of the launched activity.
634     */
635    private class IntentContentStrategy implements ContentStrategy {
636
637        private final String mTag;
638        private final Intent mIntent;
639
640        private View mLaunchedView;
641
642        private IntentContentStrategy(String tag, Intent intent) {
643            mTag = tag;
644            mIntent = intent;
645        }
646
647        public View getContentView() {
648            if (mLocalActivityManager == null) {
649                throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?");
650            }
651            final Window w = mLocalActivityManager.startActivity(
652                    mTag, mIntent);
653            final View wd = w != null ? w.getDecorView() : null;
654            if (mLaunchedView != wd && mLaunchedView != null) {
655                if (mLaunchedView.getParent() != null) {
656                    mTabContent.removeView(mLaunchedView);
657                }
658            }
659            mLaunchedView = wd;
660
661            // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activities for now so they can get
662            // focus if none of their children have it. They need focus to be able to
663            // display menu items.
664            //
665            // Replace this with something better when Bug 628886 is fixed...
666            //
667            if (mLaunchedView != null) {
668                mLaunchedView.setVisibility(View.VISIBLE);
669                mLaunchedView.setFocusableInTouchMode(true);
670                ((ViewGroup) mLaunchedView).setDescendantFocusability(
671                        FOCUS_AFTER_DESCENDANTS);
672            }
673            return mLaunchedView;
674        }
675
676        public void tabClosed() {
677            if (mLaunchedView != null) {
678                mLaunchedView.setVisibility(View.GONE);
679            }
680        }
681    }
682
683}
684