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