[go: nahoru, domu]

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