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